procedure-cli 0.1.1 → 0.1.3
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/dist/app.js +6 -2
- package/dist/app.js.map +1 -1
- package/dist/components/guttered-select.d.ts +6 -1
- package/dist/components/guttered-select.js +33 -8
- package/dist/components/guttered-select.js.map +1 -1
- package/dist/lib/fs.d.ts +1 -0
- package/dist/lib/fs.js +4 -0
- package/dist/lib/fs.js.map +1 -1
- package/dist/lib/template.d.ts +18 -0
- package/dist/lib/template.js +67 -2
- package/dist/lib/template.js.map +1 -1
- package/dist/steps/generation.js +36 -3
- package/dist/steps/generation.js.map +1 -1
- package/dist/steps/product-context.js +20 -1
- package/dist/steps/product-context.js.map +1 -1
- package/dist/steps/stack-style.js +19 -13
- package/dist/steps/stack-style.js.map +1 -1
- package/package.json +5 -2
- package/src/app.tsx +5 -2
- package/src/components/guttered-select.tsx +76 -9
- package/src/lib/fs.ts +5 -0
- package/src/lib/template.ts +104 -2
- package/src/steps/generation.tsx +65 -4
- package/src/steps/product-context.tsx +31 -6
- package/src/steps/stack-style.tsx +28 -15
- package/templates/CLAUDE.md.hbs +15 -0
- package/templates/bin/release.sh.hbs +101 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "procedure-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "CLI-based AI Agent powered by AI SDK 6, Z.ai, and OpenAI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"dev": "tsx src/cli.tsx",
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
|
-
"lint": "tsc --noEmit"
|
|
13
|
+
"lint": "tsc --noEmit",
|
|
14
|
+
"release:patch": "\"$HOME/bin/procedure-release\" patch",
|
|
15
|
+
"release:minor": "\"$HOME/bin/procedure-release\" minor",
|
|
16
|
+
"release:major": "\"$HOME/bin/procedure-release\" major"
|
|
14
17
|
},
|
|
15
18
|
"keywords": [
|
|
16
19
|
"cli",
|
package/src/app.tsx
CHANGED
|
@@ -21,7 +21,7 @@ const STEP_NAMES = [
|
|
|
21
21
|
"Architecture",
|
|
22
22
|
"Product Context",
|
|
23
23
|
"Generation",
|
|
24
|
-
"
|
|
24
|
+
"Setup",
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
27
|
const EMPTY_ANSWERS: WizardAnswers = {
|
|
@@ -83,7 +83,10 @@ function getSummary(stepIndex: number, answers: WizardAnswers): string {
|
|
|
83
83
|
case 5:
|
|
84
84
|
return answers.generationSkipped ? "skipped" : "files generated";
|
|
85
85
|
case 6: {
|
|
86
|
-
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
if (answers.setupPowerline) parts.push("Powerline");
|
|
88
|
+
parts.push("Git init");
|
|
89
|
+
return parts.join(", ");
|
|
87
90
|
}
|
|
88
91
|
default:
|
|
89
92
|
return "done";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { TextInput } from "@inkjs/ui";
|
|
3
4
|
// Note: Box flexDirection="column" kept here for multi-option rendering
|
|
4
5
|
|
|
5
6
|
interface Option {
|
|
@@ -62,27 +63,45 @@ interface GutteredMultiSelectProps {
|
|
|
62
63
|
options: Option[];
|
|
63
64
|
initialSelected?: string[];
|
|
64
65
|
onSubmit: (values: string[]) => void;
|
|
66
|
+
/** When true, adds a "✎ Other" option that opens a free text input */
|
|
67
|
+
allowCustom?: boolean;
|
|
68
|
+
/** Placeholder shown in the custom text input */
|
|
69
|
+
customPlaceholder?: string;
|
|
65
70
|
}
|
|
66
71
|
|
|
72
|
+
const CUSTOM_SENTINEL = "__custom__";
|
|
73
|
+
|
|
67
74
|
/**
|
|
68
75
|
* Multi-select with gutter. Space to toggle, Enter to confirm.
|
|
69
76
|
* Shows ● for selected, ○ for unselected. Hint at bottom.
|
|
77
|
+
* When allowCustom is true, an "Other" option lets users type custom values.
|
|
70
78
|
*/
|
|
71
|
-
export function GutteredMultiSelect({
|
|
79
|
+
export function GutteredMultiSelect({
|
|
80
|
+
options,
|
|
81
|
+
initialSelected,
|
|
82
|
+
onSubmit,
|
|
83
|
+
allowCustom,
|
|
84
|
+
customPlaceholder,
|
|
85
|
+
}: GutteredMultiSelectProps) {
|
|
86
|
+
const allOptions = allowCustom
|
|
87
|
+
? [...options, { label: "Other (type custom)", value: CUSTOM_SENTINEL, description: "Add your own" }]
|
|
88
|
+
: options;
|
|
89
|
+
|
|
72
90
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
73
91
|
const [selected, setSelected] = useState<Set<string>>(
|
|
74
92
|
new Set(initialSelected ?? [])
|
|
75
93
|
);
|
|
94
|
+
const [phase, setPhase] = useState<"selecting" | "custom">("selecting");
|
|
76
95
|
|
|
77
96
|
useInput((input, key) => {
|
|
78
97
|
if (key.upArrow) {
|
|
79
98
|
setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev));
|
|
80
99
|
} else if (key.downArrow) {
|
|
81
100
|
setActiveIndex((prev) =>
|
|
82
|
-
prev <
|
|
101
|
+
prev < allOptions.length - 1 ? prev + 1 : prev
|
|
83
102
|
);
|
|
84
103
|
} else if (input === " ") {
|
|
85
|
-
const option =
|
|
104
|
+
const option = allOptions[activeIndex];
|
|
86
105
|
if (option) {
|
|
87
106
|
setSelected((prev) => {
|
|
88
107
|
const next = new Set(prev);
|
|
@@ -95,15 +114,54 @@ export function GutteredMultiSelect({ options, initialSelected, onSubmit }: Gutt
|
|
|
95
114
|
});
|
|
96
115
|
}
|
|
97
116
|
} else if (key.return) {
|
|
98
|
-
|
|
117
|
+
if (selected.has(CUSTOM_SENTINEL)) {
|
|
118
|
+
setSelected((prev) => {
|
|
119
|
+
const next = new Set(prev);
|
|
120
|
+
next.delete(CUSTOM_SENTINEL);
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
setPhase("custom");
|
|
124
|
+
} else {
|
|
125
|
+
onSubmit(Array.from(selected));
|
|
126
|
+
}
|
|
99
127
|
}
|
|
100
|
-
});
|
|
128
|
+
}, { isActive: phase === "selecting" });
|
|
129
|
+
|
|
130
|
+
if (phase === "custom") {
|
|
131
|
+
const currentSelections = Array.from(selected);
|
|
132
|
+
return (
|
|
133
|
+
<Box flexDirection="column">
|
|
134
|
+
{currentSelections.length > 0 && (
|
|
135
|
+
<Text>
|
|
136
|
+
<Text dimColor>{"│ "}</Text>
|
|
137
|
+
<Text color="green">{"Selected: "}</Text>
|
|
138
|
+
<Text>{currentSelections.join(", ")}</Text>
|
|
139
|
+
</Text>
|
|
140
|
+
)}
|
|
141
|
+
<Text>
|
|
142
|
+
<Text dimColor>{"│ "}</Text>
|
|
143
|
+
<Text bold>{"Add custom (comma-separated):"}</Text>
|
|
144
|
+
</Text>
|
|
145
|
+
<Text>
|
|
146
|
+
<Text dimColor>{"│ "}</Text>
|
|
147
|
+
<TextInput
|
|
148
|
+
placeholder={customPlaceholder ?? "Type here, press enter to confirm"}
|
|
149
|
+
onSubmit={(value) => {
|
|
150
|
+
const custom = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
151
|
+
onSubmit([...currentSelections, ...custom]);
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
</Text>
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
101
158
|
|
|
102
159
|
return (
|
|
103
160
|
<Box flexDirection="column">
|
|
104
|
-
{
|
|
161
|
+
{allOptions.map((option, index) => {
|
|
105
162
|
const isActive = index === activeIndex;
|
|
106
163
|
const isSelected = selected.has(option.value);
|
|
164
|
+
const isCustomOption = option.value === CUSTOM_SENTINEL;
|
|
107
165
|
return (
|
|
108
166
|
<Text key={option.value}>
|
|
109
167
|
<Text dimColor>{"│ "}</Text>
|
|
@@ -113,11 +171,11 @@ export function GutteredMultiSelect({ options, initialSelected, onSubmit }: Gutt
|
|
|
113
171
|
<Text>{" "}</Text>
|
|
114
172
|
)}
|
|
115
173
|
{isSelected ? (
|
|
116
|
-
<Text color="green">{"● "}</Text>
|
|
174
|
+
<Text color={isCustomOption ? "yellow" : "green"}>{"● "}</Text>
|
|
117
175
|
) : (
|
|
118
176
|
<Text dimColor>{"○ "}</Text>
|
|
119
177
|
)}
|
|
120
|
-
<Text bold={isActive}>{option.label}</Text>
|
|
178
|
+
<Text bold={isActive}>{isCustomOption ? `✎ ${option.label}` : option.label}</Text>
|
|
121
179
|
{option.description && (
|
|
122
180
|
<Text dimColor>{" — " + option.description}</Text>
|
|
123
181
|
)}
|
|
@@ -125,13 +183,22 @@ export function GutteredMultiSelect({ options, initialSelected, onSubmit }: Gutt
|
|
|
125
183
|
);
|
|
126
184
|
})}
|
|
127
185
|
<Text dimColor>{"│ "}{" ↑↓ move, space toggle, enter confirm"}</Text>
|
|
128
|
-
{selected.size > 0 && (
|
|
186
|
+
{selected.size > 0 && !selected.has(CUSTOM_SENTINEL) && (
|
|
129
187
|
<Text>
|
|
130
188
|
<Text dimColor>{"│ "}</Text>
|
|
131
189
|
<Text color="green">{"Selected: "}</Text>
|
|
132
190
|
<Text>{Array.from(selected).join(", ")}</Text>
|
|
133
191
|
</Text>
|
|
134
192
|
)}
|
|
193
|
+
{selected.size > 0 && selected.has(CUSTOM_SENTINEL) && (
|
|
194
|
+
<Text>
|
|
195
|
+
<Text dimColor>{"│ "}</Text>
|
|
196
|
+
<Text color="green">{"Selected: "}</Text>
|
|
197
|
+
<Text>{Array.from(selected).filter((v) => v !== CUSTOM_SENTINEL).join(", ")}</Text>
|
|
198
|
+
{selected.size > 1 && <Text dimColor>{" + custom"}</Text>}
|
|
199
|
+
{selected.size === 1 && <Text dimColor>{"custom (on confirm)"}</Text>}
|
|
200
|
+
</Text>
|
|
201
|
+
)}
|
|
135
202
|
</Box>
|
|
136
203
|
);
|
|
137
204
|
}
|
package/src/lib/fs.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
|
|
5
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -16,3 +17,7 @@ export function fileExists(filePath: string): boolean {
|
|
|
16
17
|
export function resolveTemplatePath(templateName: string): string {
|
|
17
18
|
return resolve(__dirname, '..', '..', 'templates', templateName);
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
export function homeBinPath(filename: string): string {
|
|
22
|
+
return resolve(homedir(), 'bin', filename);
|
|
23
|
+
}
|
package/src/lib/template.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
|
|
2
2
|
import { resolve, dirname } from 'node:path';
|
|
3
3
|
import Handlebars from 'handlebars';
|
|
4
|
-
import { ensureDir, resolveTemplatePath } from './fs.js';
|
|
4
|
+
import { ensureDir, resolveTemplatePath, homeBinPath } from './fs.js';
|
|
5
5
|
import type { WizardAnswers } from './types.js';
|
|
6
6
|
|
|
7
|
+
Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b);
|
|
8
|
+
|
|
7
9
|
export function renderTemplate(
|
|
8
10
|
templatePath: string,
|
|
9
11
|
data: Record<string, unknown>,
|
|
@@ -57,3 +59,103 @@ export function scaffoldAll(targetDir: string, data: WizardAnswers): void {
|
|
|
57
59
|
ensureDir(dirname(gitignoreDest));
|
|
58
60
|
writeFileSync(gitignoreDest, content, 'utf-8');
|
|
59
61
|
}
|
|
62
|
+
|
|
63
|
+
export interface ReleaseConflicts {
|
|
64
|
+
releaseScriptExists: boolean;
|
|
65
|
+
packageJsonHasRelease: boolean;
|
|
66
|
+
packageJsonExists: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function checkReleaseConflicts(
|
|
70
|
+
projectName: string,
|
|
71
|
+
targetDir: string,
|
|
72
|
+
): ReleaseConflicts {
|
|
73
|
+
const scriptPath = homeBinPath(`${projectName}-release`);
|
|
74
|
+
const pkgPath = resolve(targetDir, 'package.json');
|
|
75
|
+
|
|
76
|
+
let packageJsonExists = false;
|
|
77
|
+
let packageJsonHasRelease = false;
|
|
78
|
+
|
|
79
|
+
if (existsSync(pkgPath)) {
|
|
80
|
+
packageJsonExists = true;
|
|
81
|
+
try {
|
|
82
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
83
|
+
packageJsonHasRelease = !!pkg.scripts?.['release:patch'];
|
|
84
|
+
} catch {
|
|
85
|
+
// malformed package.json — treat as no release scripts
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
releaseScriptExists: existsSync(scriptPath),
|
|
91
|
+
packageJsonHasRelease,
|
|
92
|
+
packageJsonExists,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ScaffoldReleaseResult {
|
|
97
|
+
scriptPath: string;
|
|
98
|
+
skipped: boolean;
|
|
99
|
+
reason?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function scaffoldRelease(
|
|
103
|
+
projectName: string,
|
|
104
|
+
data: WizardAnswers,
|
|
105
|
+
): ScaffoldReleaseResult {
|
|
106
|
+
const scriptPath = homeBinPath(`${projectName}-release`);
|
|
107
|
+
|
|
108
|
+
if (existsSync(scriptPath)) {
|
|
109
|
+
return { scriptPath, skipped: true, reason: 'already exists' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const templatePath = resolveTemplatePath('bin/release.sh.hbs');
|
|
113
|
+
const rendered = renderTemplate(templatePath, data as unknown as Record<string, unknown>);
|
|
114
|
+
ensureDir(dirname(scriptPath));
|
|
115
|
+
writeFileSync(scriptPath, rendered, 'utf-8');
|
|
116
|
+
chmodSync(scriptPath, 0o755);
|
|
117
|
+
|
|
118
|
+
return { scriptPath, skipped: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface PackageJsonReleaseResult {
|
|
122
|
+
created: boolean;
|
|
123
|
+
modified: boolean;
|
|
124
|
+
skipped: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function ensurePackageJsonReleaseScripts(
|
|
128
|
+
targetDir: string,
|
|
129
|
+
projectName: string,
|
|
130
|
+
): PackageJsonReleaseResult {
|
|
131
|
+
const pkgPath = resolve(targetDir, 'package.json');
|
|
132
|
+
const releaseScripts = {
|
|
133
|
+
'release:patch': `"$HOME/bin/${projectName}-release" patch`,
|
|
134
|
+
'release:minor': `"$HOME/bin/${projectName}-release" minor`,
|
|
135
|
+
'release:major': `"$HOME/bin/${projectName}-release" major`,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (existsSync(pkgPath)) {
|
|
139
|
+
try {
|
|
140
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
141
|
+
if (pkg.scripts?.['release:patch']) {
|
|
142
|
+
return { created: false, modified: false, skipped: true };
|
|
143
|
+
}
|
|
144
|
+
pkg.scripts = { ...pkg.scripts, ...releaseScripts };
|
|
145
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
146
|
+
return { created: false, modified: true, skipped: false };
|
|
147
|
+
} catch {
|
|
148
|
+
// malformed — skip
|
|
149
|
+
return { created: false, modified: false, skipped: true };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const pkg = {
|
|
154
|
+
name: projectName,
|
|
155
|
+
version: '0.1.0',
|
|
156
|
+
scripts: releaseScripts,
|
|
157
|
+
};
|
|
158
|
+
ensureDir(dirname(pkgPath));
|
|
159
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
160
|
+
return { created: true, modified: false, skipped: false };
|
|
161
|
+
}
|
package/src/steps/generation.tsx
CHANGED
|
@@ -2,7 +2,14 @@ import React, { useEffect, useState } from "react";
|
|
|
2
2
|
import { Text } from "ink";
|
|
3
3
|
import { ConfirmInput, Spinner } from "@inkjs/ui";
|
|
4
4
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
scaffoldAll,
|
|
7
|
+
checkConflicts,
|
|
8
|
+
checkReleaseConflicts,
|
|
9
|
+
scaffoldRelease,
|
|
10
|
+
ensurePackageJsonReleaseScripts,
|
|
11
|
+
} from "../lib/template.js";
|
|
12
|
+
import { homeBinPath } from "../lib/fs.js";
|
|
6
13
|
import type { WizardAnswers } from "../lib/types.js";
|
|
7
14
|
|
|
8
15
|
interface Props {
|
|
@@ -24,13 +31,36 @@ export default function Generation({ answers, onComplete }: Props) {
|
|
|
24
31
|
"summary" | "running" | "done" | "error"
|
|
25
32
|
>("summary");
|
|
26
33
|
const [errorMsg, setErrorMsg] = useState("");
|
|
34
|
+
const [releaseResult, setReleaseResult] = useState("");
|
|
27
35
|
|
|
28
36
|
useEffect(() => {
|
|
29
37
|
if (phase !== "running") return;
|
|
30
38
|
|
|
31
39
|
try {
|
|
32
40
|
const targetDir = process.cwd();
|
|
41
|
+
const projectName = answers.projectName || "untitled";
|
|
42
|
+
|
|
33
43
|
scaffoldAll(targetDir, answers);
|
|
44
|
+
|
|
45
|
+
// Release script scaffolding
|
|
46
|
+
const releaseRes = scaffoldRelease(projectName, answers);
|
|
47
|
+
const pkgRes = ensurePackageJsonReleaseScripts(targetDir, projectName);
|
|
48
|
+
|
|
49
|
+
const parts: string[] = [];
|
|
50
|
+
if (releaseRes.skipped) {
|
|
51
|
+
parts.push(`~/bin/${projectName}-release (skipped, ${releaseRes.reason})`);
|
|
52
|
+
} else {
|
|
53
|
+
parts.push(`~/bin/${projectName}-release (created)`);
|
|
54
|
+
}
|
|
55
|
+
if (pkgRes.skipped) {
|
|
56
|
+
parts.push("package.json release scripts (skipped, already present)");
|
|
57
|
+
} else if (pkgRes.modified) {
|
|
58
|
+
parts.push("package.json release scripts (added)");
|
|
59
|
+
} else if (pkgRes.created) {
|
|
60
|
+
parts.push("package.json (created with release scripts)");
|
|
61
|
+
}
|
|
62
|
+
setReleaseResult(parts.join(", "));
|
|
63
|
+
|
|
34
64
|
setPhase("done");
|
|
35
65
|
const timer = setTimeout(() => onComplete({ generationSkipped: false }), 1500);
|
|
36
66
|
return () => clearTimeout(timer);
|
|
@@ -59,6 +89,18 @@ export default function Generation({ answers, onComplete }: Props) {
|
|
|
59
89
|
.join(", ");
|
|
60
90
|
|
|
61
91
|
const conflicts = checkConflicts(process.cwd());
|
|
92
|
+
const projectName = answers.projectName || "untitled";
|
|
93
|
+
const releaseConflicts = checkReleaseConflicts(projectName, process.cwd());
|
|
94
|
+
|
|
95
|
+
const scriptDisplay = `~/bin/${projectName}-release`;
|
|
96
|
+
const scriptStatus = releaseConflicts.releaseScriptExists
|
|
97
|
+
? "exists, will skip"
|
|
98
|
+
: "new";
|
|
99
|
+
const pkgStatus = releaseConflicts.packageJsonHasRelease
|
|
100
|
+
? "has release scripts, will skip"
|
|
101
|
+
: releaseConflicts.packageJsonExists
|
|
102
|
+
? "will add release scripts"
|
|
103
|
+
: "will create with release scripts";
|
|
62
104
|
|
|
63
105
|
return (
|
|
64
106
|
<>
|
|
@@ -114,6 +156,18 @@ export default function Generation({ answers, onComplete }: Props) {
|
|
|
114
156
|
<GutterLine>
|
|
115
157
|
<Text> </Text>
|
|
116
158
|
</GutterLine>
|
|
159
|
+
<GutterLine>
|
|
160
|
+
<Text>Release setup:</Text>
|
|
161
|
+
</GutterLine>
|
|
162
|
+
<GutterLine>
|
|
163
|
+
<Text> {scriptDisplay} → {scriptStatus}</Text>
|
|
164
|
+
</GutterLine>
|
|
165
|
+
<GutterLine>
|
|
166
|
+
<Text> package.json → {pkgStatus}</Text>
|
|
167
|
+
</GutterLine>
|
|
168
|
+
<GutterLine>
|
|
169
|
+
<Text> </Text>
|
|
170
|
+
</GutterLine>
|
|
117
171
|
<GutterLine>
|
|
118
172
|
<Text>Proceed? </Text>
|
|
119
173
|
<ConfirmInput onConfirm={handleConfirm} onCancel={handleCancel} />
|
|
@@ -139,8 +193,15 @@ export default function Generation({ answers, onComplete }: Props) {
|
|
|
139
193
|
}
|
|
140
194
|
|
|
141
195
|
return (
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
196
|
+
<>
|
|
197
|
+
<GutterLine>
|
|
198
|
+
<Text color="green">All project files generated successfully.</Text>
|
|
199
|
+
</GutterLine>
|
|
200
|
+
{releaseResult && (
|
|
201
|
+
<GutterLine>
|
|
202
|
+
<Text color="green">Release: {releaseResult}</Text>
|
|
203
|
+
</GutterLine>
|
|
204
|
+
)}
|
|
205
|
+
</>
|
|
145
206
|
);
|
|
146
207
|
}
|
|
@@ -12,6 +12,23 @@ interface Props {
|
|
|
12
12
|
|
|
13
13
|
type Phase = "problem" | "users" | "techStack" | "coreFeatures" | "nonGoals";
|
|
14
14
|
|
|
15
|
+
const CORE_FEATURE_OPTIONS = [
|
|
16
|
+
{ label: "Authentication", value: "Authentication", description: "Login, signup, sessions" },
|
|
17
|
+
{ label: "Authorization", value: "Authorization", description: "Roles, permissions, RBAC" },
|
|
18
|
+
{ label: "API / REST", value: "API / REST", description: "RESTful endpoints" },
|
|
19
|
+
{ label: "GraphQL API", value: "GraphQL API", description: "GraphQL schema & resolvers" },
|
|
20
|
+
{ label: "Dashboard", value: "Dashboard", description: "Admin or user dashboard" },
|
|
21
|
+
{ label: "CRUD Operations", value: "CRUD Operations", description: "Create, read, update, delete" },
|
|
22
|
+
{ label: "File Upload", value: "File Upload", description: "Upload and manage files" },
|
|
23
|
+
{ label: "Search", value: "Search", description: "Full-text or filtered search" },
|
|
24
|
+
{ label: "Notifications", value: "Notifications", description: "Email, push, in-app" },
|
|
25
|
+
{ label: "Payments", value: "Payments", description: "Billing, subscriptions, checkout" },
|
|
26
|
+
{ label: "Real-time", value: "Real-time", description: "WebSockets, live updates" },
|
|
27
|
+
{ label: "Analytics", value: "Analytics", description: "Usage tracking, metrics" },
|
|
28
|
+
{ label: "CLI", value: "CLI", description: "Command-line interface" },
|
|
29
|
+
{ label: "CI/CD", value: "CI/CD", description: "Continuous integration & deployment" },
|
|
30
|
+
];
|
|
31
|
+
|
|
15
32
|
const TECH_STACK_OPTIONS = [
|
|
16
33
|
{ label: "React", value: "React", description: "UI component library" },
|
|
17
34
|
{ label: "Next.js", value: "Next.js", description: "Full-stack React framework" },
|
|
@@ -98,6 +115,10 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
98
115
|
advanceToNext("techStack", values.join(", "));
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
function handleCoreFeaturesSubmit(values: string[]) {
|
|
119
|
+
advanceToNext("coreFeatures", values.join(", "));
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
// Build completed fields display
|
|
102
123
|
const completedFields = PHASE_ORDER.slice(0, phaseIndex).map((key) => (
|
|
103
124
|
<GutterLine key={key}>
|
|
@@ -153,13 +174,17 @@ export default function ProductContext({ initialValues, onComplete }: Props) {
|
|
|
153
174
|
)}
|
|
154
175
|
|
|
155
176
|
{currentPhase === "coreFeatures" && (
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
<>
|
|
178
|
+
<GutterLine>
|
|
179
|
+
<Text bold>Core features:</Text>
|
|
180
|
+
</GutterLine>
|
|
181
|
+
<GutteredMultiSelect
|
|
182
|
+
options={CORE_FEATURE_OPTIONS}
|
|
183
|
+
onSubmit={handleCoreFeaturesSubmit}
|
|
184
|
+
allowCustom
|
|
185
|
+
customPlaceholder="e.g. Export PDF, Onboarding flow..."
|
|
161
186
|
/>
|
|
162
|
-
|
|
187
|
+
</>
|
|
163
188
|
)}
|
|
164
189
|
|
|
165
190
|
{currentPhase === "nonGoals" && (
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
2
|
import { Text } from "ink";
|
|
3
|
-
import { TextInput } from "@inkjs/ui";
|
|
4
3
|
import { GutterLine } from "../components/gutter-line.js";
|
|
5
4
|
import { GutteredSelect, GutteredMultiSelect } from "../components/guttered-select.js";
|
|
6
5
|
import type { WizardAnswers } from "../lib/types.js";
|
|
@@ -42,6 +41,17 @@ const FRAMEWORK_OPTIONS = [
|
|
|
42
41
|
{ label: "Rails", value: "Rails", description: "Convention-over-config Ruby web" },
|
|
43
42
|
];
|
|
44
43
|
|
|
44
|
+
const CODE_STYLE_OPTIONS = [
|
|
45
|
+
{ label: "Strict mode", value: "Strict mode", description: "TypeScript strict, ESLint strict" },
|
|
46
|
+
{ label: "camelCase / PascalCase", value: "camelCase for variables, PascalCase for types", description: "Standard JS/TS naming" },
|
|
47
|
+
{ label: "ESM imports", value: "ESM imports, no CommonJS require", description: "Modern module syntax" },
|
|
48
|
+
{ label: "Import ordering", value: "stdlib → external → internal import ordering", description: "Grouped imports" },
|
|
49
|
+
{ label: "Return errors, don't throw", value: "Return errors, don't throw", description: "Explicit error handling" },
|
|
50
|
+
{ label: "Functional style", value: "Prefer functional patterns", description: "Pure functions, immutability" },
|
|
51
|
+
{ label: "No any type", value: "No any type, use unknown", description: "Strict type safety" },
|
|
52
|
+
{ label: "JSDoc comments", value: "JSDoc for public APIs", description: "Documentation standard" },
|
|
53
|
+
];
|
|
54
|
+
|
|
45
55
|
/** Suggest frameworks based on selected languages */
|
|
46
56
|
function getFrameworkPreselect(languages: string[]): string[] {
|
|
47
57
|
const langSet = new Set(languages.map((l) => l.toLowerCase()));
|
|
@@ -128,6 +138,8 @@ export default function StackStyle({ onComplete }: Props) {
|
|
|
128
138
|
</GutterLine>
|
|
129
139
|
<GutteredMultiSelect
|
|
130
140
|
options={LANGUAGE_OPTIONS}
|
|
141
|
+
allowCustom
|
|
142
|
+
customPlaceholder="e.g. Elixir, Zig, C++"
|
|
131
143
|
onSubmit={(values) => {
|
|
132
144
|
setSelectedLanguages(values);
|
|
133
145
|
setAdvancedStep("framework");
|
|
@@ -149,6 +161,8 @@ export default function StackStyle({ onComplete }: Props) {
|
|
|
149
161
|
<GutteredMultiSelect
|
|
150
162
|
options={FRAMEWORK_OPTIONS}
|
|
151
163
|
initialSelected={getFrameworkPreselect(selectedLanguages)}
|
|
164
|
+
allowCustom
|
|
165
|
+
customPlaceholder="e.g. Hono, Remix, Astro"
|
|
152
166
|
onSubmit={(values) => {
|
|
153
167
|
setSelectedFrameworks(values);
|
|
154
168
|
setAdvancedStep("codeStyle");
|
|
@@ -168,21 +182,20 @@ export default function StackStyle({ onComplete }: Props) {
|
|
|
168
182
|
<Text dimColor>Frameworks: {selectedFrameworks.join(", ")}</Text>
|
|
169
183
|
</GutterLine>
|
|
170
184
|
<GutterLine>
|
|
171
|
-
<Text bold>Code style conventions
|
|
172
|
-
<TextInput
|
|
173
|
-
placeholder="e.g. strict mode, camelCase, ESM imports"
|
|
174
|
-
onSubmit={(value) => {
|
|
175
|
-
onComplete({
|
|
176
|
-
language: selectedLanguages.join(", "),
|
|
177
|
-
framework: selectedFrameworks.join(", "),
|
|
178
|
-
codeStyle: value
|
|
179
|
-
.split(",")
|
|
180
|
-
.map((s) => s.trim())
|
|
181
|
-
.filter(Boolean),
|
|
182
|
-
});
|
|
183
|
-
}}
|
|
184
|
-
/>
|
|
185
|
+
<Text bold>Code style conventions:</Text>
|
|
185
186
|
</GutterLine>
|
|
187
|
+
<GutteredMultiSelect
|
|
188
|
+
options={CODE_STYLE_OPTIONS}
|
|
189
|
+
allowCustom
|
|
190
|
+
customPlaceholder="e.g. tabs over spaces, max 80 chars"
|
|
191
|
+
onSubmit={(values) => {
|
|
192
|
+
onComplete({
|
|
193
|
+
language: selectedLanguages.join(", "),
|
|
194
|
+
framework: selectedFrameworks.join(", "),
|
|
195
|
+
codeStyle: values,
|
|
196
|
+
});
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
186
199
|
</>
|
|
187
200
|
);
|
|
188
201
|
}
|
package/templates/CLAUDE.md.hbs
CHANGED
|
@@ -94,6 +94,21 @@ Two tracking files live at the project root. Both are append-only logs — never
|
|
|
94
94
|
- **Then**: Fix only unfixed findings → append a new `CF-` entry to CODE-FIXED.md (write to CODE-FIXED.md only, never modify CODE-REVIEW.md).
|
|
95
95
|
- **Never** perform a code review during a fixing process.
|
|
96
96
|
|
|
97
|
+
## Release
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
{{packageManager}} run release:patch # Bump patch (0.1.0 → 0.1.1)
|
|
101
|
+
{{packageManager}} run release:minor # Bump minor (0.1.0 → 0.2.0)
|
|
102
|
+
{{packageManager}} run release:major # Bump major (0.1.0 → 1.0.0)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The release script (`~/bin/{{projectName}}-release`) will:
|
|
106
|
+
1. Verify git working tree is clean
|
|
107
|
+
2. Run build
|
|
108
|
+
3. Bump version via `npm version`
|
|
109
|
+
4. Push branch + tags to origin
|
|
110
|
+
5. Publish to registry
|
|
111
|
+
|
|
97
112
|
## Core Principles
|
|
98
113
|
|
|
99
114
|
- **Simplicity First**: Make every change as simple as possible. Minimal code impact.
|