procedure-cli 0.1.11 → 0.1.13
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/.claude/settings.local.json +2 -1
- package/AGENTS.md +82 -61
- package/CODE-FIXED.md +81 -0
- package/CODE-REVIEW.md +172 -0
- package/README.md +105 -69
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -1
- package/dist/steps/build-test.js +32 -3
- package/dist/steps/build-test.js.map +1 -1
- package/dist/steps/powerline.js +11 -3
- package/dist/steps/powerline.js.map +1 -1
- package/dist/steps/product-context.js +54 -19
- package/dist/steps/product-context.js.map +1 -1
- package/dist/steps/project-info.js +30 -3
- package/dist/steps/project-info.js.map +1 -1
- package/docs/PRD.md +55 -44
- package/docs/USER-STORIES.md +241 -98
- package/package.json +1 -1
- package/src/cli.tsx +13 -0
- package/src/steps/build-test.tsx +41 -6
- package/src/steps/powerline.tsx +17 -1
- package/src/steps/product-context.tsx +100 -39
- package/src/steps/project-info.tsx +57 -10
package/src/steps/build-test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Text } from "ink";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
3
|
import { TextInput } from "@inkjs/ui";
|
|
4
4
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
5
|
import type { WizardAnswers } from "../lib/types.js";
|
|
@@ -29,23 +29,47 @@ export default function BuildTest({ initialValues, onComplete }: Props) {
|
|
|
29
29
|
const [fieldIndex, setFieldIndex] = useState(0);
|
|
30
30
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
|
31
31
|
const [error, setError] = useState("");
|
|
32
|
+
const [liveInput, setLiveInput] = useState("");
|
|
32
33
|
|
|
33
34
|
const current = FIELDS[fieldIndex]!;
|
|
34
35
|
const preset = initialValues?.[current.key] as string | undefined;
|
|
35
36
|
const defaultVal = preset || current.fallback;
|
|
36
37
|
|
|
38
|
+
// Reset live input when navigating to a different field
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setLiveInput("");
|
|
41
|
+
}, [fieldIndex]);
|
|
42
|
+
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (key.shift && key.tab && fieldIndex > 0) {
|
|
45
|
+
setFieldIndex((f) => f - 1);
|
|
46
|
+
setError("");
|
|
47
|
+
} else if (key.tab && !key.shift) {
|
|
48
|
+
const val = liveInput.trim() || answers[current.key] || defaultVal;
|
|
49
|
+
if (current.required && !val.trim()) {
|
|
50
|
+
setError(`${current.label} is required`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setError("");
|
|
54
|
+
const next = { ...answers, [current.key]: val };
|
|
55
|
+
setAnswers(next);
|
|
56
|
+
if (fieldIndex < FIELDS.length - 1) {
|
|
57
|
+
setFieldIndex((f) => f + 1);
|
|
58
|
+
} else {
|
|
59
|
+
onComplete(next as unknown as Partial<WizardAnswers>);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
37
64
|
function handleSubmit(value: string) {
|
|
38
65
|
const val = value.trim() || defaultVal;
|
|
39
|
-
|
|
40
66
|
if (current.required && !val) {
|
|
41
67
|
setError(`${current.label} is required`);
|
|
42
68
|
return;
|
|
43
69
|
}
|
|
44
|
-
|
|
45
70
|
setError("");
|
|
46
71
|
const next = { ...answers, [current.key]: val };
|
|
47
72
|
setAnswers(next);
|
|
48
|
-
|
|
49
73
|
if (fieldIndex < FIELDS.length - 1) {
|
|
50
74
|
setFieldIndex(fieldIndex + 1);
|
|
51
75
|
} else {
|
|
@@ -53,6 +77,9 @@ export default function BuildTest({ initialValues, onComplete }: Props) {
|
|
|
53
77
|
}
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
const isFirst = fieldIndex === 0;
|
|
81
|
+
const isLast = fieldIndex === FIELDS.length - 1;
|
|
82
|
+
|
|
56
83
|
return (
|
|
57
84
|
<>
|
|
58
85
|
{FIELDS.slice(0, fieldIndex).map((f) => (
|
|
@@ -67,13 +94,21 @@ export default function BuildTest({ initialValues, onComplete }: Props) {
|
|
|
67
94
|
<Text bold>{current.label}</Text>
|
|
68
95
|
<Text color={C.overlay1}> ({defaultVal})</Text>
|
|
69
96
|
<Text bold>: </Text>
|
|
70
|
-
<TextInput placeholder={defaultVal} onSubmit={handleSubmit} />
|
|
97
|
+
<TextInput placeholder={defaultVal} onChange={setLiveInput} onSubmit={handleSubmit} />
|
|
71
98
|
</GutterLine>
|
|
99
|
+
|
|
72
100
|
{error && (
|
|
73
101
|
<GutterLine>
|
|
74
102
|
<Text color={C.red}>{error}</Text>
|
|
75
103
|
</GutterLine>
|
|
76
104
|
)}
|
|
105
|
+
|
|
106
|
+
<GutterLine>
|
|
107
|
+
<Text color={C.overlay1}>
|
|
108
|
+
{!isFirst ? "Shift+Tab prev " : ""}
|
|
109
|
+
{isLast ? "Tab / Enter confirm" : "Tab next Enter confirm"}
|
|
110
|
+
</Text>
|
|
111
|
+
</GutterLine>
|
|
77
112
|
</>
|
|
78
113
|
);
|
|
79
114
|
}
|
package/src/steps/powerline.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import { Text } from "ink";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
3
|
import { ConfirmInput, Spinner } from "@inkjs/ui";
|
|
4
4
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
5
|
import { setupPowerline } from "../lib/powerline.js";
|
|
@@ -38,6 +38,16 @@ export default function Powerline({ answers, onComplete }: Props) {
|
|
|
38
38
|
const [errorMsg, setErrorMsg] = useState("");
|
|
39
39
|
const [setupResult, setSetupResult] = useState<SetupResult>({});
|
|
40
40
|
|
|
41
|
+
useInput(
|
|
42
|
+
(input, key) => {
|
|
43
|
+
if (key.shift && key.tab) {
|
|
44
|
+
if (phase === "ask-git") setPhase("ask-powerline");
|
|
45
|
+
else if (phase === "ask-release") setPhase("ask-git");
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ isActive: phase === "ask-git" || phase === "ask-release" }
|
|
49
|
+
);
|
|
50
|
+
|
|
41
51
|
function runSetup(powerline: boolean, git: boolean, release: boolean) {
|
|
42
52
|
setPhase("running");
|
|
43
53
|
try {
|
|
@@ -129,6 +139,9 @@ export default function Powerline({ answers, onComplete }: Props) {
|
|
|
129
139
|
onCancel={() => { setWantGit(false); setPhase("ask-release"); }}
|
|
130
140
|
/>
|
|
131
141
|
</GutterLine>
|
|
142
|
+
<GutterLine>
|
|
143
|
+
<Text color={C.overlay1}>{"Shift+Tab prev y/n answer"}</Text>
|
|
144
|
+
</GutterLine>
|
|
132
145
|
</>
|
|
133
146
|
);
|
|
134
147
|
}
|
|
@@ -173,6 +186,9 @@ export default function Powerline({ answers, onComplete }: Props) {
|
|
|
173
186
|
onCancel={() => { setWantRelease(false); runSetup(wantPowerline, wantGit, false); }}
|
|
174
187
|
/>
|
|
175
188
|
</GutterLine>
|
|
189
|
+
<GutterLine>
|
|
190
|
+
<Text color={C.overlay1}>{"Shift+Tab prev y/n answer"}</Text>
|
|
191
|
+
</GutterLine>
|
|
176
192
|
</>
|
|
177
193
|
);
|
|
178
194
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Text } from "ink";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
3
|
import { TextInput } from "@inkjs/ui";
|
|
4
4
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
5
|
import { GutteredMultiSelect } from "../components/guttered-select.js";
|
|
@@ -57,6 +57,7 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
57
57
|
const [phaseIndex, setPhaseIndex] = useState(0);
|
|
58
58
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
|
59
59
|
const [error, setError] = useState("");
|
|
60
|
+
const [liveInput, setLiveInput] = useState("");
|
|
60
61
|
|
|
61
62
|
// Prefill tech stack from Stack & Style selections (language + framework)
|
|
62
63
|
const prefillTechStack: string[] = [];
|
|
@@ -74,6 +75,56 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
const currentPhase = PHASE_ORDER[phaseIndex]!;
|
|
78
|
+
const isFirst = phaseIndex === 0;
|
|
79
|
+
const isLast = phaseIndex === PHASE_ORDER.length - 1;
|
|
80
|
+
|
|
81
|
+
// Reset live input when navigating to a different field
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setLiveInput("");
|
|
84
|
+
}, [phaseIndex]);
|
|
85
|
+
|
|
86
|
+
const isMultiSelectPhase = currentPhase === "techStack" || currentPhase === "coreFeatures";
|
|
87
|
+
|
|
88
|
+
useInput((input, key) => {
|
|
89
|
+
if (key.shift && key.tab && phaseIndex > 0) {
|
|
90
|
+
setPhaseIndex((i) => i - 1);
|
|
91
|
+
setError("");
|
|
92
|
+
} else if (key.tab && !key.shift && !isMultiSelectPhase) {
|
|
93
|
+
// Tab only advances text phases — multi-select phases use Enter to confirm
|
|
94
|
+
const required = currentPhase === "problem" || currentPhase === "users";
|
|
95
|
+
const val = liveInput.trim() || answers[currentPhase] || "";
|
|
96
|
+
if (required && !val) {
|
|
97
|
+
setError("This field is required");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
setError("");
|
|
101
|
+
const next = { ...answers, [currentPhase]: val };
|
|
102
|
+
setAnswers(next);
|
|
103
|
+
if (phaseIndex < PHASE_ORDER.length - 1) {
|
|
104
|
+
setPhaseIndex((i) => i + 1);
|
|
105
|
+
} else {
|
|
106
|
+
finalize(next);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
function finalize(data: Record<string, string>) {
|
|
112
|
+
onComplete({
|
|
113
|
+
problem: data.problem,
|
|
114
|
+
users: data.users,
|
|
115
|
+
techStack: data.techStack,
|
|
116
|
+
coreFeatures: (data.coreFeatures || "")
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((s) => s.trim())
|
|
119
|
+
.filter(Boolean),
|
|
120
|
+
nonGoals: (data.nonGoals || "")
|
|
121
|
+
.split(",")
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean),
|
|
124
|
+
userStories: [],
|
|
125
|
+
envVars: [],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
77
128
|
|
|
78
129
|
function advanceToNext(key: string, value: string) {
|
|
79
130
|
const next = { ...answers, [key]: value };
|
|
@@ -83,21 +134,7 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
83
134
|
if (phaseIndex < PHASE_ORDER.length - 1) {
|
|
84
135
|
setPhaseIndex(phaseIndex + 1);
|
|
85
136
|
} else {
|
|
86
|
-
|
|
87
|
-
problem: next.problem,
|
|
88
|
-
users: next.users,
|
|
89
|
-
techStack: next.techStack,
|
|
90
|
-
coreFeatures: (next.coreFeatures || "")
|
|
91
|
-
.split(",")
|
|
92
|
-
.map((s) => s.trim())
|
|
93
|
-
.filter(Boolean),
|
|
94
|
-
nonGoals: (next.nonGoals || "")
|
|
95
|
-
.split(",")
|
|
96
|
-
.map((s) => s.trim())
|
|
97
|
-
.filter(Boolean),
|
|
98
|
-
userStories: [],
|
|
99
|
-
envVars: [],
|
|
100
|
-
});
|
|
137
|
+
finalize(next);
|
|
101
138
|
}
|
|
102
139
|
}
|
|
103
140
|
|
|
@@ -105,7 +142,7 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
105
142
|
return (value: string) => {
|
|
106
143
|
const val = value.trim();
|
|
107
144
|
if (required && !val) {
|
|
108
|
-
setError(
|
|
145
|
+
setError("This field is required");
|
|
109
146
|
return;
|
|
110
147
|
}
|
|
111
148
|
advanceToNext(key, val);
|
|
@@ -142,23 +179,35 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
142
179
|
{completedFields}
|
|
143
180
|
|
|
144
181
|
{currentPhase === "problem" && (
|
|
145
|
-
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
182
|
+
<>
|
|
183
|
+
<GutterLine>
|
|
184
|
+
<Text bold>What problem does this solve? </Text>
|
|
185
|
+
<TextInput
|
|
186
|
+
placeholder="Describe the core problem..."
|
|
187
|
+
onChange={setLiveInput}
|
|
188
|
+
onSubmit={handleTextSubmit("problem", true)}
|
|
189
|
+
/>
|
|
190
|
+
</GutterLine>
|
|
191
|
+
<GutterLine>
|
|
192
|
+
<Text color={C.overlay1}>{"Tab next Enter confirm"}</Text>
|
|
193
|
+
</GutterLine>
|
|
194
|
+
</>
|
|
152
195
|
)}
|
|
153
196
|
|
|
154
197
|
{currentPhase === "users" && (
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
198
|
+
<>
|
|
199
|
+
<GutterLine>
|
|
200
|
+
<Text bold>Who are the users? </Text>
|
|
201
|
+
<TextInput
|
|
202
|
+
placeholder={usersPlaceholder}
|
|
203
|
+
onChange={setLiveInput}
|
|
204
|
+
onSubmit={handleTextSubmit("users", true)}
|
|
205
|
+
/>
|
|
206
|
+
</GutterLine>
|
|
207
|
+
<GutterLine>
|
|
208
|
+
<Text color={C.overlay1}>{"Shift+Tab prev Tab next Enter confirm"}</Text>
|
|
209
|
+
</GutterLine>
|
|
210
|
+
</>
|
|
162
211
|
)}
|
|
163
212
|
|
|
164
213
|
{currentPhase === "techStack" && (
|
|
@@ -171,6 +220,9 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
171
220
|
initialSelected={prefillTechStack}
|
|
172
221
|
onSubmit={handleTechStackSubmit}
|
|
173
222
|
/>
|
|
223
|
+
<GutterLine>
|
|
224
|
+
<Text color={C.overlay1}>{"Shift+Tab prev ↑↓ move Space toggle Enter confirm"}</Text>
|
|
225
|
+
</GutterLine>
|
|
174
226
|
</>
|
|
175
227
|
)}
|
|
176
228
|
|
|
@@ -185,17 +237,26 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
185
237
|
allowCustom
|
|
186
238
|
customPlaceholder="e.g. Export PDF, Onboarding flow..."
|
|
187
239
|
/>
|
|
240
|
+
<GutterLine>
|
|
241
|
+
<Text color={C.overlay1}>{"Shift+Tab prev ↑↓ move Space toggle Enter confirm"}</Text>
|
|
242
|
+
</GutterLine>
|
|
188
243
|
</>
|
|
189
244
|
)}
|
|
190
245
|
|
|
191
246
|
{currentPhase === "nonGoals" && (
|
|
192
|
-
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
247
|
+
<>
|
|
248
|
+
<GutterLine>
|
|
249
|
+
<Text bold>Non-goals (comma-separated): </Text>
|
|
250
|
+
<TextInput
|
|
251
|
+
placeholder="e.g. Mobile app, Real-time sync..."
|
|
252
|
+
onChange={setLiveInput}
|
|
253
|
+
onSubmit={handleTextSubmit("nonGoals", false)}
|
|
254
|
+
/>
|
|
255
|
+
</GutterLine>
|
|
256
|
+
<GutterLine>
|
|
257
|
+
<Text color={C.overlay1}>{"Shift+Tab prev Tab / Enter confirm"}</Text>
|
|
258
|
+
</GutterLine>
|
|
259
|
+
</>
|
|
199
260
|
)}
|
|
200
261
|
|
|
201
262
|
{error && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Text } from "ink";
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
3
|
import { TextInput } from "@inkjs/ui";
|
|
4
4
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
5
|
import { GutteredSelect } from "../components/guttered-select.js";
|
|
@@ -38,8 +38,39 @@ export default function ProjectInfo({ onComplete }: Props) {
|
|
|
38
38
|
const [stepIndex, setStepIndex] = useState(0);
|
|
39
39
|
const [answers, setAnswers] = useState<Record<string, string>>({});
|
|
40
40
|
const [error, setError] = useState("");
|
|
41
|
+
const [liveInput, setLiveInput] = useState("");
|
|
41
42
|
|
|
42
43
|
const currentStep = STEP_ORDER[stepIndex]!;
|
|
44
|
+
const isFirst = stepIndex === 0;
|
|
45
|
+
|
|
46
|
+
// Reset live input when navigating to a different field
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setLiveInput("");
|
|
49
|
+
}, [stepIndex]);
|
|
50
|
+
|
|
51
|
+
const isSelectStep = currentStep === "packageManager" || currentStep === "license";
|
|
52
|
+
|
|
53
|
+
useInput((input, key) => {
|
|
54
|
+
if (key.shift && key.tab && stepIndex > 0) {
|
|
55
|
+
setStepIndex((i) => i - 1);
|
|
56
|
+
setError("");
|
|
57
|
+
} else if (key.tab && !key.shift && !isSelectStep) {
|
|
58
|
+
// Tab only advances text steps — select steps require an explicit Enter selection
|
|
59
|
+
const val = liveInput.trim() || answers[currentStep] || "";
|
|
60
|
+
if (!val) {
|
|
61
|
+
setError(
|
|
62
|
+
currentStep === "projectName"
|
|
63
|
+
? "Project name is required"
|
|
64
|
+
: "Description is required"
|
|
65
|
+
);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
setError("");
|
|
69
|
+
const next = { ...answers, [currentStep]: val };
|
|
70
|
+
setAnswers(next);
|
|
71
|
+
setStepIndex((i) => i + 1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
43
74
|
|
|
44
75
|
function advance(key: string, value: string) {
|
|
45
76
|
const next = { ...answers, [key]: value };
|
|
@@ -89,17 +120,27 @@ export default function ProjectInfo({ onComplete }: Props) {
|
|
|
89
120
|
{completed}
|
|
90
121
|
|
|
91
122
|
{currentStep === "projectName" && (
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
95
|
-
|
|
123
|
+
<>
|
|
124
|
+
<GutterLine>
|
|
125
|
+
<Text bold>Project name: </Text>
|
|
126
|
+
<TextInput placeholder="..." onChange={setLiveInput} onSubmit={handleTextSubmit} />
|
|
127
|
+
</GutterLine>
|
|
128
|
+
<GutterLine>
|
|
129
|
+
<Text color={C.overlay1}>{"Tab next Enter confirm"}</Text>
|
|
130
|
+
</GutterLine>
|
|
131
|
+
</>
|
|
96
132
|
)}
|
|
97
133
|
|
|
98
134
|
{currentStep === "description" && (
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
<>
|
|
136
|
+
<GutterLine>
|
|
137
|
+
<Text bold>One-line description: </Text>
|
|
138
|
+
<TextInput placeholder="..." onChange={setLiveInput} onSubmit={handleTextSubmit} />
|
|
139
|
+
</GutterLine>
|
|
140
|
+
<GutterLine>
|
|
141
|
+
<Text color={C.overlay1}>{"Shift+Tab prev Tab next Enter confirm"}</Text>
|
|
142
|
+
</GutterLine>
|
|
143
|
+
</>
|
|
103
144
|
)}
|
|
104
145
|
|
|
105
146
|
{currentStep === "packageManager" && (
|
|
@@ -111,6 +152,9 @@ export default function ProjectInfo({ onComplete }: Props) {
|
|
|
111
152
|
options={PACKAGE_MANAGER_OPTIONS}
|
|
112
153
|
onChange={handleSelectChange("packageManager")}
|
|
113
154
|
/>
|
|
155
|
+
<GutterLine>
|
|
156
|
+
<Text color={C.overlay1}>{"Shift+Tab prev ↑↓ move Enter select"}</Text>
|
|
157
|
+
</GutterLine>
|
|
114
158
|
</>
|
|
115
159
|
)}
|
|
116
160
|
|
|
@@ -123,6 +167,9 @@ export default function ProjectInfo({ onComplete }: Props) {
|
|
|
123
167
|
options={LICENSE_OPTIONS}
|
|
124
168
|
onChange={handleSelectChange("license")}
|
|
125
169
|
/>
|
|
170
|
+
<GutterLine>
|
|
171
|
+
<Text color={C.overlay1}>{"Shift+Tab prev ↑↓ move Enter select"}</Text>
|
|
172
|
+
</GutterLine>
|
|
126
173
|
</>
|
|
127
174
|
)}
|
|
128
175
|
|