ship-safe 5.0.0 → 6.0.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.
@@ -1,349 +1,349 @@
1
- /**
2
- * Init Command
3
- * ============
4
- *
5
- * Initialize security configurations in the current project.
6
- * Copies pre-configured security files from ship-safe.
7
- *
8
- * USAGE:
9
- * ship-safe init Copy all security configs
10
- * ship-safe init --gitignore Only copy .gitignore
11
- * ship-safe init --headers Only copy security headers
12
- * ship-safe init -f Force overwrite existing files
13
- *
14
- * FILES COPIED:
15
- * - .gitignore (merged with existing if present)
16
- * - nextjs-security-headers.js (for Next.js projects)
17
- */
18
-
19
- import fs from 'fs';
20
- import path from 'path';
21
- import { fileURLToPath } from 'url';
22
- import chalk from 'chalk';
23
- import * as output from '../utils/output.js';
24
-
25
- // Get the directory of this module (for finding config files)
26
- const __filename = fileURLToPath(import.meta.url);
27
- const __dirname = path.dirname(__filename);
28
- const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
29
-
30
- // =============================================================================
31
- // MAIN INIT FUNCTION
32
- // =============================================================================
33
-
34
- export async function initCommand(options = {}) {
35
- const targetDir = process.cwd();
36
-
37
- console.log();
38
- output.header('Initializing Security Configs');
39
- console.log();
40
- console.log(chalk.gray(`Target directory: ${targetDir}`));
41
- console.log();
42
-
43
- const results = {
44
- copied: [],
45
- skipped: [],
46
- merged: [],
47
- errors: []
48
- };
49
-
50
- // Determine which files to copy.
51
- // If a specific flag is set, only run that category.
52
- // With no flags, run everything.
53
- const hasSpecificFlag = options.gitignore || options.headers || options.agents;
54
- const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
55
- const copyHeaders = hasSpecificFlag ? !!options.headers : true;
56
- const copyAgents = hasSpecificFlag ? !!options.agents : true;
57
-
58
- // Copy .gitignore
59
- if (copyGitignore) {
60
- await handleGitignore(targetDir, options.force, results);
61
- }
62
-
63
- // Copy security headers
64
- if (copyHeaders) {
65
- await handleSecurityHeaders(targetDir, options.force, results);
66
- }
67
-
68
- // Append security rules to AI agent instruction files
69
- if (copyAgents) {
70
- await handleAgentFiles(targetDir, options.force, results);
71
- }
72
-
73
- // Print summary
74
- printSummary(results);
75
- }
76
-
77
- // =============================================================================
78
- // GITIGNORE HANDLING
79
- // =============================================================================
80
-
81
- async function handleGitignore(targetDir, force, results) {
82
- // Note: We use 'gitignore-template' because npm excludes dotfiles from packages
83
- const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
84
- const targetPath = path.join(targetDir, '.gitignore');
85
-
86
- // Check if source exists
87
- if (!fs.existsSync(sourcePath)) {
88
- results.errors.push({
89
- file: '.gitignore',
90
- error: 'Source file not found in ship-safe package'
91
- });
92
- return;
93
- }
94
-
95
- const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
96
-
97
- // Check if target exists
98
- if (fs.existsSync(targetPath)) {
99
- if (force) {
100
- // Overwrite
101
- fs.writeFileSync(targetPath, sourceContent);
102
- results.copied.push('.gitignore (overwritten)');
103
- } else {
104
- // Merge: append ship-safe patterns to existing
105
- const existingContent = fs.readFileSync(targetPath, 'utf-8');
106
-
107
- // Check if already has ship-safe content
108
- if (existingContent.includes('# SHIP SAFE')) {
109
- results.skipped.push('.gitignore (already contains ship-safe patterns)');
110
- return;
111
- }
112
-
113
- // Append ship-safe section
114
- const mergedContent = existingContent.trim() + '\n\n' +
115
- '# =============================================================================\n' +
116
- '# SHIP SAFE ADDITIONS\n' +
117
- '# Added by: npx ship-safe init\n' +
118
- '# =============================================================================\n\n' +
119
- extractSecurityPatterns(sourceContent);
120
-
121
- fs.writeFileSync(targetPath, mergedContent);
122
- results.merged.push('.gitignore');
123
- }
124
- } else {
125
- // Create new
126
- fs.writeFileSync(targetPath, sourceContent);
127
- results.copied.push('.gitignore');
128
- }
129
- }
130
-
131
- /**
132
- * Extract the most important security patterns from our .gitignore
133
- */
134
- function extractSecurityPatterns(fullGitignore) {
135
- // Extract key sections
136
- const patterns = `
137
- # Environment files
138
- .env
139
- .env.local
140
- .env*.local
141
- *.env
142
-
143
- # Private keys & certificates
144
- *.pem
145
- *.key
146
- *.p12
147
- *.pfx
148
-
149
- # Credentials
150
- *credentials*
151
- *.secrets.json
152
- secrets.yml
153
- secrets.yaml
154
-
155
- # Service accounts
156
- **/service-account*.json
157
- *-firebase-adminsdk-*.json
158
-
159
- # AWS
160
- .aws/credentials
161
-
162
- # Database files
163
- *.sqlite
164
- *.sqlite3
165
- *.db
166
-
167
- # Logs (may contain sensitive data)
168
- *.log
169
- logs/
170
- `;
171
-
172
- return patterns.trim();
173
- }
174
-
175
- // =============================================================================
176
- // SECURITY HEADERS HANDLING
177
- // =============================================================================
178
-
179
- async function handleSecurityHeaders(targetDir, force, results) {
180
- const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
181
- const targetPath = path.join(targetDir, 'security-headers.config.js');
182
-
183
- // Check if source exists
184
- if (!fs.existsSync(sourcePath)) {
185
- results.errors.push({
186
- file: 'security-headers.config.js',
187
- error: 'Source file not found in ship-safe package'
188
- });
189
- return;
190
- }
191
-
192
- // Detect if this is a Next.js project
193
- const packageJsonPath = path.join(targetDir, 'package.json');
194
- let isNextProject = false;
195
-
196
- if (fs.existsSync(packageJsonPath)) {
197
- try {
198
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
199
- isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
200
- } catch {
201
- // Ignore parse errors
202
- }
203
- }
204
-
205
- // Check if target exists
206
- if (fs.existsSync(targetPath) && !force) {
207
- results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
208
- return;
209
- }
210
-
211
- // Copy the file
212
- const content = fs.readFileSync(sourcePath, 'utf-8');
213
- fs.writeFileSync(targetPath, content);
214
- results.copied.push('security-headers.config.js');
215
-
216
- // Show integration instructions
217
- if (isNextProject) {
218
- console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
219
- console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
220
- console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
221
- console.log();
222
- }
223
- }
224
-
225
- // =============================================================================
226
- // AGENT FILE HANDLING
227
- // =============================================================================
228
-
229
- // Marker used for idempotency — if this string is present we already added our section.
230
- const AGENT_MARKER = '<!-- ship-safe -->';
231
-
232
- // The security rules block appended to existing agent instruction files.
233
- const AGENT_SECTION = `
234
- ## Security (ship-safe)
235
- ${AGENT_MARKER}
236
- - Always run \`npx ship-safe scan .\` before committing code
237
- - Never hardcode API keys, tokens, or credentials in source files — use environment variables
238
- - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
239
- `;
240
-
241
- // Standalone content for a new Cursor rules file.
242
- const CURSOR_RULE_CONTENT = `---
243
- description: Prevent API key leaks and credential exposure in generated code
244
- alwaysApply: true
245
- ---
246
-
247
- - Always run \`npx ship-safe scan .\` before committing code
248
- - Never hardcode API keys, tokens, or credentials in source files — use environment variables
249
- - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
250
- `;
251
-
252
- async function handleAgentFiles(targetDir, force, results) {
253
- // Files where we append a section if they already exist, or create if they don't.
254
- const appendTargets = [
255
- { file: 'CLAUDE.md', label: 'CLAUDE.md' },
256
- { file: '.windsurfrules', label: '.windsurfrules' },
257
- { file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
258
- ];
259
-
260
- for (const { file, label } of appendTargets) {
261
- const targetPath = path.join(targetDir, file);
262
-
263
- if (fs.existsSync(targetPath)) {
264
- const existing = fs.readFileSync(targetPath, 'utf-8');
265
- if (existing.includes(AGENT_MARKER)) {
266
- results.skipped.push(`${label} (already contains ship-safe rules)`);
267
- continue;
268
- }
269
- if (force || true) { // always append unless already present
270
- fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
271
- results.merged.push(label);
272
- }
273
- } else {
274
- // Ensure parent directory exists (e.g. .github/)
275
- const parentDir = path.dirname(targetPath);
276
- if (!fs.existsSync(parentDir)) {
277
- fs.mkdirSync(parentDir, { recursive: true });
278
- }
279
- fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
280
- results.copied.push(label);
281
- }
282
- }
283
-
284
- // Cursor rules — dedicated file, no merging needed.
285
- const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
286
- const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
287
-
288
- if (fs.existsSync(cursorRulesFile) && !force) {
289
- results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
290
- } else {
291
- if (!fs.existsSync(cursorRulesDir)) {
292
- fs.mkdirSync(cursorRulesDir, { recursive: true });
293
- }
294
- fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
295
- results.copied.push('.cursor/rules/ship-safe.mdc');
296
- }
297
- }
298
-
299
- // =============================================================================
300
- // SUMMARY
301
- // =============================================================================
302
-
303
- function printSummary(results) {
304
- console.log();
305
- console.log(chalk.cyan('='.repeat(60)));
306
- console.log(chalk.cyan.bold(' Summary'));
307
- console.log(chalk.cyan('='.repeat(60)));
308
- console.log();
309
-
310
- if (results.copied.length > 0) {
311
- console.log(chalk.green.bold('Created:'));
312
- for (const file of results.copied) {
313
- console.log(chalk.green(` \u2714 ${file}`));
314
- }
315
- console.log();
316
- }
317
-
318
- if (results.merged.length > 0) {
319
- console.log(chalk.blue.bold('Merged:'));
320
- for (const file of results.merged) {
321
- console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
322
- }
323
- console.log();
324
- }
325
-
326
- if (results.skipped.length > 0) {
327
- console.log(chalk.yellow.bold('Skipped:'));
328
- for (const file of results.skipped) {
329
- console.log(chalk.yellow(` \u2192 ${file}`));
330
- }
331
- console.log();
332
- }
333
-
334
- if (results.errors.length > 0) {
335
- console.log(chalk.red.bold('Errors:'));
336
- for (const { file, error } of results.errors) {
337
- console.log(chalk.red(` \u2718 ${file}: ${error}`));
338
- }
339
- console.log();
340
- }
341
-
342
- // Next steps
343
- console.log(chalk.cyan('Next steps:'));
344
- console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
345
- console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
346
- console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
347
- console.log();
348
- console.log(chalk.cyan('='.repeat(60)));
349
- }
1
+ /**
2
+ * Init Command
3
+ * ============
4
+ *
5
+ * Initialize security configurations in the current project.
6
+ * Copies pre-configured security files from ship-safe.
7
+ *
8
+ * USAGE:
9
+ * ship-safe init Copy all security configs
10
+ * ship-safe init --gitignore Only copy .gitignore
11
+ * ship-safe init --headers Only copy security headers
12
+ * ship-safe init -f Force overwrite existing files
13
+ *
14
+ * FILES COPIED:
15
+ * - .gitignore (merged with existing if present)
16
+ * - nextjs-security-headers.js (for Next.js projects)
17
+ */
18
+
19
+ import fs from 'fs';
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import chalk from 'chalk';
23
+ import * as output from '../utils/output.js';
24
+
25
+ // Get the directory of this module (for finding config files)
26
+ const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
27
+ const __dirname = path.dirname(__filename);
28
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
29
+
30
+ // =============================================================================
31
+ // MAIN INIT FUNCTION
32
+ // =============================================================================
33
+
34
+ export async function initCommand(options = {}) {
35
+ const targetDir = process.cwd();
36
+
37
+ console.log();
38
+ output.header('Initializing Security Configs');
39
+ console.log();
40
+ console.log(chalk.gray(`Target directory: ${targetDir}`));
41
+ console.log();
42
+
43
+ const results = {
44
+ copied: [],
45
+ skipped: [],
46
+ merged: [],
47
+ errors: []
48
+ };
49
+
50
+ // Determine which files to copy.
51
+ // If a specific flag is set, only run that category.
52
+ // With no flags, run everything.
53
+ const hasSpecificFlag = options.gitignore || options.headers || options.agents;
54
+ const copyGitignore = hasSpecificFlag ? !!options.gitignore : true;
55
+ const copyHeaders = hasSpecificFlag ? !!options.headers : true;
56
+ const copyAgents = hasSpecificFlag ? !!options.agents : true;
57
+
58
+ // Copy .gitignore
59
+ if (copyGitignore) {
60
+ await handleGitignore(targetDir, options.force, results);
61
+ }
62
+
63
+ // Copy security headers
64
+ if (copyHeaders) {
65
+ await handleSecurityHeaders(targetDir, options.force, results);
66
+ }
67
+
68
+ // Append security rules to AI agent instruction files
69
+ if (copyAgents) {
70
+ await handleAgentFiles(targetDir, options.force, results);
71
+ }
72
+
73
+ // Print summary
74
+ printSummary(results);
75
+ }
76
+
77
+ // =============================================================================
78
+ // GITIGNORE HANDLING
79
+ // =============================================================================
80
+
81
+ async function handleGitignore(targetDir, force, results) {
82
+ // Note: We use 'gitignore-template' because npm excludes dotfiles from packages
83
+ const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'gitignore-template');
84
+ const targetPath = path.join(targetDir, '.gitignore');
85
+
86
+ // Check if source exists
87
+ if (!fs.existsSync(sourcePath)) {
88
+ results.errors.push({
89
+ file: '.gitignore',
90
+ error: 'Source file not found in ship-safe package'
91
+ });
92
+ return;
93
+ }
94
+
95
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
96
+
97
+ // Check if target exists
98
+ if (fs.existsSync(targetPath)) {
99
+ if (force) {
100
+ // Overwrite
101
+ fs.writeFileSync(targetPath, sourceContent);
102
+ results.copied.push('.gitignore (overwritten)');
103
+ } else {
104
+ // Merge: append ship-safe patterns to existing
105
+ const existingContent = fs.readFileSync(targetPath, 'utf-8');
106
+
107
+ // Check if already has ship-safe content
108
+ if (existingContent.includes('# SHIP SAFE')) {
109
+ results.skipped.push('.gitignore (already contains ship-safe patterns)');
110
+ return;
111
+ }
112
+
113
+ // Append ship-safe section
114
+ const mergedContent = existingContent.trim() + '\n\n' +
115
+ '# =============================================================================\n' +
116
+ '# SHIP SAFE ADDITIONS\n' +
117
+ '# Added by: npx ship-safe init\n' +
118
+ '# =============================================================================\n\n' +
119
+ extractSecurityPatterns(sourceContent);
120
+
121
+ fs.writeFileSync(targetPath, mergedContent);
122
+ results.merged.push('.gitignore');
123
+ }
124
+ } else {
125
+ // Create new
126
+ fs.writeFileSync(targetPath, sourceContent);
127
+ results.copied.push('.gitignore');
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Extract the most important security patterns from our .gitignore
133
+ */
134
+ function extractSecurityPatterns(fullGitignore) {
135
+ // Extract key sections
136
+ const patterns = `
137
+ # Environment files
138
+ .env
139
+ .env.local
140
+ .env*.local
141
+ *.env
142
+
143
+ # Private keys & certificates
144
+ *.pem
145
+ *.key
146
+ *.p12
147
+ *.pfx
148
+
149
+ # Credentials
150
+ *credentials*
151
+ *.secrets.json
152
+ secrets.yml
153
+ secrets.yaml
154
+
155
+ # Service accounts
156
+ **/service-account*.json
157
+ *-firebase-adminsdk-*.json
158
+
159
+ # AWS
160
+ .aws/credentials
161
+
162
+ # Database files
163
+ *.sqlite
164
+ *.sqlite3
165
+ *.db
166
+
167
+ # Logs (may contain sensitive data)
168
+ *.log
169
+ logs/
170
+ `;
171
+
172
+ return patterns.trim();
173
+ }
174
+
175
+ // =============================================================================
176
+ // SECURITY HEADERS HANDLING
177
+ // =============================================================================
178
+
179
+ async function handleSecurityHeaders(targetDir, force, results) {
180
+ const sourcePath = path.join(PACKAGE_ROOT, 'configs', 'nextjs-security-headers.js');
181
+ const targetPath = path.join(targetDir, 'security-headers.config.js');
182
+
183
+ // Check if source exists
184
+ if (!fs.existsSync(sourcePath)) {
185
+ results.errors.push({
186
+ file: 'security-headers.config.js',
187
+ error: 'Source file not found in ship-safe package'
188
+ });
189
+ return;
190
+ }
191
+
192
+ // Detect if this is a Next.js project
193
+ const packageJsonPath = path.join(targetDir, 'package.json');
194
+ let isNextProject = false;
195
+
196
+ if (fs.existsSync(packageJsonPath)) {
197
+ try {
198
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
199
+ isNextProject = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
200
+ } catch {
201
+ // Ignore parse errors
202
+ }
203
+ }
204
+
205
+ // Check if target exists
206
+ if (fs.existsSync(targetPath) && !force) {
207
+ results.skipped.push('security-headers.config.js (already exists, use -f to overwrite)');
208
+ return;
209
+ }
210
+
211
+ // Copy the file
212
+ const content = fs.readFileSync(sourcePath, 'utf-8');
213
+ fs.writeFileSync(targetPath, content);
214
+ results.copied.push('security-headers.config.js');
215
+
216
+ // Show integration instructions
217
+ if (isNextProject) {
218
+ console.log(chalk.cyan('\nNext.js detected! Add to your next.config.js:\n'));
219
+ console.log(chalk.gray(' const { securityHeadersConfig } = require(\'./security-headers.config.js\');'));
220
+ console.log(chalk.gray(' module.exports = { ...securityHeadersConfig, /* your config */ };'));
221
+ console.log();
222
+ }
223
+ }
224
+
225
+ // =============================================================================
226
+ // AGENT FILE HANDLING
227
+ // =============================================================================
228
+
229
+ // Marker used for idempotency — if this string is present we already added our section.
230
+ const AGENT_MARKER = '<!-- ship-safe -->';
231
+
232
+ // The security rules block appended to existing agent instruction files.
233
+ const AGENT_SECTION = `
234
+ ## Security (ship-safe)
235
+ ${AGENT_MARKER}
236
+ - Always run \`npx ship-safe scan .\` before committing code
237
+ - Never hardcode API keys, tokens, or credentials in source files — use environment variables
238
+ - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
239
+ `;
240
+
241
+ // Standalone content for a new Cursor rules file.
242
+ const CURSOR_RULE_CONTENT = `---
243
+ description: Prevent API key leaks and credential exposure in generated code
244
+ alwaysApply: true
245
+ ---
246
+
247
+ - Always run \`npx ship-safe scan .\` before committing code
248
+ - Never hardcode API keys, tokens, or credentials in source files — use environment variables
249
+ - If hardcoded secrets are found or generated, run \`npx ship-safe remediate\` to auto-fix them
250
+ `;
251
+
252
+ async function handleAgentFiles(targetDir, force, results) {
253
+ // Files where we append a section if they already exist, or create if they don't.
254
+ const appendTargets = [
255
+ { file: 'CLAUDE.md', label: 'CLAUDE.md' },
256
+ { file: '.windsurfrules', label: '.windsurfrules' },
257
+ { file: path.join('.github', 'copilot-instructions.md'), label: '.github/copilot-instructions.md' },
258
+ ];
259
+
260
+ for (const { file, label } of appendTargets) {
261
+ const targetPath = path.join(targetDir, file);
262
+
263
+ if (fs.existsSync(targetPath)) {
264
+ const existing = fs.readFileSync(targetPath, 'utf-8');
265
+ if (existing.includes(AGENT_MARKER)) {
266
+ results.skipped.push(`${label} (already contains ship-safe rules)`);
267
+ continue;
268
+ }
269
+ if (force || true) { // always append unless already present
270
+ fs.writeFileSync(targetPath, existing.trimEnd() + '\n' + AGENT_SECTION);
271
+ results.merged.push(label);
272
+ }
273
+ } else {
274
+ // Ensure parent directory exists (e.g. .github/)
275
+ const parentDir = path.dirname(targetPath);
276
+ if (!fs.existsSync(parentDir)) {
277
+ fs.mkdirSync(parentDir, { recursive: true });
278
+ }
279
+ fs.writeFileSync(targetPath, AGENT_SECTION.trim() + '\n');
280
+ results.copied.push(label);
281
+ }
282
+ }
283
+
284
+ // Cursor rules — dedicated file, no merging needed.
285
+ const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
286
+ const cursorRulesFile = path.join(cursorRulesDir, 'ship-safe.mdc');
287
+
288
+ if (fs.existsSync(cursorRulesFile) && !force) {
289
+ results.skipped.push('.cursor/rules/ship-safe.mdc (already exists, use -f to overwrite)');
290
+ } else {
291
+ if (!fs.existsSync(cursorRulesDir)) {
292
+ fs.mkdirSync(cursorRulesDir, { recursive: true });
293
+ }
294
+ fs.writeFileSync(cursorRulesFile, CURSOR_RULE_CONTENT.trim() + '\n');
295
+ results.copied.push('.cursor/rules/ship-safe.mdc');
296
+ }
297
+ }
298
+
299
+ // =============================================================================
300
+ // SUMMARY
301
+ // =============================================================================
302
+
303
+ function printSummary(results) {
304
+ console.log();
305
+ console.log(chalk.cyan('='.repeat(60)));
306
+ console.log(chalk.cyan.bold(' Summary'));
307
+ console.log(chalk.cyan('='.repeat(60)));
308
+ console.log();
309
+
310
+ if (results.copied.length > 0) {
311
+ console.log(chalk.green.bold('Created:'));
312
+ for (const file of results.copied) {
313
+ console.log(chalk.green(` \u2714 ${file}`));
314
+ }
315
+ console.log();
316
+ }
317
+
318
+ if (results.merged.length > 0) {
319
+ console.log(chalk.blue.bold('Merged:'));
320
+ for (const file of results.merged) {
321
+ console.log(chalk.blue(` \u2194 ${file} (appended ship-safe patterns)`));
322
+ }
323
+ console.log();
324
+ }
325
+
326
+ if (results.skipped.length > 0) {
327
+ console.log(chalk.yellow.bold('Skipped:'));
328
+ for (const file of results.skipped) {
329
+ console.log(chalk.yellow(` \u2192 ${file}`));
330
+ }
331
+ console.log();
332
+ }
333
+
334
+ if (results.errors.length > 0) {
335
+ console.log(chalk.red.bold('Errors:'));
336
+ for (const { file, error } of results.errors) {
337
+ console.log(chalk.red(` \u2718 ${file}: ${error}`));
338
+ }
339
+ console.log();
340
+ }
341
+
342
+ // Next steps
343
+ console.log(chalk.cyan('Next steps:'));
344
+ console.log(chalk.white(' 1.') + ' Review the copied files and customize for your project');
345
+ console.log(chalk.white(' 2.') + ' Run ' + chalk.cyan('npx ship-safe scan .') + ' to check for secrets');
346
+ console.log(chalk.white(' 3.') + ' Run ' + chalk.cyan('npx ship-safe checklist') + ' before launching');
347
+ console.log();
348
+ console.log(chalk.cyan('='.repeat(60)));
349
+ }