@lkangd/cc-env 1.1.1 → 1.2.0

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.
Files changed (78) hide show
  1. package/LICENSE +15 -0
  2. package/dist/cli.js +68 -6
  3. package/dist/commands/completion.js +60 -0
  4. package/dist/commands/doctor.js +73 -0
  5. package/dist/commands/preset/edit.js +16 -11
  6. package/dist/commands/preset/rename.js +16 -0
  7. package/dist/commands/run.js +9 -1
  8. package/dist/ink/preset-edit-app.js +112 -0
  9. package/package.json +11 -2
  10. package/.claude/settings.json +0 -6
  11. package/.claude/settings.local.json +0 -8
  12. package/.nvmrc +0 -1
  13. package/CHANGELOG.md +0 -71
  14. package/docs/product-specs/index.draft.md +0 -106
  15. package/docs/product-specs/index.md +0 -911
  16. package/docs/product-specs/optional.md +0 -42
  17. package/docs/references/claude-code-env.md +0 -224
  18. package/docs/superpowers/plans/2026-04-24-cc-env-init-shell-migration.md +0 -1331
  19. package/docs/superpowers/plans/2026-04-24-cc-env.md +0 -1666
  20. package/docs/superpowers/plans/2026-04-26-preset-create-interactive-refactor.md +0 -1432
  21. package/docs/superpowers/specs/2026-04-24-cc-env-design.md +0 -438
  22. package/docs/superpowers/specs/2026-04-24-cc-env-init-shell-migration-design.md +0 -181
  23. package/docs/superpowers/specs/2026-04-26-preset-create-interactive-refactor-design.md +0 -78
  24. package/src/cli.ts +0 -340
  25. package/src/commands/init.ts +0 -139
  26. package/src/commands/preset/create.ts +0 -96
  27. package/src/commands/preset/delete.ts +0 -62
  28. package/src/commands/preset/show.ts +0 -51
  29. package/src/commands/restore.ts +0 -150
  30. package/src/commands/run.ts +0 -158
  31. package/src/core/errors.ts +0 -13
  32. package/src/core/find-claude.ts +0 -70
  33. package/src/core/format.ts +0 -29
  34. package/src/core/fs.ts +0 -18
  35. package/src/core/gitignore.ts +0 -26
  36. package/src/core/logger.ts +0 -11
  37. package/src/core/mask.ts +0 -17
  38. package/src/core/paths.ts +0 -41
  39. package/src/core/process-env.ts +0 -11
  40. package/src/core/schema.ts +0 -55
  41. package/src/core/spawn.ts +0 -36
  42. package/src/flows/init-flow.ts +0 -61
  43. package/src/flows/preset-create-flow.ts +0 -129
  44. package/src/flows/restore-flow.ts +0 -144
  45. package/src/ink/init-app.tsx +0 -110
  46. package/src/ink/preset-create-app.tsx +0 -451
  47. package/src/ink/preset-delete-app.tsx +0 -114
  48. package/src/ink/preset-show-app.tsx +0 -76
  49. package/src/ink/restore-app.tsx +0 -230
  50. package/src/ink/run-preset-select-app.tsx +0 -83
  51. package/src/ink/summary.tsx +0 -91
  52. package/src/services/claude-settings-env-service.ts +0 -72
  53. package/src/services/history-service.ts +0 -48
  54. package/src/services/preset-service.ts +0 -72
  55. package/src/services/project-env-service.ts +0 -128
  56. package/src/services/project-state-service.ts +0 -31
  57. package/src/services/settings-env-service.ts +0 -40
  58. package/src/services/shell-env-service.ts +0 -112
  59. package/src/types.d.ts +0 -19
  60. package/tests/cli/help.test.ts +0 -133
  61. package/tests/cli/init.test.ts +0 -76
  62. package/tests/cli/restore.test.ts +0 -172
  63. package/tests/commands/create.test.ts +0 -263
  64. package/tests/commands/output.test.ts +0 -119
  65. package/tests/commands/run.test.ts +0 -218
  66. package/tests/core/gitignore.test.ts +0 -98
  67. package/tests/core/paths.test.ts +0 -24
  68. package/tests/core/schema-mask.test.ts +0 -182
  69. package/tests/core/spawn.test.ts +0 -47
  70. package/tests/flows/init-flow.test.ts +0 -40
  71. package/tests/flows/preset-create-flow.test.ts +0 -225
  72. package/tests/flows/restore-flow.test.ts +0 -157
  73. package/tests/integration/init-restore.test.ts +0 -406
  74. package/tests/services/claude-shell.test.ts +0 -183
  75. package/tests/services/storage.test.ts +0 -143
  76. package/tsconfig.build.json +0 -9
  77. package/tsconfig.json +0 -22
  78. package/vitest.config.ts +0 -8
