agentic-dev 0.2.10 → 0.2.11

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 (291) hide show
  1. package/README.md +23 -9
  2. package/bin/agentic-dev.mjs +656 -124
  3. package/lib/scaffold.mjs +109 -6
  4. package/package.json +1 -1
  5. package/client/admin/.dockerignore +0 -3
  6. package/client/admin/.env.example +0 -1
  7. package/client/admin/Dockerfile +0 -16
  8. package/client/admin/Dockerfile.dev +0 -18
  9. package/client/admin/README.md +0 -20
  10. package/client/admin/index.html +0 -12
  11. package/client/admin/package.json +0 -41
  12. package/client/admin/postcss.config.js +0 -6
  13. package/client/admin/scripts/ui-parity-admin-adapter.mjs +0 -65
  14. package/client/admin/src/api/alerts.ts +0 -33
  15. package/client/admin/src/api/client.ts +0 -71
  16. package/client/admin/src/api/orders.ts +0 -33
  17. package/client/admin/src/api/support.ts +0 -11
  18. package/client/admin/src/app/App.tsx +0 -23
  19. package/client/admin/src/auth/AuthProvider.tsx +0 -122
  20. package/client/admin/src/auth/ProtectedRoute.tsx +0 -22
  21. package/client/admin/src/auth/auth-client.ts +0 -38
  22. package/client/admin/src/auth/types.ts +0 -18
  23. package/client/admin/src/components/AdminNotificationsDrawer.tsx +0 -162
  24. package/client/admin/src/components/AdminShell.tsx +0 -76
  25. package/client/admin/src/components/ui/button.tsx +0 -34
  26. package/client/admin/src/components/ui/input.tsx +0 -21
  27. package/client/admin/src/lib/cn.ts +0 -6
  28. package/client/admin/src/lib/specRouteCatalog.json +0 -30
  29. package/client/admin/src/lib/specScreens.json +0 -22
  30. package/client/admin/src/main.tsx +0 -17
  31. package/client/admin/src/pages/AdminDashboardPage.tsx +0 -171
  32. package/client/admin/src/pages/AdminLoginPage.tsx +0 -75
  33. package/client/admin/src/pages/AdminQueuePage.tsx +0 -107
  34. package/client/admin/src/pages/AdminSupportPage.tsx +0 -61
  35. package/client/admin/src/styles/globals.css +0 -17
  36. package/client/admin/src/theme-vars.ts +0 -18
  37. package/client/admin/src/theme.ts +0 -25
  38. package/client/admin/src/vite-env.d.ts +0 -1
  39. package/client/admin/tailwind.config.js +0 -8
  40. package/client/admin/tsconfig.json +0 -25
  41. package/client/admin/vite.config.ts +0 -12
  42. package/client/landing/.dockerignore +0 -3
  43. package/client/landing/.env.example +0 -1
  44. package/client/landing/Dockerfile +0 -16
  45. package/client/landing/Dockerfile.dev +0 -18
  46. package/client/landing/README.md +0 -18
  47. package/client/landing/index.html +0 -12
  48. package/client/landing/package.json +0 -41
  49. package/client/landing/postcss.config.js +0 -6
  50. package/client/landing/scripts/ui-parity-landing-adapter.mjs +0 -65
  51. package/client/landing/src/App.tsx +0 -21
  52. package/client/landing/src/api/catalog.ts +0 -30
  53. package/client/landing/src/api/client.ts +0 -30
  54. package/client/landing/src/auth/AuthProvider.tsx +0 -122
  55. package/client/landing/src/auth/ProtectedRoute.tsx +0 -22
  56. package/client/landing/src/auth/auth-client.ts +0 -38
  57. package/client/landing/src/auth/types.ts +0 -18
  58. package/client/landing/src/components/LandingShell.tsx +0 -34
  59. package/client/landing/src/lib/specRouteCatalog.json +0 -23
  60. package/client/landing/src/lib/specScreens.json +0 -17
  61. package/client/landing/src/main.tsx +0 -17
  62. package/client/landing/src/pages/LandingHomePage.tsx +0 -215
  63. package/client/landing/src/pages/LandingLoginPage.tsx +0 -90
  64. package/client/landing/src/pages/LandingWorkspacePage.tsx +0 -126
  65. package/client/landing/src/styles/globals.css +0 -17
  66. package/client/landing/src/theme-vars.ts +0 -16
  67. package/client/landing/src/theme.ts +0 -21
  68. package/client/landing/src/vite-env.d.ts +0 -1
  69. package/client/landing/tailwind.config.js +0 -8
  70. package/client/landing/tsconfig.json +0 -25
  71. package/client/landing/vite.config.ts +0 -12
  72. package/client/mobile/.dockerignore +0 -2
  73. package/client/mobile/.env.example +0 -1
  74. package/client/mobile/Dockerfile +0 -16
  75. package/client/mobile/Dockerfile.dev +0 -18
  76. package/client/mobile/README.md +0 -19
  77. package/client/mobile/index.html +0 -12
  78. package/client/mobile/package.json +0 -42
  79. package/client/mobile/postcss.config.js +0 -6
  80. package/client/mobile/scripts/ui-parity-mobile-adapter.mjs +0 -67
  81. package/client/mobile/src/App.tsx +0 -1
  82. package/client/mobile/src/api/client.ts +0 -62
  83. package/client/mobile/src/api/fulfillment.ts +0 -55
  84. package/client/mobile/src/api/shipping.ts +0 -56
  85. package/client/mobile/src/app/App.tsx +0 -23
  86. package/client/mobile/src/auth/AuthProvider.tsx +0 -122
  87. package/client/mobile/src/auth/ProtectedRoute.tsx +0 -27
  88. package/client/mobile/src/auth/auth-client.ts +0 -38
  89. package/client/mobile/src/auth/types.ts +0 -18
  90. package/client/mobile/src/components/InShell.tsx +0 -74
  91. package/client/mobile/src/components/ui/button.tsx +0 -35
  92. package/client/mobile/src/components/ui/card.tsx +0 -15
  93. package/client/mobile/src/components/ui/input.tsx +0 -21
  94. package/client/mobile/src/lib/cn.ts +0 -6
  95. package/client/mobile/src/lib/specRouteCatalog.json +0 -26
  96. package/client/mobile/src/lib/specScreens.json +0 -22
  97. package/client/mobile/src/lib/useSpeechRecognitionInput.ts +0 -271
  98. package/client/mobile/src/main.tsx +0 -17
  99. package/client/mobile/src/pages/DashboardPage.tsx +0 -172
  100. package/client/mobile/src/pages/FulfillmentPage.tsx +0 -138
  101. package/client/mobile/src/pages/LoginPage.tsx +0 -74
  102. package/client/mobile/src/pages/ShippingPage.tsx +0 -338
  103. package/client/mobile/src/styles/globals.css +0 -23
  104. package/client/mobile/src/theme-vars.ts +0 -16
  105. package/client/mobile/src/theme.ts +0 -21
  106. package/client/mobile/src/vite-env.d.ts +0 -1
  107. package/client/mobile/tailwind.config.js +0 -8
  108. package/client/mobile/tsconfig.json +0 -25
  109. package/client/mobile/vite.config.ts +0 -12
  110. package/client/web/.dockerignore +0 -3
  111. package/client/web/.env.example +0 -1
  112. package/client/web/Dockerfile +0 -16
  113. package/client/web/Dockerfile.dev +0 -18
  114. package/client/web/README.md +0 -47
  115. package/client/web/index.html +0 -12
  116. package/client/web/package.json +0 -42
  117. package/client/web/postcss.config.js +0 -6
  118. package/client/web/scripts/ui-parity-web-adapter.mjs +0 -66
  119. package/client/web/src/api/client.ts +0 -30
  120. package/client/web/src/api/orders.ts +0 -42
  121. package/client/web/src/app/App.tsx +0 -21
  122. package/client/web/src/auth/AuthProvider.tsx +0 -122
  123. package/client/web/src/auth/ProtectedRoute.tsx +0 -22
  124. package/client/web/src/auth/auth-client.ts +0 -38
  125. package/client/web/src/auth/types.ts +0 -18
  126. package/client/web/src/components/AppShell.tsx +0 -59
  127. package/client/web/src/components/ui/button.tsx +0 -35
  128. package/client/web/src/components/ui/card.tsx +0 -7
  129. package/client/web/src/components/ui/input.tsx +0 -21
  130. package/client/web/src/lib/cn.ts +0 -6
  131. package/client/web/src/lib/specRouteCatalog.json +0 -23
  132. package/client/web/src/lib/specScreens.json +0 -17
  133. package/client/web/src/main.tsx +0 -17
  134. package/client/web/src/pages/DashboardPage.tsx +0 -158
  135. package/client/web/src/pages/LoginPage.tsx +0 -72
  136. package/client/web/src/pages/OrdersPage.tsx +0 -123
  137. package/client/web/src/styles/globals.css +0 -17
  138. package/client/web/src/theme-vars.ts +0 -18
  139. package/client/web/src/theme.ts +0 -25
  140. package/client/web/src/vite-env.d.ts +0 -1
  141. package/client/web/tailwind.config.js +0 -8
  142. package/client/web/tsconfig.json +0 -25
  143. package/client/web/vite.config.ts +0 -12
  144. package/server/.dockerignore +0 -4
  145. package/server/.env.example +0 -19
  146. package/server/Dockerfile +0 -22
  147. package/server/Dockerfile.dev +0 -19
  148. package/server/README.md +0 -33
  149. package/server/__init__.py +0 -0
  150. package/server/api/__init__.py +0 -1
  151. package/server/api/http/__init__.py +0 -4
  152. package/server/api/http/app.py +0 -53
  153. package/server/api/http/router.py +0 -24
  154. package/server/config.py +0 -52
  155. package/server/contexts/__init__.py +0 -12
  156. package/server/contexts/alerts/__init__.py +0 -1
  157. package/server/contexts/alerts/application/__init__.py +0 -13
  158. package/server/contexts/alerts/application/services.py +0 -41
  159. package/server/contexts/alerts/contracts/__init__.py +0 -3
  160. package/server/contexts/alerts/contracts/http/__init__.py +0 -3
  161. package/server/contexts/alerts/contracts/http/router.py +0 -37
  162. package/server/contexts/alerts/domain/__init__.py +0 -15
  163. package/server/contexts/alerts/domain/models.py +0 -29
  164. package/server/contexts/alerts/infrastructure/__init__.py +0 -11
  165. package/server/contexts/alerts/infrastructure/repository.py +0 -41
  166. package/server/contexts/auth/__init__.py +0 -1
  167. package/server/contexts/auth/application/__init__.py +0 -3
  168. package/server/contexts/auth/application/ports.py +0 -10
  169. package/server/contexts/auth/application/services.py +0 -64
  170. package/server/contexts/auth/contracts/__init__.py +0 -4
  171. package/server/contexts/auth/contracts/http/__init__.py +0 -4
  172. package/server/contexts/auth/contracts/http/dependencies.py +0 -37
  173. package/server/contexts/auth/contracts/http/router.py +0 -19
  174. package/server/contexts/auth/domain/__init__.py +0 -3
  175. package/server/contexts/auth/domain/models.py +0 -24
  176. package/server/contexts/auth/infrastructure/__init__.py +0 -4
  177. package/server/contexts/auth/infrastructure/adapters/memory.py +0 -19
  178. package/server/contexts/auth/infrastructure/adapters/mongodb.py +0 -24
  179. package/server/contexts/auth/infrastructure/adapters/sqlalchemy.py +0 -74
  180. package/server/contexts/auth/infrastructure/repository.py +0 -28
  181. package/server/contexts/catalog/__init__.py +0 -1
  182. package/server/contexts/catalog/application/__init__.py +0 -28
  183. package/server/contexts/catalog/application/ports.py +0 -15
  184. package/server/contexts/catalog/application/services.py +0 -154
  185. package/server/contexts/catalog/contracts/__init__.py +0 -3
  186. package/server/contexts/catalog/contracts/http/__init__.py +0 -3
  187. package/server/contexts/catalog/contracts/http/router.py +0 -60
  188. package/server/contexts/catalog/domain/__init__.py +0 -45
  189. package/server/contexts/catalog/domain/models.py +0 -113
  190. package/server/contexts/catalog/infrastructure/__init__.py +0 -4
  191. package/server/contexts/catalog/infrastructure/adapters/memory.py +0 -62
  192. package/server/contexts/catalog/infrastructure/repository.py +0 -8
  193. package/server/contexts/fulfillment/__init__.py +0 -1
  194. package/server/contexts/fulfillment/application/__init__.py +0 -13
  195. package/server/contexts/fulfillment/application/ports.py +0 -20
  196. package/server/contexts/fulfillment/application/services.py +0 -85
  197. package/server/contexts/fulfillment/contracts/__init__.py +0 -3
  198. package/server/contexts/fulfillment/contracts/http/__init__.py +0 -3
  199. package/server/contexts/fulfillment/contracts/http/router.py +0 -40
  200. package/server/contexts/fulfillment/domain/__init__.py +0 -25
  201. package/server/contexts/fulfillment/domain/models.py +0 -73
  202. package/server/contexts/fulfillment/infrastructure/__init__.py +0 -13
  203. package/server/contexts/fulfillment/infrastructure/adapters/memory.py +0 -43
  204. package/server/contexts/fulfillment/infrastructure/repository.py +0 -97
  205. package/server/contexts/health/__init__.py +0 -1
  206. package/server/contexts/health/application/__init__.py +0 -3
  207. package/server/contexts/health/application/services.py +0 -2
  208. package/server/contexts/health/contracts/__init__.py +0 -3
  209. package/server/contexts/health/contracts/http/__init__.py +0 -3
  210. package/server/contexts/health/contracts/http/router.py +0 -10
  211. package/server/contexts/inventory/__init__.py +0 -1
  212. package/server/contexts/inventory/application/__init__.py +0 -28
  213. package/server/contexts/inventory/application/ports.py +0 -11
  214. package/server/contexts/inventory/application/services.py +0 -214
  215. package/server/contexts/inventory/contracts/__init__.py +0 -3
  216. package/server/contexts/inventory/contracts/http/__init__.py +0 -3
  217. package/server/contexts/inventory/contracts/http/router.py +0 -82
  218. package/server/contexts/inventory/domain/__init__.py +0 -33
  219. package/server/contexts/inventory/domain/models.py +0 -93
  220. package/server/contexts/inventory/infrastructure/__init__.py +0 -4
  221. package/server/contexts/inventory/infrastructure/adapters/memory.py +0 -24
  222. package/server/contexts/inventory/infrastructure/repository.py +0 -8
  223. package/server/contexts/orders/__init__.py +0 -1
  224. package/server/contexts/orders/application/__init__.py +0 -19
  225. package/server/contexts/orders/application/services.py +0 -127
  226. package/server/contexts/orders/contracts/__init__.py +0 -3
  227. package/server/contexts/orders/contracts/http/__init__.py +0 -3
  228. package/server/contexts/orders/contracts/http/router.py +0 -82
  229. package/server/contexts/orders/domain/__init__.py +0 -29
  230. package/server/contexts/orders/domain/models.py +0 -95
  231. package/server/contexts/orders/infrastructure/__init__.py +0 -7
  232. package/server/contexts/orders/infrastructure/repository.py +0 -104
  233. package/server/contexts/shipping/__init__.py +0 -1
  234. package/server/contexts/shipping/application/__init__.py +0 -13
  235. package/server/contexts/shipping/application/services.py +0 -92
  236. package/server/contexts/shipping/contracts/__init__.py +0 -3
  237. package/server/contexts/shipping/contracts/http/__init__.py +0 -3
  238. package/server/contexts/shipping/contracts/http/router.py +0 -40
  239. package/server/contexts/shipping/domain/__init__.py +0 -19
  240. package/server/contexts/shipping/domain/models.py +0 -48
  241. package/server/contexts/shipping/infrastructure/__init__.py +0 -9
  242. package/server/contexts/shipping/infrastructure/repository.py +0 -50
  243. package/server/contexts/support/__init__.py +0 -1
  244. package/server/contexts/support/application/__init__.py +0 -13
  245. package/server/contexts/support/application/services.py +0 -29
  246. package/server/contexts/support/contracts/__init__.py +0 -3
  247. package/server/contexts/support/contracts/http/__init__.py +0 -3
  248. package/server/contexts/support/contracts/http/router.py +0 -40
  249. package/server/contexts/support/domain/__init__.py +0 -13
  250. package/server/contexts/support/domain/models.py +0 -27
  251. package/server/contexts/support/infrastructure/__init__.py +0 -11
  252. package/server/contexts/support/infrastructure/repository.py +0 -70
  253. package/server/contexts/user/__init__.py +0 -1
  254. package/server/contexts/user/application/__init__.py +0 -3
  255. package/server/contexts/user/application/ports.py +0 -11
  256. package/server/contexts/user/application/services.py +0 -44
  257. package/server/contexts/user/contracts/__init__.py +0 -3
  258. package/server/contexts/user/contracts/http/__init__.py +0 -3
  259. package/server/contexts/user/contracts/http/router.py +0 -26
  260. package/server/contexts/user/domain/__init__.py +0 -3
  261. package/server/contexts/user/domain/models.py +0 -22
  262. package/server/contexts/user/infrastructure/__init__.py +0 -3
  263. package/server/contexts/user/infrastructure/adapters/memory.py +0 -27
  264. package/server/contexts/user/infrastructure/adapters/mongodb.py +0 -41
  265. package/server/contexts/user/infrastructure/adapters/sqlalchemy.py +0 -94
  266. package/server/contexts/user/infrastructure/factory.py +0 -28
  267. package/server/data/README.md +0 -24
  268. package/server/data/bootstrap/alerts.json +0 -38
  269. package/server/data/bootstrap/auth_accounts.json +0 -18
  270. package/server/data/bootstrap/catalog_products.json +0 -179
  271. package/server/data/bootstrap/fulfillment_events.json +0 -5
  272. package/server/data/bootstrap/fulfillment_notes.json +0 -5
  273. package/server/data/bootstrap/fulfillment_tasks.json +0 -50
  274. package/server/data/bootstrap/inventory_levels.json +0 -80
  275. package/server/data/bootstrap/orders.json +0 -62
  276. package/server/data/bootstrap/shipping_shipments.json +0 -50
  277. package/server/data/bootstrap/support_faqs.json +0 -26
  278. package/server/data/bootstrap/users.json +0 -20
  279. package/server/data/bootstrap_loader.py +0 -15
  280. package/server/docker-entrypoint.sh +0 -56
  281. package/server/main.py +0 -3
  282. package/server/pyproject.toml +0 -36
  283. package/server/shared/__init__.py +0 -1
  284. package/server/shared/application/__init__.py +0 -3
  285. package/server/shared/application/health.py +0 -2
  286. package/server/shared/infrastructure/__init__.py +0 -10
  287. package/server/shared/infrastructure/runtime.py +0 -6
  288. package/server/shared/infrastructure/security.py +0 -33
  289. package/server/tests/e2e/test_domain_feature_flows.py +0 -483
  290. package/server/tests/test_health.py +0 -49
  291. package/server/uv.lock +0 -1169
