procedure-cli 0.1.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.
Files changed (104) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.env.example +2 -0
  3. package/AGENTS.md +113 -0
  4. package/CLAUDE.md +136 -0
  5. package/CODE-FIXED.md +124 -0
  6. package/CODE-REVIEW.md +253 -0
  7. package/README.md +130 -0
  8. package/config/defaults.json +8 -0
  9. package/config/powerline-config.json +52 -0
  10. package/config/stacks/typescript-node.json +15 -0
  11. package/dist/app.d.ts +1 -0
  12. package/dist/app.js +131 -0
  13. package/dist/app.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.js +6 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/components/banner.d.ts +1 -0
  18. package/dist/components/banner.js +11 -0
  19. package/dist/components/banner.js.map +1 -0
  20. package/dist/components/gutter-line.d.ts +5 -0
  21. package/dist/components/gutter-line.js +9 -0
  22. package/dist/components/gutter-line.js.map +1 -0
  23. package/dist/components/guttered-select.d.ts +25 -0
  24. package/dist/components/guttered-select.js +68 -0
  25. package/dist/components/guttered-select.js.map +1 -0
  26. package/dist/components/step-indicator.d.ts +7 -0
  27. package/dist/components/step-indicator.js +12 -0
  28. package/dist/components/step-indicator.js.map +1 -0
  29. package/dist/components/timeline.d.ts +13 -0
  30. package/dist/components/timeline.js +26 -0
  31. package/dist/components/timeline.js.map +1 -0
  32. package/dist/lib/fs.d.ts +3 -0
  33. package/dist/lib/fs.js +15 -0
  34. package/dist/lib/fs.js.map +1 -0
  35. package/dist/lib/git.d.ts +4 -0
  36. package/dist/lib/git.js +37 -0
  37. package/dist/lib/git.js.map +1 -0
  38. package/dist/lib/powerline.d.ts +1 -0
  39. package/dist/lib/powerline.js +30 -0
  40. package/dist/lib/powerline.js.map +1 -0
  41. package/dist/lib/template.d.ts +9 -0
  42. package/dist/lib/template.js +46 -0
  43. package/dist/lib/template.js.map +1 -0
  44. package/dist/lib/types.d.ts +44 -0
  45. package/dist/lib/types.js +2 -0
  46. package/dist/lib/types.js.map +1 -0
  47. package/dist/providers/openai.d.ts +1 -0
  48. package/dist/providers/openai.js +5 -0
  49. package/dist/providers/openai.js.map +1 -0
  50. package/dist/providers/zai.d.ts +1 -0
  51. package/dist/providers/zai.js +7 -0
  52. package/dist/providers/zai.js.map +1 -0
  53. package/dist/steps/architecture.d.ts +6 -0
  54. package/dist/steps/architecture.js +39 -0
  55. package/dist/steps/architecture.js.map +1 -0
  56. package/dist/steps/build-test.d.ts +7 -0
  57. package/dist/steps/build-test.js +38 -0
  58. package/dist/steps/build-test.js.map +1 -0
  59. package/dist/steps/generation.d.ts +7 -0
  60. package/dist/steps/generation.js +60 -0
  61. package/dist/steps/generation.js.map +1 -0
  62. package/dist/steps/powerline.d.ts +7 -0
  63. package/dist/steps/powerline.js +62 -0
  64. package/dist/steps/powerline.js.map +1 -0
  65. package/dist/steps/product-context.d.ts +7 -0
  66. package/dist/steps/product-context.js +90 -0
  67. package/dist/steps/product-context.js.map +1 -0
  68. package/dist/steps/project-info.d.ts +6 -0
  69. package/dist/steps/project-info.js +61 -0
  70. package/dist/steps/project-info.js.map +1 -0
  71. package/dist/steps/stack-style.d.ts +6 -0
  72. package/dist/steps/stack-style.js +67 -0
  73. package/dist/steps/stack-style.js.map +1 -0
  74. package/docs/GIAI-THICH-CLAUDE-MD.md +206 -0
  75. package/docs/PRD.md +130 -0
  76. package/docs/USER-STORIES.md +181 -0
  77. package/package.json +38 -0
  78. package/src/app.tsx +201 -0
  79. package/src/cli.tsx +6 -0
  80. package/src/components/banner.tsx +23 -0
  81. package/src/components/gutter-line.tsx +10 -0
  82. package/src/components/guttered-select.tsx +137 -0
  83. package/src/components/step-indicator.tsx +19 -0
  84. package/src/components/timeline.tsx +49 -0
  85. package/src/lib/fs.ts +18 -0
  86. package/src/lib/git.ts +41 -0
  87. package/src/lib/powerline.ts +48 -0
  88. package/src/lib/template.ts +59 -0
  89. package/src/lib/types.ts +68 -0
  90. package/src/providers/openai.ts +5 -0
  91. package/src/providers/zai.ts +7 -0
  92. package/src/steps/architecture.tsx +71 -0
  93. package/src/steps/build-test.tsx +78 -0
  94. package/src/steps/generation.tsx +146 -0
  95. package/src/steps/powerline.tsx +149 -0
  96. package/src/steps/product-context.tsx +182 -0
  97. package/src/steps/project-info.tsx +135 -0
  98. package/src/steps/stack-style.tsx +117 -0
  99. package/templates/.env.example.hbs +6 -0
  100. package/templates/CLAUDE.md.hbs +105 -0
  101. package/templates/README.md.hbs +26 -0
  102. package/templates/docs/PRD.md.hbs +29 -0
  103. package/templates/docs/USER-STORIES.md.hbs +46 -0
  104. package/tsconfig.json +17 -0
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "procedure-cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI-based AI Agent powered by AI SDK 6, Z.ai, and OpenAI",
5
+ "type": "module",
6
+ "bin": {
7
+ "procedure": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx src/cli.tsx",
11
+ "build": "tsc",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "tsc --noEmit"
14
+ },
15
+ "keywords": [
16
+ "cli",
17
+ "ai",
18
+ "agent",
19
+ "ink"
20
+ ],
21
+ "author": "",
22
+ "license": "ISC",
23
+ "dependencies": {
24
+ "@ai-sdk/openai": "^3.0.30",
25
+ "@ai-sdk/openai-compatible": "^2.0.30",
26
+ "@inkjs/ui": "^2.0.0",
27
+ "ai": "^6.0.94",
28
+ "handlebars": "^4.7.8",
29
+ "ink": "^6.8.0",
30
+ "react": "^19.2.4"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.3.0",
34
+ "@types/react": "^19.2.14",
35
+ "tsx": "^4.21.0",
36
+ "typescript": "^5.9.3"
37
+ }
38
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,201 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import type { WizardAnswers } from "./lib/types.js";
4
+ import type { TimelineStep } from "./components/timeline.js";
5
+
6
+ import Banner from "./components/banner.js";
7
+ import Timeline from "./components/timeline.js";
8
+
9
+ import ProjectInfo from "./steps/project-info.js";
10
+ import StackStyle from "./steps/stack-style.js";
11
+ import BuildTest from "./steps/build-test.js";
12
+ import Architecture from "./steps/architecture.js";
13
+ import ProductContext from "./steps/product-context.js";
14
+ import Generation from "./steps/generation.js";
15
+ import Powerline from "./steps/powerline.js";
16
+
17
+ const STEP_NAMES = [
18
+ "Project Info",
19
+ "Stack & Style",
20
+ "Build & Test",
21
+ "Architecture",
22
+ "Product Context",
23
+ "Generation",
24
+ "Powerline",
25
+ ] as const;
26
+
27
+ const EMPTY_ANSWERS: WizardAnswers = {
28
+ projectName: "",
29
+ description: "",
30
+ packageManager: "npm",
31
+ license: "ISC",
32
+ codeStyle: [],
33
+ framework: "",
34
+ language: "",
35
+ buildCommand: "",
36
+ testCommand: "",
37
+ typecheckCommand: "",
38
+ lintCommand: "",
39
+ prCommand: "",
40
+ architecture: "",
41
+ architectureNotes: "",
42
+ problem: "",
43
+ users: "",
44
+ coreFeatures: [],
45
+ nonGoals: [],
46
+ techStack: "",
47
+ userStories: [],
48
+ envVars: [],
49
+ generationSkipped: false,
50
+ setupPowerline: false,
51
+ };
52
+
53
+ function getSummary(stepIndex: number, answers: WizardAnswers): string {
54
+ switch (stepIndex) {
55
+ case 0: {
56
+ const name = answers.projectName || "untitled";
57
+ const desc = answers.description || "no description";
58
+ return `${name} — ${desc}`;
59
+ }
60
+ case 1: {
61
+ const parts: string[] = [];
62
+ if (answers.language) parts.push(answers.language);
63
+ if (answers.framework) parts.push(answers.framework);
64
+ return parts.length > 0 ? parts.join(", ") : "configured";
65
+ }
66
+ case 2: {
67
+ const cmds: string[] = [];
68
+ if (answers.buildCommand) cmds.push(`build: ${answers.buildCommand}`);
69
+ if (answers.testCommand) cmds.push(`test: ${answers.testCommand}`);
70
+ return cmds.length > 0 ? cmds.join(", ") : "configured";
71
+ }
72
+ case 3: {
73
+ return answers.architecture || "configured";
74
+ }
75
+ case 4: {
76
+ const features = answers.coreFeatures.length;
77
+ const nonGoals = answers.nonGoals.length;
78
+ const parts: string[] = [];
79
+ if (features > 0) parts.push(`${features} feature${features !== 1 ? "s" : ""}`);
80
+ if (nonGoals > 0) parts.push(`${nonGoals} non-goal${nonGoals !== 1 ? "s" : ""}`);
81
+ return parts.length > 0 ? parts.join(", ") : "configured";
82
+ }
83
+ case 5:
84
+ return answers.generationSkipped ? "skipped" : "files generated";
85
+ case 6: {
86
+ return answers.setupPowerline ? "Powerline: yes" : "Powerline: no";
87
+ }
88
+ default:
89
+ return "done";
90
+ }
91
+ }
92
+
93
+ export default function App() {
94
+ const [currentStep, setCurrentStep] = useState(0);
95
+ const [answers, setAnswers] = useState<WizardAnswers>(EMPTY_ANSWERS);
96
+ const [finished, setFinished] = useState(false);
97
+
98
+ useInput((input, key) => {
99
+ if (key.escape) {
100
+ process.exit(0);
101
+ }
102
+ });
103
+
104
+ function handleStepComplete(partial: Partial<WizardAnswers>) {
105
+ const merged = { ...answers, ...partial };
106
+ setAnswers(merged);
107
+
108
+ if (currentStep < STEP_NAMES.length - 1) {
109
+ setCurrentStep(currentStep + 1);
110
+ } else {
111
+ setFinished(true);
112
+ }
113
+ }
114
+
115
+ if (finished) {
116
+ const steps: TimelineStep[] = STEP_NAMES.map((name, i) => ({
117
+ name,
118
+ status: "completed" as const,
119
+ summary: getSummary(i, answers),
120
+ }));
121
+
122
+ return (
123
+ <Box flexDirection="column" padding={1}>
124
+ <Banner />
125
+ <Timeline steps={steps}>
126
+ <Box />
127
+ </Timeline>
128
+ <Box marginTop={1} flexDirection="column">
129
+ <Text bold color="green">
130
+ {"✔"} Procedure CLI setup complete!
131
+ </Text>
132
+ <Text> </Text>
133
+ {answers.generationSkipped ? (
134
+ <Text dimColor>Generation was skipped — no files were written.</Text>
135
+ ) : (
136
+ <>
137
+ <Text>
138
+ Project <Text bold color="cyan">"{answers.projectName}"</Text> has been scaffolded.
139
+ </Text>
140
+ <Text dimColor>
141
+ Check the generated CLAUDE.md, PRD.md, and USER-STORIES.md.
142
+ </Text>
143
+ </>
144
+ )}
145
+ </Box>
146
+ </Box>
147
+ );
148
+ }
149
+
150
+ const steps: TimelineStep[] = STEP_NAMES.map((name, i) => {
151
+ if (i < currentStep) {
152
+ return { name, status: "completed" as const, summary: getSummary(i, answers) };
153
+ }
154
+ if (i === currentStep) {
155
+ return { name, status: "active" as const };
156
+ }
157
+ return { name, status: "pending" as const };
158
+ });
159
+
160
+ const activeContent = (
161
+ <>
162
+ {currentStep === 0 && (
163
+ <ProjectInfo onComplete={handleStepComplete} />
164
+ )}
165
+ {currentStep === 1 && (
166
+ <StackStyle onComplete={handleStepComplete} />
167
+ )}
168
+ {currentStep === 2 && (
169
+ <BuildTest
170
+ initialValues={answers}
171
+ onComplete={handleStepComplete}
172
+ />
173
+ )}
174
+ {currentStep === 3 && (
175
+ <Architecture onComplete={handleStepComplete} />
176
+ )}
177
+ {currentStep === 4 && (
178
+ <ProductContext
179
+ initialValues={answers}
180
+ onComplete={handleStepComplete}
181
+ />
182
+ )}
183
+ {currentStep === 5 && (
184
+ <Generation
185
+ answers={answers}
186
+ onComplete={handleStepComplete}
187
+ />
188
+ )}
189
+ {currentStep === 6 && (
190
+ <Powerline answers={answers} onComplete={handleStepComplete} />
191
+ )}
192
+ </>
193
+ );
194
+
195
+ return (
196
+ <Box flexDirection="column" padding={1}>
197
+ <Banner />
198
+ <Timeline steps={steps}>{activeContent}</Timeline>
199
+ </Box>
200
+ );
201
+ }
package/src/cli.tsx ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import React from "react";
3
+ import { render } from "ink";
4
+ import App from "./app.js";
5
+
6
+ render(<App />);
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+
4
+ const BANNER_LINES = [
5
+ "█▀█ █▀█ █▀█ █▀▀ █▀▀ █▀▄ █ █ █▀█ █▀▀",
6
+ "█▀▀ █▀▄ █ █ █ █▀▀ █ █ █ █ █▀▄ █▀▀",
7
+ "▀ ▀ ▀ ▀▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀▀ ▀ ▀ ▀▀▀",
8
+ ];
9
+
10
+ export default function Banner() {
11
+ return (
12
+ <Box flexDirection="column" marginBottom={0}>
13
+ {BANNER_LINES.map((line, i) => (
14
+ <Text key={i} color="cyan">
15
+ {line}
16
+ </Text>
17
+ ))}
18
+ <Text>Bootstrap any project with a battle-tested</Text>
19
+ <Text>CLAUDE.md, PRD, user stories, and powerline.</Text>
20
+ <Text dimColor>{"─".repeat(43)}</Text>
21
+ </Box>
22
+ );
23
+ }
@@ -0,0 +1,10 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+
4
+ export function GutterLine({ children }: { children?: React.ReactNode }) {
5
+ return <Text><Text dimColor>{"│ "}</Text>{children}</Text>;
6
+ }
7
+
8
+ export function EmptyGutter() {
9
+ return <Text dimColor>{"│"}</Text>;
10
+ }
@@ -0,0 +1,137 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ // Note: Box flexDirection="column" kept here for multi-option rendering
4
+
5
+ interface Option {
6
+ label: string;
7
+ value: string;
8
+ description?: string;
9
+ }
10
+
11
+ interface GutteredSelectProps {
12
+ options: Option[];
13
+ onChange: (value: string) => void;
14
+ }
15
+
16
+ /**
17
+ * A custom Select component that renders each option with the vertical
18
+ * gutter prefix (│) so the timeline line stays continuous.
19
+ */
20
+ export function GutteredSelect({ options, onChange }: GutteredSelectProps) {
21
+ const [activeIndex, setActiveIndex] = useState(0);
22
+
23
+ useInput((input, key) => {
24
+ if (key.upArrow) {
25
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
26
+ } else if (key.downArrow) {
27
+ setActiveIndex((prev) =>
28
+ prev < options.length - 1 ? prev + 1 : prev
29
+ );
30
+ } else if (key.return) {
31
+ const selected = options[activeIndex];
32
+ if (selected) {
33
+ onChange(selected.value);
34
+ }
35
+ }
36
+ });
37
+
38
+ return (
39
+ <Box flexDirection="column">
40
+ {options.map((option, index) => {
41
+ const isActive = index === activeIndex;
42
+ return (
43
+ <Text key={option.value}>
44
+ <Text dimColor>{"│ "}</Text>
45
+ {isActive ? (
46
+ <Text color="cyan" bold>{"❯ "}</Text>
47
+ ) : (
48
+ <Text>{" "}</Text>
49
+ )}
50
+ <Text bold={isActive}>{option.label}</Text>
51
+ {option.description && (
52
+ <Text dimColor>{" " + option.description}</Text>
53
+ )}
54
+ </Text>
55
+ );
56
+ })}
57
+ </Box>
58
+ );
59
+ }
60
+
61
+ interface GutteredMultiSelectProps {
62
+ options: Option[];
63
+ initialSelected?: string[];
64
+ onSubmit: (values: string[]) => void;
65
+ }
66
+
67
+ /**
68
+ * Multi-select with gutter. Space to toggle, Enter to confirm.
69
+ * Shows ● for selected, ○ for unselected. Hint at bottom.
70
+ */
71
+ export function GutteredMultiSelect({ options, initialSelected, onSubmit }: GutteredMultiSelectProps) {
72
+ const [activeIndex, setActiveIndex] = useState(0);
73
+ const [selected, setSelected] = useState<Set<string>>(
74
+ new Set(initialSelected ?? [])
75
+ );
76
+
77
+ useInput((input, key) => {
78
+ if (key.upArrow) {
79
+ setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
80
+ } else if (key.downArrow) {
81
+ setActiveIndex((prev) =>
82
+ prev < options.length - 1 ? prev + 1 : prev
83
+ );
84
+ } else if (input === " ") {
85
+ const option = options[activeIndex];
86
+ if (option) {
87
+ setSelected((prev) => {
88
+ const next = new Set(prev);
89
+ if (next.has(option.value)) {
90
+ next.delete(option.value);
91
+ } else {
92
+ next.add(option.value);
93
+ }
94
+ return next;
95
+ });
96
+ }
97
+ } else if (key.return) {
98
+ onSubmit(Array.from(selected));
99
+ }
100
+ });
101
+
102
+ return (
103
+ <Box flexDirection="column">
104
+ {options.map((option, index) => {
105
+ const isActive = index === activeIndex;
106
+ const isSelected = selected.has(option.value);
107
+ return (
108
+ <Text key={option.value}>
109
+ <Text dimColor>{"│ "}</Text>
110
+ {isActive ? (
111
+ <Text color="cyan" bold>{"❯ "}</Text>
112
+ ) : (
113
+ <Text>{" "}</Text>
114
+ )}
115
+ {isSelected ? (
116
+ <Text color="green">{"● "}</Text>
117
+ ) : (
118
+ <Text dimColor>{"○ "}</Text>
119
+ )}
120
+ <Text bold={isActive}>{option.label}</Text>
121
+ {option.description && (
122
+ <Text dimColor>{" — " + option.description}</Text>
123
+ )}
124
+ </Text>
125
+ );
126
+ })}
127
+ <Text dimColor>{"│ "}{" ↑↓ move, space toggle, enter confirm"}</Text>
128
+ {selected.size > 0 && (
129
+ <Text>
130
+ <Text dimColor>{"│ "}</Text>
131
+ <Text color="green">{"Selected: "}</Text>
132
+ <Text>{Array.from(selected).join(", ")}</Text>
133
+ </Text>
134
+ )}
135
+ </Box>
136
+ );
137
+ }
@@ -0,0 +1,19 @@
1
+ import React from "react";
2
+ import { Text } from "ink";
3
+
4
+ export type StepStatus = "completed" | "active" | "pending";
5
+
6
+ interface Props {
7
+ status: StepStatus;
8
+ label: string;
9
+ }
10
+
11
+ export default function StepIndicator({ status, label }: Props) {
12
+ if (status === "completed") {
13
+ return <Text><Text color="green">{"◇"}</Text>{" "}<Text color="green">{label}</Text></Text>;
14
+ }
15
+ if (status === "active") {
16
+ return <Text><Text color="green" bold>{"◆"}</Text>{" "}<Text bold color="green">{label}</Text></Text>;
17
+ }
18
+ return <Text><Text dimColor>{"○"}</Text>{" "}<Text dimColor>{label}</Text></Text>;
19
+ }
@@ -0,0 +1,49 @@
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
+
6
+ export interface TimelineStep {
7
+ name: string;
8
+ status: StepStatus;
9
+ summary?: string;
10
+ }
11
+
12
+ interface Props {
13
+ steps: TimelineStep[];
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ export default function Timeline({ steps, children }: Props) {
18
+ const elements: React.ReactNode[] = [];
19
+
20
+ steps.forEach((step, i) => {
21
+ const isLast = i === steps.length - 1;
22
+
23
+ // Step indicator line
24
+ elements.push(
25
+ <StepIndicator key={`ind-${i}`} status={step.status} label={step.name} />
26
+ );
27
+
28
+ // Completed step: show summary on SEPARATE line with gutter
29
+ if (step.status === "completed" && step.summary) {
30
+ elements.push(
31
+ <Text key={`sum-${i}`} dimColor>{"│ "}{step.summary}</Text>
32
+ );
33
+ }
34
+
35
+ // Active step: render children (they handle their own GutterLine)
36
+ if (step.status === "active") {
37
+ elements.push(
38
+ <React.Fragment key={`content-${i}`}>{children}</React.Fragment>
39
+ );
40
+ }
41
+
42
+ // Gutter spacer between steps
43
+ if (!isLast) {
44
+ elements.push(<Text key={`bar-${i}`} dimColor>{"│"}</Text>);
45
+ }
46
+ });
47
+
48
+ return <Box flexDirection="column">{elements}</Box>;
49
+ }
package/src/lib/fs.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { mkdirSync, existsSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ export function ensureDir(dirPath: string): void {
9
+ mkdirSync(dirPath, { recursive: true });
10
+ }
11
+
12
+ export function fileExists(filePath: string): boolean {
13
+ return existsSync(filePath);
14
+ }
15
+
16
+ export function resolveTemplatePath(templateName: string): string {
17
+ return resolve(__dirname, '..', '..', 'templates', templateName);
18
+ }
package/src/lib/git.ts ADDED
@@ -0,0 +1,41 @@
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
+ }
@@ -0,0 +1,48 @@
1
+ import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { ensureDir, fileExists, resolveTemplatePath } from './fs.js';
4
+
5
+ interface ClaudeSettings {
6
+ statusLine?: {
7
+ type: string;
8
+ command: string;
9
+ padding: number;
10
+ };
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export function setupPowerline(targetDir: string): void {
15
+ const claudeDir = resolve(targetDir, '.claude');
16
+ ensureDir(claudeDir);
17
+
18
+ const settingsPath = resolve(claudeDir, 'settings.json');
19
+
20
+ let settings: ClaudeSettings = {};
21
+ if (fileExists(settingsPath)) {
22
+ try {
23
+ const raw = readFileSync(settingsPath, 'utf-8');
24
+ settings = JSON.parse(raw) as ClaudeSettings;
25
+ } catch {
26
+ // Malformed settings.json — start fresh, original is not overwritten destructively
27
+ settings = {};
28
+ }
29
+ }
30
+
31
+ settings.statusLine = {
32
+ type: 'command',
33
+ command:
34
+ 'npx -y @owloops/claude-powerline@latest --config=.claude/powerline-config.json',
35
+ padding: 0,
36
+ };
37
+
38
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
39
+
40
+ // Copy bundled powerline-config.json to target .claude/
41
+ const bundledConfig = resolve(
42
+ resolveTemplatePath('..'),
43
+ 'config',
44
+ 'powerline-config.json',
45
+ );
46
+ const destConfig = resolve(claudeDir, 'powerline-config.json');
47
+ copyFileSync(bundledConfig, destConfig);
48
+ }
@@ -0,0 +1,59 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import Handlebars from 'handlebars';
4
+ import { ensureDir, resolveTemplatePath } from './fs.js';
5
+ import type { WizardAnswers } from './types.js';
6
+
7
+ export function renderTemplate(
8
+ templatePath: string,
9
+ data: Record<string, unknown>,
10
+ ): string {
11
+ const source = readFileSync(templatePath, 'utf-8');
12
+ const template = Handlebars.compile(source);
13
+ return template(data);
14
+ }
15
+
16
+ export function writeTemplate(
17
+ templatePath: string,
18
+ outputPath: string,
19
+ data: Record<string, unknown>,
20
+ ): void {
21
+ const rendered = renderTemplate(templatePath, data);
22
+ ensureDir(dirname(outputPath));
23
+ writeFileSync(outputPath, rendered, 'utf-8');
24
+ }
25
+
26
+ const TEMPLATE_MAP: Array<{ template: string; output: string }> = [
27
+ { template: 'CLAUDE.md.hbs', output: 'CLAUDE.md' },
28
+ { template: 'README.md.hbs', output: 'README.md' },
29
+ { template: 'docs/PRD.md.hbs', output: 'docs/PRD.md' },
30
+ { template: 'docs/USER-STORIES.md.hbs', output: 'docs/USER-STORIES.md' },
31
+ { template: '.env.example.hbs', output: '.env.example' },
32
+ ];
33
+
34
+ /**
35
+ * Check which output files already exist in the target directory.
36
+ * Returns a list of file paths that would be overwritten.
37
+ */
38
+ export function checkConflicts(targetDir: string): string[] {
39
+ const allOutputs = [
40
+ ...TEMPLATE_MAP.map((t) => t.output),
41
+ '.gitignore',
42
+ ];
43
+ return allOutputs.filter((f) => existsSync(resolve(targetDir, f)));
44
+ }
45
+
46
+ export function scaffoldAll(targetDir: string, data: WizardAnswers): void {
47
+ for (const { template, output } of TEMPLATE_MAP) {
48
+ const templatePath = resolveTemplatePath(template);
49
+ const outputPath = resolve(targetDir, output);
50
+ writeTemplate(templatePath, outputPath, data as unknown as Record<string, unknown>);
51
+ }
52
+
53
+ // Copy static .gitignore (no templating needed)
54
+ const gitignoreSrc = resolveTemplatePath('.gitignore');
55
+ const gitignoreDest = resolve(targetDir, '.gitignore');
56
+ const content = readFileSync(gitignoreSrc, 'utf-8');
57
+ ensureDir(dirname(gitignoreDest));
58
+ writeFileSync(gitignoreDest, content, 'utf-8');
59
+ }