skillfish 1.0.8 → 1.0.9
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/LICENSE +657 -13
- package/README.md +29 -7
- package/dist/commands/add.js +21 -11
- package/dist/commands/list.js +20 -4
- package/dist/commands/remove.js +6 -5
- package/dist/commands/update.d.ts +5 -0
- package/dist/commands/update.js +327 -0
- package/dist/index.js +2 -0
- package/dist/lib/agents.d.ts +4 -4
- package/dist/lib/github.d.ts +13 -1
- package/dist/lib/github.js +39 -2
- package/dist/lib/installer.d.ts +6 -4
- package/dist/lib/installer.js +15 -2
- package/dist/lib/manifest.d.ts +45 -0
- package/dist/lib/manifest.js +100 -0
- package/dist/utils.d.ts +25 -6
- package/dist/utils.js +4 -6
- package/package.json +32 -3
package/dist/lib/github.js
CHANGED
|
@@ -107,7 +107,7 @@ export async function fetchDefaultBranch(owner, repo) {
|
|
|
107
107
|
if (!res.ok) {
|
|
108
108
|
throw new GitHubApiError(`GitHub API returned status ${res.status}`);
|
|
109
109
|
}
|
|
110
|
-
const data = await res.json();
|
|
110
|
+
const data = (await res.json());
|
|
111
111
|
if (typeof data.default_branch !== 'string' || !data.default_branch) {
|
|
112
112
|
throw new GitHubApiError('Repository metadata missing or invalid default_branch field');
|
|
113
113
|
}
|
|
@@ -171,6 +171,42 @@ export async function fetchSkillMdContent(owner, repo, path, branch) {
|
|
|
171
171
|
return null;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Fetch the tree SHA for a repository branch.
|
|
176
|
+
* Used for update checks - compares stored SHA with current SHA.
|
|
177
|
+
*
|
|
178
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
179
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
180
|
+
* @throws {NetworkError} On network errors
|
|
181
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
182
|
+
*/
|
|
183
|
+
export async function fetchTreeSha(owner, repo, branch) {
|
|
184
|
+
const headers = { 'User-Agent': 'skillfish' };
|
|
185
|
+
const controller = new AbortController();
|
|
186
|
+
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
187
|
+
try {
|
|
188
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}`;
|
|
189
|
+
const res = await fetchWithRetry(url, { headers, signal: controller.signal });
|
|
190
|
+
checkRateLimit(res);
|
|
191
|
+
if (res.status === 404) {
|
|
192
|
+
throw new RepoNotFoundError(owner, repo);
|
|
193
|
+
}
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
throw new GitHubApiError(`GitHub API returned status ${res.status}`);
|
|
196
|
+
}
|
|
197
|
+
const data = (await res.json());
|
|
198
|
+
if (typeof data.sha !== 'string' || !/^[a-f0-9]{40}$/.test(data.sha)) {
|
|
199
|
+
throw new GitHubApiError('Invalid or missing sha field in tree response');
|
|
200
|
+
}
|
|
201
|
+
return data.sha;
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
wrapApiError(err);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
clearTimeout(timeoutId);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
174
210
|
/**
|
|
175
211
|
* Find all SKILL.md files in a GitHub repository.
|
|
176
212
|
* Fetches the default branch, then searches for skills on that branch.
|
|
@@ -199,7 +235,8 @@ export async function findAllSkillMdFiles(owner, repo) {
|
|
|
199
235
|
throw new GitHubApiError('Unexpected response format from GitHub API.');
|
|
200
236
|
}
|
|
201
237
|
const paths = extractSkillPaths(rawData, SKILL_FILENAME);
|
|
202
|
-
|
|
238
|
+
const sha = rawData.sha ?? '';
|
|
239
|
+
return { paths, branch, sha };
|
|
203
240
|
}
|
|
204
241
|
catch (err) {
|
|
205
242
|
wrapApiError(err);
|
package/dist/lib/installer.d.ts
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Agent } from './agents.js';
|
|
6
6
|
export interface InstallResult {
|
|
7
|
-
installed:
|
|
7
|
+
installed: {
|
|
8
8
|
skill: string;
|
|
9
9
|
agent: string;
|
|
10
10
|
path: string;
|
|
11
|
-
}
|
|
12
|
-
skipped:
|
|
11
|
+
}[];
|
|
12
|
+
skipped: {
|
|
13
13
|
skill: string;
|
|
14
14
|
agent: string;
|
|
15
15
|
reason: string;
|
|
16
|
-
}
|
|
16
|
+
}[];
|
|
17
17
|
warnings: string[];
|
|
18
18
|
failed: boolean;
|
|
19
19
|
failureReason?: string;
|
|
@@ -23,6 +23,8 @@ export interface InstallOptions {
|
|
|
23
23
|
baseDir: string;
|
|
24
24
|
/** Branch to clone from. If not specified, giget will use the repository's default branch. */
|
|
25
25
|
branch?: string;
|
|
26
|
+
/** Tree SHA for manifest tracking. If provided, .skillfish.json will be written. */
|
|
27
|
+
sha?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface CopyResult {
|
|
28
30
|
warnings: string[];
|
package/dist/lib/installer.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Skill installation logic.
|
|
3
3
|
* Handles downloading, validating, and installing skills to agent directories.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, cpSync, rmSync, lstatSync, readdirSync
|
|
5
|
+
import { existsSync, mkdirSync, cpSync, rmSync, lstatSync, readdirSync } from 'fs';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { randomUUID } from 'crypto';
|
|
9
9
|
import { downloadTemplate } from 'giget';
|
|
10
10
|
import { SKILL_FILENAME } from './github.js';
|
|
11
|
+
import { writeManifest, MANIFEST_VERSION } from './manifest.js';
|
|
11
12
|
/**
|
|
12
13
|
* Validates a branch name for safe use in giget source strings.
|
|
13
14
|
* Git branch names can contain alphanumerics, dots, hyphens, underscores, and slashes.
|
|
@@ -98,7 +99,7 @@ export async function installSkill(owner, repo, skillPath, skillName, agents, op
|
|
|
98
99
|
warnings: [],
|
|
99
100
|
failed: false,
|
|
100
101
|
};
|
|
101
|
-
const { force, baseDir, branch } = options;
|
|
102
|
+
const { force, baseDir, branch, sha } = options;
|
|
102
103
|
const tmpDir = join(homedir(), '.cache', 'skillfish', `${owner}-${repo}-${randomUUID()}`);
|
|
103
104
|
mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
|
|
104
105
|
try {
|
|
@@ -144,6 +145,18 @@ export async function installSkill(owner, repo, skillPath, skillName, agents, op
|
|
|
144
145
|
// Use safe copy to skip symlinks (security: prevents symlink attacks)
|
|
145
146
|
const copyResult = safeCopyDir(tmpDir, destDir);
|
|
146
147
|
result.warnings.push(...copyResult.warnings.map((w) => `${skillName}: ${w}`));
|
|
148
|
+
// Write manifest for tracking if SHA is provided
|
|
149
|
+
if (sha && branch) {
|
|
150
|
+
const manifest = {
|
|
151
|
+
version: MANIFEST_VERSION,
|
|
152
|
+
owner,
|
|
153
|
+
repo,
|
|
154
|
+
path: skillPath === SKILL_FILENAME ? '.' : skillPath,
|
|
155
|
+
branch,
|
|
156
|
+
sha,
|
|
157
|
+
};
|
|
158
|
+
writeManifest(destDir, manifest);
|
|
159
|
+
}
|
|
147
160
|
result.installed.push({
|
|
148
161
|
skill: skillName,
|
|
149
162
|
agent: agent.name,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest handling for skill tracking.
|
|
3
|
+
* Each installed skill has a .skillfish.json file that tracks its origin.
|
|
4
|
+
*/
|
|
5
|
+
export declare const MANIFEST_FILENAME = ".skillfish.json";
|
|
6
|
+
export declare const MANIFEST_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Manifest schema for tracking installed skills.
|
|
9
|
+
* Stored in .skillfish.json within each skill directory.
|
|
10
|
+
*/
|
|
11
|
+
export interface SkillManifest {
|
|
12
|
+
/** Schema version for future migrations */
|
|
13
|
+
version: 1;
|
|
14
|
+
/** GitHub repository owner */
|
|
15
|
+
owner: string;
|
|
16
|
+
/** GitHub repository name */
|
|
17
|
+
repo: string;
|
|
18
|
+
/** Path within repo (e.g., "skills/my-skill" or ".") */
|
|
19
|
+
path: string;
|
|
20
|
+
/** Branch at install time */
|
|
21
|
+
branch: string;
|
|
22
|
+
/** Tree SHA at install time (from git/trees response) */
|
|
23
|
+
sha: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read manifest from a skill directory.
|
|
27
|
+
*
|
|
28
|
+
* @param skillDir - Path to the skill directory
|
|
29
|
+
* @returns Parsed manifest or null if not found/invalid
|
|
30
|
+
*/
|
|
31
|
+
export declare function readManifest(skillDir: string): SkillManifest | null;
|
|
32
|
+
/**
|
|
33
|
+
* Write manifest to a skill directory.
|
|
34
|
+
*
|
|
35
|
+
* @param skillDir - Path to the skill directory
|
|
36
|
+
* @param manifest - Manifest data to write
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeManifest(skillDir: string, manifest: SkillManifest): void;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a skill directory has a manifest.
|
|
41
|
+
*
|
|
42
|
+
* @param skillDir - Path to the skill directory
|
|
43
|
+
* @returns true if manifest exists
|
|
44
|
+
*/
|
|
45
|
+
export declare function hasManifest(skillDir: string): boolean;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest handling for skill tracking.
|
|
3
|
+
* Each installed skill has a .skillfish.json file that tracks its origin.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { isValidName } from './constants.js';
|
|
8
|
+
import { isValidPath } from '../utils.js';
|
|
9
|
+
// === Constants ===
|
|
10
|
+
export const MANIFEST_FILENAME = '.skillfish.json';
|
|
11
|
+
export const MANIFEST_VERSION = 1;
|
|
12
|
+
/** Git SHA format: 40 hexadecimal characters */
|
|
13
|
+
const SHA_PATTERN = /^[a-f0-9]{40}$/;
|
|
14
|
+
/** Git branch name pattern (alphanumerics, dots, hyphens, underscores, slashes) */
|
|
15
|
+
const BRANCH_PATTERN = /^[\w./-]+$/;
|
|
16
|
+
// === Functions ===
|
|
17
|
+
/**
|
|
18
|
+
* Read manifest from a skill directory.
|
|
19
|
+
*
|
|
20
|
+
* @param skillDir - Path to the skill directory
|
|
21
|
+
* @returns Parsed manifest or null if not found/invalid
|
|
22
|
+
*/
|
|
23
|
+
export function readManifest(skillDir) {
|
|
24
|
+
const manifestPath = join(skillDir, MANIFEST_FILENAME);
|
|
25
|
+
if (!existsSync(manifestPath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(manifestPath, 'utf-8');
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
// Validate manifest structure
|
|
32
|
+
if (!isValidManifest(data)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// JSON parse error or file read error
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Write manifest to a skill directory.
|
|
44
|
+
*
|
|
45
|
+
* @param skillDir - Path to the skill directory
|
|
46
|
+
* @param manifest - Manifest data to write
|
|
47
|
+
*/
|
|
48
|
+
export function writeManifest(skillDir, manifest) {
|
|
49
|
+
const manifestPath = join(skillDir, MANIFEST_FILENAME);
|
|
50
|
+
const content = JSON.stringify(manifest, null, 2);
|
|
51
|
+
writeFileSync(manifestPath, content, 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a skill directory has a manifest.
|
|
55
|
+
*
|
|
56
|
+
* @param skillDir - Path to the skill directory
|
|
57
|
+
* @returns true if manifest exists
|
|
58
|
+
*/
|
|
59
|
+
export function hasManifest(skillDir) {
|
|
60
|
+
return existsSync(join(skillDir, MANIFEST_FILENAME));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Type guard to validate manifest structure and content.
|
|
64
|
+
* Validates both field types and content to prevent tampered manifests
|
|
65
|
+
* from causing unintended API requests or file operations.
|
|
66
|
+
*/
|
|
67
|
+
function isValidManifest(data) {
|
|
68
|
+
if (typeof data !== 'object' || data === null) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const obj = data;
|
|
72
|
+
// Check types first
|
|
73
|
+
if (obj.version !== MANIFEST_VERSION ||
|
|
74
|
+
typeof obj.owner !== 'string' ||
|
|
75
|
+
typeof obj.repo !== 'string' ||
|
|
76
|
+
typeof obj.path !== 'string' ||
|
|
77
|
+
typeof obj.branch !== 'string' ||
|
|
78
|
+
typeof obj.sha !== 'string') {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// Validate content to prevent tampered manifests
|
|
82
|
+
const { owner, repo, path, branch, sha } = obj;
|
|
83
|
+
// Owner and repo must be valid GitHub names
|
|
84
|
+
if (!isValidName(owner) || !isValidName(repo)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
// Path must be safe (no traversal) - "." is valid for root
|
|
88
|
+
if (path !== '.' && !isValidPath(path)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
// Branch must match git branch pattern
|
|
92
|
+
if (!BRANCH_PATTERN.test(branch) || branch.length > 255) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
// SHA must be valid git SHA format (40 hex chars)
|
|
96
|
+
if (!SHA_PATTERN.test(sha)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -10,23 +10,23 @@ export declare function isValidPath(pathStr: string): boolean;
|
|
|
10
10
|
/**
|
|
11
11
|
* Type for GitHub tree API item.
|
|
12
12
|
*/
|
|
13
|
-
export
|
|
13
|
+
export interface GitTreeItem {
|
|
14
14
|
path: string;
|
|
15
15
|
type: string;
|
|
16
16
|
mode?: string;
|
|
17
17
|
sha?: string;
|
|
18
18
|
size?: number;
|
|
19
19
|
url?: string;
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
/**
|
|
22
22
|
* Type for GitHub tree API response.
|
|
23
23
|
*/
|
|
24
|
-
export
|
|
24
|
+
export interface GitTreeResponse {
|
|
25
25
|
tree?: GitTreeItem[];
|
|
26
26
|
sha?: string;
|
|
27
27
|
url?: string;
|
|
28
28
|
truncated?: boolean;
|
|
29
|
-
}
|
|
29
|
+
}
|
|
30
30
|
/**
|
|
31
31
|
* Type guard for GitHub tree API response.
|
|
32
32
|
* Validates the response structure at runtime.
|
|
@@ -94,11 +94,11 @@ export interface BaseJsonOutput {
|
|
|
94
94
|
*/
|
|
95
95
|
export interface AddJsonOutput extends BaseJsonOutput {
|
|
96
96
|
installed: InstalledSkill[];
|
|
97
|
-
skipped:
|
|
97
|
+
skipped: {
|
|
98
98
|
skill: string;
|
|
99
99
|
agent: string;
|
|
100
100
|
reason: string;
|
|
101
|
-
}
|
|
101
|
+
}[];
|
|
102
102
|
skills_found?: string[];
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
@@ -114,6 +114,25 @@ export interface ListJsonOutput extends BaseJsonOutput {
|
|
|
114
114
|
export interface RemoveJsonOutput extends BaseJsonOutput {
|
|
115
115
|
removed: InstalledSkill[];
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Outdated skill information for update command.
|
|
119
|
+
*/
|
|
120
|
+
export interface OutdatedSkill {
|
|
121
|
+
skill: string;
|
|
122
|
+
agent: string;
|
|
123
|
+
path: string;
|
|
124
|
+
location: 'global' | 'project';
|
|
125
|
+
localSha: string;
|
|
126
|
+
remoteSha: string;
|
|
127
|
+
source: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* JSON output for the `update` command.
|
|
131
|
+
*/
|
|
132
|
+
export interface UpdateJsonOutput extends BaseJsonOutput {
|
|
133
|
+
outdated: OutdatedSkill[];
|
|
134
|
+
updated: InstalledSkill[];
|
|
135
|
+
}
|
|
117
136
|
/** @deprecated Use AddJsonOutput instead */
|
|
118
137
|
export type JsonOutput = AddJsonOutput;
|
|
119
138
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -86,9 +86,7 @@ export function deriveSkillName(skillPath, repoName) {
|
|
|
86
86
|
* "my_cool_skill" → "My Cool Skill"
|
|
87
87
|
*/
|
|
88
88
|
export function toTitleCase(str) {
|
|
89
|
-
return str
|
|
90
|
-
.replace(/[-_]/g, ' ')
|
|
91
|
-
.replace(/\b\w/g, char => char.toUpperCase());
|
|
89
|
+
return str.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase());
|
|
92
90
|
}
|
|
93
91
|
/**
|
|
94
92
|
* Truncate text to a maximum length, adding ellipsis if needed.
|
|
@@ -105,14 +103,14 @@ export function extractSkillPaths(data, skillFilename = 'SKILL.md') {
|
|
|
105
103
|
if (!data.tree)
|
|
106
104
|
return [];
|
|
107
105
|
return data.tree
|
|
108
|
-
.filter(item => item.type === 'blob' && item.path.endsWith(skillFilename))
|
|
109
|
-
.map(item => item.path);
|
|
106
|
+
.filter((item) => item.type === 'blob' && item.path.endsWith(skillFilename))
|
|
107
|
+
.map((item) => item.path);
|
|
110
108
|
}
|
|
111
109
|
/**
|
|
112
110
|
* Sleep for a specified duration.
|
|
113
111
|
*/
|
|
114
112
|
export function sleep(ms) {
|
|
115
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
113
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
114
|
}
|
|
117
115
|
/**
|
|
118
116
|
* Process items with bounded concurrency.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillfish",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "Install AI agent skills from GitHub with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,9 +24,15 @@
|
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|
|
26
26
|
"dev": "tsc --watch",
|
|
27
|
-
"prepublishOnly": "npm run build",
|
|
27
|
+
"prepublishOnly": "npm run lint && npm test && npm run build",
|
|
28
28
|
"test": "vitest run",
|
|
29
|
-
"test:watch": "vitest"
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"lint:fix": "eslint . --fix",
|
|
32
|
+
"format": "prettier --write .",
|
|
33
|
+
"format:check": "prettier --check .",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"prepare": "husky"
|
|
30
36
|
},
|
|
31
37
|
"keywords": [
|
|
32
38
|
"cli",
|
|
@@ -54,6 +60,13 @@
|
|
|
54
60
|
"author": "Graeme Knox",
|
|
55
61
|
"license": "AGPL-3.0",
|
|
56
62
|
"homepage": "https://skill.fish",
|
|
63
|
+
"funding": {
|
|
64
|
+
"type": "github",
|
|
65
|
+
"url": "https://github.com/sponsors/knoxgraeme"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
57
70
|
"dependencies": {
|
|
58
71
|
"@clack/prompts": "^0.11.0",
|
|
59
72
|
"commander": "^14.0.2",
|
|
@@ -61,8 +74,24 @@
|
|
|
61
74
|
"picocolors": "^1.1.1"
|
|
62
75
|
},
|
|
63
76
|
"devDependencies": {
|
|
77
|
+
"@eslint/js": "^9.39.2",
|
|
64
78
|
"@types/node": "^20.0.0",
|
|
79
|
+
"eslint": "^9.39.2",
|
|
80
|
+
"eslint-config-prettier": "^10.1.8",
|
|
81
|
+
"husky": "^9.1.7",
|
|
82
|
+
"lint-staged": "^16.2.7",
|
|
83
|
+
"prettier": "^3.8.1",
|
|
65
84
|
"typescript": "^5.4.0",
|
|
85
|
+
"typescript-eslint": "^8.53.1",
|
|
66
86
|
"vitest": "^4.0.17"
|
|
87
|
+
},
|
|
88
|
+
"lint-staged": {
|
|
89
|
+
"*.ts": [
|
|
90
|
+
"eslint --fix",
|
|
91
|
+
"prettier --write"
|
|
92
|
+
],
|
|
93
|
+
"*.{json,yml,yaml}": [
|
|
94
|
+
"prettier --write"
|
|
95
|
+
]
|
|
67
96
|
}
|
|
68
97
|
}
|