openspec-sdd-e2e-kit 0.1.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/README.md +63 -0
- package/bin/sdd-e2e-kit.mjs +53 -0
- package/kit/.codex/skills/feature-to-e2e/SKILL.md +188 -0
- package/kit/.codex/skills/feature-to-e2e/agents/openai.yaml +4 -0
- package/kit/.codex/skills/openspec-apply-change/SKILL.md +180 -0
- package/kit/.codex/skills/openspec-archive-change/SKILL.md +157 -0
- package/kit/.codex/skills/openspec-continue-change/SKILL.md +136 -0
- package/kit/.codex/skills/openspec-explore/SKILL.md +292 -0
- package/kit/.codex/skills/openspec-full-spec-discovery/SKILL.md +356 -0
- package/kit/.codex/skills/openspec-full-spec-discovery/references/backlog-row-to-main-spec.md +447 -0
- package/kit/.codex/skills/openspec-new-change/SKILL.md +92 -0
- package/kit/.codex/skills/openspec-propose/SKILL.md +132 -0
- package/kit/.codex/skills/spec-to-gherkin/SKILL.md +686 -0
- package/kit/SDD_E2E_FLOW.md +268 -0
- package/kit/manifest.json +78 -0
- package/kit/openspec/config.yaml +18 -0
- package/kit/openspec/schemas/sdd-e2e/schema.yaml +128 -0
- package/kit/openspec/schemas/sdd-e2e/templates/acceptance-coverage.md +9 -0
- package/kit/openspec/schemas/sdd-e2e/templates/design.md +29 -0
- package/kit/openspec/schemas/sdd-e2e/templates/feature.feature +13 -0
- package/kit/openspec/schemas/sdd-e2e/templates/proposal.md +23 -0
- package/kit/openspec/schemas/sdd-e2e/templates/spec.md +21 -0
- package/kit/openspec/schemas/sdd-e2e/templates/tasks.md +16 -0
- package/kit/openspec/schemas/sdd-e2e/templates/test-cases.md +35 -0
- package/kit/openspec/schemas/sdd-e2e.yaml +160 -0
- package/kit/openspec/sdd-e2e-flow.md +290 -0
- package/kit/openspec/sdd-e2e-maintenance.md +98 -0
- package/kit/scripts/sdd/check-report.mjs +34 -0
- package/kit/scripts/sdd/lib.mjs +290 -0
- package/kit/scripts/sdd/lint-features.mjs +60 -0
- package/kit/scripts/sdd/lint-tasks.mjs +41 -0
- package/kit/scripts/sdd/self-test.mjs +185 -0
- package/kit/scripts/sdd/summarize-acceptance.mjs +41 -0
- package/package.json +19 -0
- package/src/check.mjs +86 -0
- package/src/diff.mjs +101 -0
- package/src/install.mjs +159 -0
- package/src/lib.mjs +221 -0
package/src/check.mjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
collectAssetFiles,
|
|
6
|
+
getPackageScriptDiff,
|
|
7
|
+
loadManifest,
|
|
8
|
+
parseCommonArgs,
|
|
9
|
+
summarizeByStatus,
|
|
10
|
+
} from './lib.mjs';
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`Usage: node tools/sdd-e2e-kit/check.mjs [target] [--source <path>] [--json]
|
|
14
|
+
|
|
15
|
+
Checks whether a target project has the SDD-E2E kit assets and package scripts.
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
target Project root to check. Defaults to current working directory.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--source <path> Source project root for kit assets. Defaults to this repository root.
|
|
22
|
+
--json Print machine-readable JSON.
|
|
23
|
+
-h, --help
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const options = parseCommonArgs(process.argv.slice(2));
|
|
28
|
+
|
|
29
|
+
if (options.help) {
|
|
30
|
+
printHelp();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const manifest = loadManifest();
|
|
35
|
+
const expectedFiles = collectAssetFiles(options.source, manifest);
|
|
36
|
+
|
|
37
|
+
const assetResults = expectedFiles.map((relativePath) => {
|
|
38
|
+
const absolutePath = path.join(options.target, relativePath);
|
|
39
|
+
const exists = fs.existsSync(absolutePath);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
relativePath,
|
|
43
|
+
status: exists ? 'present' : 'missing',
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const scriptResults = getPackageScriptDiff(options.target, manifest);
|
|
48
|
+
const hasMissingAssets = assetResults.some((item) => item.status !== 'present');
|
|
49
|
+
const hasScriptDrift = scriptResults.some((item) => item.status !== 'same');
|
|
50
|
+
|
|
51
|
+
if (options.json) {
|
|
52
|
+
console.log(
|
|
53
|
+
JSON.stringify(
|
|
54
|
+
{
|
|
55
|
+
target: options.target,
|
|
56
|
+
assets: assetResults,
|
|
57
|
+
packageScripts: scriptResults,
|
|
58
|
+
ok: !hasMissingAssets && !hasScriptDrift,
|
|
59
|
+
},
|
|
60
|
+
null,
|
|
61
|
+
2,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
console.log(`SDD-E2E kit check: ${options.target}`);
|
|
66
|
+
console.log(`Assets: ${JSON.stringify(summarizeByStatus(assetResults))}`);
|
|
67
|
+
console.log(`Package scripts: ${JSON.stringify(summarizeByStatus(scriptResults))}`);
|
|
68
|
+
|
|
69
|
+
const missingAssets = assetResults.filter((item) => item.status !== 'present');
|
|
70
|
+
if (missingAssets.length > 0) {
|
|
71
|
+
console.log('\nMissing assets:');
|
|
72
|
+
for (const item of missingAssets) {
|
|
73
|
+
console.log(` - ${item.relativePath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scriptIssues = scriptResults.filter((item) => item.status !== 'same');
|
|
78
|
+
if (scriptIssues.length > 0) {
|
|
79
|
+
console.log('\nPackage script issues:');
|
|
80
|
+
for (const item of scriptIssues) {
|
|
81
|
+
console.log(` - ${item.name}: ${item.status}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
process.exit(hasMissingAssets || hasScriptDrift ? 1 : 0);
|
package/src/diff.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
collectAssetFiles,
|
|
4
|
+
collectTargetAssetFiles,
|
|
5
|
+
compareFile,
|
|
6
|
+
getPackageScriptDiff,
|
|
7
|
+
loadManifest,
|
|
8
|
+
parseCommonArgs,
|
|
9
|
+
summarizeByStatus,
|
|
10
|
+
} from './lib.mjs';
|
|
11
|
+
|
|
12
|
+
function printHelp() {
|
|
13
|
+
console.log(`Usage: node tools/sdd-e2e-kit/diff.mjs [target] [source] [--json]
|
|
14
|
+
|
|
15
|
+
Compares target project assets with the kit source assets.
|
|
16
|
+
|
|
17
|
+
Arguments:
|
|
18
|
+
target Project root to compare. Defaults to current working directory.
|
|
19
|
+
source Source project root for kit assets. Defaults to this repository root.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--source <path> Alternative source project root.
|
|
23
|
+
--json Print machine-readable JSON.
|
|
24
|
+
-h, --help
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const options = parseCommonArgs(process.argv.slice(2));
|
|
29
|
+
|
|
30
|
+
if (options.help) {
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const manifest = loadManifest();
|
|
36
|
+
const sourceFiles = collectAssetFiles(options.source, manifest);
|
|
37
|
+
const targetFiles = collectTargetAssetFiles(options.target, manifest);
|
|
38
|
+
const sourceSet = new Set(sourceFiles);
|
|
39
|
+
const targetSet = new Set(targetFiles);
|
|
40
|
+
|
|
41
|
+
const fileResults = sourceFiles.map((relativePath) =>
|
|
42
|
+
compareFile(options.source, options.target, relativePath),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
for (const relativePath of targetFiles) {
|
|
46
|
+
if (!sourceSet.has(relativePath)) {
|
|
47
|
+
fileResults.push({ status: 'extra', relativePath });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const relativePath of sourceFiles) {
|
|
52
|
+
if (!targetSet.has(relativePath)) {
|
|
53
|
+
const existing = fileResults.find((item) => item.relativePath === relativePath);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
fileResults.push({ status: 'missing', relativePath });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const scriptResults = getPackageScriptDiff(options.target, manifest);
|
|
61
|
+
const driftFiles = fileResults.filter((item) => item.status !== 'same');
|
|
62
|
+
const driftScripts = scriptResults.filter((item) => item.status !== 'same');
|
|
63
|
+
const ok = driftFiles.length === 0 && driftScripts.length === 0;
|
|
64
|
+
|
|
65
|
+
if (options.json) {
|
|
66
|
+
console.log(
|
|
67
|
+
JSON.stringify(
|
|
68
|
+
{
|
|
69
|
+
source: options.source,
|
|
70
|
+
target: options.target,
|
|
71
|
+
files: fileResults,
|
|
72
|
+
packageScripts: scriptResults,
|
|
73
|
+
ok,
|
|
74
|
+
},
|
|
75
|
+
null,
|
|
76
|
+
2,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`SDD-E2E kit diff`);
|
|
81
|
+
console.log(`Source: ${options.source}`);
|
|
82
|
+
console.log(`Target: ${options.target}`);
|
|
83
|
+
console.log(`Files: ${JSON.stringify(summarizeByStatus(fileResults))}`);
|
|
84
|
+
console.log(`Package scripts: ${JSON.stringify(summarizeByStatus(scriptResults))}`);
|
|
85
|
+
|
|
86
|
+
if (driftFiles.length > 0) {
|
|
87
|
+
console.log('\nFile drift:');
|
|
88
|
+
for (const item of driftFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath))) {
|
|
89
|
+
console.log(` - ${item.status}: ${item.relativePath}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (driftScripts.length > 0) {
|
|
94
|
+
console.log('\nPackage script drift:');
|
|
95
|
+
for (const item of driftScripts) {
|
|
96
|
+
console.log(` - ${item.status}: ${item.name}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
process.exit(ok ? 0 : 1);
|
package/src/install.mjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
collectAssetFiles,
|
|
6
|
+
compareFile,
|
|
7
|
+
loadManifest,
|
|
8
|
+
parseCommonArgs,
|
|
9
|
+
readPackageJson,
|
|
10
|
+
summarizeByStatus,
|
|
11
|
+
} from './lib.mjs';
|
|
12
|
+
|
|
13
|
+
function printHelp() {
|
|
14
|
+
console.log(`Usage: node tools/sdd-e2e-kit/install.mjs [target] [--source <path>] [--dry-run] [--force]
|
|
15
|
+
|
|
16
|
+
Installs the SDD-E2E kit assets into a target project.
|
|
17
|
+
|
|
18
|
+
Arguments:
|
|
19
|
+
target Project root to install into. Defaults to current working directory.
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--source <path> Source project root for kit assets. Defaults to this repository root.
|
|
23
|
+
--dry-run Print planned writes without changing files.
|
|
24
|
+
--force Overwrite changed target files and conflicting package scripts.
|
|
25
|
+
-h, --help
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function copyFile(sourcePath, targetPath, dryRun) {
|
|
30
|
+
if (dryRun) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
35
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installPackageScripts(targetRoot, manifest, options) {
|
|
39
|
+
const { packagePath, data } = readPackageJson(targetRoot);
|
|
40
|
+
|
|
41
|
+
if (!data) {
|
|
42
|
+
return {
|
|
43
|
+
changed: false,
|
|
44
|
+
conflicts: Object.keys(manifest.packageScripts),
|
|
45
|
+
results: Object.keys(manifest.packageScripts).map((name) => ({
|
|
46
|
+
name,
|
|
47
|
+
status: 'package-missing',
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const next = { ...data, scripts: { ...(data.scripts ?? {}) } };
|
|
53
|
+
const results = [];
|
|
54
|
+
let changed = false;
|
|
55
|
+
const conflicts = [];
|
|
56
|
+
|
|
57
|
+
for (const [name, expected] of Object.entries(manifest.packageScripts)) {
|
|
58
|
+
const actual = next.scripts[name];
|
|
59
|
+
|
|
60
|
+
if (actual === expected) {
|
|
61
|
+
results.push({ name, status: 'same' });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (actual && actual !== expected && !options.force) {
|
|
66
|
+
conflicts.push(name);
|
|
67
|
+
results.push({ name, status: 'conflict' });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
next.scripts[name] = expected;
|
|
72
|
+
changed = true;
|
|
73
|
+
results.push({ name, status: actual ? 'overwritten' : 'created' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (changed && !options.dryRun) {
|
|
77
|
+
fs.writeFileSync(packagePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { changed, conflicts, results };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const options = parseCommonArgs(process.argv.slice(2));
|
|
84
|
+
|
|
85
|
+
if (options.help) {
|
|
86
|
+
printHelp();
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const manifest = loadManifest();
|
|
91
|
+
const files = collectAssetFiles(options.source, manifest);
|
|
92
|
+
const fileResults = [];
|
|
93
|
+
const conflicts = [];
|
|
94
|
+
const sourceMissing = [];
|
|
95
|
+
|
|
96
|
+
for (const relativePath of files) {
|
|
97
|
+
const sourcePath = path.join(options.source, relativePath);
|
|
98
|
+
const targetPath = path.join(options.target, relativePath);
|
|
99
|
+
const comparison = compareFile(options.source, options.target, relativePath);
|
|
100
|
+
|
|
101
|
+
if (comparison.status === 'source-missing') {
|
|
102
|
+
sourceMissing.push(relativePath);
|
|
103
|
+
fileResults.push(comparison);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (comparison.status === 'same') {
|
|
108
|
+
fileResults.push(comparison);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (comparison.status === 'changed' && !options.force) {
|
|
113
|
+
conflicts.push(relativePath);
|
|
114
|
+
fileResults.push({ status: 'conflict', relativePath });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
copyFile(sourcePath, targetPath, options.dryRun);
|
|
119
|
+
fileResults.push({
|
|
120
|
+
status: comparison.status === 'missing' ? 'created' : 'overwritten',
|
|
121
|
+
relativePath,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const packageResult = installPackageScripts(options.target, manifest, options);
|
|
126
|
+
const hasProblems = sourceMissing.length > 0 || conflicts.length > 0 || packageResult.conflicts.length > 0;
|
|
127
|
+
|
|
128
|
+
console.log(`SDD-E2E kit install${options.dryRun ? ' (dry run)' : ''}`);
|
|
129
|
+
console.log(`Source: ${options.source}`);
|
|
130
|
+
console.log(`Target: ${options.target}`);
|
|
131
|
+
console.log(`Files: ${JSON.stringify(summarizeByStatus(fileResults))}`);
|
|
132
|
+
console.log(`Package scripts: ${JSON.stringify(summarizeByStatus(packageResult.results))}`);
|
|
133
|
+
|
|
134
|
+
if (sourceMissing.length > 0) {
|
|
135
|
+
console.log('\nSource assets missing:');
|
|
136
|
+
for (const item of sourceMissing) {
|
|
137
|
+
console.log(` - ${item}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (conflicts.length > 0) {
|
|
142
|
+
console.log('\nFile conflicts, not overwritten:');
|
|
143
|
+
for (const item of conflicts) {
|
|
144
|
+
console.log(` - ${item}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (packageResult.conflicts.length > 0) {
|
|
149
|
+
console.log('\nPackage script conflicts, not overwritten:');
|
|
150
|
+
for (const item of packageResult.conflicts) {
|
|
151
|
+
console.log(` - ${item}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (hasProblems) {
|
|
156
|
+
console.log('\nUse --force only after reviewing conflicts.');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.exit(hasProblems ? 1 : 0);
|
package/src/lib.mjs
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const kitDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export const repoRoot = path.resolve(kitDir, '..');
|
|
8
|
+
export const defaultSourceRoot = path.join(repoRoot, 'kit');
|
|
9
|
+
|
|
10
|
+
const ignoredFileNames = new Set(['.DS_Store']);
|
|
11
|
+
|
|
12
|
+
export function loadManifest(sourceRoot = defaultSourceRoot) {
|
|
13
|
+
const manifestPath = path.join(sourceRoot, 'manifest.json');
|
|
14
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toPosix(filePath) {
|
|
18
|
+
return filePath.split(path.sep).join('/');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveRoot(input = process.cwd()) {
|
|
22
|
+
return path.resolve(input);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function walkFiles(root) {
|
|
26
|
+
if (!fs.existsSync(root)) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
31
|
+
const files = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (ignoredFileNames.has(entry.name)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fullPath = path.join(root, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
files.push(...walkFiles(fullPath));
|
|
41
|
+
} else if (entry.isFile()) {
|
|
42
|
+
files.push(fullPath);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return files.sort();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function collectAssetFiles(root, manifest) {
|
|
50
|
+
const files = [];
|
|
51
|
+
|
|
52
|
+
for (const asset of manifest.assets) {
|
|
53
|
+
const assetPath = path.join(root, asset.path);
|
|
54
|
+
|
|
55
|
+
if (asset.type === 'file') {
|
|
56
|
+
files.push(asset.path);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (asset.type === 'directory') {
|
|
61
|
+
for (const file of walkFiles(assetPath)) {
|
|
62
|
+
files.push(toPosix(path.relative(root, file)));
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`Unsupported asset type: ${asset.type}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return [...new Set(files)].sort();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function collectTargetAssetFiles(root, manifest) {
|
|
74
|
+
return collectAssetFiles(root, manifest);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hashFile(filePath) {
|
|
78
|
+
const hash = createHash('sha256');
|
|
79
|
+
hash.update(fs.readFileSync(filePath));
|
|
80
|
+
return hash.digest('hex');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function compareFile(sourceRoot, targetRoot, relativePath) {
|
|
84
|
+
const sourcePath = path.join(sourceRoot, relativePath);
|
|
85
|
+
const targetPath = path.join(targetRoot, relativePath);
|
|
86
|
+
|
|
87
|
+
if (!fs.existsSync(sourcePath)) {
|
|
88
|
+
return { status: 'source-missing', relativePath };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!fs.existsSync(targetPath)) {
|
|
92
|
+
return { status: 'missing', relativePath };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sourceHash = hashFile(sourcePath);
|
|
96
|
+
const targetHash = hashFile(targetPath);
|
|
97
|
+
|
|
98
|
+
if (sourceHash === targetHash) {
|
|
99
|
+
return { status: 'same', relativePath };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { status: 'changed', relativePath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readPackageJson(root) {
|
|
106
|
+
const packagePath = path.join(root, 'package.json');
|
|
107
|
+
if (!fs.existsSync(packagePath)) {
|
|
108
|
+
return { packagePath, data: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
packagePath,
|
|
113
|
+
data: JSON.parse(fs.readFileSync(packagePath, 'utf8')),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getPackageScriptDiff(root, manifest) {
|
|
118
|
+
const { data } = readPackageJson(root);
|
|
119
|
+
|
|
120
|
+
if (!data) {
|
|
121
|
+
return Object.entries(manifest.packageScripts).map(([name, expected]) => ({
|
|
122
|
+
name,
|
|
123
|
+
expected,
|
|
124
|
+
actual: null,
|
|
125
|
+
status: 'package-missing',
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const scripts = data.scripts ?? {};
|
|
130
|
+
return Object.entries(manifest.packageScripts).map(([name, expected]) => {
|
|
131
|
+
const actual = scripts[name] ?? null;
|
|
132
|
+
|
|
133
|
+
if (actual === null) {
|
|
134
|
+
return { name, expected, actual, status: 'missing' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (actual !== expected) {
|
|
138
|
+
return { name, expected, actual, status: 'changed' };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { name, expected, actual, status: 'same' };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function printRows(rows, emptyMessage) {
|
|
146
|
+
if (rows.length === 0) {
|
|
147
|
+
console.log(emptyMessage);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
console.log(row);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function parseCommonArgs(argv) {
|
|
157
|
+
const options = {
|
|
158
|
+
dryRun: false,
|
|
159
|
+
force: false,
|
|
160
|
+
json: false,
|
|
161
|
+
source: defaultSourceRoot,
|
|
162
|
+
target: process.cwd(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const positional = [];
|
|
166
|
+
|
|
167
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
168
|
+
const arg = argv[index];
|
|
169
|
+
|
|
170
|
+
if (arg === '--dry-run') {
|
|
171
|
+
options.dryRun = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (arg === '--force') {
|
|
176
|
+
options.force = true;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (arg === '--json') {
|
|
181
|
+
options.json = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (arg === '--source') {
|
|
186
|
+
index += 1;
|
|
187
|
+
if (!argv[index]) {
|
|
188
|
+
throw new Error('--source requires a path');
|
|
189
|
+
}
|
|
190
|
+
options.source = argv[index];
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (arg === '--help' || arg === '-h') {
|
|
195
|
+
options.help = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
positional.push(arg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (positional[0]) {
|
|
203
|
+
options.target = positional[0];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (positional[1]) {
|
|
207
|
+
options.source = positional[1];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
options.target = resolveRoot(options.target);
|
|
211
|
+
options.source = resolveRoot(options.source);
|
|
212
|
+
|
|
213
|
+
return options;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function summarizeByStatus(results) {
|
|
217
|
+
return results.reduce((summary, item) => {
|
|
218
|
+
summary[item.status] = (summary[item.status] ?? 0) + 1;
|
|
219
|
+
return summary;
|
|
220
|
+
}, {});
|
|
221
|
+
}
|