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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "procedure-cli",
3
- "version": "0.1.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
- "Powerline",
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
- return answers.setupPowerline ? "Powerline: yes" : "Powerline: no";
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({ options, initialSelected, onSubmit }: GutteredMultiSelectProps) {
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 < options.length - 1 ? prev + 1 : prev
101
+ prev < allOptions.length - 1 ? prev + 1 : prev
83
102
  );
84
103
  } else if (input === " ") {
85
- const option = options[activeIndex];
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
- onSubmit(Array.from(selected));
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
- {options.map((option, index) => {
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
+ }
@@ -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
+ }
@@ -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 { scaffoldAll, checkConflicts } from "../lib/template.js";
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
- <GutterLine>
143
- <Text color="green">All project files generated successfully.</Text>
144
- </GutterLine>
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
- <GutterLine>
157
- <Text bold>Core features (comma-separated): </Text>
158
- <TextInput
159
- placeholder="e.g. Auth, Dashboard, API..."
160
- onSubmit={handleTextSubmit("coreFeatures", true)}
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
- </GutterLine>
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 (comma-separated): </Text>
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
  }
@@ -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.