@nlaprell/shipit 1.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.
Files changed (160) hide show
  1. package/.cursor/commands/create_intent_from_issue.md +28 -0
  2. package/.cursor/commands/create_pr.md +28 -0
  3. package/.cursor/commands/dashboard.md +39 -0
  4. package/.cursor/commands/deploy.md +152 -0
  5. package/.cursor/commands/drift_check.md +36 -0
  6. package/.cursor/commands/fix.md +39 -0
  7. package/.cursor/commands/generate_release_plan.md +31 -0
  8. package/.cursor/commands/generate_roadmap.md +38 -0
  9. package/.cursor/commands/help.md +37 -0
  10. package/.cursor/commands/init_project.md +26 -0
  11. package/.cursor/commands/kill.md +72 -0
  12. package/.cursor/commands/new_intent.md +68 -0
  13. package/.cursor/commands/pr.md +77 -0
  14. package/.cursor/commands/revert-plan.md +58 -0
  15. package/.cursor/commands/risk.md +64 -0
  16. package/.cursor/commands/rollback.md +43 -0
  17. package/.cursor/commands/scope_project.md +53 -0
  18. package/.cursor/commands/ship.md +345 -0
  19. package/.cursor/commands/status.md +71 -0
  20. package/.cursor/commands/suggest.md +44 -0
  21. package/.cursor/commands/test_shipit.md +197 -0
  22. package/.cursor/commands/verify.md +50 -0
  23. package/.cursor/rules/architect.mdc +84 -0
  24. package/.cursor/rules/assumption-extractor.mdc +95 -0
  25. package/.cursor/rules/docs.mdc +66 -0
  26. package/.cursor/rules/implementer.mdc +112 -0
  27. package/.cursor/rules/pm.mdc +136 -0
  28. package/.cursor/rules/qa.mdc +97 -0
  29. package/.cursor/rules/security.mdc +90 -0
  30. package/.cursor/rules/steward.mdc +99 -0
  31. package/.cursor/rules/test-runner.mdc +196 -0
  32. package/AGENTS.md +121 -0
  33. package/README.md +264 -0
  34. package/_system/architecture/CANON.md +159 -0
  35. package/_system/architecture/invariants.yml +87 -0
  36. package/_system/architecture/project-schema.json +98 -0
  37. package/_system/architecture/workflow-state-layout.md +68 -0
  38. package/_system/artifacts/SYSTEM_STATE.md +43 -0
  39. package/_system/artifacts/confidence-calibration.json +16 -0
  40. package/_system/artifacts/dependencies.md +46 -0
  41. package/_system/artifacts/framework-files-manifest.json +179 -0
  42. package/_system/artifacts/usage.json +1 -0
  43. package/_system/behaviors/DO_RELEASE.md +371 -0
  44. package/_system/behaviors/DO_RELEASE_AI.md +329 -0
  45. package/_system/behaviors/PREPARE_RELEASE.md +373 -0
  46. package/_system/behaviors/PREPARE_RELEASE_AI.md +234 -0
  47. package/_system/behaviors/WORK_ROOT_PLATFORM_ISSUES.md +140 -0
  48. package/_system/behaviors/WORK_TEST_PLAN_ISSUES.md +380 -0
  49. package/_system/do-not-repeat/abandoned-designs.md +18 -0
  50. package/_system/do-not-repeat/bad-patterns.md +19 -0
  51. package/_system/do-not-repeat/failed-experiments.md +18 -0
  52. package/_system/do-not-repeat/rejected-libraries.md +19 -0
  53. package/_system/drift/baselines.md +49 -0
  54. package/_system/drift/metrics.md +33 -0
  55. package/_system/golden-data/.gitkeep +0 -0
  56. package/_system/golden-data/README.md +47 -0
  57. package/_system/reports/mutation/mutation.html +492 -0
  58. package/_system/security/audit-allowlist.json +4 -0
  59. package/bin/create-shipit-app +29 -0
  60. package/bin/shipit +183 -0
  61. package/cli/src/commands/check.js +82 -0
  62. package/cli/src/commands/create.js +195 -0
  63. package/cli/src/commands/init.js +267 -0
  64. package/cli/src/commands/upgrade.js +196 -0
  65. package/cli/src/utils/config.js +27 -0
  66. package/cli/src/utils/file-copy.js +144 -0
  67. package/cli/src/utils/gitignore-merge.js +44 -0
  68. package/cli/src/utils/manifest.js +105 -0
  69. package/cli/src/utils/package-json-merge.js +163 -0
  70. package/cli/src/utils/project-json-merge.js +57 -0
  71. package/cli/src/utils/prompts.js +30 -0
  72. package/cli/src/utils/stack-detection.js +56 -0
  73. package/cli/src/utils/stack-files.js +364 -0
  74. package/cli/src/utils/upgrade-backup.js +159 -0
  75. package/cli/src/utils/version.js +64 -0
  76. package/dashboard-app/README.md +73 -0
  77. package/dashboard-app/eslint.config.js +23 -0
  78. package/dashboard-app/index.html +13 -0
  79. package/dashboard-app/package.json +30 -0
  80. package/dashboard-app/pnpm-lock.yaml +2721 -0
  81. package/dashboard-app/public/dashboard.json +66 -0
  82. package/dashboard-app/public/vite.svg +1 -0
  83. package/dashboard-app/src/App.css +141 -0
  84. package/dashboard-app/src/App.tsx +155 -0
  85. package/dashboard-app/src/assets/react.svg +1 -0
  86. package/dashboard-app/src/index.css +68 -0
  87. package/dashboard-app/src/main.tsx +10 -0
  88. package/dashboard-app/tsconfig.app.json +28 -0
  89. package/dashboard-app/tsconfig.json +4 -0
  90. package/dashboard-app/tsconfig.node.json +26 -0
  91. package/dashboard-app/vite.config.ts +7 -0
  92. package/package.json +116 -0
  93. package/scripts/README.md +70 -0
  94. package/scripts/audit-check.sh +125 -0
  95. package/scripts/calibration-report.sh +198 -0
  96. package/scripts/check-readiness.sh +155 -0
  97. package/scripts/collect-metrics.sh +116 -0
  98. package/scripts/command-manifest.yml +131 -0
  99. package/scripts/create-test-plan-issue.sh +110 -0
  100. package/scripts/dashboard-start.sh +16 -0
  101. package/scripts/deploy.sh +170 -0
  102. package/scripts/drift-check.sh +93 -0
  103. package/scripts/execute-rollback.sh +177 -0
  104. package/scripts/export-dashboard-json.js +208 -0
  105. package/scripts/fix-intents.sh +239 -0
  106. package/scripts/generate-dashboard.sh +136 -0
  107. package/scripts/generate-docs.sh +279 -0
  108. package/scripts/generate-project-context.sh +142 -0
  109. package/scripts/generate-release-plan.sh +443 -0
  110. package/scripts/generate-roadmap.sh +189 -0
  111. package/scripts/generate-system-state.sh +95 -0
  112. package/scripts/gh/create-intent-from-issue.sh +82 -0
  113. package/scripts/gh/create-issue-from-intent.sh +59 -0
  114. package/scripts/gh/create-pr.sh +41 -0
  115. package/scripts/gh/link-issue.sh +44 -0
  116. package/scripts/gh/on-ship-update-issue.sh +42 -0
  117. package/scripts/headless/README.md +8 -0
  118. package/scripts/headless/call-llm.js +109 -0
  119. package/scripts/headless/run-phase.sh +99 -0
  120. package/scripts/help.sh +271 -0
  121. package/scripts/init-project.sh +976 -0
  122. package/scripts/kill-intent.sh +125 -0
  123. package/scripts/lib/common.sh +29 -0
  124. package/scripts/lib/intent.sh +61 -0
  125. package/scripts/lib/progress.sh +57 -0
  126. package/scripts/lib/suggest-next.sh +131 -0
  127. package/scripts/lib/validate-intents.sh +240 -0
  128. package/scripts/lib/verify-outputs.sh +55 -0
  129. package/scripts/lib/workflow_state.sh +201 -0
  130. package/scripts/new-intent.sh +271 -0
  131. package/scripts/publish-npm.sh +28 -0
  132. package/scripts/scope-project.sh +380 -0
  133. package/scripts/setup-worktrees.sh +125 -0
  134. package/scripts/status.sh +278 -0
  135. package/scripts/suggest.sh +173 -0
  136. package/scripts/test-headless.sh +47 -0
  137. package/scripts/test-shipit.sh +52 -0
  138. package/scripts/test-workflow-state.sh +49 -0
  139. package/scripts/usage-report.sh +47 -0
  140. package/scripts/usage.sh +58 -0
  141. package/scripts/validate-cursor.sh +151 -0
  142. package/scripts/validate-project.sh +71 -0
  143. package/scripts/validate-vscode.sh +146 -0
  144. package/scripts/verify.sh +153 -0
  145. package/scripts/workflow-orchestrator.sh +97 -0
  146. package/scripts/workflow-templates/01_analysis.md.tpl +25 -0
  147. package/scripts/workflow-templates/02_plan.md.tpl +30 -0
  148. package/scripts/workflow-templates/03_implementation.md.tpl +25 -0
  149. package/scripts/workflow-templates/04_verification.md.tpl +29 -0
  150. package/scripts/workflow-templates/05_release_notes.md.tpl +16 -0
  151. package/scripts/workflow-templates/05_verification_legacy.md.tpl +6 -0
  152. package/scripts/workflow-templates/active.md.tpl +18 -0
  153. package/scripts/workflow-templates/phases.yml +39 -0
  154. package/stryker.conf.json +8 -0
  155. package/work/intent/templates/api-endpoint.md +124 -0
  156. package/work/intent/templates/bugfix.md +116 -0
  157. package/work/intent/templates/frontend-feature.md +115 -0
  158. package/work/intent/templates/generic.md +122 -0
  159. package/work/intent/templates/infra-change.md +121 -0
  160. package/work/intent/templates/refactor.md +116 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * shipit init command - Attach ShipIt to existing project
