procedure-cli 0.1.12 → 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.
Files changed (45) hide show
  1. package/README.md +15 -3
  2. package/dist/steps/build-test.js +32 -3
  3. package/dist/steps/build-test.js.map +1 -1
  4. package/dist/steps/powerline.js +11 -3
  5. package/dist/steps/powerline.js.map +1 -1
  6. package/dist/steps/product-context.js +54 -19
  7. package/dist/steps/product-context.js.map +1 -1
  8. package/dist/steps/project-info.js +30 -3
  9. package/dist/steps/project-info.js.map +1 -1
  10. package/package.json +5 -1
  11. package/.claude/settings.local.json +0 -26
  12. package/.env.example +0 -2
  13. package/AGENTS.md +0 -113
  14. package/CLAUDE.md +0 -138
  15. package/CODE-FIXED.md +0 -197
  16. package/CODE-REVIEW.md +0 -431
  17. package/config/defaults.json +0 -8
  18. package/config/powerline-config.json +0 -52
  19. package/config/stacks/typescript-node.json +0 -15
  20. package/docs/GIAI-THICH-CLAUDE-MD.md +0 -206
  21. package/docs/PRD.md +0 -140
  22. package/docs/USER-STORIES.md +0 -264
  23. package/src/app.tsx +0 -213
  24. package/src/cli.tsx +0 -19
  25. package/src/components/banner.tsx +0 -23
  26. package/src/components/gutter-line.tsx +0 -16
  27. package/src/components/guttered-select.tsx +0 -231
  28. package/src/components/step-indicator.tsx +0 -32
  29. package/src/components/timeline.tsx +0 -57
  30. package/src/lib/fs.ts +0 -23
  31. package/src/lib/git.ts +0 -41
  32. package/src/lib/powerline.ts +0 -48
  33. package/src/lib/template.ts +0 -161
  34. package/src/lib/types.ts +0 -70
  35. package/src/providers/openai.ts +0 -5
  36. package/src/providers/zai.ts +0 -7
  37. package/src/steps/architecture.tsx +0 -72
  38. package/src/steps/build-test.tsx +0 -79
  39. package/src/steps/generation.tsx +0 -176
  40. package/src/steps/powerline.tsx +0 -238
  41. package/src/steps/product-context.tsx +0 -208
  42. package/src/steps/project-info.tsx +0 -136
  43. package/src/steps/stack-style.tsx +0 -304
  44. package/src/theme.ts +0 -15
  45. 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
- }