@plaited/development-skills 0.6.3 → 0.6.4
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/.plaited/rules/accuracy.md +27 -53
- package/.plaited/rules/bun.md +29 -0
- package/.plaited/rules/core.md +43 -0
- package/.plaited/rules/documentation.md +11 -29
- package/.plaited/rules/modules.md +27 -0
- package/.plaited/rules/testing.md +21 -191
- package/.plaited/rules/workflow.md +40 -0
- package/.plaited/skills/code-documentation/SKILL.md +47 -0
- package/.plaited/skills/code-documentation/references/internal-templates.md +113 -0
- package/.plaited/skills/code-documentation/references/maintenance.md +164 -0
- package/.plaited/skills/code-documentation/references/public-api-templates.md +100 -0
- package/.plaited/skills/code-documentation/references/type-documentation.md +116 -0
- package/.plaited/skills/code-documentation/references/workflow.md +60 -0
- package/.plaited/skills/optimize-agents-md/SKILL.md +187 -0
- package/.plaited/skills/scaffold-rules/SKILL.md +76 -0
- package/.plaited/skills/typescript-lsp/SKILL.md +249 -0
- package/.plaited/skills/validate-skill/SKILL.md +108 -0
- package/package.json +1 -1
- package/src/scaffold-rules.ts +95 -318
- package/src/tests/scaffold-rules.spec.ts +178 -311
- package/.plaited/rules/bun-apis.md +0 -80
- package/.plaited/rules/code-review.md +0 -239
- package/.plaited/rules/git-workflow.md +0 -31
- package/.plaited/rules/github.md +0 -154
- package/.plaited/rules/module-organization.md +0 -85
package/src/scaffold-rules.ts
CHANGED
|
@@ -1,269 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* Scaffold development rules
|
|
3
|
+
* Scaffold development rules - Copy bundled rules and create symlinks
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Copies rules from the package to `.plaited/rules/` (canonical location),
|
|
6
|
+
* creates symlinks for `.claude/` and `.cursor/` agent directories,
|
|
7
|
+
* and falls back to appending links in `AGENTS.md` if no agent dirs exist.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Options:
|
|
13
|
-
* - --rules-dir, -d: Custom rules directory path (overrides default .plaited/rules)
|
|
14
|
-
* - --rules, -r: Filter to specific rules (can be used multiple times)
|
|
15
|
-
* - --list, -l: List available rules without full output
|
|
16
|
-
*
|
|
17
|
-
* Output includes:
|
|
18
|
-
* - agentsMdSection: Marker-wrapped section with markdown links for AGENTS.md
|
|
19
|
-
* - claudeMdReference: Short reference snippet pointing to @AGENTS.md
|
|
20
|
-
* - templates: Processed rule content for each selected rule
|
|
21
|
-
*
|
|
22
|
-
* Template syntax:
|
|
23
|
-
* - {{LINK:rule-id}} - Cross-reference to another rule
|
|
24
|
-
* - {{#if development-skills}}...{{/if}} - Conditional block (always true when using CLI)
|
|
25
|
-
* - {{^if development-skills}}...{{/if}} - Inverse conditional
|
|
26
|
-
* - <!-- RULE TEMPLATE ... --> - Template header (removed)
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```bash
|
|
30
|
-
* # Default: outputs to .plaited/rules/
|
|
31
|
-
* bunx @plaited/development-skills scaffold-rules
|
|
32
|
-
*
|
|
33
|
-
* # Custom rules directory
|
|
34
|
-
* bunx @plaited/development-skills scaffold-rules --rules-dir=.cursor/rules
|
|
35
|
-
*
|
|
36
|
-
* # Filter specific rules
|
|
37
|
-
* bunx @plaited/development-skills scaffold-rules --rules testing --rules bun-apis
|
|
38
|
-
*
|
|
39
|
-
* # List available rules
|
|
40
|
-
* bunx @plaited/development-skills scaffold-rules --list
|
|
41
|
-
* ```
|
|
9
|
+
* @throws When source rules directory cannot be read
|
|
10
|
+
* @throws When target directory cannot be created (permissions)
|
|
11
|
+
* @throws When symlink creation fails (existing file, not directory)
|
|
42
12
|
*/
|
|
43
13
|
|
|
44
|
-
import { readdir } from 'node:fs/promises'
|
|
14
|
+
import { mkdir, readdir, readlink, stat, symlink } from 'node:fs/promises'
|
|
45
15
|
import { join } from 'node:path'
|
|
46
16
|
import { parseArgs } from 'node:util'
|
|
47
17
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
* @remarks
|
|
52
|
-
* These markers allow scaffold-rules to update CLAUDE.md and AGENTS.md
|
|
53
|
-
* without destroying user content outside the marked section.
|
|
54
|
-
*
|
|
55
|
-
* Use these markers to implement idempotent updates:
|
|
56
|
-
* 1. Find existing markers in file content
|
|
57
|
-
* 2. Replace content between markers (inclusive) with new section
|
|
58
|
-
* 3. If no markers exist, append section to end of file
|
|
59
|
-
*
|
|
60
|
-
* @property start - Opening marker to place before rules section
|
|
61
|
-
* @property end - Closing marker to place after rules section
|
|
62
|
-
*
|
|
63
|
-
* @public
|
|
64
|
-
*/
|
|
65
|
-
export const MARKERS = {
|
|
66
|
-
start: '<!-- PLAITED-RULES-START -->',
|
|
67
|
-
end: '<!-- PLAITED-RULES-END -->',
|
|
68
|
-
} as const
|
|
18
|
+
/** Agents that get symlinks to .plaited/rules (not .plaited itself) */
|
|
19
|
+
const SYMLINK_AGENTS = ['.claude', '.cursor'] as const
|
|
69
20
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @remarks
|
|
74
|
-
* All agents use `.plaited/rules/` as the default location.
|
|
75
|
-
* This provides consistency and allows both CLAUDE.md and AGENTS.md
|
|
76
|
-
* to reference the same rule files.
|
|
77
|
-
*/
|
|
78
|
-
const UNIFIED_RULES_PATH = '.plaited/rules' as const
|
|
21
|
+
/** All supported agent directories (including .plaited which gets direct copy) */
|
|
22
|
+
const ALL_AGENTS = ['.plaited', ...SYMLINK_AGENTS] as const
|
|
79
23
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
24
|
+
/** Canonical rules location */
|
|
25
|
+
const TARGET_RULES = '.plaited/rules' as const
|
|
83
26
|
|
|
84
27
|
/**
|
|
85
|
-
*
|
|
28
|
+
* NOTE: This tool only scaffolds RULES, not skills.
|
|
29
|
+
* Skills symlinks (.claude/skills -> ../.plaited/skills) are managed separately
|
|
30
|
+
* via the skills-installer or manual setup.
|
|
86
31
|
*/
|
|
87
|
-
export type ProcessedTemplate = {
|
|
88
|
-
filename: string
|
|
89
|
-
content: string
|
|
90
|
-
description: string
|
|
91
|
-
}
|
|
92
32
|
|
|
93
33
|
/**
|
|
94
|
-
*
|
|
95
|
-
*/
|
|
96
|
-
export type ScaffoldOutput = {
|
|
97
|
-
rulesPath: string
|
|
98
|
-
/** Marker-wrapped section with markdown links for AGENTS.md */
|
|
99
|
-
agentsMdSection: string
|
|
100
|
-
/** Short reference snippet for CLAUDE.md pointing to AGENTS.md */
|
|
101
|
-
claudeMdReference: string
|
|
102
|
-
templates: Record<string, ProcessedTemplate>
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Process template conditionals
|
|
107
|
-
*
|
|
108
|
-
* Handles:
|
|
109
|
-
* - {{#if development-skills}}...{{/if}} - Always true (using our CLI means dev-skills is installed)
|
|
110
|
-
* - {{^if development-skills}}...{{/if}} - Always false
|
|
111
|
-
*
|
|
112
|
-
* Processes iteratively to handle nested conditionals correctly.
|
|
34
|
+
* Check if path is a directory
|
|
113
35
|
*/
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// Process iteratively until no more changes (handles nested conditionals)
|
|
121
|
-
while (result !== previousResult && iterations < maxIterations) {
|
|
122
|
-
previousResult = result
|
|
123
|
-
iterations++
|
|
124
|
-
|
|
125
|
-
// Process positive conditionals {{#if development-skills}}...{{/if}}
|
|
126
|
-
// Always true - include the block content
|
|
127
|
-
result = result.replace(
|
|
128
|
-
/\{\{#if development-skills\}\}((?:(?!\{\{#if )(?!\{\{\^if )(?!\{\{\/if\}\})[\s\S])*?)\{\{\/if\}\}/g,
|
|
129
|
-
(_, block) => block,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
// Process inverse conditionals {{^if development-skills}}...{{/if}}
|
|
133
|
-
// Always false - remove the block content
|
|
134
|
-
result = result.replace(
|
|
135
|
-
/\{\{\^if development-skills\}\}((?:(?!\{\{#if )(?!\{\{\^if )(?!\{\{\/if\}\})[\s\S])*?)\{\{\/if\}\}/g,
|
|
136
|
-
() => '',
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (iterations >= maxIterations) {
|
|
141
|
-
console.warn('Warning: Max iterations reached in template processing. Some conditionals may be unprocessed.')
|
|
36
|
+
const isDirectory = async (path: string): Promise<boolean> => {
|
|
37
|
+
try {
|
|
38
|
+
const s = await stat(path)
|
|
39
|
+
return s.isDirectory()
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
142
42
|
}
|
|
143
|
-
|
|
144
|
-
return result
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Process template variables
|
|
149
|
-
*
|
|
150
|
-
* Handles:
|
|
151
|
-
* - {{LINK:rule-id}} - Generate cross-reference path
|
|
152
|
-
* - {{RULES_PATH}} - Rules path
|
|
153
|
-
*/
|
|
154
|
-
const processVariables = (content: string, context: TemplateContext): string => {
|
|
155
|
-
let result = content
|
|
156
|
-
|
|
157
|
-
// Replace {{LINK:rule-id}} with path reference
|
|
158
|
-
result = result.replace(/\{\{LINK:(\w+)\}\}/g, (_, ruleId) => {
|
|
159
|
-
return `${context.rulesPath}/${ruleId}.md`
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// Replace {{RULES_PATH}}
|
|
163
|
-
result = result.replace(/\{\{RULES_PATH\}\}/g, context.rulesPath)
|
|
164
|
-
|
|
165
|
-
return result
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Remove template headers
|
|
170
|
-
*/
|
|
171
|
-
const removeTemplateHeaders = (content: string): string => {
|
|
172
|
-
return content.replace(/<!--[\s\S]*?-->\n*/g, '')
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Extract description from rule content
|
|
177
|
-
*/
|
|
178
|
-
const extractDescription = (content: string): string => {
|
|
179
|
-
// Look for first paragraph or heading after main title
|
|
180
|
-
const lines = content.split('\n')
|
|
181
|
-
let description = ''
|
|
182
|
-
|
|
183
|
-
for (let i = 1; i < lines.length; i++) {
|
|
184
|
-
// Non-null assertion safe: loop condition guarantees i < lines.length
|
|
185
|
-
const line = lines[i]!.trim()
|
|
186
|
-
if (line && !line.startsWith('#') && !line.startsWith('**')) {
|
|
187
|
-
description = line
|
|
188
|
-
break
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return description || 'Development rule'
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Process a template with context
|
|
197
|
-
*/
|
|
198
|
-
const processTemplate = (content: string, context: TemplateContext): string => {
|
|
199
|
-
let result = content
|
|
200
|
-
|
|
201
|
-
// 1. Remove template headers
|
|
202
|
-
result = removeTemplateHeaders(result)
|
|
203
|
-
|
|
204
|
-
// 2. Process conditionals
|
|
205
|
-
result = processConditionals(result)
|
|
206
|
-
|
|
207
|
-
// 3. Process variables
|
|
208
|
-
result = processVariables(result, context)
|
|
209
|
-
|
|
210
|
-
// 4. Clean up extra blank lines
|
|
211
|
-
result = result.replace(/\n{3,}/g, '\n\n')
|
|
212
|
-
|
|
213
|
-
return result
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Generate marker-wrapped reference snippet for CLAUDE.md
|
|
218
|
-
*
|
|
219
|
-
* @remarks
|
|
220
|
-
* Claude Code uses `@file.md` syntax to include file contents.
|
|
221
|
-
* This generates a short reference pointing to AGENTS.md as the single source of truth.
|
|
222
|
-
* The markers allow this section to be updated without affecting other content.
|
|
223
|
-
*/
|
|
224
|
-
const generateClaudeMdReference = (): string => {
|
|
225
|
-
const lines = [
|
|
226
|
-
MARKERS.start,
|
|
227
|
-
'',
|
|
228
|
-
'## Project Rules',
|
|
229
|
-
'',
|
|
230
|
-
'See @AGENTS.md for shared development rules.',
|
|
231
|
-
'',
|
|
232
|
-
MARKERS.end,
|
|
233
|
-
]
|
|
234
|
-
|
|
235
|
-
return lines.join('\n')
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Generate marker-wrapped section for AGENTS.md with dual format
|
|
240
|
-
*
|
|
241
|
-
* @remarks
|
|
242
|
-
* AGENTS.md uses both formats for maximum compatibility:
|
|
243
|
-
* - `@path` syntax for Claude Code to load file contents
|
|
244
|
-
* - `[name](path)` markdown links for other tools and GitHub rendering
|
|
245
|
-
* The markers allow this section to be updated without affecting other content.
|
|
246
|
-
*/
|
|
247
|
-
const generateAgentsMdSection = (templates: Record<string, ProcessedTemplate>, rulesPath: string): string => {
|
|
248
|
-
const lines = [
|
|
249
|
-
MARKERS.start,
|
|
250
|
-
'',
|
|
251
|
-
'## Rules',
|
|
252
|
-
'',
|
|
253
|
-
`This project uses modular development rules stored in \`${rulesPath}/\`.`,
|
|
254
|
-
'Each rule file covers a specific topic:',
|
|
255
|
-
'',
|
|
256
|
-
]
|
|
257
|
-
|
|
258
|
-
for (const [ruleId, template] of Object.entries(templates)) {
|
|
259
|
-
// Dual format: @ syntax for Claude Code, markdown link for other tools
|
|
260
|
-
lines.push(`- @${rulesPath}/${template.filename} - [${ruleId}](${rulesPath}/${template.filename})`)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
lines.push('')
|
|
264
|
-
lines.push(MARKERS.end)
|
|
265
|
-
|
|
266
|
-
return lines.join('\n')
|
|
267
43
|
}
|
|
268
44
|
|
|
269
45
|
/**
|
|
@@ -273,101 +49,102 @@ export const scaffoldRules = async (args: string[]): Promise<void> => {
|
|
|
273
49
|
const { values } = parseArgs({
|
|
274
50
|
args,
|
|
275
51
|
options: {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
short: 'r',
|
|
279
|
-
multiple: true,
|
|
280
|
-
},
|
|
281
|
-
'rules-dir': {
|
|
282
|
-
type: 'string',
|
|
283
|
-
short: 'd',
|
|
284
|
-
},
|
|
285
|
-
list: {
|
|
286
|
-
type: 'boolean',
|
|
287
|
-
short: 'l',
|
|
288
|
-
},
|
|
52
|
+
list: { type: 'boolean', short: 'l' },
|
|
53
|
+
'dry-run': { type: 'boolean', short: 'n' },
|
|
289
54
|
},
|
|
290
55
|
allowPositionals: true,
|
|
291
56
|
strict: false,
|
|
292
57
|
})
|
|
293
58
|
|
|
294
|
-
const
|
|
295
|
-
const customRulesDir = values['rules-dir'] as string | undefined
|
|
59
|
+
const dryRun = values['dry-run'] as boolean | undefined
|
|
296
60
|
const listOnly = values.list as boolean | undefined
|
|
297
61
|
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
// Read template files
|
|
302
|
-
const templateFiles = await readdir(packageRulesDir)
|
|
303
|
-
|
|
304
|
-
// Filter to .md files
|
|
305
|
-
const mdFiles = templateFiles.filter((f) => f.endsWith('.md'))
|
|
306
|
-
const availableRuleIds = mdFiles.map((f) => f.replace('.md', ''))
|
|
62
|
+
const sourceRules = join(import.meta.dir, '../.plaited/rules')
|
|
63
|
+
const cwd = process.cwd()
|
|
307
64
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (invalidRules.length > 0) {
|
|
312
|
-
console.error(`Warning: Unknown rules: ${invalidRules.join(', ')}`)
|
|
313
|
-
console.error(`Available rules: ${availableRuleIds.join(', ')}`)
|
|
314
|
-
}
|
|
315
|
-
}
|
|
65
|
+
// Get available rules
|
|
66
|
+
const files = await readdir(sourceRules)
|
|
67
|
+
const rules = files.filter((f) => f.endsWith('.md'))
|
|
316
68
|
|
|
317
|
-
//
|
|
69
|
+
// --list: just output available rules
|
|
318
70
|
if (listOnly) {
|
|
319
|
-
|
|
320
|
-
console.log(JSON.stringify(listOutput, null, 2))
|
|
71
|
+
console.log(JSON.stringify({ rules: rules.map((f) => f.replace('.md', '')) }))
|
|
321
72
|
return
|
|
322
73
|
}
|
|
323
74
|
|
|
324
|
-
|
|
325
|
-
const rulesToProcess = rulesFilter ? mdFiles.filter((f) => rulesFilter.includes(f.replace('.md', ''))) : mdFiles
|
|
75
|
+
const actions: string[] = []
|
|
326
76
|
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
77
|
+
// Check for agent directories BEFORE copying (since copy creates .plaited/)
|
|
78
|
+
// This determines whether to fall back to AGENTS.md append
|
|
79
|
+
let hadAgentDirBeforeScaffold = false
|
|
80
|
+
for (const agent of ALL_AGENTS) {
|
|
81
|
+
if (await isDirectory(join(cwd, agent))) {
|
|
82
|
+
hadAgentDirBeforeScaffold = true
|
|
83
|
+
break
|
|
84
|
+
}
|
|
335
85
|
}
|
|
336
86
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
87
|
+
// 1. Copy rules to .plaited/rules/ (canonical location, serves .plaited agent)
|
|
88
|
+
const targetDir = join(cwd, TARGET_RULES)
|
|
89
|
+
if (!dryRun) {
|
|
90
|
+
await mkdir(targetDir, { recursive: true })
|
|
91
|
+
}
|
|
340
92
|
|
|
341
|
-
|
|
342
|
-
|
|
93
|
+
for (const file of rules) {
|
|
94
|
+
const src = join(sourceRules, file)
|
|
95
|
+
const dest = join(targetDir, file)
|
|
96
|
+
if (!dryRun) {
|
|
97
|
+
await Bun.write(dest, await Bun.file(src).text())
|
|
98
|
+
}
|
|
99
|
+
actions.push(`copy: ${TARGET_RULES}/${file}`)
|
|
100
|
+
}
|
|
343
101
|
|
|
344
|
-
|
|
345
|
-
|
|
102
|
+
// 2. Symlink for other agents (.claude, .cursor)
|
|
103
|
+
for (const agent of SYMLINK_AGENTS) {
|
|
104
|
+
const agentDir = join(cwd, agent)
|
|
105
|
+
if (await isDirectory(agentDir)) {
|
|
106
|
+
const rulesLink = join(agentDir, 'rules')
|
|
107
|
+
|
|
108
|
+
// Check if symlink already exists and points to right place
|
|
109
|
+
try {
|
|
110
|
+
const existing = await readlink(rulesLink)
|
|
111
|
+
if (existing === '../.plaited/rules') {
|
|
112
|
+
actions.push(`skip: ${agent}/rules (symlink exists)`)
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Doesn't exist or not a symlink - proceed to create
|
|
117
|
+
}
|
|
346
118
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
content: processed,
|
|
350
|
-
description: extractDescription(processed),
|
|
119
|
+
if (!dryRun) {
|
|
120
|
+
await symlink('../.plaited/rules', rulesLink)
|
|
351
121
|
}
|
|
352
|
-
|
|
353
|
-
console.error(`Error processing template ${file}:`, error)
|
|
354
|
-
process.exit(1)
|
|
122
|
+
actions.push(`symlink: ${agent}/rules -> ../.plaited/rules`)
|
|
355
123
|
}
|
|
356
124
|
}
|
|
357
125
|
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
126
|
+
// 3. Fallback: append to AGENTS.md only if NO agent directories existed before copy
|
|
127
|
+
if (!hadAgentDirBeforeScaffold) {
|
|
128
|
+
const agentsMdPath = join(cwd, 'AGENTS.md')
|
|
129
|
+
const agentsMd = Bun.file(agentsMdPath)
|
|
130
|
+
|
|
131
|
+
if (await agentsMd.exists()) {
|
|
132
|
+
const content = await agentsMd.text()
|
|
133
|
+
if (content.includes('.plaited/rules')) {
|
|
134
|
+
actions.push('skip: AGENTS.md (already has rules)')
|
|
135
|
+
} else {
|
|
136
|
+
const links = rules.map((f) => `- [${f.replace('.md', '')}](${TARGET_RULES}/${f})`).join('\n')
|
|
137
|
+
const section = `\n## Rules\n\n${links}\n`
|
|
138
|
+
|
|
139
|
+
if (!dryRun) {
|
|
140
|
+
await Bun.write(agentsMdPath, content + section)
|
|
141
|
+
}
|
|
142
|
+
actions.push('append: AGENTS.md (rules section)')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
368
145
|
}
|
|
369
146
|
|
|
370
|
-
console.log(JSON.stringify(
|
|
147
|
+
console.log(JSON.stringify({ dryRun: !!dryRun, targetRules: TARGET_RULES, actions }, null, 2))
|
|
371
148
|
}
|
|
372
149
|
|
|
373
150
|
// CLI entry point
|