procedure-cli 0.1.13 → 0.1.14
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/dist/steps/build-test.js +1 -1
- package/dist/steps/build-test.js.map +1 -1
- package/dist/steps/product-context.js +1 -1
- package/dist/steps/product-context.js.map +1 -1
- package/dist/steps/project-info.js +1 -1
- package/dist/steps/project-info.js.map +1 -1
- package/package.json +5 -1
- package/.claude/settings.local.json +0 -27
- package/.env.example +0 -2
- package/AGENTS.md +0 -134
- package/CLAUDE.md +0 -138
- package/CODE-FIXED.md +0 -252
- package/CODE-REVIEW.md +0 -558
- package/config/defaults.json +0 -8
- package/config/powerline-config.json +0 -52
- package/config/stacks/typescript-node.json +0 -15
- package/docs/GIAI-THICH-CLAUDE-MD.md +0 -206
- package/docs/PRD.md +0 -141
- package/docs/USER-STORIES.md +0 -324
- package/src/app.tsx +0 -213
- package/src/cli.tsx +0 -19
- package/src/components/banner.tsx +0 -23
- package/src/components/gutter-line.tsx +0 -16
- package/src/components/guttered-select.tsx +0 -231
- package/src/components/step-indicator.tsx +0 -32
- package/src/components/timeline.tsx +0 -57
- package/src/lib/fs.ts +0 -23
- package/src/lib/git.ts +0 -41
- package/src/lib/powerline.ts +0 -48
- package/src/lib/template.ts +0 -161
- package/src/lib/types.ts +0 -70
- package/src/providers/openai.ts +0 -5
- package/src/providers/zai.ts +0 -7
- package/src/steps/architecture.tsx +0 -72
- package/src/steps/build-test.tsx +0 -114
- package/src/steps/generation.tsx +0 -176
- package/src/steps/powerline.tsx +0 -254
- package/src/steps/product-context.tsx +0 -269
- package/src/steps/project-info.tsx +0 -183
- package/src/steps/stack-style.tsx +0 -304
- package/src/theme.ts +0 -15
- package/tsconfig.json +0 -17
package/src/app.tsx
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { C } from "./theme.js";
|
|
4
|
-
import type { WizardAnswers } from "./lib/types.js";
|
|
5
|
-
import type { TimelineStep } from "./components/timeline.js";
|
|
6
|
-
|
|
7
|
-
import Banner from "./components/banner.js";
|
|
8
|
-
import Timeline from "./components/timeline.js";
|
|
9
|
-
|
|
10
|
-
import ProjectInfo from "./steps/project-info.js";
|
|
11
|
-
import StackStyle from "./steps/stack-style.js";
|
|
12
|
-
import BuildTest from "./steps/build-test.js";
|
|
13
|
-
import Architecture from "./steps/architecture.js";
|
|
14
|
-
import ProductContext from "./steps/product-context.js";
|
|
15
|
-
import Generation from "./steps/generation.js";
|
|
16
|
-
import Powerline from "./steps/powerline.js";
|
|
17
|
-
|
|
18
|
-
const STEP_NAMES = [
|
|
19
|
-
"Project Info",
|
|
20
|
-
"Stack & Style",
|
|
21
|
-
"Build & Test",
|
|
22
|
-
"Architecture",
|
|
23
|
-
"Product Context",
|
|
24
|
-
"Generation",
|
|
25
|
-
"Setup",
|
|
26
|
-
] as const;
|
|
27
|
-
|
|
28
|
-
const EMPTY_ANSWERS: WizardAnswers = {
|
|
29
|
-
projectName: "",
|
|
30
|
-
description: "",
|
|
31
|
-
packageManager: "npm",
|
|
32
|
-
license: "ISC",
|
|
33
|
-
codeStyle: [],
|
|
34
|
-
framework: "",
|
|
35
|
-
language: "",
|
|
36
|
-
buildCommand: "",
|
|
37
|
-
testCommand: "",
|
|
38
|
-
typecheckCommand: "",
|
|
39
|
-
lintCommand: "",
|
|
40
|
-
prCommand: "",
|
|
41
|
-
architecture: "",
|
|
42
|
-
architectureNotes: "",
|
|
43
|
-
problem: "",
|
|
44
|
-
users: "",
|
|
45
|
-
coreFeatures: [],
|
|
46
|
-
nonGoals: [],
|
|
47
|
-
techStack: "",
|
|
48
|
-
userStories: [],
|
|
49
|
-
envVars: [],
|
|
50
|
-
generationSkipped: false,
|
|
51
|
-
setupPowerline: false,
|
|
52
|
-
setupGit: false,
|
|
53
|
-
setupRelease: false,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
function getSummary(stepIndex: number, answers: WizardAnswers): string {
|
|
57
|
-
switch (stepIndex) {
|
|
58
|
-
case 0: {
|
|
59
|
-
const name = answers.projectName || "untitled";
|
|
60
|
-
const desc = answers.description || "no description";
|
|
61
|
-
return `${name} — ${desc}`;
|
|
62
|
-
}
|
|
63
|
-
case 1: {
|
|
64
|
-
const parts: string[] = [];
|
|
65
|
-
if (answers.language) parts.push(answers.language);
|
|
66
|
-
if (answers.framework) parts.push(answers.framework);
|
|
67
|
-
return parts.length > 0 ? parts.join(", ") : "configured";
|
|
68
|
-
}
|
|
69
|
-
case 2: {
|
|
70
|
-
const cmds: string[] = [];
|
|
71
|
-
if (answers.buildCommand) cmds.push(`build: ${answers.buildCommand}`);
|
|
72
|
-
if (answers.testCommand) cmds.push(`test: ${answers.testCommand}`);
|
|
73
|
-
return cmds.length > 0 ? cmds.join(", ") : "configured";
|
|
74
|
-
}
|
|
75
|
-
case 3: {
|
|
76
|
-
return answers.architecture || "configured";
|
|
77
|
-
}
|
|
78
|
-
case 4: {
|
|
79
|
-
const features = answers.coreFeatures.length;
|
|
80
|
-
const nonGoals = answers.nonGoals.length;
|
|
81
|
-
const parts: string[] = [];
|
|
82
|
-
if (features > 0) parts.push(`${features} feature${features !== 1 ? "s" : ""}`);
|
|
83
|
-
if (nonGoals > 0) parts.push(`${nonGoals} non-goal${nonGoals !== 1 ? "s" : ""}`);
|
|
84
|
-
return parts.length > 0 ? parts.join(", ") : "configured";
|
|
85
|
-
}
|
|
86
|
-
case 5:
|
|
87
|
-
return answers.generationSkipped ? "skipped" : "files generated";
|
|
88
|
-
case 6: {
|
|
89
|
-
const parts: string[] = [];
|
|
90
|
-
if (answers.setupPowerline) parts.push("Powerline");
|
|
91
|
-
if (answers.setupGit) parts.push("Git");
|
|
92
|
-
if (answers.setupRelease) parts.push("Release scripts");
|
|
93
|
-
return parts.length > 0 ? parts.join(", ") : "skipped";
|
|
94
|
-
}
|
|
95
|
-
default:
|
|
96
|
-
return "done";
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export default function App() {
|
|
101
|
-
const [currentStep, setCurrentStep] = useState(0);
|
|
102
|
-
const [answers, setAnswers] = useState<WizardAnswers>(EMPTY_ANSWERS);
|
|
103
|
-
const [finished, setFinished] = useState(false);
|
|
104
|
-
|
|
105
|
-
useInput((input, key) => {
|
|
106
|
-
if (key.escape) {
|
|
107
|
-
process.exit(0);
|
|
108
|
-
}
|
|
109
|
-
if (finished && key.return) {
|
|
110
|
-
process.exit(0);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
function handleStepComplete(partial: Partial<WizardAnswers>) {
|
|
115
|
-
const merged = { ...answers, ...partial };
|
|
116
|
-
setAnswers(merged);
|
|
117
|
-
|
|
118
|
-
if (currentStep < STEP_NAMES.length - 1) {
|
|
119
|
-
setCurrentStep(currentStep + 1);
|
|
120
|
-
} else {
|
|
121
|
-
setFinished(true);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (finished) {
|
|
126
|
-
const steps: TimelineStep[] = STEP_NAMES.map((name, i) => ({
|
|
127
|
-
name,
|
|
128
|
-
status: "completed" as const,
|
|
129
|
-
summary: getSummary(i, answers),
|
|
130
|
-
}));
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<Box flexDirection="column" padding={1}>
|
|
134
|
-
<Banner />
|
|
135
|
-
<Timeline steps={steps}>
|
|
136
|
-
<Box />
|
|
137
|
-
</Timeline>
|
|
138
|
-
<Box marginTop={1} flexDirection="column">
|
|
139
|
-
<Text bold color={C.green}>
|
|
140
|
-
{"✔"} Procedure CLI setup complete!
|
|
141
|
-
</Text>
|
|
142
|
-
<Text> </Text>
|
|
143
|
-
{answers.generationSkipped ? (
|
|
144
|
-
<Text color={C.overlay1}>Generation was skipped — no files were written.</Text>
|
|
145
|
-
) : (
|
|
146
|
-
<>
|
|
147
|
-
<Text>
|
|
148
|
-
Project <Text bold color={C.sapphire}>"{answers.projectName}"</Text> has been scaffolded.
|
|
149
|
-
</Text>
|
|
150
|
-
<Text color={C.overlay1}>
|
|
151
|
-
Check the generated CLAUDE.md, PRD.md, and USER-STORIES.md.
|
|
152
|
-
</Text>
|
|
153
|
-
</>
|
|
154
|
-
)}
|
|
155
|
-
<Text> </Text>
|
|
156
|
-
<Text color={C.overlay1}>Press Enter to exit.</Text>
|
|
157
|
-
</Box>
|
|
158
|
-
</Box>
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const steps: TimelineStep[] = STEP_NAMES.map((name, i) => {
|
|
163
|
-
if (i < currentStep) {
|
|
164
|
-
return { name, status: "completed" as const, summary: getSummary(i, answers) };
|
|
165
|
-
}
|
|
166
|
-
if (i === currentStep) {
|
|
167
|
-
return { name, status: "active" as const };
|
|
168
|
-
}
|
|
169
|
-
return { name, status: "pending" as const };
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const activeContent = (
|
|
173
|
-
<>
|
|
174
|
-
{currentStep === 0 && (
|
|
175
|
-
<ProjectInfo onComplete={handleStepComplete} />
|
|
176
|
-
)}
|
|
177
|
-
{currentStep === 1 && (
|
|
178
|
-
<StackStyle onComplete={handleStepComplete} />
|
|
179
|
-
)}
|
|
180
|
-
{currentStep === 2 && (
|
|
181
|
-
<BuildTest
|
|
182
|
-
initialValues={answers}
|
|
183
|
-
onComplete={handleStepComplete}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
{currentStep === 3 && (
|
|
187
|
-
<Architecture onComplete={handleStepComplete} />
|
|
188
|
-
)}
|
|
189
|
-
{currentStep === 4 && (
|
|
190
|
-
<ProductContext
|
|
191
|
-
initialValues={answers}
|
|
192
|
-
onComplete={handleStepComplete}
|
|
193
|
-
/>
|
|
194
|
-
)}
|
|
195
|
-
{currentStep === 5 && (
|
|
196
|
-
<Generation
|
|
197
|
-
answers={answers}
|
|
198
|
-
onComplete={handleStepComplete}
|
|
199
|
-
/>
|
|
200
|
-
)}
|
|
201
|
-
{currentStep === 6 && (
|
|
202
|
-
<Powerline answers={answers} onComplete={handleStepComplete} />
|
|
203
|
-
)}
|
|
204
|
-
</>
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
return (
|
|
208
|
-
<Box flexDirection="column" padding={1}>
|
|
209
|
-
<Banner />
|
|
210
|
-
<Timeline steps={steps}>{activeContent}</Timeline>
|
|
211
|
-
</Box>
|
|
212
|
-
);
|
|
213
|
-
}
|
package/src/cli.tsx
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import React from "react";
|
|
3
|
-
import { render } from "ink";
|
|
4
|
-
import { readdirSync } from "node:fs";
|
|
5
|
-
import App from "./app.js";
|
|
6
|
-
|
|
7
|
-
const entries = readdirSync(process.cwd()).filter((e) => e !== ".DS_Store");
|
|
8
|
-
if (entries.length > 0) {
|
|
9
|
-
const sample = entries.slice(0, 3).join(", ");
|
|
10
|
-
const extra = entries.length > 3 ? `, +${entries.length - 3} more` : "";
|
|
11
|
-
console.error(`\nWarning: current directory is not empty.`);
|
|
12
|
-
console.error(` Found: ${sample}${extra}`);
|
|
13
|
-
console.error(` Existing files can be overwritten during Step 6 (Generation).`);
|
|
14
|
-
console.error(` Recommended for a fresh project:`);
|
|
15
|
-
console.error(` mkdir my-project && cd my-project`);
|
|
16
|
-
console.error(` npx procedure-cli (or: npx @b3awesome/procedure)\n`);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
render(<App />);
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { C } from "../theme.js";
|
|
4
|
-
|
|
5
|
-
const BANNER_LINES = [
|
|
6
|
-
"█▀█ █▀█ █▀█ █▀▀ █▀▀ █▀▄ █ █ █▀█ █▀▀",
|
|
7
|
-
"█▀▀ █▀▄ █ █ █ █▀▀ █ █ █ █ █▀▄ █▀▀",
|
|
8
|
-
"▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀▀▀",
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
export default function Banner() {
|
|
12
|
-
return (
|
|
13
|
-
<Box flexDirection="column" marginBottom={0}>
|
|
14
|
-
{BANNER_LINES.map((line, i) => (
|
|
15
|
-
<Text key={i} color={C.mauve}>
|
|
16
|
-
{line}
|
|
17
|
-
</Text>
|
|
18
|
-
))}
|
|
19
|
-
<Text>Bootsrap any project with a battle-tested Claude.md</Text>
|
|
20
|
-
<Text color={C.overlay1}>{"─".repeat(43)}</Text>
|
|
21
|
-
</Box>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { C } from "../theme.js";
|
|
4
|
-
|
|
5
|
-
export function GutterLine({ children }: { children?: React.ReactNode }) {
|
|
6
|
-
return (
|
|
7
|
-
<Box flexDirection="row">
|
|
8
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
9
|
-
{children}
|
|
10
|
-
</Box>
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function EmptyGutter() {
|
|
15
|
-
return <Text color={C.overlay1}>{"│"}</Text>;
|
|
16
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import { TextInput } from "@inkjs/ui";
|
|
4
|
-
import { C } from "../theme.js";
|
|
5
|
-
// Note: Box flexDirection="column" kept here for multi-option rendering
|
|
6
|
-
|
|
7
|
-
interface Option {
|
|
8
|
-
label: string;
|
|
9
|
-
value: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface GutteredSelectProps {
|
|
14
|
-
options: Option[];
|
|
15
|
-
onChange: (value: string) => void;
|
|
16
|
-
maxVisible?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A custom Select component that renders each option with the vertical
|
|
21
|
-
* gutter prefix (│) so the timeline line stays continuous.
|
|
22
|
-
* Shows at most `maxVisible` options at a time with scroll indicators.
|
|
23
|
-
*/
|
|
24
|
-
export function GutteredSelect({ options, onChange, maxVisible = 5 }: GutteredSelectProps) {
|
|
25
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
26
|
-
|
|
27
|
-
useInput((input, key) => {
|
|
28
|
-
if (key.upArrow) {
|
|
29
|
-
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
30
|
-
} else if (key.downArrow) {
|
|
31
|
-
setActiveIndex((prev) =>
|
|
32
|
-
prev < options.length - 1 ? prev + 1 : prev
|
|
33
|
-
);
|
|
34
|
-
} else if (key.return) {
|
|
35
|
-
const selected = options[activeIndex];
|
|
36
|
-
if (selected) {
|
|
37
|
-
onChange(selected.value);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Sliding window: keep active item visible
|
|
43
|
-
const windowSize = Math.min(maxVisible, options.length);
|
|
44
|
-
const scrollOffset = Math.min(
|
|
45
|
-
Math.max(0, activeIndex - Math.floor(windowSize / 2)),
|
|
46
|
-
Math.max(0, options.length - windowSize)
|
|
47
|
-
);
|
|
48
|
-
const visibleOptions = options.slice(scrollOffset, scrollOffset + windowSize);
|
|
49
|
-
const showScrollUp = scrollOffset > 0;
|
|
50
|
-
const showScrollDown = scrollOffset + windowSize < options.length;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<Box flexDirection="column">
|
|
54
|
-
{showScrollUp && (
|
|
55
|
-
<Text>
|
|
56
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
57
|
-
<Text color={C.overlay1}>{"↑ more"}</Text>
|
|
58
|
-
</Text>
|
|
59
|
-
)}
|
|
60
|
-
{visibleOptions.map((option, visIndex) => {
|
|
61
|
-
const index = scrollOffset + visIndex;
|
|
62
|
-
const isActive = index === activeIndex;
|
|
63
|
-
return (
|
|
64
|
-
<Text key={option.value}>
|
|
65
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
66
|
-
{isActive ? (
|
|
67
|
-
<Text color={C.mauve} bold>{"❯ "}</Text>
|
|
68
|
-
) : (
|
|
69
|
-
<Text>{" "}</Text>
|
|
70
|
-
)}
|
|
71
|
-
<Text bold={isActive}>{option.label}</Text>
|
|
72
|
-
{option.description && (
|
|
73
|
-
<Text color={C.overlay1}>{" — " + option.description}</Text>
|
|
74
|
-
)}
|
|
75
|
-
</Text>
|
|
76
|
-
);
|
|
77
|
-
})}
|
|
78
|
-
{showScrollDown && (
|
|
79
|
-
<Text>
|
|
80
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
81
|
-
<Text color={C.overlay1}>{"↓ more"}</Text>
|
|
82
|
-
</Text>
|
|
83
|
-
)}
|
|
84
|
-
<Text color={C.overlay1}>{"│ "}{" ↑↓ move, enter select"}</Text>
|
|
85
|
-
</Box>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
interface GutteredMultiSelectProps {
|
|
90
|
-
options: Option[];
|
|
91
|
-
initialSelected?: string[];
|
|
92
|
-
onSubmit: (values: string[]) => void;
|
|
93
|
-
/** When true, adds a "✎ Other" option that opens a free text input */
|
|
94
|
-
allowCustom?: boolean;
|
|
95
|
-
/** Placeholder shown in the custom text input */
|
|
96
|
-
customPlaceholder?: string;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const CUSTOM_SENTINEL = "__custom__";
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Multi-select with gutter. Space to toggle, Enter to confirm.
|
|
103
|
-
* Shows ● for selected, ○ for unselected. Hint at bottom.
|
|
104
|
-
* When allowCustom is true, an "Other" option lets users type custom values.
|
|
105
|
-
*/
|
|
106
|
-
export function GutteredMultiSelect({
|
|
107
|
-
options,
|
|
108
|
-
initialSelected,
|
|
109
|
-
onSubmit,
|
|
110
|
-
allowCustom,
|
|
111
|
-
customPlaceholder,
|
|
112
|
-
}: GutteredMultiSelectProps) {
|
|
113
|
-
const allOptions = allowCustom
|
|
114
|
-
? [...options, { label: "Other (type custom)", value: CUSTOM_SENTINEL, description: "Add your own" }]
|
|
115
|
-
: options;
|
|
116
|
-
|
|
117
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
118
|
-
const [selected, setSelected] = useState<Set<string>>(
|
|
119
|
-
new Set(initialSelected ?? [])
|
|
120
|
-
);
|
|
121
|
-
const [phase, setPhase] = useState<"selecting" | "custom">("selecting");
|
|
122
|
-
|
|
123
|
-
useInput((input, key) => {
|
|
124
|
-
if (key.upArrow) {
|
|
125
|
-
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
126
|
-
} else if (key.downArrow) {
|
|
127
|
-
setActiveIndex((prev) =>
|
|
128
|
-
prev < allOptions.length - 1 ? prev + 1 : prev
|
|
129
|
-
);
|
|
130
|
-
} else if (input === " ") {
|
|
131
|
-
const option = allOptions[activeIndex];
|
|
132
|
-
if (option) {
|
|
133
|
-
setSelected((prev) => {
|
|
134
|
-
const next = new Set(prev);
|
|
135
|
-
if (next.has(option.value)) {
|
|
136
|
-
next.delete(option.value);
|
|
137
|
-
} else {
|
|
138
|
-
next.add(option.value);
|
|
139
|
-
}
|
|
140
|
-
return next;
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
} else if (key.return) {
|
|
144
|
-
if (selected.has(CUSTOM_SENTINEL)) {
|
|
145
|
-
setSelected((prev) => {
|
|
146
|
-
const next = new Set(prev);
|
|
147
|
-
next.delete(CUSTOM_SENTINEL);
|
|
148
|
-
return next;
|
|
149
|
-
});
|
|
150
|
-
setPhase("custom");
|
|
151
|
-
} else {
|
|
152
|
-
onSubmit(Array.from(selected));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}, { isActive: phase === "selecting" });
|
|
156
|
-
|
|
157
|
-
if (phase === "custom") {
|
|
158
|
-
const currentSelections = Array.from(selected);
|
|
159
|
-
return (
|
|
160
|
-
<Box flexDirection="column">
|
|
161
|
-
{currentSelections.length > 0 && (
|
|
162
|
-
<Box flexDirection="row">
|
|
163
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
164
|
-
<Text color={C.green}>{"Selected: "}</Text>
|
|
165
|
-
<Text>{currentSelections.join(", ")}</Text>
|
|
166
|
-
</Box>
|
|
167
|
-
)}
|
|
168
|
-
<Box flexDirection="row">
|
|
169
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
170
|
-
<Text bold>{"Add custom (comma-separated):"}</Text>
|
|
171
|
-
</Box>
|
|
172
|
-
<Box flexDirection="row">
|
|
173
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
174
|
-
<TextInput
|
|
175
|
-
placeholder={customPlaceholder ?? "Type here, press enter to confirm"}
|
|
176
|
-
onSubmit={(value) => {
|
|
177
|
-
const custom = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
178
|
-
onSubmit([...currentSelections, ...custom]);
|
|
179
|
-
}}
|
|
180
|
-
/>
|
|
181
|
-
</Box>
|
|
182
|
-
</Box>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return (
|
|
187
|
-
<Box flexDirection="column">
|
|
188
|
-
{allOptions.map((option, index) => {
|
|
189
|
-
const isActive = index === activeIndex;
|
|
190
|
-
const isSelected = selected.has(option.value);
|
|
191
|
-
const isCustomOption = option.value === CUSTOM_SENTINEL;
|
|
192
|
-
return (
|
|
193
|
-
<Text key={option.value}>
|
|
194
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
195
|
-
{isActive ? (
|
|
196
|
-
<Text color={C.mauve} bold>{"❯ "}</Text>
|
|
197
|
-
) : (
|
|
198
|
-
<Text>{" "}</Text>
|
|
199
|
-
)}
|
|
200
|
-
{isSelected ? (
|
|
201
|
-
<Text color={isCustomOption ? C.teal : C.green}>{"● "}</Text>
|
|
202
|
-
) : (
|
|
203
|
-
<Text color={C.overlay0}>{"○ "}</Text>
|
|
204
|
-
)}
|
|
205
|
-
<Text bold={isActive}>{isCustomOption ? `✎ ${option.label}` : option.label}</Text>
|
|
206
|
-
{option.description && (
|
|
207
|
-
<Text color={C.overlay1}>{" — " + option.description}</Text>
|
|
208
|
-
)}
|
|
209
|
-
</Text>
|
|
210
|
-
);
|
|
211
|
-
})}
|
|
212
|
-
<Text color={C.overlay1}>{"│ "}{" ↑↓ move, space toggle, enter confirm"}</Text>
|
|
213
|
-
{selected.size > 0 && !selected.has(CUSTOM_SENTINEL) && (
|
|
214
|
-
<Text>
|
|
215
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
216
|
-
<Text color={C.green}>{"Selected: "}</Text>
|
|
217
|
-
<Text>{Array.from(selected).join(", ")}</Text>
|
|
218
|
-
</Text>
|
|
219
|
-
)}
|
|
220
|
-
{selected.size > 0 && selected.has(CUSTOM_SENTINEL) && (
|
|
221
|
-
<Text>
|
|
222
|
-
<Text color={C.overlay1}>{"│ "}</Text>
|
|
223
|
-
<Text color={C.green}>{"Selected: "}</Text>
|
|
224
|
-
<Text>{Array.from(selected).filter((v) => v !== CUSTOM_SENTINEL).join(", ")}</Text>
|
|
225
|
-
{selected.size > 1 && <Text color={C.overlay1}>{" + custom"}</Text>}
|
|
226
|
-
{selected.size === 1 && <Text color={C.overlay1}>{"custom (on confirm)"}</Text>}
|
|
227
|
-
</Text>
|
|
228
|
-
)}
|
|
229
|
-
</Box>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Text } from "ink";
|
|
3
|
-
import { C } from "../theme.js";
|
|
4
|
-
|
|
5
|
-
export type StepStatus = "completed" | "active" | "pending";
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
status: StepStatus;
|
|
9
|
-
label: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const ACTIVE_FRAMES = ["◆", "◈", "◇", "◈"] as const;
|
|
13
|
-
|
|
14
|
-
export default function StepIndicator({ status, label }: Props) {
|
|
15
|
-
const [frame, setFrame] = useState(0);
|
|
16
|
-
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (status !== "active") return;
|
|
19
|
-
const id = setTimeout(() => {
|
|
20
|
-
setFrame((f) => (f + 1) % ACTIVE_FRAMES.length);
|
|
21
|
-
}, 350);
|
|
22
|
-
return () => clearTimeout(id);
|
|
23
|
-
}, [status, frame]);
|
|
24
|
-
|
|
25
|
-
if (status === "completed") {
|
|
26
|
-
return <Text><Text color={C.green}>{"◇"}</Text>{" "}<Text color={C.green}>{label}</Text></Text>;
|
|
27
|
-
}
|
|
28
|
-
if (status === "active") {
|
|
29
|
-
return <Text><Text color={C.mauve} bold>{ACTIVE_FRAMES[frame]}</Text>{" "}<Text bold color={C.mauve}>{label}</Text></Text>;
|
|
30
|
-
}
|
|
31
|
-
return <Text><Text color={C.overlay0}>{"○"}</Text>{" "}<Text color={C.overlay0}>{label}</Text></Text>;
|
|
32
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import StepIndicator from "./step-indicator.js";
|
|
4
|
-
import type { StepStatus } from "./step-indicator.js";
|
|
5
|
-
import { C } from "../theme.js";
|
|
6
|
-
|
|
7
|
-
export interface TimelineStep {
|
|
8
|
-
name: string;
|
|
9
|
-
status: StepStatus;
|
|
10
|
-
summary?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Props {
|
|
14
|
-
steps: TimelineStep[];
|
|
15
|
-
children: React.ReactNode;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function Timeline({ steps, children }: Props) {
|
|
19
|
-
const elements: React.ReactNode[] = [];
|
|
20
|
-
|
|
21
|
-
// Opening corner
|
|
22
|
-
elements.push(<Text key="top" color={C.overlay1}>{"┌"}</Text>);
|
|
23
|
-
|
|
24
|
-
steps.forEach((step, i) => {
|
|
25
|
-
const isLast = i === steps.length - 1;
|
|
26
|
-
const isActive = step.status === "active";
|
|
27
|
-
|
|
28
|
-
// Step indicator line
|
|
29
|
-
elements.push(
|
|
30
|
-
<StepIndicator key={`ind-${i}`} status={step.status} label={step.name} />
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
// Completed step: show summary on SEPARATE line with gutter
|
|
34
|
-
if (step.status === "completed" && step.summary) {
|
|
35
|
-
elements.push(
|
|
36
|
-
<Text key={`sum-${i}`} color={C.overlay1}>{"│ "}{step.summary}</Text>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Active step: render children (they handle their own GutterLine)
|
|
41
|
-
if (isActive) {
|
|
42
|
-
elements.push(
|
|
43
|
-
<React.Fragment key={`content-${i}`}>{children}</React.Fragment>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Gutter spacer between steps
|
|
48
|
-
if (!isLast) {
|
|
49
|
-
elements.push(<Text key={`bar-${i}`} color={C.overlay1}>{"│"}</Text>);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Closing corner
|
|
54
|
-
elements.push(<Text key="bottom" color={C.overlay1}>{"└"}</Text>);
|
|
55
|
-
|
|
56
|
-
return <Box flexDirection="column">{elements}</Box>;
|
|
57
|
-
}
|
package/src/lib/fs.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, existsSync } from 'node:fs';
|
|
2
|
-
import { resolve, dirname } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
|
|
9
|
-
export function ensureDir(dirPath: string): void {
|
|
10
|
-
mkdirSync(dirPath, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function fileExists(filePath: string): boolean {
|
|
14
|
-
return existsSync(filePath);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function resolveTemplatePath(templateName: string): string {
|
|
18
|
-
return resolve(__dirname, '..', '..', 'templates', templateName);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function homeBinPath(filename: string): string {
|
|
22
|
-
return resolve(homedir(), 'bin', filename);
|
|
23
|
-
}
|
package/src/lib/git.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
function hasGitIdentity(): boolean {
|
|
4
|
-
try {
|
|
5
|
-
execSync('git config user.name', { stdio: 'ignore' });
|
|
6
|
-
execSync('git config user.email', { stdio: 'ignore' });
|
|
7
|
-
return true;
|
|
8
|
-
} catch {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function hasStagedFiles(targetDir: string): boolean {
|
|
14
|
-
try {
|
|
15
|
-
const result = execSync('git status --porcelain', { cwd: targetDir, encoding: 'utf-8' });
|
|
16
|
-
return result.trim().length > 0;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function initGit(targetDir: string): { committed: boolean; reason?: string } {
|
|
23
|
-
const opts = { cwd: targetDir, stdio: 'ignore' as const };
|
|
24
|
-
|
|
25
|
-
execSync('git init', opts);
|
|
26
|
-
execSync('git add -A', opts);
|
|
27
|
-
|
|
28
|
-
if (!hasStagedFiles(targetDir)) {
|
|
29
|
-
return { committed: false, reason: 'Nothing to commit — directory is empty.' };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!hasGitIdentity()) {
|
|
33
|
-
return {
|
|
34
|
-
committed: false,
|
|
35
|
-
reason: 'Git user.name/user.email not configured. Run: git config --global user.name "Your Name" && git config --global user.email "you@example.com"',
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
execSync('git commit -m "Initial commit \u2014 scaffolded by Procedure CLI"', opts);
|
|
40
|
-
return { committed: true };
|
|
41
|
-
}
|