@@ -1,230 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
2
- import { Box, Text, useApp, useInput } from 'ink'
3
-
4
- import { advanceRestoreFlow, type RestoreFlowState } from '../flows/restore-flow.js'
5
- import { EnvEntries, EnvSummary } from './summary.js'
6
-
7
- export function RestoreApp({
8
- state,
9
- onSubmit,
10
- }: {
11
- state: RestoreFlowState
12
- onSubmit?: (result: {
13
- confirmed: boolean
14
- timestamp?: string
15
- targetType?: 'settings' | 'preset'
16
- targetName?: string
17
- }) => void
18
- }) {
19
- const { exit } = useApp()
20
- const [currentState, setCurrentState] = useState(state)
21
- const [cursor, setCursor] = useState(0)
22
- const recordAtCursor = currentState.records[cursor]
23
- const selectedRecord = currentState.records.find(
24
- (record) => record.timestamp === currentState.selectedTimestamp,
25
- )
26
- const activeRecord = currentState.step === 'record'
27
- ? recordAtCursor
28
- : selectedRecord ?? currentState.records[0]
29
- const restoreEntries = useMemo(
30
- () =>
31
- activeRecord
32
- ? Object.entries(
33
- activeRecord.action === 'init'
34
- ? Object.fromEntries(activeRecord.sources.flatMap((s) => Object.entries(s.backup)))
35
- : activeRecord.backup,
36
- ).sort(([left], [right]) => left.localeCompare(right)) as [string, string][]
37
- : [],
38
- [activeRecord],
39
- )
40
-
41
- const fromFiles = useMemo(() => {
42
- if (!activeRecord || activeRecord.action !== 'init') {
43
- return []
44
- }
45
-
46
- return activeRecord.shellWrites.map((sw) => sw.filePath)
47
- }, [activeRecord])
48
-
49
- const toFiles = useMemo(() => {
50
- if (!activeRecord || activeRecord.action !== 'init') {
51
- return []
52
- }
53
-
54
- return activeRecord.sources.map((s) => s.file)
55
- }, [activeRecord])
56
-
57
- useEffect(() => {
58
- setCurrentState(state)
59
- setCursor(0)
60
- }, [state])
61
-
62
- useEffect(() => {
63
- if (!onSubmit || currentState.records.length !== 0) {
64
- return
65
- }
66
-
67
- onSubmit({
68
- confirmed: false,
69
- })
70
- exit()
71
- }, [currentState.records.length, exit, onSubmit])
72
-
73
- useInput((input, key) => {
74
- if (!onSubmit) {
75
- return
76
- }
77
-
78
- if (key.escape || input.toLowerCase() === 'q') {
79
- onSubmit({ confirmed: false })
80
- exit()
81
- return
82
- }
83
-
84
- if (currentState.step === 'record') {
85
- if (key.upArrow || input === 'k') {
86
- setCursor((value) => Math.max(0, value - 1))
87
- return
88
- }
89
-
90
- if (key.downArrow || input === 'j') {
91
- setCursor((value) => Math.min(currentState.records.length - 1, value + 1))
92
- return
93
- }
94
- }
95
-
96
- if (!key.return || !activeRecord) {
97
- return
98
- }
99
-
100
- if (currentState.step === 'record') {
101
- setCurrentState((previousState) =>
102
- advanceRestoreFlow(previousState, {
103
- type: 'select-record',
104
- timestamp: activeRecord.timestamp,
105
- }),
106
- )
107
- return
108
- }
109
-
110
- if (currentState.step === 'target') {
111
- if (activeRecord.action !== 'restore') {
112
- return
113
- }
114
-
115
- setCurrentState((previousState) =>
116
- advanceRestoreFlow(previousState, {
117
- type: 'select-target',
118
- targetType: activeRecord.targetType,
119
- ...(activeRecord.targetType === 'preset'
120
- ? { targetName: activeRecord.targetName }
121
- : {}),
122
- }),
123
- )
124
- return
125
- }
126
-
127
- if (currentState.step === 'confirm') {
128
- onSubmit({
129
- confirmed: true,
130
- timestamp: activeRecord.timestamp,
131
- ...(currentState.targetType ? { targetType: currentState.targetType } : {}),
132
- ...(currentState.targetType === 'preset' && 'targetName' in currentState
133
- ? { targetName: currentState.targetName }
134
- : {}),
135
- })
136
- exit()
137
- }
138
- })
139
-
140
- return (
141
- <Box flexDirection="column">
142
- <Text>Restore record</Text>
143
- {currentState.step === 'record' ? (
144
- <>
145
- <Text dimColor>↑/k ↓/j navigate · enter confirm · q cancel</Text>
146
- <Box marginTop={1}>
147
- <Box flexDirection="column" width={28} marginRight={2}>
148
- <Text bold color="cyan">History</Text>
149
- <Box flexDirection="column" marginTop={1}>
150
- {currentState.records.map((record, index) => (
151
- <Text key={record.timestamp}>
152
- {index === cursor ? '❯ ' : ' '}
153
- {record.timestamp}
154
- </Text>
155
- ))}
156
- </Box>
157
- </Box>
158
- <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="green" paddingX={1}>
159
- <Text bold color="green">Preview</Text>
160
- {activeRecord?.action === 'init' ? (
161
- <Box flexDirection="column">
162
- {fromFiles.length > 0 ? (
163
- <Box flexDirection="column">
164
- <Text dimColor>From:</Text>
165
- {fromFiles.map((file) => (
166
- <Text key={file} color="cyan"> {file}</Text>
167
- ))}
168
- </Box>
169
- ) : null}
170
- {toFiles.length > 0 ? (
171
- <Box flexDirection="column">
172
- <Text dimColor>To:</Text>
173
- {toFiles.map((file) => (
174
- <Text key={file} color="cyan"> {file}</Text>
175
- ))}
176
- </Box>
177
- ) : null}
178
- </Box>
179
- ) : (
180
- <Text dimColor>
181
- Restore to {activeRecord?.targetType === 'preset' ? `preset ${activeRecord.targetName}` : activeRecord?.targetType ?? 'settings'}
182
- </Text>
183
- )}
184
- <Box flexDirection="column" marginTop={1}>
185
- <EnvEntries entries={restoreEntries} />
186
- </Box>
187
- </Box>
188
- </Box>
189
- </>
190
- ) : null}
191
- {currentState.step === 'target' ? (
192
- <Text>
193
- Select target for {selectedRecord?.timestamp ?? 'record'}: settings or preset
194
- </Text>
195
- ) : null}
196
- {currentState.step === 'confirm' && selectedRecord?.action === 'init' ? (
197
- <Box flexDirection="column" marginTop={1}>
198
- <Text>
199
- Confirm restore from <Text color="cyan">{selectedRecord.timestamp}</Text>
200
- </Text>
201
- <EnvSummary
202
- title="Will restore"
203
- entries={restoreEntries}
204
- {...(fromFiles.length > 0 ? { fromFiles } : {})}
205
- {...(toFiles.length > 0 ? { toFiles } : {})}
206
- />
207
- </Box>
208
- ) : null}
209
- {currentState.step === 'confirm' && selectedRecord?.action !== 'init' ? (
210
- <Box flexDirection="column" marginTop={1}>
211
- <Text>
212
- Confirm restore from <Text color="cyan">{selectedRecord?.timestamp ?? 'record'}</Text> to{' '}
213
- <Text color="green">
214
- {currentState.targetType === 'preset'
215
- ? `preset ${currentState.targetName}`
216
- : currentState.targetType ?? 'settings'}
217
- </Text>
218
- </Text>
219
- <EnvSummary title="Will restore" entries={restoreEntries} />
220
- </Box>
221
- ) : null}
222
- {currentState.step === 'done' ? (
223
- <Text color="green">{'\n'}Restore complete</Text>
224
- ) : null}
225
- {currentState.step !== 'done' ? (
226
- <Text>Press Enter to confirm or q to cancel</Text>
227
- ) : null}
228
- </Box>
229
- )
230
- }
@@ -1,83 +0,0 @@
1
- import React, { useMemo, useState } from 'react'
2
- import { Box, Text, useApp, useInput } from 'ink'
3
-
4
- import type { EnvMap } from '../core/schema.js'
5
- import { EnvEntries } from './summary.js'
6
-
7
- export type PresetSource = 'global' | 'project'
8
-
9
- export type PresetSelectItem = {
10
- name: string
11
- env: EnvMap
12
- source: PresetSource
13
- }
14
-
15
- type Props = {
16
- presets: Array<PresetSelectItem>
17
- defaultIndex?: number
18
- onSubmit: (preset: PresetSelectItem) => void
19
- }
20
-
21
- export function RunPresetSelectApp({ presets, defaultIndex = 0, onSubmit }: Props) {
22
- const { exit } = useApp()
23
- const [cursor, setCursor] = useState(defaultIndex)
24
- const active = presets[cursor]
25
-
26
- const entries = useMemo(
27
- () =>
28
- active
29
- ? (Object.entries(active.env).sort(([a], [b]) => a.localeCompare(b)) as [string, string][])
30
- : [],
31
- [active],
32
- )
33
-
34
- useInput((input, key) => {
35
- if (key.escape || input.toLowerCase() === 'q') {
36
- exit()
37
- return
38
- }
39
-
40
- if (key.upArrow || input === 'k') {
41
- setCursor((c) => Math.max(0, c - 1))
42
- return
43
- }
44
-
45
- if (key.downArrow || input === 'j') {
46
- setCursor((c) => Math.min(presets.length - 1, c + 1))
47
- return
48
- }
49
-
50
- if (key.return && active) {
51
- onSubmit(active)
52
- exit()
53
- }
54
- })
55
-
56
- return (
57
- <Box flexDirection="column">
58
- <Text bold>Select a preset</Text>
59
- <Text dimColor>↑/k ↓/j navigate · enter select · q cancel</Text>
60
- <Box marginTop={1}>
61
- <Box flexDirection="column" width={28} marginRight={2}>
62
- <Text bold color="cyan">Presets</Text>
63
- <Box flexDirection="column" marginTop={1}>
64
- {presets.map((p, i) => (
65
- <Box key={`${p.source}:${p.name}`}>
66
- <Text>{i === cursor ? '❯ ' : ' '}</Text>
67
- <Text {...(p.source === 'project' ? { color: 'yellow' } : {})}>{p.name}</Text>
68
- <Text dimColor> ({p.source})</Text>
69
- </Box>
70
- ))}
71
- </Box>
72
- </Box>
73
- <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="green" paddingX={1}>
74
- <Text bold color="green">{active?.name ?? 'Preview'}</Text>
75
- <Text dimColor>{active?.source === 'project' ? 'Project preset' : 'Global preset'}</Text>
76
- <Box flexDirection="column" marginTop={1}>
77
- <EnvEntries entries={entries} />
78
- </Box>
79
- </Box>
80
- </Box>
81
- </Box>
82
- )
83
- }
@@ -1,91 +0,0 @@
1
- import React from 'react'
2
- import type { ReactNode } from 'react'
3
- import { Box, Text, render } from 'ink'
4
-
5
- const h = React.createElement
6
-
7
- import { maskValue } from '../core/mask.js'
8
- import type { EnvMap } from '../core/schema.js'
9
-
10
- export function EnvEntries({ entries, mask }: { entries: [string, string][]; mask?: boolean }) {
11
- if (entries.length === 0) {
12
- return <Text dimColor>none</Text>
13
- }
14
- return <>{entries.map(([key, value]) => (
15
- <Box key={key}>
16
- <Text color="yellow">• </Text>
17
- <Text color="magenta">{key}</Text>
18
- <Text dimColor>=</Text>
19
- <Text color="white">{mask ? maskValue(key, value) : value}</Text>
20
- </Box>
21
- ))}</>
22
- }
23
-
24
- export function EnvSummary({
25
- title,
26
- entries,
27
- description,
28
- fromFiles,
29
- toFiles,
30
- footer,
31
- mask,
32
- }: {
33
- title: string
34
- entries: [string, string][]
35
- description?: string
36
- fromFiles?: string[]
37
- toFiles?: string[]
38
- footer?: ReactNode
39
- mask?: boolean
40
- }) {
41
- return (
42
- <Box flexDirection="column">
43
- {description ? <Text dimColor>{description}</Text> : null}
44
- {fromFiles && fromFiles.length > 0 ? (
45
- <Box flexDirection="column">
46
- <Text dimColor>From:</Text>
47
- {fromFiles.map((file) => (
48
- <Text key={file} color="cyan"> {file}</Text>
49
- ))}
50
- </Box>
51
- ) : null}
52
- {toFiles && toFiles.length > 0 ? (
53
- <Box flexDirection="column">
54
- <Text dimColor>To:</Text>
55
- {toFiles.map((file) => (
56
- <Text key={file} color="cyan"> {file}</Text>
57
- ))}
58
- </Box>
59
- ) : null}
60
- <Box flexDirection="column" marginTop={1} borderStyle="round" borderColor="green" paddingX={1}>
61
- <Text bold color="green">{title}</Text>
62
- <EnvEntries entries={entries} {...(mask ? { mask } : {})} />
63
- </Box>
64
- {footer ?? null}
65
- </Box>
66
- )
67
- }
68
-
69
- export async function renderEnvSummary(props: {
70
- title: string
71
- description?: string
72
- env: EnvMap
73
- fromFiles?: string[]
74
- toFiles?: string[]
75
- footer?: ReactNode
76
- }): Promise<void> {
77
- const entries: [string, string][] = Object.entries(props.env).sort(([a], [b]) => a.localeCompare(b)) as [string, string][]
78
- const app = render(
79
- h(EnvSummary, {
80
- title: props.title,
81
- entries,
82
- mask: true,
83
- ...(props.description ? { description: props.description } : {}),
84
- ...(props.fromFiles ? { fromFiles: props.fromFiles } : {}),
85
- ...(props.toFiles ? { toFiles: props.toFiles } : {}),
86
- ...(props.footer ? { footer: props.footer } : {}),
87
- }),
88
- )
89
- app.unmount()
90
- await app.waitUntilExit()
91
- }
@@ -1,72 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
-
3
- import { atomicWriteFile } from '../core/fs.js'
4
- import {
5
- resolveClaudeSettingsLocalPath,
6
- resolveClaudeSettingsPath,
7
- resolveProjectSettingsLocalPath,
8
- resolveProjectSettingsPath,
9
- } from '../core/paths.js'
10
- import { envMapSchema, type EnvMap } from '../core/schema.js'
11
-
12
- export type ClaudeSettingsSource = {
13
- path: string
14
- exists: boolean
15
- env: EnvMap
16
- }
17
-
18
- export function createClaudeSettingsEnvService({ homeDir, cwd }: { homeDir?: string; cwd?: string } = {}) {
19
- const paths = [
20
- resolveClaudeSettingsPath(homeDir),
21
- resolveClaudeSettingsLocalPath(homeDir),
22
- resolveProjectSettingsPath(cwd),
23
- resolveProjectSettingsLocalPath(cwd),
24
- ]
25
-
26
- async function readOne(path: string): Promise<ClaudeSettingsSource> {
27
- try {
28
- const content = await readFile(path, 'utf8')
29
- const json = JSON.parse(content) as { env?: unknown }
30
- return {
31
- path,
32
- exists: true,
33
- env: envMapSchema.parse(json.env ?? {}),
34
- }
35
- } catch (error) {
36
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
37
- return {
38
- path,
39
- exists: false,
40
- env: envMapSchema.parse({}),
41
- }
42
- }
43
-
44
- throw error
45
- }
46
- }
47
-
48
- async function writeOne(path: string, env: EnvMap): Promise<void> {
49
- let json: Record<string, unknown> = {}
50
-
51
- try {
52
- const content = await readFile(path, 'utf8')
53
- json = JSON.parse(content) as Record<string, unknown>
54
- } catch (error) {
55
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
56
- throw error
57
- }
58
- }
59
-
60
- json.env = envMapSchema.parse(env)
61
- await atomicWriteFile(path, `${JSON.stringify(json, null, 2)}\n`)
62
- }
63
-
64
- return {
65
- read: () => Promise.all(paths.map(readOne)),
66
- write: async (sources: Array<{ path: string; env: EnvMap }>) => {
67
- for (const { path, env } of sources) {
68
- await writeOne(path, env)
69
- }
70
- },
71
- }
72
- }
@@ -1,48 +0,0 @@
1
- import { readdir, readFile } from 'node:fs/promises'
2
- import { dirname, join } from 'node:path'
3
-
4
- import { atomicWriteFile, ensureParentDir } from '../core/fs.js'
5
- import { resolveHistoryPath } from '../core/paths.js'
6
- import { historySchema } from '../core/schema.js'
7
- import type { HistoryRecord } from '../core/schema.js'
8
-
9
- function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
10
- return error instanceof Error && 'code' in error
11
- }
12
-
13
- export function createHistoryService(globalRoot: string) {
14
- return {
15
- async write(record: HistoryRecord): Promise<HistoryRecord> {
16
- const stored = historySchema.parse(record)
17
- const filePath = resolveHistoryPath(globalRoot, stored.timestamp)
18
- await ensureParentDir(filePath)
19
- await atomicWriteFile(filePath, `${JSON.stringify(stored, null, 2)}\n`)
20
- return stored
21
- },
22
-
23
- async list(): Promise<HistoryRecord[]> {
24
- const dirPath = dirname(resolveHistoryPath(globalRoot, 'placeholder'))
25
-
26
- try {
27
- const entries = await readdir(dirPath, { withFileTypes: true })
28
- const fileNames = entries
29
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
30
- .map((entry) => entry.name)
31
- .sort((a, b) => b.localeCompare(a))
32
-
33
- return Promise.all(
34
- fileNames.map(async (fileName) => {
35
- const content = await readFile(join(dirPath, fileName), 'utf8')
36
- return historySchema.parse(JSON.parse(content))
37
- }),
38
- )
39
- } catch (error) {
40
- if (isErrnoException(error) && error.code === 'ENOENT') {
41
- return []
42
- }
43
-
44
- throw error
45
- }
46
- },
47
- }
48
- }
@@ -1,72 +0,0 @@
1
- import { readdir, readFile, rm } from 'node:fs/promises'
2
- import { dirname } from 'node:path'
3
-
4
- import { CliError } from '../core/errors.js'
5
- import { atomicWriteFile } from '../core/fs.js'
6
- import { resolvePresetPath } from '../core/paths.js'
7
- import { presetSchema, type Preset } from '../core/schema.js'
8
-
9
- type StoredPreset = Preset & { filePath: string }
10
-
11
- export function createPresetService(globalRoot: string) {
12
- function getPath(name: string): string {
13
- return resolvePresetPath(globalRoot, name)
14
- }
15
-
16
- return {
17
- getPath,
18
-
19
- async write(preset: Preset): Promise<StoredPreset> {
20
- const parsed = presetSchema.parse(preset)
21
- const filePath = getPath(parsed.name)
22
- await atomicWriteFile(filePath, `${JSON.stringify(parsed, null, 2)}\n`)
23
- return { ...parsed, filePath }
24
- },
25
-
26
- async read(name: string): Promise<StoredPreset> {
27
- const filePath = getPath(name)
28
-
29
- try {
30
- const content = await readFile(filePath, 'utf8')
31
- const preset = presetSchema.parse(JSON.parse(content))
32
- return { ...preset, filePath }
33
- } catch (error) {
34
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
35
- throw new CliError(`Preset not found: ${name}`)
36
- }
37
-
38
- throw error
39
- }
40
- },
41
-
42
- async listNames(): Promise<string[]> {
43
- const dirPath = dirname(getPath('placeholder'))
44
-
45
- try {
46
- const entries = await readdir(dirPath, { withFileTypes: true })
47
- return entries
48
- .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
49
- .map((entry) => entry.name.slice(0, -'.json'.length))
50
- .sort()
51
- } catch (error) {
52
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
53
- return []
54
- }
55
-
56
- throw error
57
- }
58
- },
59
-
60
- async remove(name: string): Promise<void> {
61
- const filePath = getPath(name)
62
-
63
- try {
64
- await rm(filePath)
65
- } catch (error) {
66
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
67
- throw error
68
- }
69
- }
70
- },
71
- }
72
- }