@@ -5,15 +5,86 @@ import process from "node:process";
5
5
  import * as readline from "node:readline";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import {
8
+ DEFAULT_TEMPLATE_OWNER,
8
9
  ensureTargetDir,
9
10
  fetchTemplateRepos,
10
11
  installTemplateRepo,
11
12
  parseArgs,
12
13
  resolveTemplateRepo,
13
- selectTemplateRepo,
14
14
  usage,
15
15
  } from "../lib/scaffold.mjs";
16
16
 
17
+ const DEFAULT_TARGET_DIR = ".";
18
+ const DEFAULT_APP_MODE = "fullstack";
19
+ const DEFAULT_AI_PROVIDERS = ["codex", "claude"];
20
+
21
+ const GITHUB_AUTH_CHOICES = [
22
+ {
23
+ label: "public",
24
+ value: "public",
25
+ description: "Use public template-* repos from say828 without a token",
26
+ },
27
+ {
28
+ label: "env",
29
+ value: "env",
30
+ description: "Use GH_TOKEN, GITHUB_TOKEN, or AGENTIC_GITHUB_TOKEN from the shell",
31
+ },
32
+ {
33
+ label: "pat",
34
+ value: "pat",
35
+ description: "Paste a GitHub PAT for this run only",
36
+ },
37
+ ];
38
+
39
+ const APP_MODE_CHOICES = [
40
+ {
41
+ label: "fullstack",
42
+ value: "fullstack",
43
+ description: "Install dependencies and run frontend parity bootstrap",
44
+ },
45
+ {
46
+ label: "frontend",
47
+ value: "frontend",
48
+ description: "Keep frontend-focused setup with browser install and parity bootstrap",
49
+ },
50
+ {
51
+ label: "backend",
52
+ value: "backend",
53
+ description: "Install workspace dependencies but skip browser install and parity bootstrap",
54
+ },
55
+ ];
56
+
57
+ const AI_PROVIDER_CHOICES = [
58
+ {
59
+ label: "codex",
60
+ value: "codex",
61
+ description: "Keep Codex workspace config in the generated repo",
62
+ },
63
+ {
64
+ label: "claude",
65
+ value: "claude",
66
+ description: "Keep Claude workspace config in the generated repo",
67
+ },
68
+ {
69
+ label: "ollama",
70
+ value: "ollama",
71
+ description: "Record Ollama as a provider in setup metadata",
72
+ },
73
+ ];
74
+
75
+ const CONFIRM_CHOICES = [
76
+ {
77
+ label: "Proceed",
78
+ value: "proceed",
79
+ description: "Run clone, install, and bootstrap now",
80
+ },
81
+ {
82
+ label: "Cancel",
83
+ value: "cancel",
84
+ description: "Stop without writing files",
85
+ },
86
+ ];
87
+
17
88
  function clearMenu(lines) {
18
89
  if (lines <= 0) {
19
90
  return;
@@ -22,29 +93,318 @@ function clearMenu(lines) {
22
93
  readline.clearScreenDown(process.stdout);
23
94
  }
24
95
 
25
- function renderRepoSelect(label, repos, cursor) {
26
- const lines = [`${label} Use ↑/↓ and Enter.`];
27
- for (let index = 0; index < repos.length; index += 1) {
28
- const pointer = index === cursor ? ">" : " ";
29
- const summary = repos[index].description ? ` - ${repos[index].description}` : "";
30
- lines.push(`${pointer} ${repos[index].name}${summary}`);
96
+ function inferProjectName(targetDir) {
97
+ const normalized = (targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR;
98
+ const resolved = path.resolve(process.cwd(), normalized);
99
+ return path.basename(resolved);
100
+ }
101
+
102
+ function directoryHasUserFiles(targetDir) {
103
+ const normalized = (targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR;
104
+ const resolved = path.resolve(process.cwd(), normalized);
105
+ if (!fs.existsSync(resolved)) {
106
+ return false;
107
+ }
108
+
109
+ const entries = fs
110
+ .readdirSync(resolved, { withFileTypes: true })
111
+ .filter((entry) => entry.name !== ".git");
112
+ return entries.length > 0;
113
+ }
114
+
115
+ function normalizeProviders(providers) {
116
+ const normalized = Array.isArray(providers)
117
+ ? providers.map((provider) => provider.trim()).filter(Boolean)
118
+ : [];
119
+ if (normalized.length > 0) {
120
+ return [...new Set(normalized)];
121
+ }
122
+ return [...DEFAULT_AI_PROVIDERS];
123
+ }
124
+
125
+ function uniqueProviders(providers) {
126
+ return Array.isArray(providers)
127
+ ? [...new Set(providers.map((provider) => provider.trim()).filter(Boolean))]
128
+ : [];
129
+ }
130
+
131
+ function hydrateOptions(options) {
132
+ const state = {
133
+ targetDir: (options.targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR,
134
+ projectName: (options.projectName || "").trim(),
135
+ template: (options.template || "").trim(),
136
+ githubAuthMode: (options.githubAuthMode || "public").trim() || "public",
137
+ githubPat: options.githubPat || "",
138
+ appMode: (options.appMode || DEFAULT_APP_MODE).trim() || DEFAULT_APP_MODE,
139
+ aiProviders: normalizeProviders(options.aiProviders),
140
+ force: Boolean(options.force),
141
+ skipBootstrap: Boolean(options.skipBootstrap),
142
+ owner: options.owner || DEFAULT_TEMPLATE_OWNER,
143
+ };
144
+
145
+ if (!state.projectName) {
146
+ state.projectName = inferProjectName(state.targetDir);
147
+ }
148
+
149
+ return state;
150
+ }
151
+
152
+ function applyRuntimeGitHubAuth(state) {
153
+ if (state.githubAuthMode === "pat" && state.githubPat) {
154
+ process.env.AGENTIC_GITHUB_TOKEN = state.githubPat;
155
+ return;
156
+ }
157
+
158
+ if (state.githubAuthMode === "public") {
159
+ delete process.env.AGENTIC_GITHUB_TOKEN;
160
+ }
161
+ }
162
+
163
+ function sanitizeStateForInstall(state) {
164
+ return {
165
+ ...state,
166
+ targetDir: (state.targetDir || DEFAULT_TARGET_DIR).trim() || DEFAULT_TARGET_DIR,
167
+ projectName: (state.projectName || inferProjectName(state.targetDir)).trim(),
168
+ aiProviders: normalizeProviders(state.aiProviders),
169
+ };
170
+ }
171
+
172
+ function buildTemplateChoices(repos) {
173
+ return repos.map((repo) => ({
174
+ label: repo.name,
175
+ value: repo.name,
176
+ description: repo.description || "Public template repo",
177
+ }));
178
+ }
179
+
180
+ function buildSteps(state, repos) {
181
+ const steps = [
182
+ {
183
+ key: "targetDir",
184
+ type: "text",
185
+ label: "Project directory",
186
+ description: "Type the destination directory, then press Enter. Use ←/→ to move between screens.",
187
+ placeholder: DEFAULT_TARGET_DIR,
188
+ },
189
+ {
190
+ key: "projectName",
191
+ type: "text",
192
+ label: "Project name",
193
+ description: "Written into scaffold metadata and Claude workspace nickname.",
194
+ placeholder: inferProjectName(state.targetDir),
195
+ },
196
+ {
197
+ key: "githubAuthMode",
198
+ type: "single",
199
+ label: "GitHub auth mode",
200
+ description: "Choose how this run should access template repos.",
201
+ choices: GITHUB_AUTH_CHOICES,
202
+ },
203
+ ];
204
+
205
+ if (state.githubAuthMode === "pat") {
206
+ steps.push({
207
+ key: "githubPat",
208
+ type: "password",
209
+ label: "GitHub PAT",
210
+ description: "Used only for this run. The token is not written into the generated repo.",
211
+ placeholder: "",
212
+ });
213
+ }
214
+
215
+ steps.push(
216
+ {
217
+ key: "template",
218
+ type: "single",
219
+ label: `Template repo from ${state.owner}`,
220
+ description: "Pick the public template-* repo to scaffold.",
221
+ choices: buildTemplateChoices(repos),
222
+ },
223
+ {
224
+ key: "appMode",
225
+ type: "single",
226
+ label: "App mode",
227
+ description: "Choose how much of the bootstrap flow should run after clone.",
228
+ choices: APP_MODE_CHOICES,
229
+ },
230
+ {
231
+ key: "aiProviders",
232
+ type: "multi",
233
+ label: "AI providers",
234
+ description: "Use Space to toggle. At least one provider must stay selected.",
235
+ choices: AI_PROVIDER_CHOICES,
236
+ },
237
+ );
238
+
239
+ if (!state.force && directoryHasUserFiles(state.targetDir)) {
240
+ steps.push({
241
+ key: "force",
242
+ type: "single",
243
+ label: "Target directory is not empty",
244
+ description: "Choose whether scaffolding may continue in the current directory.",
245
+ choices: [
246
+ {
247
+ label: "Continue",
248
+ value: true,
249
+ description: "Allow scaffolding into the existing directory",
250
+ },
251
+ {
252
+ label: "Cancel",
253
+ value: false,
254
+ description: "Stop without changing files",
255
+ },
256
+ ],
257
+ });
258
+ }
259
+
260
+ steps.push({
261
+ key: "confirm",
262
+ type: "confirm",
263
+ label: "Review and run",
264
+ description: "Confirm every choice before the CLI starts cloning or installing.",
265
+ choices: CONFIRM_CHOICES,
266
+ });
267
+
268
+ return steps;
269
+ }
270
+
271
+ function stepValue(state, step) {
272
+ if (step.key === "targetDir") {
273
+ return state.targetDir || step.placeholder || DEFAULT_TARGET_DIR;
274
+ }
275
+ if (step.key === "projectName") {
276
+ return state.projectName || step.placeholder || inferProjectName(state.targetDir);
277
+ }
278
+ if (step.key === "githubPat") {
279
+ return state.githubPat || "";
280
+ }
281
+ return state[step.key];
282
+ }
283
+
284
+ function renderHeader(step, index, total) {
285
+ return [
286
+ `Agentic Dev Setup ${index + 1}/${total}`,
287
+ step.label,
288
+ step.description,
289
+ "",
290
+ ];
291
+ }
292
+
293
+ function renderTextStep(step, state, buffers, index, total) {
294
+ const lines = renderHeader(step, index, total);
295
+ if (!buffers.has(step.key)) {
296
+ buffers.set(step.key, stepValue(state, step) || "");
297
+ }
298
+
299
+ const rawValue = buffers.get(step.key);
300
+ const displayValue =
301
+ step.type === "password" ? "*".repeat(rawValue.length) : rawValue || step.placeholder || "";
302
+
303
+ lines.push(`Value: ${displayValue}`);
304
+ lines.push("");
305
+ lines.push("Controls: type text, Backspace to edit, Enter or → to continue, ← to go back.");
306
+ process.stdout.write(`${lines.join("\n")}\n`);
307
+ return lines.length;
308
+ }
309
+
310
+ function renderSingleChoiceStep(step, state, cursors, index, total) {
311
+ const lines = renderHeader(step, index, total);
312
+ if (!cursors.has(step.key)) {
313
+ const currentValue = stepValue(state, step);
314
+ const currentIndex = Math.max(
315
+ 0,
316
+ step.choices.findIndex((choice) => choice.value === currentValue),
317
+ );
318
+ cursors.set(step.key, currentIndex >= 0 ? currentIndex : 0);
31
319
  }
320
+
321
+ const cursor = cursors.get(step.key);
322
+ step.choices.forEach((choice, choiceIndex) => {
323
+ const pointer = choiceIndex === cursor ? ">" : " ";
324
+ const suffix = choice.description ? ` - ${choice.description}` : "";
325
+ lines.push(`${pointer} ${choice.label}${suffix}`);
326
+ });
327
+
328
+ lines.push("");
329
+ lines.push("Controls: ↑/↓ to choose, Enter or → to continue, ← to go back.");
32
330
  process.stdout.write(`${lines.join("\n")}\n`);
33
331
  return lines.length;
34
332
  }
35
333
 
36
- function renderChoiceSelect(label, choices, cursor) {
37
- const lines = [`${label} Use ↑/↓ and Enter.`];
38
- for (let index = 0; index < choices.length; index += 1) {
39
- const pointer = index === cursor ? ">" : " ";
40
- const summary = choices[index].description ? ` - ${choices[index].description}` : "";
41
- lines.push(`${pointer} ${choices[index].label}${summary}`);
334
+ function renderMultiChoiceStep(step, state, cursors, index, total) {
335
+ const lines = renderHeader(step, index, total);
336
+ if (!cursors.has(step.key)) {
337
+ cursors.set(step.key, 0);
42
338
  }
339
+
340
+ const selected = new Set(uniqueProviders(stepValue(state, step)));
341
+ const cursor = cursors.get(step.key);
342
+ step.choices.forEach((choice, choiceIndex) => {
343
+ const pointer = choiceIndex === cursor ? ">" : " ";
344
+ const checked = selected.has(choice.value) ? "[x]" : "[ ]";
345
+ const suffix = choice.description ? ` - ${choice.description}` : "";
346
+ lines.push(`${pointer} ${checked} ${choice.label}${suffix}`);
347
+ });
348
+
349
+ lines.push("");
350
+ lines.push("Controls: ↑/↓ to move, Space to toggle, Enter or → to continue, ← to go back.");
43
351
  process.stdout.write(`${lines.join("\n")}\n`);
44
352
  return lines.length;
45
353
  }
46
354
 
47
- async function runArrowMenu(render, onInput) {
355
+ function executionPreview(state, selectedRepo) {
356
+ const lines = [
357
+ `Project directory: ${path.resolve(process.cwd(), state.targetDir)}`,
358
+ `Project name: ${state.projectName}`,
359
+ `GitHub auth mode: ${state.githubAuthMode}`,
360
+ `Template repo: ${selectedRepo?.name || state.template}`,
361
+ `App mode: ${state.appMode}`,
362
+ `AI providers: ${state.aiProviders.join(", ")}`,
363
+ `Allow non-empty directory: ${state.force ? "yes" : "no"}`,
364
+ ];
365
+
366
+ if (state.githubAuthMode === "pat") {
367
+ lines.push(`GitHub PAT supplied: ${state.githubPat ? "yes" : "no"}`);
368
+ }
369
+
370
+ lines.push("Run plan:");
371
+ lines.push(` 1. Clone ${selectedRepo?.name || state.template}`);
372
+ lines.push(" 2. Copy .env.example to .env if needed");
373
+ lines.push(" 3. Run pnpm install");
374
+ if (state.appMode === "backend") {
375
+ lines.push(" 4. Skip browser install and parity bootstrap");
376
+ } else {
377
+ lines.push(" 4. Install Playwright Chromium for the default frontend target");
378
+ lines.push(" 5. Run frontend parity bootstrap");
379
+ }
380
+
381
+ return lines;
382
+ }
383
+
384
+ function renderConfirmStep(step, state, repos, cursors, index, total) {
385
+ const lines = renderHeader(step, index, total);
386
+ const selectedRepo = resolveTemplateRepo(state.template, repos);
387
+ executionPreview(state, selectedRepo).forEach((line) => lines.push(line));
388
+ lines.push("");
389
+
390
+ if (!cursors.has(step.key)) {
391
+ cursors.set(step.key, 0);
392
+ }
393
+
394
+ const cursor = cursors.get(step.key);
395
+ step.choices.forEach((choice, choiceIndex) => {
396
+ const pointer = choiceIndex === cursor ? ">" : " ";
397
+ const suffix = choice.description ? ` - ${choice.description}` : "";
398
+ lines.push(`${pointer} ${choice.label}${suffix}`);
399
+ });
400
+
401
+ lines.push("");
402
+ lines.push("Controls: ↑/↓ to choose, Enter or → to confirm, ← to go back.");
403
+ process.stdout.write(`${lines.join("\n")}\n`);
404
+ return lines.length;
405
+ }
406
+
407
+ async function runInteractiveSession(render, onInput) {
48
408
  const stdin = process.stdin;
49
409
  const stdout = process.stdout;
50
410
  const previousRawMode = typeof stdin.setRawMode === "function" ? stdin.isRaw : undefined;
@@ -90,74 +450,202 @@ async function runArrowMenu(render, onInput) {
90
450
  });
91
451
  }
92
452
 
93
- async function promptForTemplateRepo(rl, repos, owner) {
94
- if (process.stdin.isTTY && process.stdout.isTTY) {
95
- console.log("");
96
- let cursor = 0;
97
- return runArrowMenu(
98
- () => renderRepoSelect(`Public template repos from ${owner}:`, repos, cursor),
99
- (chunk, rerender) => {
100
- if (chunk === "\u0003") {
101
- throw new Error("Prompt cancelled");
102
- }
103
- if (chunk === "\r" || chunk === "\n") {
104
- return repos[cursor].name;
105
- }
106
- if (chunk === "\u001b[A") {
107
- cursor = (cursor - 1 + repos.length) % repos.length;
108
- rerender();
109
- } else if (chunk === "\u001b[B") {
110
- cursor = (cursor + 1) % repos.length;
111
- rerender();
112
- }
113
- return undefined;
114
- },
115
- );
453
+ function validateStepValue(step, state, value) {
454
+ if (step.key === "targetDir") {
455
+ const trimmed = value.trim() || DEFAULT_TARGET_DIR;
456
+ return trimmed;
116
457
  }
117
458
 
118
- console.log("");
119
- console.log(`Public template repos from ${owner}:`);
120
- repos.forEach((repo, index) => {
121
- const summary = repo.description ? ` - ${repo.description}` : "";
122
- console.log(` ${index + 1}. ${repo.name}${summary}`);
123
- });
124
- console.log("");
459
+ if (step.key === "projectName") {
460
+ const trimmed = value.trim();
461
+ if (!trimmed) {
462
+ throw new Error("Project name cannot be empty.");
463
+ }
464
+ return trimmed;
465
+ }
125
466
 
126
- while (true) {
127
- const answer = await rl.question("Select template repo (number or repo name): ");
128
- try {
129
- return selectTemplateRepo(answer, repos).name;
130
- } catch (error) {
131
- console.log(error.message);
467
+ if (step.key === "githubPat") {
468
+ const trimmed = value.trim();
469
+ if (!trimmed) {
470
+ throw new Error("GitHub PAT cannot be empty.");
132
471
  }
472
+ return trimmed;
133
473
  }
474
+
475
+ if (step.key === "aiProviders") {
476
+ const providers = normalizeProviders(value);
477
+ if (providers.length === 0) {
478
+ throw new Error("Select at least one AI provider.");
479
+ }
480
+ return providers;
481
+ }
482
+
483
+ if (step.key === "force" && value === false) {
484
+ throw new Error("Prompt cancelled");
485
+ }
486
+
487
+ return value;
134
488
  }
135
489
 
136
- async function promptForChoice(rl, label, choices) {
137
- if (process.stdin.isTTY && process.stdout.isTTY) {
138
- console.log("");
139
- let cursor = 0;
140
- return runArrowMenu(
141
- () => renderChoiceSelect(label, choices, cursor),
142
- (chunk, rerender) => {
143
- if (chunk === "\u0003") {
144
- throw new Error("Prompt cancelled");
490
+ function setStateValue(state, step, value) {
491
+ if (step.key === "targetDir") {
492
+ const previousInferred = inferProjectName(state.targetDir);
493
+ state.targetDir = value;
494
+ if (!state.projectName || state.projectName === previousInferred) {
495
+ state.projectName = inferProjectName(value);
496
+ }
497
+ return;
498
+ }
499
+
500
+ state[step.key] = value;
501
+ }
502
+
503
+ async function runWizard(initialState, repos) {
504
+ const state = sanitizeStateForInstall(initialState);
505
+ const buffers = new Map();
506
+ const cursors = new Map();
507
+ let stepIndex = 0;
508
+
509
+ return runInteractiveSession(
510
+ () => {
511
+ const steps = buildSteps(state, repos);
512
+ if (stepIndex >= steps.length) {
513
+ stepIndex = steps.length - 1;
514
+ }
515
+ const step = steps[stepIndex];
516
+ process.stdout.write("\n");
517
+ if (step.type === "text" || step.type === "password") {
518
+ return renderTextStep(step, state, buffers, stepIndex, steps.length);
519
+ }
520
+ if (step.type === "multi") {
521
+ return renderMultiChoiceStep(step, state, cursors, stepIndex, steps.length);
522
+ }
523
+ if (step.type === "confirm") {
524
+ return renderConfirmStep(step, state, repos, cursors, stepIndex, steps.length);
525
+ }
526
+ return renderSingleChoiceStep(step, state, cursors, stepIndex, steps.length);
527
+ },
528
+ (chunk, rerender) => {
529
+ const steps = buildSteps(state, repos);
530
+ const step = steps[stepIndex];
531
+
532
+ if (chunk === "\u0003") {
533
+ throw new Error("Prompt cancelled");
534
+ }
535
+
536
+ if (step.type === "text" || step.type === "password") {
537
+ if (!buffers.has(step.key)) {
538
+ buffers.set(step.key, stepValue(state, step) || "");
539
+ }
540
+
541
+ if (chunk === "\u007f") {
542
+ buffers.set(step.key, buffers.get(step.key).slice(0, -1));
543
+ rerender();
544
+ return undefined;
145
545
  }
146
- if (chunk === "\r" || chunk === "\n") {
147
- return choices[cursor].value;
546
+
547
+ if (chunk === "\u001b[D") {
548
+ if (stepIndex > 0) {
549
+ stepIndex -= 1;
550
+ rerender();
551
+ }
552
+ return undefined;
148
553
  }
149
- if (chunk === "\u001b[A") {
150
- cursor = (cursor - 1 + choices.length) % choices.length;
554
+
555
+ if (chunk === "\r" || chunk === "\n" || chunk === "\u001b[C") {
556
+ const nextValue = validateStepValue(step, state, buffers.get(step.key));
557
+ setStateValue(state, step, nextValue);
558
+ const nextSteps = buildSteps(state, repos);
559
+ if (stepIndex < nextSteps.length - 1) {
560
+ stepIndex += 1;
561
+ rerender();
562
+ return undefined;
563
+ }
564
+ return sanitizeStateForInstall(state);
565
+ }
566
+
567
+ if (/^[\x20-\x7e]$/.test(chunk)) {
568
+ buffers.set(step.key, `${buffers.get(step.key)}${chunk}`);
151
569
  rerender();
152
- } else if (chunk === "\u001b[B") {
153
- cursor = (cursor + 1) % choices.length;
570
+ }
571
+ return undefined;
572
+ }
573
+
574
+ if (!cursors.has(step.key)) {
575
+ const currentValue = stepValue(state, step);
576
+ const initialIndex = Math.max(
577
+ 0,
578
+ step.choices.findIndex((choice) => choice.value === currentValue),
579
+ );
580
+ cursors.set(step.key, initialIndex >= 0 ? initialIndex : 0);
581
+ }
582
+
583
+ if (chunk === "\u001b[D") {
584
+ if (stepIndex > 0) {
585
+ stepIndex -= 1;
154
586
  rerender();
155
587
  }
156
588
  return undefined;
157
- },
158
- );
159
- }
589
+ }
590
+
591
+ if (chunk === "\u001b[A") {
592
+ const nextCursor = (cursors.get(step.key) - 1 + step.choices.length) % step.choices.length;
593
+ cursors.set(step.key, nextCursor);
594
+ rerender();
595
+ return undefined;
596
+ }
597
+
598
+ if (chunk === "\u001b[B") {
599
+ const nextCursor = (cursors.get(step.key) + 1) % step.choices.length;
600
+ cursors.set(step.key, nextCursor);
601
+ rerender();
602
+ return undefined;
603
+ }
604
+
605
+ if (step.type === "multi" && chunk === " ") {
606
+ const selected = new Set(uniqueProviders(state.aiProviders));
607
+ const currentChoice = step.choices[cursors.get(step.key)];
608
+ if (selected.has(currentChoice.value)) {
609
+ selected.delete(currentChoice.value);
610
+ } else {
611
+ selected.add(currentChoice.value);
612
+ }
613
+ state.aiProviders = [...selected];
614
+ rerender();
615
+ return undefined;
616
+ }
617
+
618
+ if (chunk === "\r" || chunk === "\n" || chunk === "\u001b[C") {
619
+ if (step.type === "multi") {
620
+ const providers = validateStepValue(step, state, state.aiProviders);
621
+ setStateValue(state, step, providers);
622
+ } else {
623
+ const selectedChoice = step.choices[cursors.get(step.key)];
624
+ if (step.type === "confirm") {
625
+ if (selectedChoice.value === "cancel") {
626
+ throw new Error("Prompt cancelled");
627
+ }
628
+ return sanitizeStateForInstall(state);
629
+ }
630
+ const nextValue = validateStepValue(step, state, selectedChoice.value);
631
+ setStateValue(state, step, nextValue);
632
+ }
160
633
 
634
+ const nextSteps = buildSteps(state, repos);
635
+ if (stepIndex < nextSteps.length - 1) {
636
+ stepIndex += 1;
637
+ rerender();
638
+ return undefined;
639
+ }
640
+ return sanitizeStateForInstall(state);
641
+ }
642
+
643
+ return undefined;
644
+ },
645
+ );
646
+ }
647
+
648
+ async function promptForChoice(rl, label, choices) {
161
649
  console.log("");
162
650
  console.log(label);
163
651
  choices.forEach((choice, index) => {
@@ -174,6 +662,7 @@ async function promptForChoice(rl, label, choices) {
174
662
  return choices[index].value;
175
663
  }
176
664
  }
665
+
177
666
  const exact = choices.find((choice) => choice.label === answer || choice.value === answer);
178
667
  if (exact) {
179
668
  return exact.value;
@@ -182,81 +671,94 @@ async function promptForChoice(rl, label, choices) {
182
671
  }
183
672
  }
184
673
 
185
- function directoryHasUserFiles(targetDir) {
186
- const resolved = path.resolve(process.cwd(), targetDir);
187
- if (!fs.existsSync(resolved)) {
188
- return false;
189
- }
674
+ async function promptForMultiChoice(rl, label, choices, defaults) {
675
+ console.log("");
676
+ console.log(label);
677
+ console.log("Enter one or more labels separated by commas.");
678
+ choices.forEach((choice, index) => {
679
+ const summary = choice.description ? ` - ${choice.description}` : "";
680
+ console.log(` ${index + 1}. ${choice.label}${summary}`);
681
+ });
682
+ console.log("");
190
683
 
191
- const entries = fs
192
- .readdirSync(resolved, { withFileTypes: true })
193
- .filter((entry) => entry.name !== ".git");
194
- return entries.length > 0;
684
+ while (true) {
685
+ const answer = (await rl.question(`Providers [${defaults.join(",")}]: `)).trim();
686
+ const selected = normalizeProviders(
687
+ answer
688
+ ? answer.split(",").map((entry) => entry.trim())
689
+ : defaults,
690
+ );
691
+ const invalid = selected.filter(
692
+ (provider) => !choices.some((choice) => choice.value === provider),
693
+ );
694
+ if (invalid.length === 0 && selected.length > 0) {
695
+ return selected;
696
+ }
697
+ console.log(`Invalid providers: ${invalid.join(", ")}`);
698
+ }
195
699
  }
196
700
 
197
- function printPlannedRun(options) {
701
+ function printFallbackPlan(state, repos) {
702
+ const selectedRepo = resolveTemplateRepo(state.template, repos);
198
703
  console.log("");
199
- console.log("Plan:");
200
- console.log(` Project directory: ${path.resolve(process.cwd(), options.targetDir)}`);
201
- console.log(` Template repo: ${options.template}`);
202
- console.log(` GitHub owner: ${options.owner}`);
203
- console.log(` Allow non-empty directory: ${options.force ? "yes" : "no"}`);
204
- console.log(` Run install/bootstrap: ${options.skipBootstrap ? "no" : "yes"}`);
704
+ executionPreview(state, selectedRepo).forEach((line) => console.log(line));
205
705
  }
206
706
 
207
- async function promptForMissing(options, repos) {
208
- if (options.yes && (!options.targetDir || !options.template)) {
209
- throw new Error("`--yes` requires both target directory and template repo.");
210
- }
211
-
212
- if (options.targetDir && options.template) {
213
- return options;
214
- }
215
-
707
+ async function promptLinearly(initialState, repos) {
708
+ const state = sanitizeStateForInstall(initialState);
216
709
  const rl = createInterface({
217
710
  input: process.stdin,
218
711
  output: process.stdout,
219
712
  });
220
713
 
221
714
  try {
222
- while (!options.targetDir) {
223
- const answer = await rl.question("Project directory: ");
224
- options.targetDir = answer.trim();
225
- if (!options.targetDir) {
226
- console.log("Project directory cannot be empty.");
715
+ const targetDirAnswer = await rl.question(`Project directory [${state.targetDir}]: `);
716
+ state.targetDir = targetDirAnswer.trim() || state.targetDir;
717
+
718
+ const defaultProjectName = inferProjectName(state.targetDir);
719
+ const projectNameAnswer = await rl.question(`Project name [${state.projectName || defaultProjectName}]: `);
720
+ state.projectName = projectNameAnswer.trim() || state.projectName || defaultProjectName;
721
+
722
+ state.githubAuthMode = await promptForChoice(rl, "GitHub auth mode", GITHUB_AUTH_CHOICES);
723
+ if (state.githubAuthMode === "pat") {
724
+ state.githubPat = (await rl.question("GitHub PAT: ")).trim();
725
+ if (!state.githubPat) {
726
+ throw new Error("GitHub PAT cannot be empty.");
227
727
  }
228
728
  }
229
729
 
230
- if (!options.template) {
231
- options.template = await promptForTemplateRepo(rl, repos, options.owner);
232
- }
233
-
234
- if (!options.force && directoryHasUserFiles(options.targetDir)) {
235
- const allowOverwrite = await promptForChoice(rl, "Target directory is not empty.", [
236
- { label: "Continue", value: true, description: "Scaffold into the existing directory" },
237
- { label: "Cancel", value: false, description: "Stop without changing files" },
730
+ state.template = await promptForChoice(rl, `Template repo from ${state.owner}`, buildTemplateChoices(repos));
731
+ state.appMode = await promptForChoice(rl, "App mode", APP_MODE_CHOICES);
732
+ state.aiProviders = await promptForMultiChoice(rl, "AI providers", AI_PROVIDER_CHOICES, state.aiProviders);
733
+
734
+ if (!state.force && directoryHasUserFiles(state.targetDir)) {
735
+ state.force = await promptForChoice(rl, "Target directory is not empty", [
736
+ {
737
+ label: "Continue",
738
+ value: true,
739
+ description: "Allow scaffolding into the existing directory",
740
+ },
741
+ {
742
+ label: "Cancel",
743
+ value: false,
744
+ description: "Stop without changing files",
745
+ },
238
746
  ]);
239
- if (!allowOverwrite) {
747
+ if (!state.force) {
240
748
  throw new Error("Prompt cancelled");
241
749
  }
242
- options.force = true;
243
750
  }
244
751
 
245
- if (!options.yes) {
246
- printPlannedRun(options);
247
- const proceed = await promptForChoice(rl, "Run scaffold now?", [
248
- { label: "Proceed", value: true, description: "Clone, install, and bootstrap now" },
249
- { label: "Cancel", value: false, description: "Stop before running commands" },
250
- ]);
251
- if (!proceed) {
252
- throw new Error("Prompt cancelled");
253
- }
752
+ printFallbackPlan(state, repos);
753
+ const confirmation = await promptForChoice(rl, "Run scaffold now?", CONFIRM_CHOICES);
754
+ if (confirmation !== "proceed") {
755
+ throw new Error("Prompt cancelled");
254
756
  }
255
757
  } finally {
256
758
  rl.close();
257
759
  }
258
760
 
259
- return options;
761
+ return sanitizeStateForInstall(state);
260
762
  }
261
763
 
262
764
  function printSuccess(result) {
@@ -277,6 +779,25 @@ function printSuccess(result) {
277
779
  }
278
780
  }
279
781
 
782
+ function validateNonInteractiveState(state) {
783
+ if (!state.targetDir) {
784
+ throw new Error("`--yes` requires a target directory.");
785
+ }
786
+ if (!state.template) {
787
+ throw new Error("`--yes` requires a template repo.");
788
+ }
789
+ if (!state.projectName) {
790
+ state.projectName = inferProjectName(state.targetDir);
791
+ }
792
+ if (state.githubAuthMode === "pat" && !state.githubPat) {
793
+ throw new Error("`--github-auth pat` requires `--github-pat`.");
794
+ }
795
+ if (!Array.isArray(state.aiProviders) || state.aiProviders.length === 0) {
796
+ state.aiProviders = [...DEFAULT_AI_PROVIDERS];
797
+ }
798
+ return state;
799
+ }
800
+
280
801
  async function main() {
281
802
  let options;
282
803
  try {
@@ -308,18 +829,29 @@ async function main() {
308
829
  }
309
830
 
310
831
  try {
311
- const repos = await fetchTemplateRepos({ owner: options.owner });
832
+ const state = hydrateOptions(options);
833
+ applyRuntimeGitHubAuth(state);
834
+ const repos = await fetchTemplateRepos({ owner: state.owner });
312
835
  if (repos.length === 0) {
313
- throw new Error(`No public template-* repos found for ${options.owner}.`);
836
+ throw new Error(`No public template-* repos found for ${state.owner}.`);
314
837
  }
315
838
 
316
- options = await promptForMissing(options, repos);
317
- const selectedRepo = resolveTemplateRepo(options.template, repos);
318
- const destinationRoot = ensureTargetDir(options.targetDir, { force: options.force });
839
+ const resolvedState = options.yes
840
+ ? validateNonInteractiveState(sanitizeStateForInstall(state))
841
+ : process.stdin.isTTY && process.stdout.isTTY
842
+ ? await runWizard(state, repos)
843
+ : await promptLinearly(state, repos);
844
+
845
+ applyRuntimeGitHubAuth(resolvedState);
846
+ const selectedRepo = resolveTemplateRepo(resolvedState.template, repos);
847
+ const destinationRoot = ensureTargetDir(resolvedState.targetDir, {
848
+ force: resolvedState.force,
849
+ });
319
850
  const result = installTemplateRepo({
320
851
  destinationRoot: path.resolve(destinationRoot),
321
852
  templateRepo: selectedRepo,
322
- skipBootstrap: options.skipBootstrap,
853
+ setupSelections: resolvedState,
854
+ skipBootstrap: resolvedState.skipBootstrap,
323
855
  });
324
856
  printSuccess(result);
325
857
  } catch (error) {