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.
@@ -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
  }
@@ -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
- onComplete({
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(`This field is required`);
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
- <GutterLine>
146
- <Text bold>What problem does this solve? </Text>
147
- <TextInput
148
- placeholder="Describe the core problem..."
149
- onSubmit={handleTextSubmit("problem", true)}
150
- />
151
- </GutterLine>
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
- <GutterLine>
156
- <Text bold>Who are the users? </Text>
157
- <TextInput
158
- placeholder={usersPlaceholder}
159
- onSubmit={handleTextSubmit("users", true)}
160
- />
161
- </GutterLine>
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
- <GutterLine>
193
- <Text bold>Non-goals (comma-separated): </Text>
194
- <TextInput
195
- placeholder="e.g. Mobile app, Real-time sync..."
196
- onSubmit={handleTextSubmit("nonGoals", false)}
197
- />
198
- </GutterLine>
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
- <GutterLine>
93
- <Text bold>Project name: </Text>
94
- <TextInput placeholder="..." onSubmit={handleTextSubmit} />
95
- </GutterLine>
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
- <GutterLine>
100
- <Text bold>One-line description: </Text>
101
- <TextInput placeholder="..." onSubmit={handleTextSubmit} />
102
- </GutterLine>
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