@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,364 @@
1
+ /**
2
+ * Stack-specific file creation utilities
3
+ */
4
+
5
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import fsExtra from 'fs-extra';
8
+
9
+ const { copySync } = fsExtra;
10
+
11
+ /**
12
+ * Create TypeScript/Node.js project files
13
+ * @param {string} projectPath - Project directory path
14
+ * @param {string} projectName - Project name
15
+ * @param {string} description - Project description
16
+ * @param {object} shipitScripts - ShipIt scripts to include
17
+ * @param {object} shipitDevDeps - ShipIt devDependencies to include
18
+ */
19
+ export function createTypeScriptNodeFiles(projectPath, projectName, description, shipitScripts, shipitDevDeps) {
20
+ const normalizedName = projectName.toLowerCase().replace(/\s+/g, '-');
21
+
22
+ // Create package.json
23
+ const packageJson = {
24
+ name: normalizedName,
25
+ version: '0.1.0',
26
+ description: description,
27
+ type: 'module',
28
+ scripts: {
29
+ test: 'vitest run',
30
+ 'test:watch': 'vitest',
31
+ 'test:coverage': 'vitest run --coverage',
32
+ 'test:mutate': 'stryker run',
33
+ lint: 'eslint . --ext .ts',
34
+ typecheck: 'tsc --noEmit',
35
+ build: 'tsc',
36
+ dev: 'tsx watch src/index.ts',
37
+ ...shipitScripts
38
+ },
39
+ keywords: [],
40
+ author: '',
41
+ license: 'MIT',
42
+ devDependencies: {
43
+ '@types/node': '^20.10.0',
44
+ '@typescript-eslint/eslint-plugin': '^6.15.0',
45
+ '@typescript-eslint/parser': '^6.15.0',
46
+ '@stryker-mutator/core': '^8.0.0',
47
+ '@stryker-mutator/vitest-runner': '^8.0.0',
48
+ '@vitest/coverage-v8': '^1.0.4',
49
+ eslint: '^8.56.0',
50
+ prettier: '^3.1.1',
51
+ tsx: '^4.7.0',
52
+ typescript: '^5.3.3',
53
+ vitest: '^1.0.4',
54
+ ...shipitDevDeps
55
+ },
56
+ dependencies: {}
57
+ };
58
+
59
+ writeFileSync(
60
+ join(projectPath, 'package.json'),
61
+ JSON.stringify(packageJson, null, 2) + '\n',
62
+ 'utf-8'
63
+ );
64
+
65
+ // Create tsconfig.json
66
+ const tsconfig = {
67
+ compilerOptions: {
68
+ target: 'ES2022',
69
+ module: 'ESNext',
70
+ lib: ['ES2022'],
71
+ moduleResolution: 'node',
72
+ strict: true,
73
+ esModuleInterop: true,
74
+ skipLibCheck: true,
75
+ forceConsistentCasingInFileNames: true,
76
+ resolveJsonModule: true,
77
+ outDir: './dist',
78
+ rootDir: './src',
79
+ declaration: true,
80
+ declarationMap: true,
81
+ sourceMap: true,
82
+ noUnusedLocals: true,
83
+ noUnusedParameters: true,
84
+ noImplicitReturns: true,
85
+ noFallthroughCasesInSwitch: true
86
+ },
87
+ include: ['src/**/*'],
88
+ exclude: ['node_modules', 'dist', '**/*.test.ts', '**/*.spec.ts']
89
+ };
90
+
91
+ writeFileSync(
92
+ join(projectPath, 'tsconfig.json'),
93
+ JSON.stringify(tsconfig, null, 2) + '\n',
94
+ 'utf-8'
95
+ );
96
+
97
+ // Create tsconfig.eslint.json
98
+ const tsconfigEslint = {
99
+ extends: './tsconfig.json',
100
+ include: ['src/**/*.ts', 'tests/**/*.ts', 'scripts/**/*.ts', '*.config.ts'],
101
+ exclude: ['node_modules', 'dist']
102
+ };
103
+
104
+ writeFileSync(
105
+ join(projectPath, 'tsconfig.eslint.json'),
106
+ JSON.stringify(tsconfigEslint, null, 2) + '\n',
107
+ 'utf-8'
108
+ );
109
+
110
+ // Create .eslintrc.json
111
+ const eslintrc = {
112
+ root: true,
113
+ parser: '@typescript-eslint/parser',
114
+ parserOptions: {
115
+ ecmaVersion: 2022,
116
+ sourceType: 'module',
117
+ project: ['./tsconfig.eslint.json'],
118
+ tsconfigRootDir: '.'
119
+ },
120
+ extends: [
121
+ 'eslint:recommended',
122
+ 'plugin:@typescript-eslint/recommended',
123
+ 'plugin:@typescript-eslint/strict-type-checked'
124
+ ],
125
+ plugins: ['@typescript-eslint'],
126
+ rules: {
127
+ '@typescript-eslint/no-explicit-any': 'error',
128
+ '@typescript-eslint/ban-types': 'error',
129
+ 'no-eval': 'error',
130
+ 'no-console': ['error', { allow: ['warn', 'error'] }]
131
+ },
132
+ overrides: [
133
+ {
134
+ files: ['scripts/**/*.ts', 'tests/**/*.ts', '**/*.config.ts'],
135
+ rules: {
136
+ 'no-console': 'off',
137
+ '@typescript-eslint/no-unsafe-assignment': 'off',
138
+ '@typescript-eslint/no-unsafe-call': 'off',
139
+ '@typescript-eslint/no-unsafe-member-access': 'off',
140
+ '@typescript-eslint/no-unsafe-return': 'off',
141
+ '@typescript-eslint/no-unsafe-argument': 'off'
142
+ }
143
+ }
144
+ ],
145
+ ignorePatterns: ['dist', 'node_modules', '*.config.js']
146
+ };
147
+
148
+ writeFileSync(
149
+ join(projectPath, '.eslintrc.json'),
150
+ JSON.stringify(eslintrc, null, 2) + '\n',
151
+ 'utf-8'
152
+ );
153
+
154
+ // Create vitest.config.ts
155
+ const vitestConfig = `import { defineConfig } from 'vitest/config';
156
+
157
+ export default defineConfig({
158
+ test: {
159
+ globals: true,
160
+ environment: 'node',
161
+ include: ['tests/**/*.test.ts'],
162
+ coverage: {
163
+ provider: 'v8',
164
+ reporter: ['text', 'json', 'html', 'lcov'],
165
+ exclude: [
166
+ 'node_modules/',
167
+ 'dist/',
168
+ 'tests/',
169
+ '**/*.test.ts',
170
+ '**/*.spec.ts',
171
+ '**/*.config.ts',
172
+ ],
173
+ },
174
+ },
175
+ });
176
+ `;
177
+
178
+ writeFileSync(join(projectPath, 'vitest.config.ts'), vitestConfig, 'utf-8');
179
+
180
+ // Create .npmrc
181
+ writeFileSync(join(projectPath, '.npmrc'), 'audit-level=high\n', 'utf-8');
182
+
183
+ // Create src/index.ts
184
+ const srcDir = join(projectPath, 'src');
185
+ if (!existsSync(srcDir)) {
186
+ mkdirSync(srcDir, { recursive: true });
187
+ }
188
+ const indexTs = `// ${projectName}
189
+ // ${description}
190
+
191
+ export const projectName = '${projectName}';
192
+ export const projectDescription = '${description}';
193
+ `;
194
+ writeFileSync(join(srcDir, 'index.ts'), indexTs, 'utf-8');
195
+ }
196
+
197
+ /**
198
+ * Create Python project files
199
+ * @param {string} projectPath - Project directory path
200
+ * @param {string} projectName - Project name
201
+ * @param {string} description - Project description
202
+ */
203
+ export function createPythonFiles(projectPath, projectName, description) {
204
+ // Create pyproject.toml if not exists
205
+ const pyprojectPath = join(projectPath, 'pyproject.toml');
206
+ if (!existsSync(pyprojectPath)) {
207
+ const pyprojectContent = `[project]
208
+ name = "${projectName}"
209
+ description = "${description}"
210
+ version = "0.1.0"
211
+ requires-python = ">=3.10"
212
+
213
+ [project.optional-dependencies]
214
+ dev = [
215
+ "pytest>=7.0.0",
216
+ "ruff>=0.1.0",
217
+ "mypy>=1.0.0",
218
+ ]
219
+ `;
220
+ writeFileSync(pyprojectPath, pyprojectContent, 'utf-8');
221
+ }
222
+
223
+ // Create requirements.txt if not exists
224
+ const requirementsPath = join(projectPath, 'requirements.txt');
225
+ if (!existsSync(requirementsPath)) {
226
+ writeFileSync(requirementsPath, '# Project dependencies\n', 'utf-8');
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Create Other stack project files
232
+ * @param {string} projectPath - Project directory path
233
+ */
234
+ export function createOtherStackFiles(projectPath) {
235
+ // Create minimal .gitignore
236
+ const gitignorePath = join(projectPath, '.gitignore');
237
+ if (!existsSync(gitignorePath)) {
238
+ writeFileSync(gitignorePath, '*.log\n.DS_Store\n.env\n', 'utf-8');
239
+ }
240
+
241
+ // Create src directory
242
+ const srcPath = join(projectPath, 'src');
243
+ if (!existsSync(srcPath)) {
244
+ mkdirSync(srcPath, { recursive: true });
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Create stack-specific CI workflow
250
+ * @param {string} projectPath - Project directory path
251
+ * @param {string} stack - Tech stack (typescript-nodejs | python | other)
252
+ */
253
+ export function createCIWorkflow(projectPath, stack) {
254
+ const workflowsDir = join(projectPath, '.github', 'workflows');
255
+ if (!existsSync(workflowsDir)) {
256
+ mkdirSync(workflowsDir, { recursive: true });
257
+ }
258
+
259
+ const ciPath = join(workflowsDir, 'ci.yml');
260
+
261
+ let ciContent = '';
262
+
263
+ if (stack === 'typescript-nodejs') {
264
+ ciContent = `name: CI
265
+
266
+ on:
267
+ push:
268
+ branches: [main, develop]
269
+ pull_request:
270
+ branches: [main, develop]
271
+
272
+ jobs:
273
+ lint:
274
+ name: Lint & Type Check
275
+ runs-on: ubuntu-latest
276
+ steps:
277
+ - uses: actions/checkout@v4
278
+ - uses: pnpm/action-setup@v2
279
+ with:
280
+ version: 10.11.0
281
+ - uses: actions/setup-node@v4
282
+ with:
283
+ node-version: '20'
284
+ cache: 'pnpm'
285
+ - run: pnpm install --frozen-lockfile
286
+ - run: pnpm lint
287
+ - run: pnpm typecheck
288
+
289
+ test:
290
+ name: Test Suite
291
+ runs-on: ubuntu-latest
292
+ steps:
293
+ - uses: actions/checkout@v4
294
+ - uses: pnpm/action-setup@v2
295
+ with:
296
+ version: 10.11.0
297
+ - uses: actions/setup-node@v4
298
+ with:
299
+ node-version: '20'
300
+ cache: 'pnpm'
301
+ - run: pnpm install --frozen-lockfile
302
+ - run: pnpm test:coverage
303
+ - name: Upload coverage
304
+ uses: codecov/codecov-action@v3
305
+ if: always()
306
+ with:
307
+ files: ./coverage/lcov.info
308
+ fail_ci_if_error: false
309
+ `;
310
+ } else if (stack === 'python') {
311
+ ciContent = `name: CI
312
+
313
+ on:
314
+ push:
315
+ branches: [main, develop]
316
+ pull_request:
317
+ branches: [main, develop]
318
+
319
+ jobs:
320
+ lint:
321
+ name: Lint & Type Check
322
+ runs-on: ubuntu-latest
323
+ steps:
324
+ - uses: actions/checkout@v4
325
+ - uses: actions/setup-python@v4
326
+ with:
327
+ python-version: '3.10'
328
+ - run: pip install -r requirements.txt
329
+ - run: ruff check .
330
+ - run: mypy .
331
+
332
+ test:
333
+ name: Test Suite
334
+ runs-on: ubuntu-latest
335
+ steps:
336
+ - uses: actions/checkout@v4
337
+ - uses: actions/setup-python@v4
338
+ with:
339
+ python-version: '3.10'
340
+ - run: pip install -r requirements.txt
341
+ - run: pytest --cov
342
+ `;
343
+ } else {
344
+ // Other stack - minimal template
345
+ ciContent = `name: CI
346
+
347
+ on:
348
+ push:
349
+ branches: [main, develop]
350
+ pull_request:
351
+ branches: [main, develop]
352
+
353
+ jobs:
354
+ build:
355
+ name: Build
356
+ runs-on: ubuntu-latest
357
+ steps:
358
+ - uses: actions/checkout@v4
359
+ - run: echo "Add your build steps here"
360
+ `;
361
+ }
362
+
363
+ writeFileSync(ciPath, ciContent, 'utf-8');
364
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Upgrade backup and file-modification detection
3
+ */
4
+
5
+ import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
6
+ import { join, dirname } from 'path';
7
+ import { createHash } from 'crypto';
8
+ import fsExtra from 'fs-extra';
9
+
10
+ const { copySync, mkdirSync } = fsExtra;
11
+
12
+ const BACKUP_DIR = '._shipit_backup';
13
+ const TIMESTAMP_FORMAT = (d) => d.toISOString().replace(/[-:]/g, '').slice(0, 15); // YYYYMMDDTHHMMSS
14
+
15
+ /**
16
+ * Calculate SHA-256 hash of file
17
+ * @param {string} filePath - Absolute path
18
+ * @returns {string|null} Hex hash or null if not found
19
+ */
20
+ export function calculateFileHash(filePath) {
21
+ if (!existsSync(filePath)) return null;
22
+ try {
23
+ const content = readFileSync(filePath);
24
+ return createHash('sha256').update(content).digest('hex');
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Check if project file differs from framework file
32
+ * @param {string} projectPath - Project root
33
+ * @param {string} frameworkRoot - Framework root
34
+ * @param {string} relativeFilePath - Path relative to root (e.g. 'scripts/verify.sh')
35
+ * @returns {boolean} True if file is modified (different from framework)
36
+ */
37
+ export function isFileModified(projectPath, frameworkRoot, relativeFilePath) {
38
+ const projectFile = join(projectPath, relativeFilePath);
39
+ const frameworkFile = join(frameworkRoot, relativeFilePath);
40
+ if (!existsSync(projectFile)) return false;
41
+ if (!existsSync(frameworkFile)) return false;
42
+ const projectHash = calculateFileHash(projectFile);
43
+ const frameworkHash = calculateFileHash(frameworkFile);
44
+ return projectHash !== frameworkHash;
45
+ }
46
+
47
+ /**
48
+ * Backup a file to ._shipit_backup/
49
+ * @param {string} projectPath - Project root
50
+ * @param {string} relativeFilePath - Path relative to project (e.g. 'scripts/verify.sh')
51
+ * @param {string} backupDir - Backup directory name (default ._shipit_backup)
52
+ * @returns {string} Path to backup file
53
+ */
54
+ export function backupFile(projectPath, relativeFilePath, backupDir = BACKUP_DIR) {
55
+ const src = join(projectPath, relativeFilePath);
56
+ if (!existsSync(src)) throw new Error(`File not found: ${relativeFilePath}`);
57
+ const timestamp = TIMESTAMP_FORMAT(new Date());
58
+ const backupRelative = `${relativeFilePath}.${timestamp}`;
59
+ const backupFull = join(projectPath, backupDir, backupRelative);
60
+ const backupParent = join(backupFull, '..');
61
+ mkdirSync(backupParent, { recursive: true });
62
+ copySync(src, backupFull, { overwrite: true });
63
+ return backupFull;
64
+ }
65
+
66
+ /**
67
+ * List backups in project's backup directory
68
+ * @param {string} projectPath - Project root
69
+ * @param {string} backupDir - Backup directory name
70
+ * @returns {Array<{path: string, original: string, timestamp: string}>}
71
+ */
72
+ export function listBackups(projectPath, backupDir = BACKUP_DIR) {
73
+ const dir = join(projectPath, backupDir);
74
+ if (!existsSync(dir)) return [];
75
+ const out = [];
76
+ function walk(current, prefix = '') {
77
+ const entries = readdirSync(current, { withFileTypes: true });
78
+ for (const e of entries) {
79
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
80
+ const full = join(current, e.name);
81
+ if (e.isDirectory()) walk(full, rel);
82
+ else if (e.isFile() && /\.\d{8}T\d{6}$/.test(e.name)) {
83
+ const original = rel.replace(/\.\d{8}T\d{6}$/, '');
84
+ const timestamp = e.name.slice(-15);
85
+ out.push({ path: full, original: join(projectPath, original), timestamp, relative: rel });
86
+ }
87
+ }
88
+ }
89
+ walk(dir);
90
+ return out.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
91
+ }
92
+
93
+ /**
94
+ * Restore a file from backup
95
+ * @param {string} backupFilePath - Full path to backup file
96
+ * @param {string|null} targetPath - Full path to restore to (if null, derived from backup path: remove backup dir and timestamp)
97
+ * @param {string} projectPath - Project root (required when targetPath is null)
98
+ * @param {string} backupDir - Backup directory name (required when targetPath is null)
99
+ * @param {boolean} removeBackup - Remove backup after restore
100
+ */
101
+ export function restoreFromBackup(backupFilePath, targetPath, projectPath = null, backupDir = BACKUP_DIR, removeBackup = false) {
102
+ if (!existsSync(backupFilePath)) throw new Error(`Backup not found: ${backupFilePath}`);
103
+ let target = targetPath;
104
+ if (!target && projectPath) {
105
+ const backupPrefix = join(projectPath, backupDir);
106
+ const rel = backupFilePath.startsWith(backupPrefix)
107
+ ? backupFilePath.slice(backupPrefix.length + 1).replace(/\.\d{8}T\d{6}$/, '')
108
+ : backupFilePath.replace(/\.\d{8}T\d{6}$/, '');
109
+ target = join(projectPath, rel);
110
+ } else if (!target) {
111
+ target = backupFilePath.replace(/\.\d{8}T\d{6}$/, '');
112
+ }
113
+ mkdirSync(dirname(target), { recursive: true });
114
+ copySync(backupFilePath, target, { overwrite: true });
115
+ if (removeBackup) fsExtra.removeSync(backupFilePath);
116
+ }
117
+
118
+ /**
119
+ * Collect all framework-owned file paths (relative) for upgrade
120
+ * @param {string} frameworkRoot - Framework root
121
+ * @param {object} manifest - Parsed framework-files-manifest
122
+ * @param {Set<string>} neverCopied - Set of paths to skip
123
+ * @returns {string[]} Relative file paths
124
+ */
125
+ export function getFrameworkFileList(frameworkRoot, manifest, neverCopied) {
126
+ const files = new Set();
127
+
128
+ function addFile(rel) {
129
+ if (neverCopied.has(rel) || rel.startsWith('.override')) return;
130
+ const full = join(frameworkRoot, rel);
131
+ if (existsSync(full) && statSync(full).isFile()) files.add(rel);
132
+ }
133
+
134
+ for (const dir of manifest.frameworkOwned?.directories || []) {
135
+ const fullDir = join(frameworkRoot, dir);
136
+ if (!existsSync(fullDir)) continue;
137
+ walkDir(fullDir, dir, (rel) => addFile(rel));
138
+ }
139
+ for (const file of manifest.frameworkOwned?.files || []) {
140
+ if (file === 'project.json') continue; // merged, not replaced
141
+ addFile(file);
142
+ }
143
+
144
+ return [...files];
145
+ }
146
+
147
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'coverage', '.turbo']);
148
+
149
+ function walkDir(fullDir, prefix, add) {
150
+ const entries = readdirSync(fullDir, { withFileTypes: true });
151
+ for (const e of entries) {
152
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
153
+ const full = join(fullDir, e.name);
154
+ if (e.isDirectory()) {
155
+ if (SKIP_DIRS.has(e.name)) continue;
156
+ walkDir(full, rel, add);
157
+ } else if (e.isFile()) add(rel);
158
+ }
159
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Version detection and comparison for ShipIt upgrade
3
+ */
4
+
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { getFrameworkRoot } from './manifest.js';
8
+
9
+ /**
10
+ * Get installed ShipIt version from package
11
+ * @returns {string} Version string (e.g. "1.0.0")
12
+ */
13
+ export function getInstalledShipItVersion() {
14
+ const root = getFrameworkRoot();
15
+ const pkgPath = join(root, 'package.json');
16
+ if (!existsSync(pkgPath)) {
17
+ throw new Error('ShipIt not found. Install: npm install -g @nlaprell/shipit');
18
+ }
19
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
20
+ return pkg.version || '0.0.0';
21
+ }
22
+
23
+ /**
24
+ * Get project's ShipIt version from project.json
25
+ * @param {string} projectPath - Project root path
26
+ * @returns {string|null} Version or null if missing
27
+ */
28
+ export function getProjectShipItVersion(projectPath) {
29
+ const projectJsonPath = join(projectPath, 'project.json');
30
+ if (!existsSync(projectJsonPath)) return null;
31
+ try {
32
+ const data = JSON.parse(readFileSync(projectJsonPath, 'utf-8'));
33
+ return data.shipitVersion ?? null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Compare two semver strings
41
+ * @param {string} installed - Installed version
42
+ * @param {string} project - Project version
43
+ * @returns {'same'|'newer'|'older'|'unknown'} Comparison result
44
+ */
45
+ export function compareVersions(installed, project) {
46
+ if (!installed || !project) return 'unknown';
47
+ const a = parseVersion(installed);
48
+ const b = parseVersion(project);
49
+ if (!a || !b) return 'unknown';
50
+ if (a.major !== b.major) return a.major > b.major ? 'newer' : 'older';
51
+ if (a.minor !== b.minor) return a.minor > b.minor ? 'newer' : 'older';
52
+ if (a.patch !== b.patch) return a.patch > b.patch ? 'newer' : 'older';
53
+ return 'same';
54
+ }
55
+
56
+ function parseVersion(v) {
57
+ const match = String(v).match(/^(\d+)\.(\d+)\.(\d+)/);
58
+ if (!match) return null;
59
+ return {
60
+ major: parseInt(match[1], 10),
61
+ minor: parseInt(match[2], 10),
62
+ patch: parseInt(match[3], 10)
63
+ };
64
+ }
@@ -0,0 +1,73 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ]);
44
+ ```
45
+
46
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
+
48
+ ```js
49
+ // eslint.config.js
50
+ import reactX from 'eslint-plugin-react-x';
51
+ import reactDom from 'eslint-plugin-react-dom';
52
+
53
+ export default defineConfig([
54
+ globalIgnores(['dist']),
55
+ {
56
+ files: ['**/*.{ts,tsx}'],
57
+ extends: [
58
+ // Other configs...
59
+ // Enable lint rules for React
60
+ reactX.configs['recommended-typescript'],
61
+ // Enable lint rules for React DOM
62
+ reactDom.configs.recommended,
63
+ ],
64
+ languageOptions: {
65
+ parserOptions: {
66
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ // other options...
70
+ },
71
+ },
72
+ ]);
73
+ ```
@@ -0,0 +1,23 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ ecmaVersion: 2020,
20
+ globals: globals.browser,
21
+ },
22
+ },
23
+ ])
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ShipIt Dashboard</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>