productkit 1.8.0 → 1.10.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 +32 -5
- package/package.json +6 -3
- package/src/cli.js +10 -1
- package/src/commands/check.js +2 -2
- package/src/commands/completion.js +26 -2
- package/src/commands/diff.js +4 -15
- package/src/commands/doctor.js +12 -4
- package/src/commands/export.js +169 -13
- package/src/commands/init.js +66 -6
- package/src/commands/list.js +17 -0
- package/src/commands/reset.js +1 -11
- package/src/commands/status.js +15 -11
- package/src/commands/update.js +42 -3
- package/src/commands/workspace.js +63 -0
- package/src/utils/fileUtils.js +57 -11
- package/templates/CLAUDE.md +37 -7
- package/templates/README.md +26 -6
- package/templates/commands/productkit.analyze.md +9 -0
- package/templates/commands/productkit.assumptions.md +9 -0
- package/templates/commands/productkit.audit.md +147 -0
- package/templates/commands/productkit.bootstrap.md +8 -1
- package/templates/commands/productkit.clarify.md +9 -0
- package/templates/commands/productkit.constitution.md +22 -1
- package/templates/commands/productkit.landscape.md +130 -0
- package/templates/commands/productkit.learn.md +80 -0
- package/templates/commands/productkit.prioritize.md +33 -7
- package/templates/commands/productkit.problem.md +10 -0
- package/templates/commands/productkit.solution.md +28 -1
- package/templates/commands/productkit.spec.md +20 -0
- package/templates/commands/productkit.stories.md +166 -0
- package/templates/commands/productkit.techreview.md +221 -0
- package/templates/commands/productkit.users.md +10 -0
- package/templates/commands/productkit.validate.md +204 -0
- package/templates/knowledge-README.md +33 -0
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ productkit init my-project
|
|
|
43
43
|
cd my-project
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
This scaffolds a project with slash commands, a `CLAUDE.md` context file, and a `.productkit/` config directory.
|
|
46
|
+
This scaffolds a project with slash commands, a `CLAUDE.md` context file, a `knowledge/` directory for research files, and a `.productkit/` config directory.
|
|
47
47
|
|
|
48
48
|
For existing projects:
|
|
49
49
|
|
|
@@ -58,6 +58,17 @@ To keep artifacts out of the project root (recommended for busy codebases):
|
|
|
58
58
|
productkit init --existing --artifact-dir docs/product
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
To create a shared workspace for multi-project orgs:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
productkit workspace my-company
|
|
65
|
+
cd my-company
|
|
66
|
+
productkit init my-app
|
|
67
|
+
productkit init admin-dashboard
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The workspace holds shared `landscape.md` and `knowledge/` that all projects inside it inherit automatically. `init` auto-detects when it's run inside a workspace.
|
|
71
|
+
|
|
61
72
|
### 2. Open Claude Code
|
|
62
73
|
|
|
63
74
|
```bash
|
|
@@ -70,18 +81,24 @@ Each command starts a guided conversation. Claude asks questions, pushes back on
|
|
|
70
81
|
|
|
71
82
|
| Step | Command | What it does | Output |
|
|
72
83
|
|------|---------|-------------|--------|
|
|
84
|
+
| 0 | `/productkit.landscape` | Capture company, team, and domain landscape | `landscape.md` |
|
|
73
85
|
| 1 | `/productkit.constitution` | Define product principles and values | `constitution.md` |
|
|
74
86
|
| 2 | `/productkit.users` | Define target user personas through dialogue | `users.md` |
|
|
75
87
|
| 3 | `/productkit.problem` | Frame the problem statement grounded in user research | `problem.md` |
|
|
76
88
|
| 4 | `/productkit.assumptions` | Extract and prioritize hidden assumptions | `assumptions.md` |
|
|
77
|
-
| 5 | `/productkit.
|
|
78
|
-
| 6 | `/productkit.
|
|
79
|
-
| 7 | `/productkit.
|
|
89
|
+
| 5 | `/productkit.validate` | Validate assumptions with interviews and surveys | `validation.md` |
|
|
90
|
+
| 6 | `/productkit.solution` | Brainstorm and evaluate solution ideas | `solution.md` |
|
|
91
|
+
| 7 | `/productkit.prioritize` | Score and rank features for v1 | `priorities.md` |
|
|
92
|
+
| 8 | `/productkit.spec` | Generate a complete product spec | `spec.md` |
|
|
80
93
|
| — | `/productkit.clarify` | Resolve ambiguities and contradictions across artifacts | Updates existing files |
|
|
81
94
|
| — | `/productkit.analyze` | Run a consistency and completeness check | Analysis in chat |
|
|
82
95
|
| — | `/productkit.bootstrap` | Auto-draft all artifacts from existing codebase | All missing artifacts |
|
|
96
|
+
| — | `/productkit.audit` | Compare spec against codebase, surface gaps | `audit.md` |
|
|
97
|
+
| — | `/productkit.learn` | Index knowledge directory into a compact summary | `knowledge-index.md` |
|
|
98
|
+
| — | `/productkit.techreview` | Review spec against codebase, flag engineering questions | `techreview.md` |
|
|
99
|
+
| — | `/productkit.stories` | Break spec into user stories with acceptance criteria | `stories.md` |
|
|
83
100
|
|
|
84
|
-
Commands build on each other — `/productkit.problem` reads your `users.md`, `/productkit.solution` reads your problem and users, and `/productkit.spec` synthesizes everything into a single document. You can run `/productkit.clarify` and `/productkit.analyze` at any stage to check your work.
|
|
101
|
+
Commands build on each other — every command reads `landscape.md` and `knowledge-index.md` for evidence, `/productkit.problem` reads your `users.md`, `/productkit.solution` reads your problem and users, and `/productkit.spec` synthesizes everything into a single document. You can run `/productkit.clarify` and `/productkit.analyze` at any stage to check your work.
|
|
85
102
|
|
|
86
103
|
### 4. Review your artifacts
|
|
87
104
|
|
|
@@ -89,13 +106,20 @@ After running the commands, your project contains:
|
|
|
89
106
|
|
|
90
107
|
```
|
|
91
108
|
my-project/
|
|
109
|
+
├── landscape.md # Company & domain landscape
|
|
92
110
|
├── constitution.md # Product principles
|
|
93
111
|
├── users.md # User personas
|
|
94
112
|
├── problem.md # Problem statement
|
|
95
113
|
├── assumptions.md # Prioritized assumptions
|
|
114
|
+
├── validation.md # Validation results & scripts
|
|
96
115
|
├── solution.md # Chosen solution
|
|
97
116
|
├── priorities.md # Ranked feature list
|
|
98
117
|
├── spec.md # Complete product spec
|
|
118
|
+
├── audit.md # Spec vs codebase audit (on demand)
|
|
119
|
+
├── knowledge-index.md # Summary index of knowledge/ files
|
|
120
|
+
├── knowledge/ # Raw research files (interviews, surveys, etc.)
|
|
121
|
+
├── techreview.md # Technical feasibility review (on demand)
|
|
122
|
+
├── stories.md # User stories by epic (on demand)
|
|
99
123
|
├── .productkit/config.json
|
|
100
124
|
├── .claude/commands/ # Slash command prompts
|
|
101
125
|
├── CLAUDE.md
|
|
@@ -114,10 +138,13 @@ These markdown files are your product foundation — share them with your team,
|
|
|
114
138
|
| `productkit init <name>` | Scaffold a new project |
|
|
115
139
|
| `productkit init --existing` | Add Product Kit to the current directory |
|
|
116
140
|
| `productkit init --minimal` | Skip constitution, start with users/problem |
|
|
141
|
+
| `productkit init --mode <solo\|team>` | Set building mode (solo builder vs team with engineers) |
|
|
117
142
|
| `productkit init --artifact-dir <dir>` | Store artifacts in a custom directory |
|
|
143
|
+
| `productkit workspace <name>` | Create a shared workspace for multi-project orgs |
|
|
118
144
|
| `productkit status` | Show progress — which artifacts exist and what's next |
|
|
119
145
|
| `productkit export` | Export all artifacts as a single combined markdown file |
|
|
120
146
|
| `productkit export --output <file>` | Export to a custom filename |
|
|
147
|
+
| `productkit export --stories-csv` | Export stories as CSV for Jira/Linear/Shortcut import |
|
|
121
148
|
| `productkit diff` | Show what changed in artifacts since last commit |
|
|
122
149
|
| `productkit diff --staged` | Show staged artifact changes |
|
|
123
150
|
| `productkit doctor` | Check project health (missing files, outdated commands) |
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "productkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Slash-command-driven product thinking toolkit for Claude Code",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"productkit": "
|
|
7
|
+
"productkit": "src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node --test test/*.test.js"
|
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
],
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "https://github.com/iamquechua/product-kit.git"
|
|
25
|
+
"url": "git+https://github.com/iamquechua/product-kit.git"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/iamquechua/product-kit#readme",
|
|
28
28
|
"bugs": {
|
|
29
29
|
"url": "https://github.com/iamquechua/product-kit/issues"
|
|
30
30
|
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
31
34
|
"author": "Douno",
|
|
32
35
|
"license": "MIT",
|
|
33
36
|
"dependencies": {
|
package/src/cli.js
CHANGED
|
@@ -12,13 +12,15 @@ const completionCommand = require('./commands/completion');
|
|
|
12
12
|
const exportCommand = require('./commands/export');
|
|
13
13
|
const diffCommand = require('./commands/diff');
|
|
14
14
|
const doctorCommand = require('./commands/doctor');
|
|
15
|
+
const workspaceCommand = require('./commands/workspace');
|
|
15
16
|
|
|
17
|
+
const { version } = require('../package.json');
|
|
16
18
|
const program = new Command();
|
|
17
19
|
|
|
18
20
|
program
|
|
19
21
|
.name('productkit')
|
|
20
22
|
.description(chalk.cyan.bold('Product thinking toolkit for Claude Code'))
|
|
21
|
-
.version(
|
|
23
|
+
.version(version);
|
|
22
24
|
|
|
23
25
|
program
|
|
24
26
|
.command('init [projectName]')
|
|
@@ -26,6 +28,7 @@ program
|
|
|
26
28
|
.option('--existing', 'Add Product Kit to the current directory')
|
|
27
29
|
.option('--minimal', 'Skip constitution, start with users/problem')
|
|
28
30
|
.option('--artifact-dir <dir>', 'Directory for artifacts (default: project root)')
|
|
31
|
+
.option('--mode <mode>', 'Building mode: solo or team')
|
|
29
32
|
.action(initCommand);
|
|
30
33
|
|
|
31
34
|
program
|
|
@@ -64,6 +67,7 @@ program
|
|
|
64
67
|
.command('export')
|
|
65
68
|
.description('Export all artifacts as a single combined markdown file')
|
|
66
69
|
.option('--output <file>', 'Output filename', 'export.md')
|
|
70
|
+
.option('--stories-csv', 'Export stories as CSV for Jira/Linear import')
|
|
67
71
|
.action(exportCommand);
|
|
68
72
|
|
|
69
73
|
program
|
|
@@ -77,6 +81,11 @@ program
|
|
|
77
81
|
.description('Check project health (missing files, outdated commands, etc.)')
|
|
78
82
|
.action(doctorCommand);
|
|
79
83
|
|
|
84
|
+
program
|
|
85
|
+
.command('workspace <name>')
|
|
86
|
+
.description('Create a shared workspace for multi-project orgs')
|
|
87
|
+
.action(workspaceCommand);
|
|
88
|
+
|
|
80
89
|
program.parse(process.argv);
|
|
81
90
|
|
|
82
91
|
if (process.argv.length === 2) {
|
package/src/commands/check.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
-
const {
|
|
2
|
+
const { execFileSync } = require('child_process');
|
|
3
3
|
|
|
4
4
|
async function check() {
|
|
5
5
|
try {
|
|
6
|
-
|
|
6
|
+
execFileSync('claude', ['--version'], { stdio: 'ignore' });
|
|
7
7
|
console.log(chalk.green('Claude Code is installed and available.'));
|
|
8
8
|
} catch {
|
|
9
9
|
console.log(chalk.red('Claude Code is not installed or not in PATH.'));
|
|
@@ -8,10 +8,14 @@ _productkit() {
|
|
|
8
8
|
'init:Initialize a new product research project'
|
|
9
9
|
'check:Verify Claude Code is installed and available'
|
|
10
10
|
'status:Show which artifacts exist and what steps remain'
|
|
11
|
+
'export:Export all artifacts as a single combined markdown file'
|
|
12
|
+
'diff:Show what changed in artifacts since last commit'
|
|
13
|
+
'doctor:Check project health'
|
|
11
14
|
'update:Refresh slash commands to the latest version'
|
|
12
15
|
'reset:Remove all artifacts and start over'
|
|
13
16
|
'list:Show available slash commands with descriptions'
|
|
14
17
|
'completion:Output shell completion script'
|
|
18
|
+
'workspace:Create a shared workspace for multi-project orgs'
|
|
15
19
|
)
|
|
16
20
|
|
|
17
21
|
_arguments -C \\
|
|
@@ -27,8 +31,20 @@ _productkit() {
|
|
|
27
31
|
init)
|
|
28
32
|
_arguments \\
|
|
29
33
|
'--existing[Add Product Kit to the current directory]' \\
|
|
34
|
+
'--minimal[Skip constitution]' \\
|
|
35
|
+
'--mode[Building mode (solo or team)]:mode:(solo team)' \\
|
|
36
|
+
'--artifact-dir[Store artifacts in a custom directory]:dir:_files -/' \\
|
|
30
37
|
'1:project name:_files -/'
|
|
31
38
|
;;
|
|
39
|
+
export)
|
|
40
|
+
_arguments \\
|
|
41
|
+
'--output[Output filename]:file:_files' \\
|
|
42
|
+
'--stories-csv[Export stories as CSV for Jira/Linear import]'
|
|
43
|
+
;;
|
|
44
|
+
diff)
|
|
45
|
+
_arguments \\
|
|
46
|
+
'--staged[Show staged changes instead of unstaged]'
|
|
47
|
+
;;
|
|
32
48
|
reset)
|
|
33
49
|
_arguments \\
|
|
34
50
|
'--force[Skip confirmation prompt]'
|
|
@@ -45,7 +61,7 @@ const BASH_COMPLETION = `_productkit() {
|
|
|
45
61
|
COMPREPLY=()
|
|
46
62
|
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
47
63
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
48
|
-
commands="init check status update reset list completion"
|
|
64
|
+
commands="init check status export diff doctor update reset list completion workspace"
|
|
49
65
|
|
|
50
66
|
case "\${prev}" in
|
|
51
67
|
productkit)
|
|
@@ -53,7 +69,15 @@ const BASH_COMPLETION = `_productkit() {
|
|
|
53
69
|
return 0
|
|
54
70
|
;;
|
|
55
71
|
init)
|
|
56
|
-
COMPREPLY=( $(compgen -W "--existing" -- "\${cur}") )
|
|
72
|
+
COMPREPLY=( $(compgen -W "--existing --minimal --mode --artifact-dir" -- "\${cur}") )
|
|
73
|
+
return 0
|
|
74
|
+
;;
|
|
75
|
+
export)
|
|
76
|
+
COMPREPLY=( $(compgen -W "--output --stories-csv" -- "\${cur}") )
|
|
77
|
+
return 0
|
|
78
|
+
;;
|
|
79
|
+
diff)
|
|
80
|
+
COMPREPLY=( $(compgen -W "--staged" -- "\${cur}") )
|
|
57
81
|
return 0
|
|
58
82
|
;;
|
|
59
83
|
reset)
|
package/src/commands/diff.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
-
const {
|
|
5
|
-
const { getArtifactDir } = require('../utils/fileUtils');
|
|
6
|
-
|
|
7
|
-
const ARTIFACT_FILES = [
|
|
8
|
-
'constitution.md',
|
|
9
|
-
'users.md',
|
|
10
|
-
'problem.md',
|
|
11
|
-
'assumptions.md',
|
|
12
|
-
'solution.md',
|
|
13
|
-
'priorities.md',
|
|
14
|
-
'spec.md',
|
|
15
|
-
];
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
|
+
const { getArtifactDir, ARTIFACT_FILES } = require('../utils/fileUtils');
|
|
16
6
|
|
|
17
7
|
async function diff(options) {
|
|
18
8
|
const root = process.cwd();
|
|
@@ -26,7 +16,7 @@ async function diff(options) {
|
|
|
26
16
|
|
|
27
17
|
// Check if git is available
|
|
28
18
|
try {
|
|
29
|
-
|
|
19
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: root, stdio: 'ignore' });
|
|
30
20
|
} catch {
|
|
31
21
|
console.error(chalk.red('Not a git repository. The diff command requires git.'));
|
|
32
22
|
process.exit(1);
|
|
@@ -44,11 +34,10 @@ async function diff(options) {
|
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
const gitArgs = options.staged ? ['diff', '--cached'] : ['diff'];
|
|
47
|
-
const cmd = ['git', ...gitArgs, '--', ...existing].join(' ');
|
|
48
37
|
|
|
49
38
|
let output;
|
|
50
39
|
try {
|
|
51
|
-
output =
|
|
40
|
+
output = execFileSync('git', [...gitArgs, '--', ...existing], { cwd: root, encoding: 'utf-8' });
|
|
52
41
|
} catch (err) {
|
|
53
42
|
// git diff returns exit code 1 when there are differences in some configs
|
|
54
43
|
output = err.stdout || '';
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
-
const {
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
5
|
|
|
6
6
|
async function doctor() {
|
|
7
7
|
const root = process.cwd();
|
|
@@ -45,12 +45,20 @@ async function doctor() {
|
|
|
45
45
|
|
|
46
46
|
// 3. Check for expected command templates
|
|
47
47
|
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'commands');
|
|
48
|
-
|
|
48
|
+
let expectedCommands;
|
|
49
|
+
try {
|
|
50
|
+
expectedCommands = fs.readdirSync(templatesDir);
|
|
51
|
+
} catch {
|
|
52
|
+
fail('Templates directory missing — Product Kit installation may be corrupted');
|
|
53
|
+
expectedCommands = [];
|
|
54
|
+
}
|
|
49
55
|
|
|
50
56
|
// Account for minimal mode
|
|
51
57
|
let config = {};
|
|
52
58
|
try { config = fs.readJsonSync(configPath); } catch {}
|
|
53
|
-
|
|
59
|
+
// Landscape is workspace-only; constitution is skipped in minimal mode
|
|
60
|
+
const skippable = ['productkit.landscape.md'];
|
|
61
|
+
if (config.minimal) skippable.push('productkit.constitution.md');
|
|
54
62
|
|
|
55
63
|
const missing = [];
|
|
56
64
|
for (const cmd of expectedCommands) {
|
|
@@ -92,7 +100,7 @@ async function doctor() {
|
|
|
92
100
|
|
|
93
101
|
// 5. Git initialized
|
|
94
102
|
try {
|
|
95
|
-
|
|
103
|
+
execFileSync('git', ['rev-parse', '--git-dir'], { cwd: root, stdio: 'ignore' });
|
|
96
104
|
pass('Git repository initialized');
|
|
97
105
|
} catch {
|
|
98
106
|
warn('No git repository — consider running git init');
|
package/src/commands/export.js
CHANGED
|
@@ -1,17 +1,148 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
-
const { getArtifactDir } = require('../utils/fileUtils');
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
const { getArtifactDir, getWorkspaceRoot, ARTIFACTS } = require('../utils/fileUtils');
|
|
5
|
+
|
|
6
|
+
function escapeCsvField(field) {
|
|
7
|
+
if (field == null) return '';
|
|
8
|
+
const str = String(field);
|
|
9
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
10
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
11
|
+
}
|
|
12
|
+
return str;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseStoriesCsv(content) {
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
// Detect mode from document headers produced by /productkit.stories
|
|
18
|
+
const isTeamMode = lines.some(l => l.trim() === '# User Stories');
|
|
19
|
+
|
|
20
|
+
if (isTeamMode) {
|
|
21
|
+
return parseTeamMode(lines);
|
|
22
|
+
}
|
|
23
|
+
return parseSoloMode(lines);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseTeamMode(lines) {
|
|
27
|
+
const headers = ['ID', 'Title', 'Epic', 'Priority', 'Estimate', 'Depends On', 'Acceptance Criteria', 'Definition of Done', 'Notes'];
|
|
28
|
+
const rows = [];
|
|
29
|
+
let currentEpic = '';
|
|
30
|
+
let currentRow = null;
|
|
31
|
+
let collectingAC = false;
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
const epicMatch = line.match(/^## Epic \d+:\s*(.+)/);
|
|
35
|
+
if (epicMatch) {
|
|
36
|
+
if (currentRow) rows.push(currentRow);
|
|
37
|
+
currentEpic = epicMatch[1].trim();
|
|
38
|
+
currentRow = null;
|
|
39
|
+
collectingAC = false;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const storyMatch = line.match(/^### (E\d+-S\d+):\s*(.*)/);
|
|
44
|
+
if (storyMatch) {
|
|
45
|
+
if (currentRow) rows.push(currentRow);
|
|
46
|
+
collectingAC = false;
|
|
47
|
+
currentRow = { ID: storyMatch[1], Title: storyMatch[2].trim(), Epic: currentEpic, Priority: '', Estimate: '', 'Depends On': '', 'Acceptance Criteria': [], 'Definition of Done': '', Notes: '' };
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!currentRow) continue;
|
|
52
|
+
|
|
53
|
+
if (collectingAC) {
|
|
54
|
+
const checkMatch = line.match(/^\s*- \[[ x]\]\s*(.+)/);
|
|
55
|
+
if (checkMatch) {
|
|
56
|
+
currentRow['Acceptance Criteria'].push(checkMatch[1].trim());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
collectingAC = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const titleMatch = line.match(/^- \*\*Title:\*\*\s*(.+)/);
|
|
63
|
+
if (titleMatch) { currentRow.Title = titleMatch[1].trim(); continue; }
|
|
64
|
+
|
|
65
|
+
const prioMatch = line.match(/^- \*\*Priority:\*\*\s*(.+)/);
|
|
66
|
+
if (prioMatch) { currentRow.Priority = prioMatch[1].trim(); continue; }
|
|
67
|
+
|
|
68
|
+
const estMatch = line.match(/^- \*\*Estimate:\*\*\s*(.+)/);
|
|
69
|
+
if (estMatch) { currentRow.Estimate = estMatch[1].trim(); continue; }
|
|
70
|
+
|
|
71
|
+
const depMatch = line.match(/^- \*\*Depends on:\*\*\s*(.+)/);
|
|
72
|
+
if (depMatch) { currentRow['Depends On'] = depMatch[1].trim(); continue; }
|
|
73
|
+
|
|
74
|
+
if (line.match(/^- \*\*Acceptance Criteria:\*\*/)) {
|
|
75
|
+
collectingAC = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dodMatch = line.match(/^- \*\*Definition of Done:\*\*\s*(.+)/);
|
|
80
|
+
if (dodMatch) { currentRow['Definition of Done'] = dodMatch[1].trim(); continue; }
|
|
81
|
+
|
|
82
|
+
const notesMatch = line.match(/^- \*\*Notes:\*\*\s*(.+)/);
|
|
83
|
+
if (notesMatch) { currentRow.Notes = notesMatch[1].trim(); continue; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (currentRow) rows.push(currentRow);
|
|
87
|
+
|
|
88
|
+
const csvRows = rows.map(r => {
|
|
89
|
+
const ac = Array.isArray(r['Acceptance Criteria']) ? r['Acceptance Criteria'].join('; ') : r['Acceptance Criteria'];
|
|
90
|
+
return headers.map(h => escapeCsvField(h === 'Acceptance Criteria' ? ac : r[h])).join(',');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return [headers.join(','), ...csvRows].join('\n') + '\n';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseSoloMode(lines) {
|
|
97
|
+
const headers = ['ID', 'Task', 'Effort', 'Depends On', 'Done When', 'Watch Out For'];
|
|
98
|
+
const rows = [];
|
|
99
|
+
let currentRow = null;
|
|
100
|
+
let collectingDone = false;
|
|
101
|
+
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
const taskMatch = line.match(/^### (T\d+):\s*(.*)/);
|
|
104
|
+
if (taskMatch) {
|
|
105
|
+
if (currentRow) rows.push(currentRow);
|
|
106
|
+
collectingDone = false;
|
|
107
|
+
currentRow = { ID: taskMatch[1], Task: taskMatch[2].trim(), Effort: '', 'Depends On': '', 'Done When': [], 'Watch Out For': '' };
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!currentRow) continue;
|
|
112
|
+
|
|
113
|
+
if (collectingDone) {
|
|
114
|
+
const checkMatch = line.match(/^\s*- \[[ x]\]\s*(.+)/);
|
|
115
|
+
if (checkMatch) {
|
|
116
|
+
currentRow['Done When'].push(checkMatch[1].trim());
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
collectingDone = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const effortMatch = line.match(/^- \*\*Effort:\*\*\s*(.+)/);
|
|
123
|
+
if (effortMatch) { currentRow.Effort = effortMatch[1].trim(); continue; }
|
|
124
|
+
|
|
125
|
+
const depMatch = line.match(/^- \*\*Depends on:\*\*\s*(.+)/);
|
|
126
|
+
if (depMatch) { currentRow['Depends On'] = depMatch[1].trim(); continue; }
|
|
127
|
+
|
|
128
|
+
if (line.match(/^- \*\*Done when:\*\*/)) {
|
|
129
|
+
collectingDone = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const watchMatch = line.match(/^- \*\*Watch out for:\*\*\s*(.+)/);
|
|
134
|
+
if (watchMatch) { currentRow['Watch Out For'] = watchMatch[1].trim(); continue; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (currentRow) rows.push(currentRow);
|
|
138
|
+
|
|
139
|
+
const csvRows = rows.map(r => {
|
|
140
|
+
const dw = Array.isArray(r['Done When']) ? r['Done When'].join('; ') : r['Done When'];
|
|
141
|
+
return headers.map(h => escapeCsvField(h === 'Done When' ? dw : r[h])).join(',');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return [headers.join(','), ...csvRows].join('\n') + '\n';
|
|
145
|
+
}
|
|
15
146
|
|
|
16
147
|
async function exportCommand(options) {
|
|
17
148
|
const root = process.cwd();
|
|
@@ -24,14 +155,38 @@ async function exportCommand(options) {
|
|
|
24
155
|
}
|
|
25
156
|
|
|
26
157
|
const artifactDir = getArtifactDir(root);
|
|
158
|
+
|
|
159
|
+
if (options.storiesCsv) {
|
|
160
|
+
const storiesPath = path.join(artifactDir, 'stories.md');
|
|
161
|
+
if (!fs.existsSync(storiesPath)) {
|
|
162
|
+
console.error(chalk.red('No stories.md found. Run /productkit.stories first.'));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const content = fs.readFileSync(storiesPath, 'utf-8');
|
|
166
|
+
const csv = parseStoriesCsv(content);
|
|
167
|
+
const outputFile = options.output === 'export.md' ? 'stories.csv' : options.output;
|
|
168
|
+
fs.writeFileSync(path.join(root, outputFile), csv);
|
|
169
|
+
console.log(chalk.green.bold(`Exported stories to ${outputFile}`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
27
173
|
const existing = ARTIFACTS.filter(a => fs.existsSync(path.join(artifactDir, a.file)));
|
|
28
174
|
|
|
29
|
-
|
|
175
|
+
// Check for workspace landscape
|
|
176
|
+
const workspaceRoot = getWorkspaceRoot(root);
|
|
177
|
+
const hasLandscape = workspaceRoot && fs.existsSync(path.join(workspaceRoot, 'landscape.md'));
|
|
178
|
+
|
|
179
|
+
if (existing.length === 0 && !hasLandscape) {
|
|
30
180
|
console.error(chalk.red('No artifacts found. Run some slash commands first.'));
|
|
31
181
|
process.exit(1);
|
|
32
182
|
}
|
|
33
183
|
|
|
34
184
|
const sections = [];
|
|
185
|
+
|
|
186
|
+
if (hasLandscape) {
|
|
187
|
+
sections.push(fs.readFileSync(path.join(workspaceRoot, 'landscape.md'), 'utf-8'));
|
|
188
|
+
}
|
|
189
|
+
|
|
35
190
|
for (const artifact of existing) {
|
|
36
191
|
const content = fs.readFileSync(path.join(artifactDir, artifact.file), 'utf-8');
|
|
37
192
|
sections.push(content);
|
|
@@ -44,7 +199,8 @@ async function exportCommand(options) {
|
|
|
44
199
|
const outputFile = options.output || 'export.md';
|
|
45
200
|
fs.writeFileSync(path.join(root, outputFile), combined);
|
|
46
201
|
|
|
47
|
-
|
|
202
|
+
const totalCount = existing.length + (hasLandscape ? 1 : 0);
|
|
203
|
+
console.log(chalk.green.bold(`Exported ${totalCount} artifact(s) to ${outputFile}`));
|
|
48
204
|
}
|
|
49
205
|
|
|
50
206
|
module.exports = exportCommand;
|
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
function promptMode() {
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(chalk.cyan('How are you building this product?'));
|
|
14
|
+
console.log(' 1. Solo — I\'m building it myself');
|
|
15
|
+
console.log(' 2. Team — I\'m working with engineers/designers');
|
|
16
|
+
console.log();
|
|
17
|
+
rl.question('Choose (1 or 2): ', (answer) => {
|
|
18
|
+
rl.close();
|
|
19
|
+
resolve(answer.trim() === '2' ? 'team' : 'solo');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
4
23
|
|
|
5
|
-
function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
24
|
+
function scaffold(projectRoot, projectName, minimal, artifactDir, mode) {
|
|
6
25
|
const templatesDir = path.join(__dirname, '..', '..', 'templates');
|
|
26
|
+
const pkgVersion = require('../../package.json').version;
|
|
7
27
|
|
|
8
28
|
// Create directories
|
|
9
29
|
fs.ensureDirSync(path.join(projectRoot, '.productkit'));
|
|
@@ -11,9 +31,21 @@ function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
|
11
31
|
|
|
12
32
|
// Write config
|
|
13
33
|
const config = {
|
|
14
|
-
version:
|
|
34
|
+
version: pkgVersion,
|
|
15
35
|
created: new Date().toISOString(),
|
|
16
36
|
};
|
|
37
|
+
|
|
38
|
+
// Detect workspace: check if parent has workspace config
|
|
39
|
+
const parentDir = path.dirname(projectRoot);
|
|
40
|
+
const parentConfigPath = path.join(parentDir, '.productkit', 'config.json');
|
|
41
|
+
try {
|
|
42
|
+
const parentConfig = fs.readJsonSync(parentConfigPath);
|
|
43
|
+
if (parentConfig.type === 'workspace') {
|
|
44
|
+
config.type = 'project';
|
|
45
|
+
config.workspace = '..';
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
|
|
17
49
|
if (minimal) {
|
|
18
50
|
config.minimal = true;
|
|
19
51
|
}
|
|
@@ -21,6 +53,10 @@ function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
|
21
53
|
config.artifact_dir = artifactDir;
|
|
22
54
|
fs.ensureDirSync(path.join(projectRoot, artifactDir));
|
|
23
55
|
}
|
|
56
|
+
if (mode) {
|
|
57
|
+
config.mode = mode;
|
|
58
|
+
}
|
|
59
|
+
config.knowledge_dir = 'knowledge';
|
|
24
60
|
fs.writeJsonSync(path.join(projectRoot, '.productkit', 'config.json'), config, { spaces: 2 });
|
|
25
61
|
|
|
26
62
|
// Copy slash command templates
|
|
@@ -28,6 +64,8 @@ function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
|
28
64
|
const commandFiles = fs.readdirSync(commandsDir);
|
|
29
65
|
for (const file of commandFiles) {
|
|
30
66
|
if (minimal && file === 'productkit.constitution.md') continue;
|
|
67
|
+
// Landscape lives at workspace level, not project level
|
|
68
|
+
if (file === 'productkit.landscape.md') continue;
|
|
31
69
|
fs.copyFileSync(
|
|
32
70
|
path.join(commandsDir, file),
|
|
33
71
|
path.join(projectRoot, '.claude', 'commands', file)
|
|
@@ -51,6 +89,14 @@ function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
|
51
89
|
fs.writeFileSync(path.join(projectRoot, 'README.md'), readme);
|
|
52
90
|
}
|
|
53
91
|
|
|
92
|
+
// Create knowledge directory with README
|
|
93
|
+
const knowledgeDir = path.join(projectRoot, 'knowledge');
|
|
94
|
+
fs.ensureDirSync(knowledgeDir);
|
|
95
|
+
fs.copyFileSync(
|
|
96
|
+
path.join(templatesDir, 'knowledge-README.md'),
|
|
97
|
+
path.join(knowledgeDir, 'README.md')
|
|
98
|
+
);
|
|
99
|
+
|
|
54
100
|
// Copy .gitignore (only for new projects)
|
|
55
101
|
if (!fs.existsSync(path.join(projectRoot, '.gitignore'))) {
|
|
56
102
|
fs.copyFileSync(
|
|
@@ -61,6 +107,20 @@ function scaffold(projectRoot, projectName, minimal, artifactDir) {
|
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
async function init(projectName, options) {
|
|
110
|
+
// Validate and resolve mode
|
|
111
|
+
let mode = options.mode;
|
|
112
|
+
if (mode && mode !== 'solo' && mode !== 'team') {
|
|
113
|
+
console.error(chalk.red('Error: --mode must be "solo" or "team"'));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (!mode) {
|
|
117
|
+
if (process.stdin.isTTY) {
|
|
118
|
+
mode = await promptMode();
|
|
119
|
+
} else {
|
|
120
|
+
mode = 'solo';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
64
124
|
if (options.existing) {
|
|
65
125
|
const projectRoot = process.cwd();
|
|
66
126
|
|
|
@@ -70,7 +130,7 @@ async function init(projectName, options) {
|
|
|
70
130
|
}
|
|
71
131
|
|
|
72
132
|
try {
|
|
73
|
-
scaffold(projectRoot, path.basename(projectRoot), options.minimal, options.artifactDir);
|
|
133
|
+
scaffold(projectRoot, path.basename(projectRoot), options.minimal, options.artifactDir, mode);
|
|
74
134
|
|
|
75
135
|
console.log(chalk.green.bold('Product Kit added to existing project!'));
|
|
76
136
|
console.log();
|
|
@@ -98,12 +158,12 @@ async function init(projectName, options) {
|
|
|
98
158
|
}
|
|
99
159
|
|
|
100
160
|
try {
|
|
101
|
-
scaffold(projectRoot, projectName, options.minimal, options.artifactDir);
|
|
161
|
+
scaffold(projectRoot, projectName, options.minimal, options.artifactDir, mode);
|
|
102
162
|
|
|
103
163
|
// Init git repo
|
|
104
|
-
const {
|
|
164
|
+
const { execFileSync } = require('child_process');
|
|
105
165
|
try {
|
|
106
|
-
|
|
166
|
+
execFileSync('git', ['init'], { cwd: projectRoot, stdio: 'ignore' });
|
|
107
167
|
} catch {
|
|
108
168
|
// Git not available, skip
|
|
109
169
|
}
|