3
+ */
4
+
5
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
6
+ import fsExtra from 'fs-extra';
7
+
8
+ const { mkdirSync } = fsExtra;
9
+ import { join, resolve } from 'path';
10
+ import { readManifest, getFrameworkRoot } from '../utils/manifest.js';
11
+ import { detectTechStack, isValidStack } from '../utils/stack-detection.js';
12
+ import { copyFrameworkFiles } from '../utils/file-copy.js';
13
+ import { mergePackageJson, getShipitScripts, getShipitDevDependencies } from '../utils/package-json-merge.js';
14
+ import { createReadlineInterface, promptUser } from '../utils/prompts.js';
15
+ import { readConfig } from '../utils/config.js';
16
+ import { createCIWorkflow } from '../utils/stack-files.js';
17
+
18
+ /**
19
+ * Check if ShipIt is already initialized
20
+ * @param {string} projectPath - Project directory path
21
+ * @returns {boolean} True if ShipIt is initialized
22
+ */
23
+ function isShipItInitialized(projectPath) {
24
+ const projectJsonPath = join(projectPath, 'project.json');
25
+ const canonPath = join(projectPath, '_system', 'architecture', 'CANON.md');
26
+ return existsSync(projectJsonPath) || existsSync(canonPath);
27
+ }
28
+
29
+ /**
30
+ * Read description from existing project files
31
+ * @param {string} projectPath - Project directory path
32
+ * @param {string} stack - Tech stack
33
+ * @returns {string|null} Description or null
34
+ */
35
+ function readDescription(projectPath, stack) {
36
+ if (stack === 'typescript-nodejs') {
37
+ const packageJsonPath = join(projectPath, 'package.json');
38
+ if (existsSync(packageJsonPath)) {
39
+ try {
40
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
41
+ return pkg.description || null;
42
+ } catch (e) {
43
+ // Ignore errors
44
+ }
45
+ }
46
+ } else if (stack === 'python') {
47
+ const pyprojectPath = join(projectPath, 'pyproject.toml');
48
+ if (existsSync(pyprojectPath)) {
49
+ // Simple extraction (could use toml parser)
50
+ const content = readFileSync(pyprojectPath, 'utf-8');
51
+ const match = content.match(/description\s*=\s*["']([^"']+)["']/);
52
+ if (match) {
53
+ return match[1];
54
+ }
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Create project.json
62
+ * @param {string} projectPath - Project directory path
63
+ * @param {object} metadata - Project metadata
64
+ */
65
+ function createProjectJson(projectPath, metadata) {
66
+ const projectJsonPath = join(projectPath, 'project.json');
67
+
68
+ const projectJson = {
69
+ name: metadata.name,
70
+ description: metadata.description,
71
+ version: metadata.version || '0.1.0',
72
+ techStack: metadata.techStack,
73
+ created: new Date().toISOString(),
74
+ highRiskDomains: metadata.highRiskDomains || [],
75
+ settings: {
76
+ humanResponseTime: 'minutes',
77
+ confidenceThreshold: 0.7,
78
+ testCoverageMinimum: 80
79
+ },
80
+ shipitVersion: metadata.shipitVersion || '1.0.0'
81
+ };
82
+
83
+ writeFileSync(projectJsonPath, JSON.stringify(projectJson, null, 2) + '\n', 'utf-8');
84
+ }
85
+
86
+ /**
87
+ * Create .override directory structure
88
+ * @param {string} projectPath - Project directory path
89
+ */
90
+ function createOverrideDirectory(projectPath) {
91
+ const overrideDirs = [
92
+ '.override/rules',
93
+ '.override/commands',
94
+ '.override/scripts',
95
+ '.override/config'
96
+ ];
97
+
98
+ for (const dir of overrideDirs) {
99
+ const dirPath = join(projectPath, dir);
100
+ if (!existsSync(dirPath)) {
101
+ mkdirSync(dirPath, { recursive: true });
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Init command implementation
108
+ * @param {object} options - Command options
109
+ */
110
+ export async function initCommand(options) {
111
+ const projectPath = resolve(options.path || process.cwd());
112
+ const nonInteractive = options.nonInteractive || process.env.SHIPIT_NON_INTERACTIVE === '1';
113
+ const force = options.force || false;
114
+
115
+ // Check if ShipIt already initialized
116
+ if (isShipItInitialized(projectPath) && !force) {
117
+ if (nonInteractive) {
118
+ throw new Error('ShipIt already initialized. Use --force to re-initialize or shipit upgrade to update.');
119
+ }
120
+ const rl = createReadlineInterface();
121
+ const answer = await promptUser(rl, 'ShipIt already initialized. Use "shipit upgrade" instead? (y/N): ');
122
+ rl.close();
123
+ if (answer.toLowerCase() !== 'y') {
124
+ console.log('Aborted.');
125
+ process.exit(0);
126
+ }
127
+ // User wants to upgrade instead - this should call upgrade command
128
+ // For now, just exit with message
129
+ console.log('Run "shipit upgrade" to update framework files.');
130
+ process.exit(0);
131
+ }
132
+
133
+ // Read config
134
+ const config = readConfig(projectPath);
135
+
136
+ // Detect or get tech stack
137
+ let techStack = options.techStack || config.techStack;
138
+ if (!techStack) {
139
+ techStack = detectTechStack(projectPath);
140
+ }
141
+
142
+ if (!techStack) {
143
+ if (nonInteractive) {
144
+ techStack = 'other'; // Default for non-interactive
145
+ } else {
146
+ const rl = createReadlineInterface();
147
+ console.log('Tech stack selection:');
148
+ console.log('1) TypeScript/Node.js (recommended)');
149
+ console.log('2) Python');
150
+ console.log('3) Other (manual setup)');
151
+ const choice = await promptUser(rl, 'Select tech stack [1=TS/Node, 2=Python, 3=Other]: ');
152
+ rl.close();
153
+
154
+ switch (choice.trim()) {
155
+ case '1': techStack = 'typescript-nodejs'; break;
156
+ case '2': techStack = 'python'; break;
157
+ case '3': techStack = 'other'; break;
158
+ default: techStack = 'typescript-nodejs';
159
+ }
160
+ }
161
+ }
162
+
163
+ if (!isValidStack(techStack)) {
164
+ throw new Error(`Invalid tech stack: ${techStack}. Must be typescript-nodejs, python, or other.`);
165
+ }
166
+
167
+ // Get description
168
+ let description = options.description || config.description;
169
+ if (!description) {
170
+ description = readDescription(projectPath, techStack);
171
+ }
172
+ if (!description && !nonInteractive) {
173
+ const rl = createReadlineInterface();
174
+ description = await promptUser(rl, 'Project description (short): ');
175
+ rl.close();
176
+ }
177
+ description = description || 'ShipIt project';
178
+
179
+ // Get high-risk domains
180
+ let highRisk = options.highRisk || config.highRisk;
181
+ if (!highRisk && !nonInteractive) {
182
+ const rl = createReadlineInterface();
183
+ console.log('High-Risk Domains (comma-separated, or "none"):');
184
+ console.log('Examples: auth, payments, permissions, infrastructure, pii');
185
+ highRisk = await promptUser(rl, 'High-risk domains: ');
186
+ rl.close();
187
+ }
188
+ highRisk = highRisk || 'none';
189
+ const highRiskArray = highRisk === 'none' ? [] : highRisk.split(',').map(s => s.trim());
190
+
191
+ // Read manifest
192
+ const manifest = readManifest();
193
+ const frameworkRoot = getFrameworkRoot();
194
+
195
+ console.log('Initializing ShipIt...');
196
+ console.log(` Tech stack: ${techStack}`);
197
+ console.log(` Description: ${description}`);
198
+
199
+ // Copy framework files
200
+ const copyResult = copyFrameworkFiles(frameworkRoot, projectPath, techStack, manifest, {
201
+ verbose: !nonInteractive,
202
+ dryRun: false
203
+ });
204
+
205
+ if (copyResult.errors.length > 0) {
206
+ console.error('Errors during file copy:');
207
+ copyResult.errors.forEach(err => console.error(` ${err}`));
208
+ throw new Error('Failed to copy some framework files');
209
+ }
210
+
211
+ // Merge package.json if exists
212
+ const packageJsonPath = join(projectPath, 'package.json');
213
+ if (existsSync(packageJsonPath) && techStack === 'typescript-nodejs') {
214
+ console.log('Merging package.json...');
215
+ const shipitScripts = getShipitScripts();
216
+ const shipitDevDeps = getShipitDevDependencies();
217
+ mergePackageJson(packageJsonPath, shipitScripts, shipitDevDeps, {
218
+ verbose: !nonInteractive
219
+ });
220
+ }
221
+
222
+ // Create project.json
223
+ const projectName = readProjectName(projectPath, techStack);
224
+ createProjectJson(projectPath, {
225
+ name: projectName,
226
+ description,
227
+ techStack,
228
+ highRiskDomains: highRiskArray,
229
+ shipitVersion: '1.0.0' // TODO: Get from package.json
230
+ });
231
+
232
+ // Create .override directory
233
+ createOverrideDirectory(projectPath);
234
+
235
+ // Create stack-specific CI workflow (only if .github/workflows doesn't exist or ci.yml doesn't exist)
236
+ const workflowsDir = join(projectPath, '.github', 'workflows');
237
+ const ciPath = join(workflowsDir, 'ci.yml');
238
+ if (!existsSync(ciPath)) {
239
+ createCIWorkflow(projectPath, techStack);
240
+ }
241
+
242
+ console.log('\n✓ ShipIt initialized successfully!');
243
+ console.log('\nNext steps:');
244
+ console.log('1. Run /scope-project to break down into features');
245
+ console.log('2. Review project.json and customize as needed');
246
+ console.log('3. Start creating intents with /new_intent');
247
+ }
248
+
249
+ /**
250
+ * Read project name from existing files
251
+ */
252
+ function readProjectName(projectPath, stack) {
253
+ if (stack === 'typescript-nodejs') {
254
+ const packageJsonPath = join(projectPath, 'package.json');
255
+ if (existsSync(packageJsonPath)) {
256
+ try {
257
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
258
+ return pkg.name || 'my-project';
259
+ } catch (e) {
260
+ // Ignore
261
+ }
262
+ }
263
+ }
264
+ // Default or extract from directory name
265
+ const dirName = projectPath.split('/').pop() || 'my-project';
266
+ return dirName;
267
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * shipit upgrade command - Upgrade ShipIt framework files
3
+ * shipit restore / shipit list-backups - Backup management
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { join, resolve } from 'path';
8
+ import { readManifest, getFrameworkRoot } from '../utils/manifest.js';
9
+ import { getInstalledShipItVersion, getProjectShipItVersion, compareVersions } from '../utils/version.js';
10
+ import {
11
+ isFileModified,
12
+ backupFile,
13
+ listBackups,
14
+ restoreFromBackup,
15
+ getFrameworkFileList
16
+ } from '../utils/upgrade-backup.js';
17
+ import { mergeProjectJson } from '../utils/project-json-merge.js';
18
+ import { mergeGitignore } from '../utils/gitignore-merge.js';
19
+ import { mergePackageJson, getShipitScripts, getShipitDevDependencies } from '../utils/package-json-merge.js';
20
+ import { createReadlineInterface, promptUser } from '../utils/prompts.js';
21
+ import fsExtra from 'fs-extra';
22
+
23
+ const { copySync, mkdirSync } = fsExtra;
24
+
25
+ const BACKUP_DIR = '._shipit_backup';
26
+
27
+ function isShipItInitialized(projectPath) {
28
+ return (
29
+ existsSync(join(projectPath, 'project.json')) ||
30
+ existsSync(join(projectPath, '_system', 'architecture', 'CANON.md'))
31
+ );
32
+ }
33
+
34
+ function getNeverCopiedSet(manifest) {
35
+ return new Set([
36
+ ...(manifest.neverCopied?.files || []),
37
+ ...(manifest.neverCopied?.directories || []),
38
+ '.override'
39
+ ]);
40
+ }
41
+
42
+ /**
43
+ * Upgrade command implementation
44
+ */
45
+ export async function upgradeCommand(options) {
46
+ const projectPath = resolve(options.path || process.cwd());
47
+ const backupDir = options.backupDir || BACKUP_DIR;
48
+ const dryRun = options.dryRun || false;
49
+ const force = options.force || false;
50
+
51
+ if (!existsSync(projectPath)) {
52
+ const err = new Error(`Directory not found: ${projectPath}`);
53
+ err.exitCode = 1;
54
+ throw err;
55
+ }
56
+
57
+ if (!isShipItInitialized(projectPath)) {
58
+ const err = new Error('ShipIt is not initialized in this project. Run "shipit init" first.');
59
+ err.exitCode = 3;
60
+ throw err;
61
+ }
62
+
63
+ const installedVersion = getInstalledShipItVersion();
64
+ const projectVersion = getProjectShipItVersion(projectPath);
65
+ const comparison = compareVersions(installedVersion, projectVersion || '0.0.0');
66
+
67
+ if (comparison === 'same') {
68
+ console.log(`Already up to date (ShipIt ${installedVersion}).`);
69
+ return;
70
+ }
71
+
72
+ if (comparison === 'older') {
73
+ console.warn(
74
+ `Warning: Project has ShipIt ${projectVersion || 'unknown'}, installed is ${installedVersion}. Project may be ahead.`
75
+ );
76
+ if (!force && !dryRun) {
77
+ const rl = createReadlineInterface();
78
+ const answer = await promptUser(rl, 'Continue anyway? (y/N): ');
79
+ rl.close();
80
+ if (answer.toLowerCase() !== 'y') {
81
+ console.log('Aborted.');
82
+ process.exit(0);
83
+ }
84
+ }
85
+ }
86
+
87
+ const manifest = readManifest();
88
+ const frameworkRoot = getFrameworkRoot();
89
+ const neverCopied = getNeverCopiedSet(manifest);
90
+ const frameworkFiles = getFrameworkFileList(frameworkRoot, manifest, neverCopied);
91
+
92
+ const modified = [];
93
+ for (const rel of frameworkFiles) {
94
+ if (isFileModified(projectPath, frameworkRoot, rel)) {
95
+ modified.push(rel);
96
+ }
97
+ }
98
+
99
+ if (modified.length > 0 && !force && !dryRun) {
100
+ console.log('The following framework files have been modified and will be backed up before replacement:');
101
+ modified.slice(0, 20).forEach((f) => console.log(` - ${f}`));
102
+ if (modified.length > 20) console.log(` ... and ${modified.length - 20} more`);
103
+ const rl = createReadlineInterface();
104
+ const answer = await promptUser(rl, 'Continue? (y/N): ');
105
+ rl.close();
106
+ if (answer.toLowerCase() !== 'y') {
107
+ console.log('Aborted.');
108
+ process.exit(0);
109
+ }
110
+ }
111
+
112
+ if (dryRun) {
113
+ console.log('\nDry run - would perform:');
114
+ console.log(` Backup ${modified.length} modified file(s)`);
115
+ console.log(` Replace ${frameworkFiles.length} framework file(s)`);
116
+ console.log(` Merge project.json (shipitVersion → ${installedVersion})`);
117
+ console.log(' Merge package.json if present');
118
+ console.log(' Merge .gitignore if present');
119
+ return;
120
+ }
121
+
122
+ // Backup modified files
123
+ for (const rel of modified) {
124
+ backupFile(projectPath, rel, backupDir);
125
+ if (options.verbose) console.log(` Backed up: ${rel}`);
126
+ }
127
+
128
+ // Replace all framework files
129
+ for (const rel of frameworkFiles) {
130
+ const src = join(frameworkRoot, rel);
131
+ const dest = join(projectPath, rel);
132
+ if (!existsSync(src)) continue;
133
+ mkdirSync(join(dest, '..'), { recursive: true });
134
+ copySync(src, dest, { overwrite: true });
135
+ }
136
+
137
+ // Merge project.json
138
+ mergeProjectJson(projectPath, installedVersion, { verbose: options.verbose });
139
+
140
+ // Merge package.json if present (use project's tech stack from project.json)
141
+ const projectJsonPath = join(projectPath, 'project.json');
142
+ let techStack = 'typescript-nodejs';
143
+ if (existsSync(projectJsonPath)) {
144
+ try {
145
+ const pj = JSON.parse(readFileSync(projectJsonPath, 'utf-8'));
146
+ techStack = pj.techStack || techStack;
147
+ } catch (_) {}
148
+ }
149
+ const packageJsonPath = join(projectPath, 'package.json');
150
+ if (existsSync(packageJsonPath) && techStack === 'typescript-nodejs') {
151
+ const shipitScripts = getShipitScripts();
152
+ const shipitDevDeps = getShipitDevDependencies();
153
+ mergePackageJson(packageJsonPath, shipitScripts, shipitDevDeps, { verbose: options.verbose });
154
+ }
155
+
156
+ // Merge .gitignore
157
+ mergeGitignore(projectPath, { verbose: options.verbose });
158
+
159
+ console.log(`\n✓ ShipIt upgraded to ${installedVersion}`);
160
+ if (modified.length > 0) {
161
+ console.log(` ${modified.length} modified file(s) backed up to ${backupDir}/`);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * List backups command
167
+ */
168
+ export async function listBackupsCommand(options) {
169
+ const projectPath = resolve(options.path || process.cwd());
170
+ const backupDir = options.backupDir || BACKUP_DIR;
171
+ const backups = listBackups(projectPath, backupDir);
172
+ if (backups.length === 0) {
173
+ console.log('No backups found.');
174
+ return;
175
+ }
176
+ console.log(`Backups in ${backupDir}/:\n`);
177
+ for (const b of backups) {
178
+ console.log(` ${b.relative} (${b.timestamp}) → ${b.original}`);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Restore from backup
184
+ */
185
+ export async function restoreCommand(backupPath, options) {
186
+ const projectPath = resolve(options.path || process.cwd());
187
+ const backupDir = options.backupDir || BACKUP_DIR;
188
+ const fullBackupPath = backupPath.startsWith('/') ? backupPath : resolve(projectPath, backupPath);
189
+ if (!existsSync(fullBackupPath)) {
190
+ const err = new Error(`Backup not found: ${backupPath}`);
191
+ err.exitCode = 1;
192
+ throw err;
193
+ }
194
+ restoreFromBackup(fullBackupPath, null, projectPath, backupDir, options.removeBackup || false);
195
+ console.log(`Restored: ${backupPath}`);
196
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Config file utilities (.shipitrc)
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ /**
9
+ * Read .shipitrc config file
10
+ * @param {string} projectPath - Project directory path
11
+ * @returns {object} Config object (empty if no config)
12
+ */
13
+ export function readConfig(projectPath) {
14
+ const configPath = join(projectPath, '.shipitrc');
15
+
16
+ if (!existsSync(configPath)) {
17
+ return {};
18
+ }
19
+
20
+ try {
21
+ const content = readFileSync(configPath, 'utf-8');
22
+ return JSON.parse(content);
23
+ } catch (error) {
24
+ console.warn(`Warning: Invalid .shipitrc file: ${error.message}`);
25
+ return {};
26
+ }
27
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * File copying utilities
3
+ */
4
+
5
+ import fsExtra from 'fs-extra';
6
+ import { join, dirname, relative } from 'path';
7
+ import { createHash } from 'crypto';
8
+ import { readFileSync } from 'fs';
9
+
10
+ const { copySync, mkdirSync, existsSync, statSync } = fsExtra;
11
+
12
+ /**
13
+ * Copy framework files to target directory
14
+ * @param {string} frameworkRoot - Framework root directory
15
+ * @param {string} targetDir - Target directory
16
+ * @param {string} stack - Tech stack (typescript-nodejs | python | other)
17
+ * @param {object} manifest - Framework files manifest
18
+ * @param {object} options - Options (verbose, dryRun)
19
+ * @returns {object} Summary of copied files
20
+ */
21
+ export function copyFrameworkFiles(frameworkRoot, targetDir, stack, manifest, options = {}) {
22
+ const { verbose = false, dryRun = false } = options;
23
+ const copied = [];
24
+ const skipped = [];
25
+ const errors = [];
26
+
27
+ // Get stack-specific files
28
+ const stackSpecific = manifest.stackSpecific[stack] || { files: [], directories: [] };
29
+ const stackFiles = new Set(stackSpecific.files || []);
30
+ const stackDirs = new Set(stackSpecific.directories || []);
31
+
32
+ // Get never-copied files/directories
33
+ const neverCopied = new Set([
34
+ ...(manifest.neverCopied?.files || []),
35
+ ...(manifest.neverCopied?.directories || []),
36
+ '.override' // Always skip override directory
37
+ ]);
38
+
39
+ // Copy framework-owned directories
40
+ for (const dir of manifest.frameworkOwned.directories || []) {
41
+ const sourcePath = join(frameworkRoot, dir);
42
+ const targetPath = join(targetDir, dir);
43
+
44
+ // Skip if never copied
45
+ if (neverCopied.has(dir) || dir.startsWith('.override')) {
46
+ skipped.push(dir);
47
+ continue;
48
+ }
49
+
50
+ // Skip stack-specific directories for other stacks
51
+ if (stackDirs.has(dir) && stack !== 'typescript-nodejs' && stack !== 'python') {
52
+ skipped.push(dir);
53
+ continue;
54
+ }
55
+
56
+ if (!existsSync(sourcePath)) {
57
+ errors.push(`Source directory not found: ${dir}`);
58
+ continue;
59
+ }
60
+
61
+ if (!dryRun) {
62
+ try {
63
+ mkdirSync(targetPath, { recursive: true });
64
+ copySync(sourcePath, targetPath, { overwrite: true });
65
+ copied.push(dir);
66
+ if (verbose) {
67
+ console.log(` Copied directory: ${dir}`);
68
+ }
69
+ } catch (error) {
70
+ errors.push(`Failed to copy ${dir}: ${error.message}`);
71
+ }
72
+ } else {
73
+ copied.push(dir);
74
+ }
75
+ }
76
+
77
+ // Copy framework-owned files
78
+ // Note: Some files like project.json are created, not copied
79
+ const createdFiles = new Set(['project.json']); // Files that are created, not copied from framework
80
+
81
+ for (const file of manifest.frameworkOwned.files || []) {
82
+ const sourcePath = join(frameworkRoot, file);
83
+ const targetPath = join(targetDir, file);
84
+
85
+ // Skip if never copied
86
+ if (neverCopied.has(file)) {
87
+ skipped.push(file);
88
+ continue;
89
+ }
90
+
91
+ // Skip files that are created (not copied)
92
+ if (createdFiles.has(file)) {
93
+ skipped.push(file);
94
+ continue;
95
+ }
96
+
97
+ // Skip stack-specific files for other stacks
98
+ if (stackFiles.has(file) && stack !== 'typescript-nodejs' && stack !== 'python') {
99
+ skipped.push(file);
100
+ continue;
101
+ }
102
+
103
+ if (!existsSync(sourcePath)) {
104
+ // Some files may not exist in framework (like project.json which is created)
105
+ if (createdFiles.has(file)) {
106
+ skipped.push(file);
107
+ continue;
108
+ }
109
+ errors.push(`Source file not found: ${file}`);
110
+ continue;
111
+ }
112
+
113
+ if (!dryRun) {
114
+ try {
115
+ mkdirSync(dirname(targetPath), { recursive: true });
116
+ copySync(sourcePath, targetPath, { overwrite: true });
117
+ copied.push(file);
118
+ if (verbose) {
119
+ console.log(` Copied file: ${file}`);
120
+ }
121
+ } catch (error) {
122
+ errors.push(`Failed to copy ${file}: ${error.message}`);
123
+ }
124
+ } else {
125
+ copied.push(file);
126
+ }
127
+ }
128
+
129
+ return { copied, skipped, errors };
130
+ }
131
+
132
+ /**
133
+ * Calculate file hash (SHA-256)
134
+ * @param {string} filePath - Path to file
135
+ * @returns {string} Hex hash
136
+ */
137
+ export function calculateFileHash(filePath) {
138
+ if (!existsSync(filePath)) {
139
+ return null;
140
+ }
141
+
142
+ const content = readFileSync(filePath);
143
+ return createHash('sha256').update(content).digest('hex');
144
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * .gitignore merge for upgrade - append ShipIt patterns if missing
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+
8
+ const SHIPIT_PATTERNS = [
9
+ '',
10
+ '# ShipIt',
11
+ '._shipit_backup/',
12
+ '_system/artifacts/usage.json',
13
+ ''
14
+ ];
15
+
16
+ /**
17
+ * Merge ShipIt patterns into .gitignore
18
+ * @param {string} projectPath - Project root
19
+ * @param {object} options - { dryRun, verbose }
20
+ * @returns {boolean} True if file was updated
21
+ */
22
+ export function mergeGitignore(projectPath, options = {}) {
23
+ const { dryRun = false, verbose = false } = options;
24
+ const gitignorePath = join(projectPath, '.gitignore');
25
+
26
+ let existing = '';
27
+ if (existsSync(gitignorePath)) {
28
+ existing = readFileSync(gitignorePath, 'utf-8');
29
+ }
30
+
31
+ const hasBackup = existing.includes('._shipit_backup');
32
+ if (hasBackup) {
33
+ if (verbose) console.log(' .gitignore already has ShipIt patterns');
34
+ return false;
35
+ }
36
+
37
+ const toAppend = SHIPIT_PATTERNS.join('\n');
38
+ const newContent = existing.trimEnd() ? `${existing.trimEnd()}\n${toAppend}\n` : `${toAppend}\n`;
39
+
40
+ if (!dryRun) {
41
+ writeFileSync(gitignorePath, newContent, 'utf-8');
42
+ }
43
+ return true;
44
+ }