simba-skills 0.1.2 → 0.3.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.
- package/README.md +4 -0
- package/package.json +4 -3
- package/src/commands/adopt.ts +35 -3
- package/src/commands/doctor.ts +12 -1
- package/src/commands/install.ts +25 -8
- package/src/commands/update.ts +60 -8
- package/src/core/config-store.ts +1 -1
- package/src/index.ts +6 -1
- package/src/tui/matrix.ts +34 -14
- package/src/utils/symlinks.ts +32 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Simba
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/simba.jpg" alt="Simba the cat" width="600">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
[](https://www.npmjs.com/package/simba-skills)
|
|
4
8
|
[](LICENSE)
|
|
5
9
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simba-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "AI skills manager - central store with symlink-based distribution across 14+ coding agents",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
"access": "public"
|
|
6
|
+
"access": "public",
|
|
7
|
+
"provenance": true
|
|
7
8
|
},
|
|
8
9
|
"type": "module",
|
|
9
10
|
"license": "MIT",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"src",
|
|
33
|
-
"!
|
|
34
|
+
"!assets"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
37
|
"dev": "bun run src/index.ts",
|
package/src/commands/adopt.ts
CHANGED
|
@@ -70,9 +70,12 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
const toAdopt: Array<{ name: string; skill: DiscoveredSkill }> = []
|
|
73
|
+
const toTakeover: Array<{ name: string; skills: DiscoveredSkill[] }> = []
|
|
74
|
+
|
|
73
75
|
for (const [name, skills] of byName) {
|
|
74
76
|
if (await skillsStore.hasSkill(name)) {
|
|
75
|
-
|
|
77
|
+
// Already in store - take over rogue copies
|
|
78
|
+
toTakeover.push({ name, skills })
|
|
76
79
|
continue
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -86,12 +89,14 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
if (toAdopt.length === 0) {
|
|
92
|
+
if (toAdopt.length === 0 && toTakeover.length === 0) {
|
|
90
93
|
console.log("\nNo new skills to adopt.")
|
|
91
94
|
return
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
if (toAdopt.length > 0) {
|
|
98
|
+
console.log(`\nAdopting ${toAdopt.length} skills...`)
|
|
99
|
+
}
|
|
95
100
|
|
|
96
101
|
if (options.dryRun) {
|
|
97
102
|
for (const { name, skill } of toAdopt) {
|
|
@@ -117,6 +122,33 @@ export async function runAdopt(options: AdoptOptions): Promise<void> {
|
|
|
117
122
|
console.log(` Adopted: ${name} (from ${skill.agentId})`)
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
// Take over rogue copies (skill already in store, but real dirs exist at agents)
|
|
126
|
+
if (toTakeover.length > 0) {
|
|
127
|
+
console.log(`\nTaking over ${toTakeover.length} rogue skills...`)
|
|
128
|
+
|
|
129
|
+
if (!options.dryRun) {
|
|
130
|
+
for (const { name, skills } of toTakeover) {
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
await rm(skill.path, { recursive: true })
|
|
133
|
+
await createSymlink(join(options.skillsDir, name), skill.path)
|
|
134
|
+
|
|
135
|
+
// Update registry assignment
|
|
136
|
+
if (!registry.skills[name].assignments[skill.agentId]) {
|
|
137
|
+
registry.skills[name].assignments[skill.agentId] = { type: "directory" }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(` Replaced: ${name} (${skill.agentId})`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
for (const { name, skills } of toTakeover) {
|
|
145
|
+
for (const skill of skills) {
|
|
146
|
+
console.log(` Would replace: ${name} (${skill.agentId})`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
120
152
|
await registryStore.save(registry)
|
|
121
153
|
console.log("\nAdoption complete!")
|
|
122
154
|
}
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from "citty"
|
|
2
|
-
import { access } from "node:fs/promises"
|
|
2
|
+
import { access, rm } from "node:fs/promises"
|
|
3
3
|
import { join } from "node:path"
|
|
4
4
|
import * as p from "@clack/prompts"
|
|
5
5
|
import { ConfigStore } from "../core/config-store"
|
|
@@ -164,6 +164,17 @@ export default defineCommand({
|
|
|
164
164
|
console.log(`Fixed: ${broken.skill} (${broken.agent})`)
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
for (const rogue of results.rogue) {
|
|
168
|
+
const agent = detected[rogue.agent]
|
|
169
|
+
if (!agent) continue
|
|
170
|
+
|
|
171
|
+
// Delete rogue directory/file and replace with symlink
|
|
172
|
+
await rm(rogue.path, { recursive: true })
|
|
173
|
+
const expectedTarget = join(getSkillsDir(), rogue.skill)
|
|
174
|
+
await createSymlink(expectedTarget, rogue.path)
|
|
175
|
+
console.log(`Fixed rogue: ${rogue.skill} (${rogue.agent})`)
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
console.log("\nRepairs complete!")
|
|
168
179
|
},
|
|
169
180
|
})
|
package/src/commands/install.ts
CHANGED
|
@@ -281,6 +281,7 @@ export interface InstallOptions {
|
|
|
281
281
|
skillsDir: string
|
|
282
282
|
registryPath: string
|
|
283
283
|
useSSH: boolean
|
|
284
|
+
skillName?: string // Install specific skill by name, skip selection
|
|
284
285
|
onSelect: (skills: DiscoveredSkill[]) => Promise<string[]>
|
|
285
286
|
}
|
|
286
287
|
|
|
@@ -346,16 +347,30 @@ export async function runInstall(options: InstallOptions): Promise<void> {
|
|
|
346
347
|
return
|
|
347
348
|
}
|
|
348
349
|
|
|
349
|
-
|
|
350
|
-
for (const skill of discovered) {
|
|
351
|
-
console.log(` * ${skill.name}${skill.description ? ` - ${skill.description}` : ""}`)
|
|
352
|
-
}
|
|
350
|
+
let selected: string[]
|
|
353
351
|
|
|
354
|
-
|
|
352
|
+
if (options.skillName) {
|
|
353
|
+
// Direct install of specific skill
|
|
354
|
+
const skill = discovered.find(s => s.name === options.skillName)
|
|
355
|
+
if (!skill) {
|
|
356
|
+
console.log(`Skill "${options.skillName}" not found in source.`)
|
|
357
|
+
console.log(`Available skills: ${discovered.map(s => s.name).join(", ")}`)
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
selected = [options.skillName]
|
|
361
|
+
console.log(`Installing skill: ${options.skillName}`)
|
|
362
|
+
} else {
|
|
363
|
+
console.log(`\nFound ${discovered.length} skills:`)
|
|
364
|
+
for (const skill of discovered) {
|
|
365
|
+
console.log(` * ${skill.name}${skill.description ? ` - ${skill.description}` : ""}`)
|
|
366
|
+
}
|
|
355
367
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
368
|
+
selected = await options.onSelect(discovered)
|
|
369
|
+
|
|
370
|
+
if (selected.length === 0) {
|
|
371
|
+
console.log("No skills selected.")
|
|
372
|
+
return
|
|
373
|
+
}
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
for (const name of selected) {
|
|
@@ -458,6 +473,7 @@ export default defineCommand({
|
|
|
458
473
|
args: {
|
|
459
474
|
source: { type: "positional", description: "GitHub repo (user/repo) or local path", required: true },
|
|
460
475
|
ssh: { type: "boolean", description: "Use SSH for GitHub repos (for private repos)", default: false },
|
|
476
|
+
skill: { type: "string", description: "Install specific skill by name (skip selection)", required: false },
|
|
461
477
|
},
|
|
462
478
|
async run({ args }) {
|
|
463
479
|
await runInstall({
|
|
@@ -465,6 +481,7 @@ export default defineCommand({
|
|
|
465
481
|
skillsDir: getSkillsDir(),
|
|
466
482
|
registryPath: getRegistryPath(),
|
|
467
483
|
useSSH: args.ssh,
|
|
484
|
+
skillName: args.skill,
|
|
468
485
|
onSelect: async (skills) => {
|
|
469
486
|
const result = await p.multiselect({
|
|
470
487
|
message: "Select skills to install:",
|
package/src/commands/update.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from "citty"
|
|
2
|
-
import { readFile, mkdir, rm } from "node:fs/promises"
|
|
3
|
-
import { join } from "node:path"
|
|
2
|
+
import { readFile, readdir, mkdir, rm } from "node:fs/promises"
|
|
3
|
+
import { join, relative } from "node:path"
|
|
4
4
|
import * as p from "@clack/prompts"
|
|
5
5
|
import simpleGit from "simple-git"
|
|
6
6
|
import { tmpdir } from "node:os"
|
|
7
|
+
import { createHash } from "node:crypto"
|
|
7
8
|
import { RegistryStore } from "../core/registry-store"
|
|
8
9
|
import { SkillsStore } from "../core/skills-store"
|
|
9
10
|
import { getSkillsDir, getRegistryPath } from "../utils/paths"
|
|
@@ -12,6 +13,50 @@ import { discoverSkills } from "./install"
|
|
|
12
13
|
import type { ManagedSkill, InstallSource } from "../core/types"
|
|
13
14
|
import matter from "gray-matter"
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Recursively get all files in a directory (sorted for deterministic hashing)
|
|
18
|
+
*/
|
|
19
|
+
async function getAllFiles(dir: string): Promise<string[]> {
|
|
20
|
+
const files: string[] = []
|
|
21
|
+
|
|
22
|
+
async function scan(currentDir: string): Promise<void> {
|
|
23
|
+
const entries = await readdir(currentDir, { withFileTypes: true })
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = join(currentDir, entry.name)
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
await scan(fullPath)
|
|
28
|
+
} else {
|
|
29
|
+
files.push(relative(dir, fullPath))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await scan(dir)
|
|
35
|
+
return files.sort()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compute a content hash for an entire skill directory.
|
|
40
|
+
* Hash includes: sorted file paths + their contents
|
|
41
|
+
*/
|
|
42
|
+
async function hashSkillDir(dir: string): Promise<string> {
|
|
43
|
+
const files = await getAllFiles(dir)
|
|
44
|
+
const hash = createHash("sha256")
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
// Include file path in hash (detects renames/additions/deletions)
|
|
48
|
+
hash.update(file)
|
|
49
|
+
hash.update("\0")
|
|
50
|
+
|
|
51
|
+
// Include file content
|
|
52
|
+
const content = await readFile(join(dir, file))
|
|
53
|
+
hash.update(content)
|
|
54
|
+
hash.update("\0")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return hash.digest("hex")
|
|
58
|
+
}
|
|
59
|
+
|
|
15
60
|
interface SkillUpdate {
|
|
16
61
|
skill: ManagedSkill
|
|
17
62
|
newPath: string
|
|
@@ -119,16 +164,23 @@ export async function runUpdate(options: UpdateOptions): Promise<void> {
|
|
|
119
164
|
continue
|
|
120
165
|
}
|
|
121
166
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
const localDir = join(options.skillsDir, skill.name)
|
|
168
|
+
|
|
169
|
+
// Hash entire directories to detect any file changes
|
|
170
|
+
const [localHash, remoteHash] = await Promise.all([
|
|
171
|
+
hashSkillDir(localDir),
|
|
172
|
+
hashSkillDir(remote.path)
|
|
173
|
+
])
|
|
127
174
|
|
|
128
|
-
if (
|
|
175
|
+
if (localHash === remoteHash) {
|
|
129
176
|
console.log(` ✓ ${skill.name}: up to date`)
|
|
130
177
|
} else {
|
|
131
178
|
console.log(` ↑ ${skill.name}: update available`)
|
|
179
|
+
|
|
180
|
+
// Still read SKILL.md for version display purposes
|
|
181
|
+
const existingContent = await readFile(join(localDir, "SKILL.md"), "utf-8")
|
|
182
|
+
const newContent = await readFile(join(remote.path, "SKILL.md"), "utf-8")
|
|
183
|
+
|
|
132
184
|
allUpdates.push({
|
|
133
185
|
skill,
|
|
134
186
|
newPath: remote.path,
|
package/src/core/config-store.ts
CHANGED
|
@@ -13,7 +13,7 @@ const AGENT_DEFINITIONS: [string, string, string, string, string][] = [
|
|
|
13
13
|
["windsurf", "Windsurf", "Windsurf", "~/.codeium/windsurf/skills", ".windsurf/skills"],
|
|
14
14
|
["amp", "Amp", "Amp", "~/.config/agents/skills", ".agents/skills"],
|
|
15
15
|
["goose", "Goose", "Goose", "~/.config/goose/skills", ".goose/skills"],
|
|
16
|
-
["opencode", "OpenCode", "OpenCode", "~/.config/opencode/
|
|
16
|
+
["opencode", "OpenCode", "OpenCode", "~/.config/opencode/skills", ".opencode/skills"],
|
|
17
17
|
["kilo", "Kilo Code", "Kilo", "~/.kilocode/skills", ".kilocode/skills"],
|
|
18
18
|
["roo", "Roo Code", "Roo", "~/.roo/skills", ".roo/skills"],
|
|
19
19
|
["antigravity", "Antigravity", "Antigrav", "~/.gemini/antigravity/skills", ".agent/skills"],
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { defineCommand, runMain } from "citty"
|
|
3
|
+
import { defineCommand, runMain, showUsage } from "citty"
|
|
4
4
|
|
|
5
5
|
const main = defineCommand({
|
|
6
6
|
meta: {
|
|
@@ -8,6 +8,11 @@ const main = defineCommand({
|
|
|
8
8
|
version: "0.2.0",
|
|
9
9
|
description: "AI skills manager",
|
|
10
10
|
},
|
|
11
|
+
async run({ cmd, rawArgs }) {
|
|
12
|
+
if (rawArgs.length === 0) {
|
|
13
|
+
await showUsage(cmd)
|
|
14
|
+
}
|
|
15
|
+
},
|
|
11
16
|
subCommands: {
|
|
12
17
|
adopt: () => import("./commands/adopt").then((m) => m.default),
|
|
13
18
|
assign: () => import("./commands/assign").then((m) => m.default),
|
package/src/tui/matrix.ts
CHANGED
|
@@ -6,8 +6,7 @@ import { AgentRegistry } from "../core/agent-registry"
|
|
|
6
6
|
import { getRegistryPath, getSkillsDir, getConfigPath, expandPath } from "../utils/paths"
|
|
7
7
|
import type { Agent, Registry } from "../core/types"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const term = termkit.terminal as typeof termkit.terminal & { showCursor: () => void }
|
|
9
|
+
const term = termkit.terminal
|
|
11
10
|
|
|
12
11
|
interface MatrixState {
|
|
13
12
|
skills: string[]
|
|
@@ -102,7 +101,7 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
102
101
|
|
|
103
102
|
term("\n")
|
|
104
103
|
term("─".repeat(21 + state.agents.length * 10) + "\n")
|
|
105
|
-
term.dim("[Space] Toggle [a] Assign all [n] None [q]
|
|
104
|
+
term.dim("[Space] Toggle [a] Assign all [n] None [Enter] Confirm [q] Cancel\n")
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
const toggle = async () => {
|
|
@@ -110,29 +109,43 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
110
109
|
const agent = state.agents[state.cursorCol]
|
|
111
110
|
const skill = state.registry.skills[skillName]
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
try {
|
|
113
|
+
if (skill.assignments[agent.id]) {
|
|
114
|
+
await skillsStore.unassignSkill(skillName, expandPath(agent.globalPath))
|
|
115
|
+
delete skill.assignments[agent.id]
|
|
116
|
+
} else {
|
|
117
|
+
await skillsStore.assignSkill(skillName, expandPath(agent.globalPath), { type: "directory" })
|
|
118
|
+
skill.assignments[agent.id] = { type: "directory" }
|
|
119
|
+
}
|
|
120
|
+
await registryStore.save(state.registry)
|
|
121
|
+
} catch (err) {
|
|
122
|
+
term.moveTo(1, state.skills.length + 8)
|
|
123
|
+
term.yellow(`Error: ${(err as Error).message}\n`)
|
|
119
124
|
}
|
|
120
|
-
|
|
121
|
-
await registryStore.save(state.registry)
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
const assignAll = async () => {
|
|
125
128
|
const skillName = state.skills[state.cursorRow]
|
|
126
129
|
const skill = state.registry.skills[skillName]
|
|
130
|
+
const errors: string[] = []
|
|
127
131
|
|
|
128
132
|
for (const agent of state.agents) {
|
|
129
133
|
if (!skill.assignments[agent.id]) {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
try {
|
|
135
|
+
await skillsStore.assignSkill(skillName, expandPath(agent.globalPath), { type: "directory" })
|
|
136
|
+
skill.assignments[agent.id] = { type: "directory" }
|
|
137
|
+
} catch (err) {
|
|
138
|
+
errors.push(`${agent.name}: ${(err as Error).message}`)
|
|
139
|
+
}
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
await registryStore.save(state.registry)
|
|
144
|
+
|
|
145
|
+
if (errors.length > 0) {
|
|
146
|
+
term.moveTo(1, state.skills.length + 8)
|
|
147
|
+
term.yellow(`Errors:\n${errors.join("\n")}\n`)
|
|
148
|
+
}
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
const unassignAll = async () => {
|
|
@@ -176,11 +189,18 @@ export async function runMatrixTUI(): Promise<void> {
|
|
|
176
189
|
case "n":
|
|
177
190
|
await unassignAll()
|
|
178
191
|
break
|
|
192
|
+
case "ENTER":
|
|
193
|
+
term.clear()
|
|
194
|
+
term.hideCursor(false)
|
|
195
|
+
term.grabInput(false)
|
|
196
|
+
term.green("Changes saved.\n")
|
|
197
|
+
process.exit(0)
|
|
179
198
|
case "q":
|
|
180
199
|
case "CTRL_C":
|
|
181
200
|
term.clear()
|
|
182
|
-
term.
|
|
201
|
+
term.hideCursor(false)
|
|
183
202
|
term.grabInput(false)
|
|
203
|
+
term.yellow("Cancelled.\n")
|
|
184
204
|
process.exit(0)
|
|
185
205
|
}
|
|
186
206
|
|
package/src/utils/symlinks.ts
CHANGED
|
@@ -3,7 +3,29 @@ import { dirname } from "node:path"
|
|
|
3
3
|
|
|
4
4
|
export async function createSymlink(source: string, target: string): Promise<void> {
|
|
5
5
|
await mkdir(dirname(target), { recursive: true })
|
|
6
|
-
|
|
6
|
+
try {
|
|
7
|
+
await symlink(source, target)
|
|
8
|
+
} catch (err) {
|
|
9
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
|
|
10
|
+
// Already exists - check if it's a symlink pointing to the right place
|
|
11
|
+
const existing = await getSymlinkTarget(target)
|
|
12
|
+
if (existing === source) return // Already correct symlink
|
|
13
|
+
|
|
14
|
+
// Check what we're dealing with
|
|
15
|
+
const stat = await lstat(target)
|
|
16
|
+
if (stat.isSymbolicLink()) {
|
|
17
|
+
await unlink(target)
|
|
18
|
+
} else if (stat.isDirectory()) {
|
|
19
|
+
// Real directory - don't delete, throw error
|
|
20
|
+
throw new Error(`Cannot create symlink: ${target} is a directory (not managed by simba)`)
|
|
21
|
+
} else {
|
|
22
|
+
await unlink(target)
|
|
23
|
+
}
|
|
24
|
+
await symlink(source, target)
|
|
25
|
+
} else {
|
|
26
|
+
throw err
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
}
|
|
8
30
|
|
|
9
31
|
export async function isSymlink(path: string): Promise<boolean> {
|
|
@@ -17,7 +39,15 @@ export async function isSymlink(path: string): Promise<boolean> {
|
|
|
17
39
|
|
|
18
40
|
export async function removeSymlink(path: string): Promise<void> {
|
|
19
41
|
try {
|
|
20
|
-
await
|
|
42
|
+
const stat = await lstat(path)
|
|
43
|
+
if (stat.isSymbolicLink()) {
|
|
44
|
+
await unlink(path)
|
|
45
|
+
} else if (stat.isDirectory()) {
|
|
46
|
+
// Real directory - don't delete, it's not managed by simba
|
|
47
|
+
throw new Error(`Cannot remove: ${path} is a directory (not managed by simba)`)
|
|
48
|
+
} else {
|
|
49
|
+
await unlink(path)
|
|
50
|
+
}
|
|
21
51
|
} catch (err) {
|
|
22
52
|
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
23
53
|
throw err
|