@fermindi/pwn-cli 0.1.1 → 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/LICENSE +21 -21
- package/README.md +265 -251
- package/cli/batch.js +333 -333
- package/cli/codespaces.js +303 -303
- package/cli/index.js +112 -91
- package/cli/inject.js +90 -67
- package/cli/knowledge.js +531 -531
- package/cli/migrate.js +466 -0
- package/cli/notify.js +135 -135
- package/cli/patterns.js +665 -665
- package/cli/save.js +206 -0
- package/cli/status.js +91 -91
- package/cli/update.js +189 -0
- package/cli/validate.js +61 -61
- package/package.json +70 -70
- package/src/core/inject.js +300 -204
- package/src/core/state.js +91 -91
- package/src/core/validate.js +202 -202
- package/src/core/workspace.js +176 -176
- package/src/index.js +20 -20
- package/src/knowledge/gc.js +308 -308
- package/src/knowledge/lifecycle.js +401 -401
- package/src/knowledge/promote.js +364 -364
- package/src/knowledge/references.js +342 -342
- package/src/patterns/matcher.js +218 -218
- package/src/patterns/registry.js +375 -375
- package/src/patterns/triggers.js +423 -423
- package/src/services/batch-service.js +849 -849
- package/src/services/notification-service.js +342 -342
- package/templates/codespaces/devcontainer.json +52 -52
- package/templates/codespaces/setup.sh +70 -70
- package/templates/workspace/.ai/README.md +164 -164
- package/templates/workspace/.ai/agents/README.md +204 -204
- package/templates/workspace/.ai/agents/claude.md +625 -625
- package/templates/workspace/.ai/config/README.md +79 -79
- package/templates/workspace/.ai/config/notifications.template.json +20 -20
- package/templates/workspace/.ai/memory/deadends.md +79 -79
- package/templates/workspace/.ai/memory/decisions.md +58 -58
- package/templates/workspace/.ai/memory/patterns.md +65 -65
- package/templates/workspace/.ai/patterns/backend/README.md +126 -126
- package/templates/workspace/.ai/patterns/frontend/README.md +103 -103
- package/templates/workspace/.ai/patterns/index.md +256 -256
- package/templates/workspace/.ai/patterns/triggers.json +1087 -1087
- package/templates/workspace/.ai/patterns/universal/README.md +141 -141
- package/templates/workspace/.ai/state.template.json +8 -8
- package/templates/workspace/.ai/tasks/active.md +77 -77
- package/templates/workspace/.ai/tasks/backlog.md +95 -95
- package/templates/workspace/.ai/workflows/batch-task.md +356 -356
package/src/core/inject.js
CHANGED
|
@@ -1,204 +1,300 @@
|
|
|
1
|
-
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync } from 'fs';
|
|
2
|
-
import { join, dirname } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import { randomUUID } from 'crypto';
|
|
5
|
-
import { initState } from './state.js';
|
|
6
|
-
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Known AI instruction files in the industry
|
|
11
|
-
* PWN should detect these and warn/offer migration
|
|
12
|
-
*/
|
|
13
|
-
export const KNOWN_AI_FILES = [
|
|
14
|
-
// Claude Code
|
|
15
|
-
{ pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions' },
|
|
16
|
-
{ pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions' },
|
|
17
|
-
{ pattern: '.claude', type: 'claude', description: 'Claude config directory' },
|
|
18
|
-
|
|
19
|
-
// Cursor
|
|
20
|
-
{ pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules' },
|
|
21
|
-
{ pattern: '.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
{ pattern: '
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
*
|
|
91
|
-
* @param {
|
|
92
|
-
* @
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1
|
+
import { existsSync, cpSync, renameSync, readFileSync, appendFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { initState } from './state.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Known AI instruction files in the industry
|
|
11
|
+
* PWN should detect these and warn/offer migration
|
|
12
|
+
*/
|
|
13
|
+
export const KNOWN_AI_FILES = [
|
|
14
|
+
// Claude Code
|
|
15
|
+
{ pattern: 'CLAUDE.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
|
|
16
|
+
{ pattern: 'claude.md', type: 'claude', description: 'Claude Code instructions', migratable: true },
|
|
17
|
+
{ pattern: '.claude', type: 'claude', description: 'Claude config directory', migratable: false },
|
|
18
|
+
|
|
19
|
+
// Cursor
|
|
20
|
+
{ pattern: '.cursorrules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
|
|
21
|
+
{ pattern: '.cursor/rules', type: 'cursor', description: 'Cursor AI rules', migratable: true },
|
|
22
|
+
{ pattern: '.cursorignore', type: 'cursor', description: 'Cursor ignore file', migratable: false },
|
|
23
|
+
|
|
24
|
+
// Cline memory-bank
|
|
25
|
+
{ pattern: 'memory-bank', type: 'memory-bank', description: 'Cline memory bank', migratable: true },
|
|
26
|
+
|
|
27
|
+
// GitHub Copilot
|
|
28
|
+
{ pattern: '.github/copilot-instructions.md', type: 'copilot', description: 'GitHub Copilot instructions', migratable: true },
|
|
29
|
+
|
|
30
|
+
// Session files (common pattern)
|
|
31
|
+
{ pattern: 'session.*.md', type: 'session', description: 'Session file (legacy pattern)', migratable: false },
|
|
32
|
+
|
|
33
|
+
// Other AI tools
|
|
34
|
+
{ pattern: '.aider', type: 'aider', description: 'Aider AI config', migratable: false },
|
|
35
|
+
{ pattern: '.continue', type: 'continue', description: 'Continue AI config', migratable: false },
|
|
36
|
+
{ pattern: '.codeium', type: 'codeium', description: 'Codeium config', migratable: false },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detect known AI instruction files in a directory
|
|
41
|
+
* @param {string} cwd - Directory to scan
|
|
42
|
+
* @returns {Object[]} Array of detected files with metadata
|
|
43
|
+
*/
|
|
44
|
+
export function detectKnownAIFiles(cwd = process.cwd()) {
|
|
45
|
+
const detected = [];
|
|
46
|
+
|
|
47
|
+
for (const known of KNOWN_AI_FILES) {
|
|
48
|
+
// Handle glob patterns like session.*.md
|
|
49
|
+
if (known.pattern.includes('*')) {
|
|
50
|
+
const regex = new RegExp('^' + known.pattern.replace('.', '\\.').replace('*', '.*') + '$');
|
|
51
|
+
try {
|
|
52
|
+
const files = readdirSync(cwd);
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
if (regex.test(file)) {
|
|
55
|
+
detected.push({
|
|
56
|
+
...known,
|
|
57
|
+
file,
|
|
58
|
+
path: join(cwd, file)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore read errors
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Direct file check
|
|
67
|
+
const filePath = join(cwd, known.pattern);
|
|
68
|
+
if (existsSync(filePath)) {
|
|
69
|
+
detected.push({
|
|
70
|
+
...known,
|
|
71
|
+
file: known.pattern,
|
|
72
|
+
path: filePath
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return detected;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the path to the workspace template
|
|
83
|
+
* @returns {string} Path to template directory
|
|
84
|
+
*/
|
|
85
|
+
export function getTemplatePath() {
|
|
86
|
+
return join(__dirname, '../../templates/workspace/.ai');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Inject PWN workspace into a project
|
|
91
|
+
* @param {object} options - Injection options
|
|
92
|
+
* @param {string} options.cwd - Target directory (defaults to process.cwd())
|
|
93
|
+
* @param {boolean} options.force - Force overwrite existing .ai/ directory
|
|
94
|
+
* @param {boolean} options.silent - Suppress console output
|
|
95
|
+
* @param {boolean} options.skipDetection - Skip detection of known AI files
|
|
96
|
+
* @returns {object} Result with success status and message
|
|
97
|
+
*/
|
|
98
|
+
export async function inject(options = {}) {
|
|
99
|
+
const {
|
|
100
|
+
cwd = process.cwd(),
|
|
101
|
+
force = false,
|
|
102
|
+
silent = false,
|
|
103
|
+
skipDetection = false
|
|
104
|
+
} = options;
|
|
105
|
+
|
|
106
|
+
const templateDir = getTemplatePath();
|
|
107
|
+
const targetDir = join(cwd, '.ai');
|
|
108
|
+
|
|
109
|
+
const log = silent ? () => {} : console.log;
|
|
110
|
+
|
|
111
|
+
// Detect known AI instruction files
|
|
112
|
+
const detectedFiles = skipDetection ? [] : detectKnownAIFiles(cwd);
|
|
113
|
+
|
|
114
|
+
// Check if .ai/ already exists
|
|
115
|
+
if (existsSync(targetDir) && !force) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
error: 'ALREADY_EXISTS',
|
|
119
|
+
message: '.ai/ directory already exists. Use --force to overwrite.',
|
|
120
|
+
detected: detectedFiles
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Backup existing AI files content before overwriting
|
|
125
|
+
const backedUpContent = {};
|
|
126
|
+
if (detectedFiles.length > 0 && detectedFiles.some(f => f.migratable)) {
|
|
127
|
+
log('📋 Backing up existing AI files...');
|
|
128
|
+
for (const file of detectedFiles.filter(f => f.migratable)) {
|
|
129
|
+
try {
|
|
130
|
+
backedUpContent[file.file] = {
|
|
131
|
+
content: readFileSync(file.path, 'utf8'),
|
|
132
|
+
type: file.type
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
// Ignore read errors
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Copy workspace template
|
|
142
|
+
log('📦 Copying workspace template...');
|
|
143
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
144
|
+
|
|
145
|
+
// Rename state.template.json → state.json
|
|
146
|
+
const templateState = join(targetDir, 'state.template.json');
|
|
147
|
+
const stateFile = join(targetDir, 'state.json');
|
|
148
|
+
|
|
149
|
+
if (existsSync(templateState)) {
|
|
150
|
+
renameSync(templateState, stateFile);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Rename notifications.template.json → notifications.json and generate unique topic
|
|
154
|
+
const templateNotify = join(targetDir, 'config', 'notifications.template.json');
|
|
155
|
+
const notifyFile = join(targetDir, 'config', 'notifications.json');
|
|
156
|
+
|
|
157
|
+
if (existsSync(templateNotify)) {
|
|
158
|
+
renameSync(templateNotify, notifyFile);
|
|
159
|
+
initNotifications(notifyFile);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Initialize state.json with current user
|
|
163
|
+
initState(cwd);
|
|
164
|
+
|
|
165
|
+
// Update .gitignore
|
|
166
|
+
updateGitignore(cwd, silent);
|
|
167
|
+
|
|
168
|
+
// Handle CLAUDE.md: backup existing and copy PWN template to root
|
|
169
|
+
let backupInfo = { backed_up: [] };
|
|
170
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
171
|
+
const backupClaudeMdPath = join(cwd, '~CLAUDE.md');
|
|
172
|
+
const templateClaudeMd = join(targetDir, 'agents', 'claude.md');
|
|
173
|
+
|
|
174
|
+
// Backup existing CLAUDE.md if present
|
|
175
|
+
if (backedUpContent['CLAUDE.md'] || backedUpContent['claude.md']) {
|
|
176
|
+
const originalName = backedUpContent['CLAUDE.md'] ? 'CLAUDE.md' : 'claude.md';
|
|
177
|
+
const originalPath = join(cwd, originalName);
|
|
178
|
+
|
|
179
|
+
if (existsSync(originalPath)) {
|
|
180
|
+
renameSync(originalPath, backupClaudeMdPath);
|
|
181
|
+
backupInfo.backed_up.push({ from: originalName, to: '~CLAUDE.md' });
|
|
182
|
+
if (!silent) {
|
|
183
|
+
console.log(`📦 Backed up ${originalName} → ~CLAUDE.md`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Copy PWN template to CLAUDE.md in root
|
|
189
|
+
if (existsSync(templateClaudeMd)) {
|
|
190
|
+
cpSync(templateClaudeMd, claudeMdPath);
|
|
191
|
+
if (!silent) {
|
|
192
|
+
console.log('📝 Created CLAUDE.md with PWN template');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Backup other AI files (not CLAUDE.md) to .ai/
|
|
197
|
+
const otherFiles = Object.fromEntries(
|
|
198
|
+
Object.entries(backedUpContent).filter(([k]) => k.toLowerCase() !== 'claude.md')
|
|
199
|
+
);
|
|
200
|
+
if (Object.keys(otherFiles).length > 0) {
|
|
201
|
+
const otherBackups = backupExistingAIFiles(targetDir, otherFiles, silent);
|
|
202
|
+
backupInfo.backed_up.push(...otherBackups.backed_up);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
message: 'PWN workspace injected successfully',
|
|
208
|
+
path: targetDir,
|
|
209
|
+
detected: detectedFiles,
|
|
210
|
+
backed_up: backupInfo.backed_up
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: 'INJECTION_FAILED',
|
|
217
|
+
message: error.message,
|
|
218
|
+
detected: detectedFiles
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Initialize notifications.json with unique topic
|
|
225
|
+
* @param {string} notifyFile - Path to notifications.json
|
|
226
|
+
*/
|
|
227
|
+
function initNotifications(notifyFile) {
|
|
228
|
+
try {
|
|
229
|
+
const content = readFileSync(notifyFile, 'utf8');
|
|
230
|
+
const config = JSON.parse(content);
|
|
231
|
+
|
|
232
|
+
// Generate unique topic ID
|
|
233
|
+
const uniqueId = randomUUID().split('-')[0]; // First segment: 8 chars
|
|
234
|
+
config.channels.ntfy.topic = `pwn-${uniqueId}`;
|
|
235
|
+
|
|
236
|
+
writeFileSync(notifyFile, JSON.stringify(config, null, 2));
|
|
237
|
+
} catch {
|
|
238
|
+
// Ignore errors - notifications will use defaults
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Backup existing AI files to .ai/ root with ~ prefix
|
|
244
|
+
* - PWN template stays as the base (it's the correct one)
|
|
245
|
+
* - User's existing files are backed up for manual merge
|
|
246
|
+
* @param {string} targetDir - Path to .ai/ directory
|
|
247
|
+
* @param {Object} backedUpContent - Map of filename -> {content, type}
|
|
248
|
+
* @param {boolean} silent - Suppress output
|
|
249
|
+
* @returns {Object} Backup info
|
|
250
|
+
*/
|
|
251
|
+
function backupExistingAIFiles(targetDir, backedUpContent, silent = false) {
|
|
252
|
+
const result = { backed_up: [] };
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Backup all existing AI files with ~ prefix
|
|
256
|
+
for (const [filename, data] of Object.entries(backedUpContent)) {
|
|
257
|
+
// Convert filename to backup name: CLAUDE.md -> ~CLAUDE.md, .cursorrules -> ~cursorrules.md
|
|
258
|
+
const safeName = filename.replace(/^\./, '').replace(/[\/\\]/g, '-');
|
|
259
|
+
const backupName = `~${safeName}${safeName.endsWith('.md') ? '' : '.md'}`;
|
|
260
|
+
const backupPath = join(targetDir, backupName);
|
|
261
|
+
|
|
262
|
+
writeFileSync(backupPath, data.content);
|
|
263
|
+
|
|
264
|
+
result.backed_up.push({ from: filename, to: backupName });
|
|
265
|
+
|
|
266
|
+
if (!silent) {
|
|
267
|
+
console.log(`📦 Backed up ${filename} → .ai/${backupName}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (!silent) {
|
|
273
|
+
console.error('⚠️ Backup warning:', error.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Update .gitignore to exclude PWN personal files
|
|
282
|
+
* @param {string} cwd - Working directory
|
|
283
|
+
* @param {boolean} silent - Suppress output
|
|
284
|
+
*/
|
|
285
|
+
function updateGitignore(cwd, silent = false) {
|
|
286
|
+
const gitignorePath = join(cwd, '.gitignore');
|
|
287
|
+
let gitignoreContent = '';
|
|
288
|
+
|
|
289
|
+
if (existsSync(gitignorePath)) {
|
|
290
|
+
gitignoreContent = readFileSync(gitignorePath, 'utf8');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!gitignoreContent.includes('.ai/state.json')) {
|
|
294
|
+
const pwnSection = '\n# PWN\n.ai/state.json\n.ai/config/notifications.json\n';
|
|
295
|
+
appendFileSync(gitignorePath, pwnSection);
|
|
296
|
+
if (!silent) {
|
|
297
|
+
console.log('📝 Updated .gitignore');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|