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.
@@ -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
- execSync('git init', { cwd: targetDir, stdio: 'ignore' });
42
- execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
43
- execSync('git commit -m "Initial project from DS Strategy Stack"', {
44
- cwd: targetDir,
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
- // Install dashboard dependencies
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('npm install', {
57
- cwd: path.join(targetDir, 'dashboard'),
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(' Dashboard dependencies installed');
61
- } catch {
62
- console.log(' Could not install dashboard deps run "cd dashboard && npm install" manually');
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
- console.log(`
67
- Project created!
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 };
@@ -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
@@ -1,3 +1,6 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
1
4
  const prompts = require('prompts');
2
5
  const { PROJECT_TYPES } = require('./project-types');
3
6
 
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
- function scaffold(targetDir, templateDir, options) {
62
- const { projectType, structure, projectName } = options;
63
- const typeConfig = PROJECT_TYPES[projectType];
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
- // 1. Copy template
66
- copyDir(templateDir, targetDir, templateDir);
67
-
68
- // 2. Determine effective module flags
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 (structure === 'essentials') {
102
- features.scoring = false;
103
- features.killConditions = false;
104
- features.evidenceGrading = false;
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
- removeIfExists(targetDir, 'memory/scoring.md');
107
- removeIfExists(targetDir, 'dashboard/scoring.html');
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
- // 4. Apply module-level removals
112
- if (!modules.discovery) {
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 (!modules.pipeline && modules.discovery) {
123
- // Pipeline off but discovery might still be on in custom
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
- if (!modules.dashboard) {
129
- removeIfExists(targetDir, 'dashboard');
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
- // 5. Remove template-repo-only files
133
- removeIfExists(targetDir, 'project.config.example.json');
134
- removeIfExists(targetDir, 'scripts/reset-to-template.sh');
135
- removeIfExists(targetDir, 'scripts/validate-placeholders.sh');
136
-
137
- // 6. Write project.config.json
138
- const config = {
139
- templateVersion: '1.0.0',
140
- templateSource: 'github.com/DiffTheEnder/DSS-Claude-Stack',
141
- projectType,
142
- projectName,
143
- projectSlug: projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
144
- oneLineDescription: '',
145
- goal: '',
146
- team: '',
147
- scope: '',
148
- outOfScope: '',
149
- strategicHypothesis: '',
150
- icpDescription: '',
151
- entityType: typeConfig.entityType,
152
- entityTypePlural: typeConfig.entityType + 's',
153
- pipelineSourceOfTruth: 'data/entities.csv',
154
- dashboardUrl: '',
155
- modules,
156
- features,
157
- scoringDimensions: typeConfig.scoringDimensions,
158
- killConditions: [],
159
- };
160
-
161
- fs.writeFileSync(
162
- path.join(targetDir, 'project.config.json'),
163
- JSON.stringify(config, null, 2) + '\n'
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
- return { modules, features };
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.1.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
  }