create-dss-project 0.1.0 → 0.2.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/lib/__tests__/project-types.test.js +41 -0
- package/lib/__tests__/scaffold.test.js +142 -0
- package/lib/index.js +36 -28
- package/lib/project-types.js +4 -0
- package/lib/prompts.js +3 -0
- package/lib/scaffold.js +178 -96
- package/package.json +7 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { PROJECT_TYPES } = require('../project-types');
|
|
2
|
+
|
|
3
|
+
describe('PROJECT_TYPES', () => {
|
|
4
|
+
test('has 10 project types', () => {
|
|
5
|
+
expect(Object.keys(PROJECT_TYPES)).toHaveLength(10);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
test('every type has required fields', () => {
|
|
9
|
+
for (const [key, type] of Object.entries(PROJECT_TYPES)) {
|
|
10
|
+
expect(type).toHaveProperty('label');
|
|
11
|
+
expect(type).toHaveProperty('description');
|
|
12
|
+
expect(type).toHaveProperty('discovery');
|
|
13
|
+
expect(type).toHaveProperty('pipeline');
|
|
14
|
+
expect(type).toHaveProperty('dashboard');
|
|
15
|
+
expect(type).toHaveProperty('entityType');
|
|
16
|
+
expect(type).toHaveProperty('scoringDimensions');
|
|
17
|
+
expect(typeof type.label).toBe('string');
|
|
18
|
+
expect(typeof type.discovery).toBe('boolean');
|
|
19
|
+
expect(Array.isArray(type.scoringDimensions)).toBe(true);
|
|
20
|
+
expect(type.scoringDimensions.length).toBeGreaterThanOrEqual(3);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('custom type enables all modules by default', () => {
|
|
25
|
+
expect(PROJECT_TYPES['custom'].discovery).toBe(true);
|
|
26
|
+
expect(PROJECT_TYPES['custom'].pipeline).toBe(true);
|
|
27
|
+
expect(PROJECT_TYPES['custom'].dashboard).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('competitor-research disables discovery and pipeline', () => {
|
|
31
|
+
expect(PROJECT_TYPES['competitor-research'].discovery).toBe(false);
|
|
32
|
+
expect(PROJECT_TYPES['competitor-research'].pipeline).toBe(false);
|
|
33
|
+
expect(PROJECT_TYPES['competitor-research'].dashboard).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('business-case disables all modules', () => {
|
|
37
|
+
expect(PROJECT_TYPES['business-case'].discovery).toBe(false);
|
|
38
|
+
expect(PROJECT_TYPES['business-case'].pipeline).toBe(false);
|
|
39
|
+
expect(PROJECT_TYPES['business-case'].dashboard).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { scaffold, validateOptions } = require('../scaffold');
|
|
5
|
+
|
|
6
|
+
describe('validateOptions', () => {
|
|
7
|
+
const tmpDir = path.join(os.tmpdir(), 'dss-test-' + Date.now());
|
|
8
|
+
const fakeTemplate = path.join(os.tmpdir(), 'dss-template-' + Date.now());
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
// Create a minimal fake template
|
|
12
|
+
fs.mkdirSync(fakeTemplate, { recursive: true });
|
|
13
|
+
fs.writeFileSync(path.join(fakeTemplate, 'CLAUDE.md'), '# Test');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
fs.rmSync(fakeTemplate, { recursive: true, force: true });
|
|
18
|
+
if (fs.existsSync(tmpDir)) {
|
|
19
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rejects empty project name', () => {
|
|
24
|
+
expect(() => validateOptions(tmpDir, fakeTemplate, {
|
|
25
|
+
projectName: '',
|
|
26
|
+
projectType: 'market-entry',
|
|
27
|
+
structure: 'full',
|
|
28
|
+
})).toThrow('Project name is required');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('rejects invalid project name characters', () => {
|
|
32
|
+
expect(() => validateOptions(tmpDir, fakeTemplate, {
|
|
33
|
+
projectName: '../bad-path',
|
|
34
|
+
projectType: 'market-entry',
|
|
35
|
+
structure: 'full',
|
|
36
|
+
})).toThrow('Invalid project name');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('rejects unknown project type', () => {
|
|
40
|
+
expect(() => validateOptions(tmpDir, fakeTemplate, {
|
|
41
|
+
projectName: 'test-project',
|
|
42
|
+
projectType: 'nonexistent',
|
|
43
|
+
structure: 'full',
|
|
44
|
+
})).toThrow('Unknown project type');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('rejects unknown structure', () => {
|
|
48
|
+
expect(() => validateOptions(tmpDir, fakeTemplate, {
|
|
49
|
+
projectName: 'test-project',
|
|
50
|
+
projectType: 'market-entry',
|
|
51
|
+
structure: 'ultra',
|
|
52
|
+
})).toThrow('Unknown structure');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('rejects non-existent template directory', () => {
|
|
56
|
+
expect(() => validateOptions(tmpDir, '/nonexistent/path', {
|
|
57
|
+
projectName: 'test-project',
|
|
58
|
+
projectType: 'market-entry',
|
|
59
|
+
structure: 'full',
|
|
60
|
+
})).toThrow('Template directory not found');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('accepts valid options', () => {
|
|
64
|
+
expect(() => validateOptions(tmpDir, fakeTemplate, {
|
|
65
|
+
projectName: 'test-project',
|
|
66
|
+
projectType: 'market-entry',
|
|
67
|
+
structure: 'full',
|
|
68
|
+
})).not.toThrow();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('scaffold', () => {
|
|
73
|
+
const fakeTemplate = path.join(os.tmpdir(), 'dss-scaffold-template-' + Date.now());
|
|
74
|
+
let targetDir;
|
|
75
|
+
|
|
76
|
+
beforeAll(() => {
|
|
77
|
+
// Create a minimal template with required structure
|
|
78
|
+
fs.mkdirSync(path.join(fakeTemplate, 'memory'), { recursive: true });
|
|
79
|
+
fs.mkdirSync(path.join(fakeTemplate, 'research', 'competitors'), { recursive: true });
|
|
80
|
+
fs.mkdirSync(path.join(fakeTemplate, 'docs'), { recursive: true });
|
|
81
|
+
fs.writeFileSync(path.join(fakeTemplate, 'CLAUDE.md'), '# {{PROJECT_NAME}}');
|
|
82
|
+
fs.writeFileSync(path.join(fakeTemplate, 'memory', 'research.md'), '# Research');
|
|
83
|
+
fs.writeFileSync(path.join(fakeTemplate, 'memory', 'decisions.md'), '# Decisions');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
targetDir = path.join(os.tmpdir(), 'dss-scaffold-out-' + Date.now());
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
if (fs.existsSync(targetDir)) {
|
|
92
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
afterAll(() => {
|
|
97
|
+
fs.rmSync(fakeTemplate, { recursive: true, force: true });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('creates project directory with config', () => {
|
|
101
|
+
const result = scaffold(targetDir, fakeTemplate, {
|
|
102
|
+
projectName: 'My Project',
|
|
103
|
+
projectType: 'market-entry',
|
|
104
|
+
structure: 'full',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(fs.existsSync(targetDir)).toBe(true);
|
|
108
|
+
expect(fs.existsSync(path.join(targetDir, 'project.config.json'))).toBe(true);
|
|
109
|
+
|
|
110
|
+
const config = JSON.parse(fs.readFileSync(path.join(targetDir, 'project.config.json'), 'utf-8'));
|
|
111
|
+
expect(config.projectName).toBe('My Project');
|
|
112
|
+
expect(config.projectType).toBe('market-entry');
|
|
113
|
+
expect(config.projectSlug).toBe('my-project');
|
|
114
|
+
expect(result.modules.discovery).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('minimal structure disables all optional modules', () => {
|
|
118
|
+
const result = scaffold(targetDir, fakeTemplate, {
|
|
119
|
+
projectName: 'Minimal Test',
|
|
120
|
+
projectType: 'market-entry',
|
|
121
|
+
structure: 'minimal',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.modules.discovery).toBe(false);
|
|
125
|
+
expect(result.modules.pipeline).toBe(false);
|
|
126
|
+
expect(result.modules.dashboard).toBe(false);
|
|
127
|
+
expect(result.features.scoring).toBe(false);
|
|
128
|
+
expect(result.features.evidenceGrading).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('cleans up on failure for non-existent target parent', () => {
|
|
132
|
+
// This test verifies the error handling path
|
|
133
|
+
expect(() => scaffold(targetDir, fakeTemplate, {
|
|
134
|
+
projectName: '',
|
|
135
|
+
projectType: 'market-entry',
|
|
136
|
+
structure: 'full',
|
|
137
|
+
})).toThrow();
|
|
138
|
+
|
|
139
|
+
// Target should not exist after cleanup
|
|
140
|
+
expect(fs.existsSync(targetDir)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
1
4
|
const fs = require('fs');
|
|
2
5
|
const path = require('path');
|
|
3
6
|
const { execSync } = require('child_process');
|
|
@@ -5,7 +8,7 @@ const { askQuestions } = require('./prompts');
|
|
|
5
8
|
const { scaffold } = require('./scaffold');
|
|
6
9
|
|
|
7
10
|
async function main(nameArg) {
|
|
8
|
-
console.log('\n DS Strategy Stack\n');
|
|
11
|
+
console.log('\n DS Strategy Stack — Your Project\'s Second Brain\n');
|
|
9
12
|
|
|
10
13
|
const answers = await askQuestions(nameArg);
|
|
11
14
|
|
|
@@ -30,41 +33,42 @@ async function main(nameArg) {
|
|
|
30
33
|
|
|
31
34
|
console.log(`\n Creating project in ${targetDir}\n`);
|
|
32
35
|
|
|
33
|
-
const { modules } = scaffold(targetDir, templateDir, {
|
|
34
|
-
projectName: answers.projectName,
|
|
35
|
-
projectType: answers.projectType,
|
|
36
|
-
structure: answers.structure,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Git init
|
|
40
36
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
stdio: 'ignore',
|
|
37
|
+
const { modules } = scaffold(targetDir, templateDir, {
|
|
38
|
+
projectName: answers.projectName,
|
|
39
|
+
projectType: answers.projectType,
|
|
40
|
+
structure: answers.structure,
|
|
46
41
|
});
|
|
47
|
-
console.log(' ✓ Git repository initialised');
|
|
48
|
-
} catch {
|
|
49
|
-
console.log(' ⚠ Could not initialise git — you can do this manually');
|
|
50
|
-
}
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
if (modules.dashboard && fs.existsSync(path.join(targetDir, 'dashboard', 'package.json'))) {
|
|
54
|
-
console.log(' ⏳ Installing dashboard dependencies...');
|
|
43
|
+
// Git init
|
|
55
44
|
try {
|
|
56
|
-
execSync('
|
|
57
|
-
|
|
45
|
+
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
|
|
46
|
+
execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
|
|
47
|
+
execSync('git commit -m "Initial project from DS Strategy Stack"', {
|
|
48
|
+
cwd: targetDir,
|
|
58
49
|
stdio: 'ignore',
|
|
59
50
|
});
|
|
60
|
-
console.log('
|
|
61
|
-
} catch {
|
|
62
|
-
console.log('
|
|
51
|
+
console.log(' \u2713 Git repository initialised');
|
|
52
|
+
} catch (_e) {
|
|
53
|
+
console.log(' \u26A0 Could not initialise git \u2014 you can do this manually');
|
|
63
54
|
}
|
|
64
|
-
}
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
56
|
+
// Install dashboard dependencies
|
|
57
|
+
if (modules.dashboard && fs.existsSync(path.join(targetDir, 'dashboard', 'package.json'))) {
|
|
58
|
+
console.log(' \u23F3 Installing dashboard dependencies...');
|
|
59
|
+
try {
|
|
60
|
+
execSync('npm install', {
|
|
61
|
+
cwd: path.join(targetDir, 'dashboard'),
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
});
|
|
64
|
+
console.log(' \u2713 Dashboard dependencies installed');
|
|
65
|
+
} catch (_e) {
|
|
66
|
+
console.log(' \u26A0 Could not install dashboard deps \u2014 run "cd dashboard && npm install" manually');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`
|
|
71
|
+
\u2705 Project created!
|
|
68
72
|
|
|
69
73
|
Next steps:
|
|
70
74
|
|
|
@@ -75,6 +79,10 @@ async function main(nameArg) {
|
|
|
75
79
|
The /onboard skill will walk you through configuring
|
|
76
80
|
your hypothesis, scoring dimensions, and kill conditions.
|
|
77
81
|
`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
module.exports = { main };
|
package/lib/project-types.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/** @type {Object<string, {label: string, description: string, discovery: boolean, pipeline: boolean, dashboard: boolean, entityType: string, scoringDimensions: string[]}>} */
|
|
1
5
|
const PROJECT_TYPES = {
|
|
2
6
|
'market-entry': {
|
|
3
7
|
label: 'Market Entry',
|
package/lib/prompts.js
CHANGED
package/lib/scaffold.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
1
4
|
const fs = require('fs');
|
|
2
5
|
const path = require('path');
|
|
3
6
|
const { PROJECT_TYPES } = require('./project-types');
|
|
@@ -16,6 +19,7 @@ const EXCLUDE_FROM_COPY = [
|
|
|
16
19
|
'dashboard/data/scoring.json',
|
|
17
20
|
'dashboard/data/timeline.json',
|
|
18
21
|
'dashboard/data/research.json',
|
|
22
|
+
'dashboard/data/meta.json',
|
|
19
23
|
'dashboard/screenshot.png',
|
|
20
24
|
'project.config.json',
|
|
21
25
|
'CONTRIBUTING.md',
|
|
@@ -23,12 +27,23 @@ const EXCLUDE_FROM_COPY = [
|
|
|
23
27
|
'.github/pull_request_template.md',
|
|
24
28
|
];
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Check if a relative path should be excluded from copying.
|
|
32
|
+
* @param {string} relativePath
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
26
35
|
function shouldExclude(relativePath) {
|
|
27
36
|
return EXCLUDE_FROM_COPY.some(ex =>
|
|
28
|
-
relativePath === ex || relativePath.startsWith(ex + '/')
|
|
37
|
+
relativePath === ex || relativePath.startsWith(ex + '/'),
|
|
29
38
|
);
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Recursively copy a directory, skipping excluded paths.
|
|
43
|
+
* @param {string} src
|
|
44
|
+
* @param {string} dest
|
|
45
|
+
* @param {string} baseDir
|
|
46
|
+
*/
|
|
32
47
|
function copyDir(src, dest, baseDir) {
|
|
33
48
|
fs.mkdirSync(dest, { recursive: true });
|
|
34
49
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
@@ -44,13 +59,17 @@ function copyDir(src, dest, baseDir) {
|
|
|
44
59
|
copyDir(srcPath, destPath, baseDir);
|
|
45
60
|
} else {
|
|
46
61
|
fs.copyFileSync(srcPath, destPath);
|
|
47
|
-
// Preserve executable permissions
|
|
48
62
|
const stat = fs.statSync(srcPath);
|
|
49
63
|
fs.chmodSync(destPath, stat.mode);
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Remove a path if it exists.
|
|
70
|
+
* @param {string} targetDir
|
|
71
|
+
* @param {string} relativePath
|
|
72
|
+
*/
|
|
54
73
|
function removeIfExists(targetDir, relativePath) {
|
|
55
74
|
const full = path.join(targetDir, relativePath);
|
|
56
75
|
if (fs.existsSync(full)) {
|
|
@@ -58,112 +77,175 @@ function removeIfExists(targetDir, relativePath) {
|
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Validate scaffold options before proceeding.
|
|
82
|
+
* @param {string} targetDir
|
|
83
|
+
* @param {string} templateDir
|
|
84
|
+
* @param {{projectName: string, projectType: string, structure: string}} options
|
|
85
|
+
* @throws {Error} If validation fails
|
|
86
|
+
*/
|
|
87
|
+
function validateOptions(targetDir, templateDir, options) {
|
|
88
|
+
const { projectName, projectType, structure } = options;
|
|
89
|
+
|
|
90
|
+
if (!projectName || typeof projectName !== 'string') {
|
|
91
|
+
throw new Error('Project name is required.');
|
|
92
|
+
}
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const modules = {
|
|
70
|
-
discovery: typeConfig.discovery,
|
|
71
|
-
pipeline: typeConfig.pipeline,
|
|
72
|
-
dashboard: typeConfig.dashboard,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const features = {
|
|
76
|
-
scoring: true,
|
|
77
|
-
killConditions: true,
|
|
78
|
-
evidenceGrading: true,
|
|
79
|
-
weeklyReports: true,
|
|
80
|
-
contextSnapshots: true,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
// 3. Apply structure-level removals
|
|
84
|
-
if (structure === 'minimal') {
|
|
85
|
-
modules.discovery = false;
|
|
86
|
-
modules.pipeline = false;
|
|
87
|
-
modules.dashboard = false;
|
|
88
|
-
features.scoring = false;
|
|
89
|
-
features.killConditions = false;
|
|
90
|
-
features.evidenceGrading = false;
|
|
91
|
-
features.weeklyReports = false;
|
|
92
|
-
features.contextSnapshots = false;
|
|
93
|
-
|
|
94
|
-
removeIfExists(targetDir, 'context');
|
|
95
|
-
removeIfExists(targetDir, 'skills/compare-options');
|
|
96
|
-
removeIfExists(targetDir, 'skills/weekly-report');
|
|
97
|
-
removeIfExists(targetDir, 'skills/rebuild-snapshots');
|
|
98
|
-
removeIfExists(targetDir, 'memory/scoring.md');
|
|
94
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/.test(projectName)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Invalid project name "${projectName}". Use letters, numbers, spaces, hyphens, or underscores.`,
|
|
97
|
+
);
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
if (!PROJECT_TYPES[projectType]) {
|
|
101
|
+
const valid = Object.keys(PROJECT_TYPES).join(', ');
|
|
102
|
+
throw new Error(`Unknown project type "${projectType}". Valid types: ${valid}`);
|
|
103
|
+
}
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
removeIfExists(targetDir, 'skills/compare-options');
|
|
105
|
+
if (!['full', 'essentials', 'minimal'].includes(structure)) {
|
|
106
|
+
throw new Error(`Unknown structure "${structure}". Valid: full, essentials, minimal`);
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
removeIfExists(targetDir, 'discovery');
|
|
114
|
-
removeIfExists(targetDir, 'memory/discovery.md');
|
|
115
|
-
removeIfExists(targetDir, 'context/pipeline-state.md');
|
|
116
|
-
removeIfExists(targetDir, 'dashboard/pipeline.html');
|
|
117
|
-
removeIfExists(targetDir, 'skills/pipeline-update');
|
|
118
|
-
removeIfExists(targetDir, 'skills/outreach-sequence');
|
|
119
|
-
removeIfExists(targetDir, 'skills/process-call');
|
|
109
|
+
if (!fs.existsSync(templateDir)) {
|
|
110
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
120
111
|
}
|
|
121
112
|
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
removeIfExists(targetDir, 'skills/pipeline-update');
|
|
125
|
-
removeIfExists(targetDir, 'skills/outreach-sequence');
|
|
113
|
+
if (fs.existsSync(targetDir)) {
|
|
114
|
+
throw new Error(`Target directory already exists: ${targetDir}`);
|
|
126
115
|
}
|
|
116
|
+
}
|
|
127
117
|
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Scaffold a new project from the template.
|
|
120
|
+
* @param {string} targetDir - Where to create the project
|
|
121
|
+
* @param {string} templateDir - Source template directory
|
|
122
|
+
* @param {{projectName: string, projectType: string, structure: string}} options
|
|
123
|
+
* @returns {{modules: Object, features: Object}}
|
|
124
|
+
*/
|
|
125
|
+
function scaffold(targetDir, templateDir, options) {
|
|
126
|
+
const { projectType, structure, projectName } = options;
|
|
127
|
+
|
|
128
|
+
validateOptions(targetDir, templateDir, options);
|
|
129
|
+
|
|
130
|
+
const typeConfig = PROJECT_TYPES[projectType];
|
|
131
|
+
|
|
132
|
+
// 1. Copy template (wrapped in try/catch for cleanup on failure)
|
|
133
|
+
try {
|
|
134
|
+
copyDir(templateDir, targetDir, templateDir);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Clean up partial copy
|
|
137
|
+
if (fs.existsSync(targetDir)) {
|
|
138
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`Failed to copy template: ${err.message}`);
|
|
130
141
|
}
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
143
|
+
try {
|
|
144
|
+
// 2. Determine effective module flags
|
|
145
|
+
const modules = {
|
|
146
|
+
discovery: typeConfig.discovery,
|
|
147
|
+
pipeline: typeConfig.pipeline,
|
|
148
|
+
dashboard: typeConfig.dashboard,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const features = {
|
|
152
|
+
scoring: true,
|
|
153
|
+
killConditions: true,
|
|
154
|
+
evidenceGrading: true,
|
|
155
|
+
weeklyReports: true,
|
|
156
|
+
contextSnapshots: true,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// 3. Apply structure-level removals
|
|
160
|
+
if (structure === 'minimal') {
|
|
161
|
+
modules.discovery = false;
|
|
162
|
+
modules.pipeline = false;
|
|
163
|
+
modules.dashboard = false;
|
|
164
|
+
features.scoring = false;
|
|
165
|
+
features.killConditions = false;
|
|
166
|
+
features.evidenceGrading = false;
|
|
167
|
+
features.weeklyReports = false;
|
|
168
|
+
features.contextSnapshots = false;
|
|
169
|
+
|
|
170
|
+
removeIfExists(targetDir, 'context');
|
|
171
|
+
removeIfExists(targetDir, 'skills/compare-options');
|
|
172
|
+
removeIfExists(targetDir, 'skills/weekly-report');
|
|
173
|
+
removeIfExists(targetDir, 'skills/rebuild-snapshots');
|
|
174
|
+
removeIfExists(targetDir, 'memory/scoring.md');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (structure === 'essentials') {
|
|
178
|
+
features.scoring = false;
|
|
179
|
+
features.killConditions = false;
|
|
180
|
+
features.evidenceGrading = false;
|
|
181
|
+
|
|
182
|
+
removeIfExists(targetDir, 'memory/scoring.md');
|
|
183
|
+
removeIfExists(targetDir, 'dashboard/scoring.html');
|
|
184
|
+
removeIfExists(targetDir, 'skills/compare-options');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 4. Apply module-level removals
|
|
188
|
+
if (!modules.discovery) {
|
|
189
|
+
removeIfExists(targetDir, 'discovery');
|
|
190
|
+
removeIfExists(targetDir, 'memory/discovery.md');
|
|
191
|
+
removeIfExists(targetDir, 'context/pipeline-state.md');
|
|
192
|
+
removeIfExists(targetDir, 'dashboard/pipeline.html');
|
|
193
|
+
removeIfExists(targetDir, 'skills/pipeline-update');
|
|
194
|
+
removeIfExists(targetDir, 'skills/outreach-sequence');
|
|
195
|
+
removeIfExists(targetDir, 'skills/process-call');
|
|
196
|
+
}
|
|
165
197
|
|
|
166
|
-
|
|
198
|
+
if (!modules.pipeline && modules.discovery) {
|
|
199
|
+
removeIfExists(targetDir, 'skills/pipeline-update');
|
|
200
|
+
removeIfExists(targetDir, 'skills/outreach-sequence');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!modules.dashboard) {
|
|
204
|
+
removeIfExists(targetDir, 'dashboard');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 5. Remove template-repo-only files
|
|
208
|
+
removeIfExists(targetDir, 'project.config.example.json');
|
|
209
|
+
removeIfExists(targetDir, 'scripts/reset-to-template.sh');
|
|
210
|
+
removeIfExists(targetDir, 'scripts/validate-placeholders.sh');
|
|
211
|
+
|
|
212
|
+
// 6. Write project.config.json
|
|
213
|
+
const config = {
|
|
214
|
+
templateVersion: '1.0.0',
|
|
215
|
+
templateSource: 'github.com/DiffTheEnder/DSS-Claude-Stack',
|
|
216
|
+
projectType,
|
|
217
|
+
projectName,
|
|
218
|
+
projectSlug: projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
|
219
|
+
oneLineDescription: '',
|
|
220
|
+
goal: '',
|
|
221
|
+
team: '',
|
|
222
|
+
scope: '',
|
|
223
|
+
outOfScope: '',
|
|
224
|
+
strategicHypothesis: '',
|
|
225
|
+
icpDescription: '',
|
|
226
|
+
entityType: typeConfig.entityType,
|
|
227
|
+
entityTypePlural: typeConfig.entityType + 's',
|
|
228
|
+
pipelineSourceOfTruth: 'data/entities.csv',
|
|
229
|
+
dashboardUrl: '',
|
|
230
|
+
modules,
|
|
231
|
+
features,
|
|
232
|
+
scoringDimensions: typeConfig.scoringDimensions,
|
|
233
|
+
killConditions: [],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
fs.writeFileSync(
|
|
237
|
+
path.join(targetDir, 'project.config.json'),
|
|
238
|
+
JSON.stringify(config, null, 2) + '\n',
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return { modules, features };
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// Clean up on any failure after the initial copy
|
|
244
|
+
if (fs.existsSync(targetDir)) {
|
|
245
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Scaffold failed: ${err.message}`);
|
|
248
|
+
}
|
|
167
249
|
}
|
|
168
250
|
|
|
169
|
-
module.exports = { scaffold };
|
|
251
|
+
module.exports = { scaffold, validateOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-dss-project",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scaffold a DS Strategy Stack project for Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-dss-project": "./bin/create-dss-project.js"
|
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
"engines": {
|
|
25
25
|
"node": ">=16.7.0"
|
|
26
26
|
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "jest --verbose"
|
|
29
|
+
},
|
|
27
30
|
"dependencies": {
|
|
28
31
|
"prompts": "^2.4.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"jest": "^29.7.0"
|
|
29
35
|
}
|
|
30
36
|
}
|