@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.
- package/.cursor/commands/create_intent_from_issue.md +28 -0
- package/.cursor/commands/create_pr.md +28 -0
- package/.cursor/commands/dashboard.md +39 -0
- package/.cursor/commands/deploy.md +152 -0
- package/.cursor/commands/drift_check.md +36 -0
- package/.cursor/commands/fix.md +39 -0
- package/.cursor/commands/generate_release_plan.md +31 -0
- package/.cursor/commands/generate_roadmap.md +38 -0
- package/.cursor/commands/help.md +37 -0
- package/.cursor/commands/init_project.md +26 -0
- package/.cursor/commands/kill.md +72 -0
- package/.cursor/commands/new_intent.md +68 -0
- package/.cursor/commands/pr.md +77 -0
- package/.cursor/commands/revert-plan.md +58 -0
- package/.cursor/commands/risk.md +64 -0
- package/.cursor/commands/rollback.md +43 -0
- package/.cursor/commands/scope_project.md +53 -0
- package/.cursor/commands/ship.md +345 -0
- package/.cursor/commands/status.md +71 -0
- package/.cursor/commands/suggest.md +44 -0
- package/.cursor/commands/test_shipit.md +197 -0
- package/.cursor/commands/verify.md +50 -0
- package/.cursor/rules/architect.mdc +84 -0
- package/.cursor/rules/assumption-extractor.mdc +95 -0
- package/.cursor/rules/docs.mdc +66 -0
- package/.cursor/rules/implementer.mdc +112 -0
- package/.cursor/rules/pm.mdc +136 -0
- package/.cursor/rules/qa.mdc +97 -0
- package/.cursor/rules/security.mdc +90 -0
- package/.cursor/rules/steward.mdc +99 -0
- package/.cursor/rules/test-runner.mdc +196 -0
- package/AGENTS.md +121 -0
- package/README.md +264 -0
- package/_system/architecture/CANON.md +159 -0
- package/_system/architecture/invariants.yml +87 -0
- package/_system/architecture/project-schema.json +98 -0
- package/_system/architecture/workflow-state-layout.md +68 -0
- package/_system/artifacts/SYSTEM_STATE.md +43 -0
- package/_system/artifacts/confidence-calibration.json +16 -0
- package/_system/artifacts/dependencies.md +46 -0
- package/_system/artifacts/framework-files-manifest.json +179 -0
- package/_system/artifacts/usage.json +1 -0
- package/_system/behaviors/DO_RELEASE.md +371 -0
- package/_system/behaviors/DO_RELEASE_AI.md +329 -0
- package/_system/behaviors/PREPARE_RELEASE.md +373 -0
- package/_system/behaviors/PREPARE_RELEASE_AI.md +234 -0
- package/_system/behaviors/WORK_ROOT_PLATFORM_ISSUES.md +140 -0
- package/_system/behaviors/WORK_TEST_PLAN_ISSUES.md +380 -0
- package/_system/do-not-repeat/abandoned-designs.md +18 -0
- package/_system/do-not-repeat/bad-patterns.md +19 -0
- package/_system/do-not-repeat/failed-experiments.md +18 -0
- package/_system/do-not-repeat/rejected-libraries.md +19 -0
- package/_system/drift/baselines.md +49 -0
- package/_system/drift/metrics.md +33 -0
- package/_system/golden-data/.gitkeep +0 -0
- package/_system/golden-data/README.md +47 -0
- package/_system/reports/mutation/mutation.html +492 -0
- package/_system/security/audit-allowlist.json +4 -0
- package/bin/create-shipit-app +29 -0
- package/bin/shipit +183 -0
- package/cli/src/commands/check.js +82 -0
- package/cli/src/commands/create.js +195 -0
- package/cli/src/commands/init.js +267 -0
- package/cli/src/commands/upgrade.js +196 -0
- package/cli/src/utils/config.js +27 -0
- package/cli/src/utils/file-copy.js +144 -0
- package/cli/src/utils/gitignore-merge.js +44 -0
- package/cli/src/utils/manifest.js +105 -0
- package/cli/src/utils/package-json-merge.js +163 -0
- package/cli/src/utils/project-json-merge.js +57 -0
- package/cli/src/utils/prompts.js +30 -0
- package/cli/src/utils/stack-detection.js +56 -0
- package/cli/src/utils/stack-files.js +364 -0
- package/cli/src/utils/upgrade-backup.js +159 -0
- package/cli/src/utils/version.js +64 -0
- package/dashboard-app/README.md +73 -0
- package/dashboard-app/eslint.config.js +23 -0
- package/dashboard-app/index.html +13 -0
- package/dashboard-app/package.json +30 -0
- package/dashboard-app/pnpm-lock.yaml +2721 -0
- package/dashboard-app/public/dashboard.json +66 -0
- package/dashboard-app/public/vite.svg +1 -0
- package/dashboard-app/src/App.css +141 -0
- package/dashboard-app/src/App.tsx +155 -0
- package/dashboard-app/src/assets/react.svg +1 -0
- package/dashboard-app/src/index.css +68 -0
- package/dashboard-app/src/main.tsx +10 -0
- package/dashboard-app/tsconfig.app.json +28 -0
- package/dashboard-app/tsconfig.json +4 -0
- package/dashboard-app/tsconfig.node.json +26 -0
- package/dashboard-app/vite.config.ts +7 -0
- package/package.json +116 -0
- package/scripts/README.md +70 -0
- package/scripts/audit-check.sh +125 -0
- package/scripts/calibration-report.sh +198 -0
- package/scripts/check-readiness.sh +155 -0
- package/scripts/collect-metrics.sh +116 -0
- package/scripts/command-manifest.yml +131 -0
- package/scripts/create-test-plan-issue.sh +110 -0
- package/scripts/dashboard-start.sh +16 -0
- package/scripts/deploy.sh +170 -0
- package/scripts/drift-check.sh +93 -0
- package/scripts/execute-rollback.sh +177 -0
- package/scripts/export-dashboard-json.js +208 -0
- package/scripts/fix-intents.sh +239 -0
- package/scripts/generate-dashboard.sh +136 -0
- package/scripts/generate-docs.sh +279 -0
- package/scripts/generate-project-context.sh +142 -0
- package/scripts/generate-release-plan.sh +443 -0
- package/scripts/generate-roadmap.sh +189 -0
- package/scripts/generate-system-state.sh +95 -0
- package/scripts/gh/create-intent-from-issue.sh +82 -0
- package/scripts/gh/create-issue-from-intent.sh +59 -0
- package/scripts/gh/create-pr.sh +41 -0
- package/scripts/gh/link-issue.sh +44 -0
- package/scripts/gh/on-ship-update-issue.sh +42 -0
- package/scripts/headless/README.md +8 -0
- package/scripts/headless/call-llm.js +109 -0
- package/scripts/headless/run-phase.sh +99 -0
- package/scripts/help.sh +271 -0
- package/scripts/init-project.sh +976 -0
- package/scripts/kill-intent.sh +125 -0
- package/scripts/lib/common.sh +29 -0
- package/scripts/lib/intent.sh +61 -0
- package/scripts/lib/progress.sh +57 -0
- package/scripts/lib/suggest-next.sh +131 -0
- package/scripts/lib/validate-intents.sh +240 -0
- package/scripts/lib/verify-outputs.sh +55 -0
- package/scripts/lib/workflow_state.sh +201 -0
- package/scripts/new-intent.sh +271 -0
- package/scripts/publish-npm.sh +28 -0
- package/scripts/scope-project.sh +380 -0
- package/scripts/setup-worktrees.sh +125 -0
- package/scripts/status.sh +278 -0
- package/scripts/suggest.sh +173 -0
- package/scripts/test-headless.sh +47 -0
- package/scripts/test-shipit.sh +52 -0
- package/scripts/test-workflow-state.sh +49 -0
- package/scripts/usage-report.sh +47 -0
- package/scripts/usage.sh +58 -0
- package/scripts/validate-cursor.sh +151 -0
- package/scripts/validate-project.sh +71 -0
- package/scripts/validate-vscode.sh +146 -0
- package/scripts/verify.sh +153 -0
- package/scripts/workflow-orchestrator.sh +97 -0
- package/scripts/workflow-templates/01_analysis.md.tpl +25 -0
- package/scripts/workflow-templates/02_plan.md.tpl +30 -0
- package/scripts/workflow-templates/03_implementation.md.tpl +25 -0
- package/scripts/workflow-templates/04_verification.md.tpl +29 -0
- package/scripts/workflow-templates/05_release_notes.md.tpl +16 -0
- package/scripts/workflow-templates/05_verification_legacy.md.tpl +6 -0
- package/scripts/workflow-templates/active.md.tpl +18 -0
- package/scripts/workflow-templates/phases.yml +39 -0
- package/stryker.conf.json +8 -0
- package/work/intent/templates/api-endpoint.md +124 -0
- package/work/intent/templates/bugfix.md +116 -0
- package/work/intent/templates/frontend-feature.md +115 -0
- package/work/intent/templates/generic.md +122 -0
- package/work/intent/templates/infra-change.md +121 -0
- 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
|
+
}
|