procedure-cli 0.1.0 → 0.1.2
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/CODE-REVIEW.md +32 -0
- 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/stack-style.js +72 -23
- package/dist/steps/stack-style.js.map +1 -1
- package/package.json +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/stack-style.tsx +116 -32
- package/templates/bin/release.sh.hbs +101 -0
|
@@ -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
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
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
|
-
import { GutteredSelect } from "../components/guttered-select.js";
|
|
4
|
+
import { GutteredSelect, GutteredMultiSelect } from "../components/guttered-select.js";
|
|
6
5
|
import type { WizardAnswers } from "../lib/types.js";
|
|
7
6
|
|
|
8
7
|
interface Props {
|
|
@@ -10,18 +9,65 @@ interface Props {
|
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
type Mode = "choosing" | "quickstart" | "advanced";
|
|
13
|
-
type
|
|
12
|
+
type AdvancedStep = "language" | "framework" | "codeStyle";
|
|
14
13
|
|
|
15
14
|
const PRESET_OPTIONS = [
|
|
16
15
|
{ label: "TypeScript + Node.js", value: "typescript-node" },
|
|
17
16
|
];
|
|
18
17
|
|
|
19
|
-
const
|
|
20
|
-
{
|
|
21
|
-
{
|
|
22
|
-
{
|
|
18
|
+
const LANGUAGE_OPTIONS = [
|
|
19
|
+
{ label: "TypeScript", value: "TypeScript", description: "Typed JavaScript, strict mode recommended" },
|
|
20
|
+
{ label: "JavaScript", value: "JavaScript", description: "Dynamic, runs everywhere" },
|
|
21
|
+
{ label: "Python", value: "Python", description: "Readable, great ecosystem for AI/ML & web" },
|
|
22
|
+
{ label: "Go", value: "Go", description: "Fast compilation, built-in concurrency" },
|
|
23
|
+
{ label: "Rust", value: "Rust", description: "Memory-safe systems programming" },
|
|
24
|
+
{ label: "Java", value: "Java", description: "Enterprise-grade, JVM ecosystem" },
|
|
25
|
+
{ label: "Ruby", value: "Ruby", description: "Developer happiness, Rails ecosystem" },
|
|
26
|
+
{ label: "PHP", value: "PHP", description: "Web-native, Laravel & WordPress" },
|
|
27
|
+
{ label: "Swift", value: "Swift", description: "Apple platforms, type-safe" },
|
|
28
|
+
{ label: "Kotlin", value: "Kotlin", description: "Modern JVM, Android-first" },
|
|
23
29
|
];
|
|
24
30
|
|
|
31
|
+
const FRAMEWORK_OPTIONS = [
|
|
32
|
+
{ label: "Node.js", value: "Node.js", description: "JS/TS server runtime" },
|
|
33
|
+
{ label: "React", value: "React", description: "Component-based UI library" },
|
|
34
|
+
{ label: "Next.js", value: "Next.js", description: "Full-stack React framework" },
|
|
35
|
+
{ label: "Vue", value: "Vue", description: "Progressive UI framework" },
|
|
36
|
+
{ label: "Svelte", value: "Svelte", description: "Compile-time UI framework" },
|
|
37
|
+
{ label: "Express", value: "Express", description: "Minimal Node.js web framework" },
|
|
38
|
+
{ label: "Fastify", value: "Fastify", description: "Fast Node.js web framework" },
|
|
39
|
+
{ label: "Django", value: "Django", description: "Batteries-included Python web" },
|
|
40
|
+
{ label: "Flask", value: "Flask", description: "Lightweight Python web" },
|
|
41
|
+
{ label: "Rails", value: "Rails", description: "Convention-over-config Ruby web" },
|
|
42
|
+
];
|
|
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
|
+
|
|
55
|
+
/** Suggest frameworks based on selected languages */
|
|
56
|
+
function getFrameworkPreselect(languages: string[]): string[] {
|
|
57
|
+
const langSet = new Set(languages.map((l) => l.toLowerCase()));
|
|
58
|
+
const preselect: string[] = [];
|
|
59
|
+
if (langSet.has("typescript") || langSet.has("javascript")) {
|
|
60
|
+
preselect.push("Node.js");
|
|
61
|
+
}
|
|
62
|
+
if (langSet.has("python")) {
|
|
63
|
+
preselect.push("Django");
|
|
64
|
+
}
|
|
65
|
+
if (langSet.has("ruby")) {
|
|
66
|
+
preselect.push("Rails");
|
|
67
|
+
}
|
|
68
|
+
return preselect;
|
|
69
|
+
}
|
|
70
|
+
|
|
25
71
|
const PRESETS: Record<string, Partial<WizardAnswers>> = {
|
|
26
72
|
"typescript-node": {
|
|
27
73
|
language: "TypeScript",
|
|
@@ -41,8 +87,9 @@ const PRESETS: Record<string, Partial<WizardAnswers>> = {
|
|
|
41
87
|
|
|
42
88
|
export default function StackStyle({ onComplete }: Props) {
|
|
43
89
|
const [mode, setMode] = useState<Mode>("choosing");
|
|
44
|
-
const [
|
|
45
|
-
const [
|
|
90
|
+
const [advancedStep, setAdvancedStep] = useState<AdvancedStep>("language");
|
|
91
|
+
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([]);
|
|
92
|
+
const [selectedFrameworks, setSelectedFrameworks] = useState<string[]>([]);
|
|
46
93
|
|
|
47
94
|
if (mode === "choosing") {
|
|
48
95
|
return (
|
|
@@ -81,37 +128,74 @@ export default function StackStyle({ onComplete }: Props) {
|
|
|
81
128
|
}
|
|
82
129
|
|
|
83
130
|
// Advanced mode
|
|
84
|
-
const
|
|
131
|
+
const advStep = advancedStep as AdvancedStep;
|
|
85
132
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
if (advStep === "language") {
|
|
134
|
+
return (
|
|
135
|
+
<>
|
|
136
|
+
<GutterLine>
|
|
137
|
+
<Text bold>Programming languages:</Text>
|
|
138
|
+
</GutterLine>
|
|
139
|
+
<GutteredMultiSelect
|
|
140
|
+
options={LANGUAGE_OPTIONS}
|
|
141
|
+
allowCustom
|
|
142
|
+
customPlaceholder="e.g. Elixir, Zig, C++"
|
|
143
|
+
onSubmit={(values) => {
|
|
144
|
+
setSelectedLanguages(values);
|
|
145
|
+
setAdvancedStep("framework");
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
89
151
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
152
|
+
if (advStep === "framework") {
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<GutterLine>
|
|
156
|
+
<Text dimColor>Languages: {selectedLanguages.join(", ")}</Text>
|
|
157
|
+
</GutterLine>
|
|
158
|
+
<GutterLine>
|
|
159
|
+
<Text bold>Frameworks:</Text>
|
|
160
|
+
</GutterLine>
|
|
161
|
+
<GutteredMultiSelect
|
|
162
|
+
options={FRAMEWORK_OPTIONS}
|
|
163
|
+
initialSelected={getFrameworkPreselect(selectedLanguages)}
|
|
164
|
+
allowCustom
|
|
165
|
+
customPlaceholder="e.g. Hono, Remix, Astro"
|
|
166
|
+
onSubmit={(values) => {
|
|
167
|
+
setSelectedFrameworks(values);
|
|
168
|
+
setAdvancedStep("codeStyle");
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
</>
|
|
172
|
+
);
|
|
99
173
|
}
|
|
100
174
|
|
|
175
|
+
// codeStyle step
|
|
101
176
|
return (
|
|
102
177
|
<>
|
|
103
|
-
{ADVANCED_FIELDS.slice(0, advancedIndex).map((f) => (
|
|
104
|
-
<GutterLine key={f.key}>
|
|
105
|
-
<Text dimColor>
|
|
106
|
-
{f.label}: {answers[f.key]}
|
|
107
|
-
</Text>
|
|
108
|
-
</GutterLine>
|
|
109
|
-
))}
|
|
110
|
-
|
|
111
178
|
<GutterLine>
|
|
112
|
-
<Text
|
|
113
|
-
|
|
179
|
+
<Text dimColor>Languages: {selectedLanguages.join(", ")}</Text>
|
|
180
|
+
</GutterLine>
|
|
181
|
+
<GutterLine>
|
|
182
|
+
<Text dimColor>Frameworks: {selectedFrameworks.join(", ")}</Text>
|
|
183
|
+
</GutterLine>
|
|
184
|
+
<GutterLine>
|
|
185
|
+
<Text bold>Code style conventions:</Text>
|
|
114
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
|
+
/>
|
|
115
199
|
</>
|
|
116
200
|
);
|
|
117
201
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
echo "Usage: $0 [patch|minor|major]"
|
|
6
|
+
exit 1
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if [[ "${1:-}" == "" ]]; then
|
|
10
|
+
usage
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
BUMP="$1"
|
|
14
|
+
if [[ "$BUMP" != "patch" && "$BUMP" != "minor" && "$BUMP" != "major" ]]; then
|
|
15
|
+
usage
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
echo "==> Checking git working tree..."
|
|
19
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
20
|
+
echo "Error: git working tree is not clean. Commit or stash changes first."
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
{{#if (eq packageManager "bun")}}
|
|
25
|
+
echo "==> Running verification..."
|
|
26
|
+
bun run build
|
|
27
|
+
|
|
28
|
+
echo "==> Bumping version ($BUMP)..."
|
|
29
|
+
npm version "$BUMP"
|
|
30
|
+
|
|
31
|
+
CURRENT_BRANCH="$(git branch --show-current)"
|
|
32
|
+
echo "==> Pushing branch and tags to origin/$CURRENT_BRANCH..."
|
|
33
|
+
git push origin "$CURRENT_BRANCH"
|
|
34
|
+
git push origin --tags
|
|
35
|
+
|
|
36
|
+
echo "==> Publishing..."
|
|
37
|
+
bun publish
|
|
38
|
+
{{else if (eq packageManager "pnpm")}}
|
|
39
|
+
echo "==> Checking npm auth..."
|
|
40
|
+
if ! npm whoami >/dev/null 2>&1; then
|
|
41
|
+
echo "Error: npm is not authenticated. Run: npm login"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo "==> Running verification..."
|
|
46
|
+
pnpm run build
|
|
47
|
+
|
|
48
|
+
echo "==> Bumping version ($BUMP)..."
|
|
49
|
+
npm version "$BUMP"
|
|
50
|
+
|
|
51
|
+
CURRENT_BRANCH="$(git branch --show-current)"
|
|
52
|
+
echo "==> Pushing branch and tags to origin/$CURRENT_BRANCH..."
|
|
53
|
+
git push origin "$CURRENT_BRANCH"
|
|
54
|
+
git push origin --tags
|
|
55
|
+
|
|
56
|
+
echo "==> Publishing to npm..."
|
|
57
|
+
pnpm publish --access public
|
|
58
|
+
{{else if (eq packageManager "yarn")}}
|
|
59
|
+
echo "==> Checking npm auth..."
|
|
60
|
+
if ! npm whoami >/dev/null 2>&1; then
|
|
61
|
+
echo "Error: npm is not authenticated. Run: npm login"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo "==> Running verification..."
|
|
66
|
+
yarn build
|
|
67
|
+
|
|
68
|
+
echo "==> Bumping version ($BUMP)..."
|
|
69
|
+
npm version "$BUMP"
|
|
70
|
+
|
|
71
|
+
CURRENT_BRANCH="$(git branch --show-current)"
|
|
72
|
+
echo "==> Pushing branch and tags to origin/$CURRENT_BRANCH..."
|
|
73
|
+
git push origin "$CURRENT_BRANCH"
|
|
74
|
+
git push origin --tags
|
|
75
|
+
|
|
76
|
+
echo "==> Publishing to npm..."
|
|
77
|
+
yarn npm publish --access public
|
|
78
|
+
{{else}}
|
|
79
|
+
echo "==> Checking npm auth..."
|
|
80
|
+
if ! npm whoami >/dev/null 2>&1; then
|
|
81
|
+
echo "Error: npm is not authenticated. Run: npm login"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo "==> Running verification..."
|
|
86
|
+
npm run build
|
|
87
|
+
|
|
88
|
+
echo "==> Bumping version ($BUMP)..."
|
|
89
|
+
npm version "$BUMP"
|
|
90
|
+
|
|
91
|
+
CURRENT_BRANCH="$(git branch --show-current)"
|
|
92
|
+
echo "==> Pushing branch and tags to origin/$CURRENT_BRANCH..."
|
|
93
|
+
git push origin "$CURRENT_BRANCH"
|
|
94
|
+
git push origin --tags
|
|
95
|
+
|
|
96
|
+
echo "==> Publishing to npm..."
|
|
97
|
+
npm publish --access public
|
|
98
|
+
{{/if}}
|
|
99
|
+
|
|
100
|
+
NEW_VERSION="$(node -p "require('./package.json').version")"
|
|
101
|
+
echo "Release complete: {{projectName}}@$NEW_VERSION"
|