codeply-cli 0.5.1 → 0.5.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.
Files changed (387) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +65 -63
  3. package/bin/codeply.js +1 -1
  4. package/lib/agent.mjs +133 -24
  5. package/lib/ai.js +268 -9
  6. package/lib/ai.mjs +6 -6
  7. package/lib/apply-patch.js +217 -217
  8. package/lib/arity-table.json +138 -138
  9. package/lib/arity.js +70 -70
  10. package/lib/auth.mjs +6 -6
  11. package/lib/bots.js +82 -17
  12. package/lib/chatgpt.js +282 -0
  13. package/lib/cloud-browser.mjs +143 -143
  14. package/lib/cloud.mjs +1250 -1144
  15. package/lib/commands.js +103 -103
  16. package/lib/config.js +83 -3
  17. package/lib/design-library/assemble-artifact.mjs +21 -21
  18. package/lib/design-library/cleanup.mjs +45 -45
  19. package/lib/design-library/index.json +22379 -22379
  20. package/lib/design-library/make-table-data.mjs +39 -39
  21. package/lib/diagnostics.js +192 -192
  22. package/lib/edit-engine.js +364 -364
  23. package/lib/github-agent.mjs +311 -311
  24. package/lib/history.js +75 -75
  25. package/lib/mail-watch.js +504 -0
  26. package/lib/mcp.js +258 -258
  27. package/lib/model-sync.js +114 -0
  28. package/lib/native-tools.mjs +16 -1
  29. package/lib/oauth-connectors.js +146 -24
  30. package/lib/permissions.js +109 -109
  31. package/lib/plugins.js +353 -353
  32. package/lib/publish.js +916 -0
  33. package/lib/reach.js +67 -0
  34. package/lib/server.mjs +457 -457
  35. package/lib/session-db.js +220 -220
  36. package/lib/share.js +168 -168
  37. package/lib/skills.js +2 -0
  38. package/lib/snapshot.js +146 -146
  39. package/lib/telemetry.js +102 -0
  40. package/lib/terminal-hints.js +154 -154
  41. package/lib/tools.mjs +316 -14
  42. package/lib/typecheck-worker.js +147 -147
  43. package/lib/web-tools.js +209 -158
  44. package/package.json +49 -49
  45. package/skills/SOURCE.md +58 -58
  46. package/skills/accessibility/SKILL.md +147 -147
  47. package/skills/agent-architecture-audit/SKILL.md +257 -257
  48. package/skills/agent-eval/SKILL.md +147 -147
  49. package/skills/agent-harness-construction/SKILL.md +74 -74
  50. package/skills/agent-introspection-debugging/SKILL.md +154 -154
  51. package/skills/agent-payment-x402/SKILL.md +225 -225
  52. package/skills/agent-self-evaluation/SKILL.md +182 -182
  53. package/skills/agent-self-evaluation/references/evaluation-criteria.md +71 -71
  54. package/skills/agent-self-evaluation/references/hook-integration.md +64 -64
  55. package/skills/agent-sort/SKILL.md +216 -216
  56. package/skills/agentic-engineering/SKILL.md +64 -64
  57. package/skills/agentic-os/SKILL.md +388 -388
  58. package/skills/ai-first-engineering/SKILL.md +52 -52
  59. package/skills/ai-regression-testing/SKILL.md +386 -386
  60. package/skills/android-clean-architecture/SKILL.md +340 -340
  61. package/skills/angular-developer/SKILL.md +155 -155
  62. package/skills/angular-developer/references/angular-animations.md +160 -160
  63. package/skills/angular-developer/references/angular-aria.md +410 -410
  64. package/skills/angular-developer/references/cli.md +86 -86
  65. package/skills/angular-developer/references/component-harnesses.md +59 -59
  66. package/skills/angular-developer/references/component-styling.md +91 -91
  67. package/skills/angular-developer/references/components.md +117 -117
  68. package/skills/angular-developer/references/creating-services.md +97 -97
  69. package/skills/angular-developer/references/data-resolvers.md +69 -69
  70. package/skills/angular-developer/references/define-routes.md +67 -67
  71. package/skills/angular-developer/references/defining-providers.md +72 -72
  72. package/skills/angular-developer/references/di-fundamentals.md +120 -120
  73. package/skills/angular-developer/references/e2e-testing.md +56 -56
  74. package/skills/angular-developer/references/effects.md +83 -83
  75. package/skills/angular-developer/references/hierarchical-injectors.md +43 -43
  76. package/skills/angular-developer/references/host-elements.md +80 -80
  77. package/skills/angular-developer/references/injection-context.md +63 -63
  78. package/skills/angular-developer/references/inputs.md +101 -101
  79. package/skills/angular-developer/references/linked-signal.md +59 -59
  80. package/skills/angular-developer/references/loading-strategies.md +61 -61
  81. package/skills/angular-developer/references/mcp.md +108 -108
  82. package/skills/angular-developer/references/navigate-to-routes.md +69 -69
  83. package/skills/angular-developer/references/outputs.md +86 -86
  84. package/skills/angular-developer/references/reactive-forms.md +122 -122
  85. package/skills/angular-developer/references/rendering-strategies.md +44 -44
  86. package/skills/angular-developer/references/resource.md +77 -77
  87. package/skills/angular-developer/references/route-animations.md +56 -56
  88. package/skills/angular-developer/references/route-guards.md +52 -52
  89. package/skills/angular-developer/references/router-lifecycle.md +45 -45
  90. package/skills/angular-developer/references/router-testing.md +87 -87
  91. package/skills/angular-developer/references/show-routes-with-outlets.md +68 -68
  92. package/skills/angular-developer/references/signal-forms.md +795 -795
  93. package/skills/angular-developer/references/signals-overview.md +94 -94
  94. package/skills/angular-developer/references/tailwind-css.md +69 -69
  95. package/skills/angular-developer/references/template-driven-forms.md +114 -114
  96. package/skills/angular-developer/references/testing-fundamentals.md +65 -65
  97. package/skills/api-connector-builder/SKILL.md +121 -121
  98. package/skills/api-design/SKILL.md +524 -524
  99. package/skills/architecture-decision-records/SKILL.md +180 -180
  100. package/skills/article-writing/SKILL.md +80 -80
  101. package/skills/automation-audit-ops/SKILL.md +143 -143
  102. package/skills/autonomous-agent-harness/SKILL.md +274 -274
  103. package/skills/autonomous-loops/SKILL.md +611 -611
  104. package/skills/backend-patterns/SKILL.md +562 -562
  105. package/skills/benchmark/SKILL.md +95 -95
  106. package/skills/benchmark-methodology/SKILL.md +191 -191
  107. package/skills/benchmark-optimization-loop/SKILL.md +71 -71
  108. package/skills/blender-motion-state-inspection/SKILL.md +165 -165
  109. package/skills/blueprint/SKILL.md +106 -106
  110. package/skills/brand-discovery/SKILL.md +145 -145
  111. package/skills/brand-discovery/references/10_purpose-why.md +40 -40
  112. package/skills/brand-discovery/references/20_positioning.md +44 -44
  113. package/skills/brand-discovery/references/30_audience-niche.md +52 -52
  114. package/skills/brand-discovery/references/40_personality-archetype.md +57 -57
  115. package/skills/brand-discovery/references/50_voice-tone.md +59 -59
  116. package/skills/brand-discovery/references/60_narrative-story.md +50 -50
  117. package/skills/brand-discovery/references/70_founder-tension.md +49 -49
  118. package/skills/brand-discovery/references/90_SYNTHESIS.md +133 -133
  119. package/skills/brand-voice/SKILL.md +98 -98
  120. package/skills/brand-voice/references/voice-profile-schema.md +55 -55
  121. package/skills/browser-qa/SKILL.md +105 -105
  122. package/skills/bun-runtime/SKILL.md +85 -85
  123. package/skills/canary-watch/SKILL.md +108 -108
  124. package/skills/carrier-relationship-management/SKILL.md +212 -212
  125. package/skills/cisco-ios-patterns/SKILL.md +164 -164
  126. package/skills/ck/SKILL.md +148 -148
  127. package/skills/claude-devfleet/SKILL.md +112 -112
  128. package/skills/click-path-audit/SKILL.md +245 -245
  129. package/skills/clickhouse-io/SKILL.md +445 -445
  130. package/skills/code-tour/SKILL.md +254 -254
  131. package/skills/codebase-onboarding/SKILL.md +234 -234
  132. package/skills/codehealth-mcp/SKILL.md +167 -167
  133. package/skills/coding-standards/SKILL.md +551 -551
  134. package/skills/competitive-platform-analysis/SKILL.md +214 -214
  135. package/skills/competitive-report-structure/SKILL.md +162 -162
  136. package/skills/compose-multiplatform-patterns/SKILL.md +300 -300
  137. package/skills/config-gc/SKILL.md +120 -120
  138. package/skills/configure-ecc/SKILL.md +385 -385
  139. package/skills/connections-optimizer/SKILL.md +190 -190
  140. package/skills/content-engine/SKILL.md +132 -132
  141. package/skills/content-hash-cache-pattern/SKILL.md +162 -162
  142. package/skills/context-budget/SKILL.md +136 -136
  143. package/skills/continuous-agent-loop/SKILL.md +46 -46
  144. package/skills/continuous-learning/SKILL.md +132 -132
  145. package/skills/continuous-learning-v2/SKILL.md +361 -361
  146. package/skills/contract-first/SKILL.md +287 -287
  147. package/skills/cost-aware-llm-pipeline/SKILL.md +184 -184
  148. package/skills/cost-tracking/SKILL.md +97 -97
  149. package/skills/council/SKILL.md +204 -204
  150. package/skills/cpp-coding-standards/SKILL.md +724 -724
  151. package/skills/cpp-testing/SKILL.md +325 -325
  152. package/skills/crosspost/SKILL.md +112 -112
  153. package/skills/csharp-testing/SKILL.md +322 -322
  154. package/skills/customer-billing-ops/SKILL.md +141 -141
  155. package/skills/customs-trade-compliance/SKILL.md +263 -263
  156. package/skills/dart-flutter-patterns/SKILL.md +564 -564
  157. package/skills/dashboard-builder/SKILL.md +109 -109
  158. package/skills/data-scraper-agent/SKILL.md +765 -765
  159. package/skills/data-throughput-accelerator/SKILL.md +74 -74
  160. package/skills/database-migrations/SKILL.md +430 -430
  161. package/skills/deep-research/SKILL.md +160 -160
  162. package/skills/defi-amm-security/SKILL.md +167 -167
  163. package/skills/delivery-gate/SKILL.md +126 -126
  164. package/skills/deployment-patterns/SKILL.md +428 -428
  165. package/skills/design-system/SKILL.md +83 -83
  166. package/skills/django-celery/SKILL.md +458 -458
  167. package/skills/django-patterns/SKILL.md +735 -735
  168. package/skills/django-security/SKILL.md +644 -644
  169. package/skills/django-tdd/SKILL.md +730 -730
  170. package/skills/django-verification/SKILL.md +470 -470
  171. package/skills/dmux-workflows/SKILL.md +192 -192
  172. package/skills/docker-patterns/SKILL.md +365 -365
  173. package/skills/documentation-lookup/SKILL.md +91 -91
  174. package/skills/dotnet-patterns/SKILL.md +322 -322
  175. package/skills/dynamic-workflow-mode/SKILL.md +124 -124
  176. package/skills/e2e-testing/SKILL.md +327 -327
  177. package/skills/ecc-guide/SKILL.md +190 -190
  178. package/skills/ecc-recipes/SKILL.md +149 -149
  179. package/skills/ecc-tools-cost-audit/SKILL.md +161 -161
  180. package/skills/email-ops/SKILL.md +122 -122
  181. package/skills/energy-procurement/SKILL.md +228 -228
  182. package/skills/enterprise-agent-ops/SKILL.md +51 -51
  183. package/skills/error-handling/SKILL.md +377 -377
  184. package/skills/eval-harness/SKILL.md +271 -271
  185. package/skills/evm-token-decimals/SKILL.md +131 -131
  186. package/skills/exa-search/SKILL.md +108 -108
  187. package/skills/fal-ai-media/SKILL.md +289 -289
  188. package/skills/fastapi-patterns/SKILL.md +514 -514
  189. package/skills/finance-billing-ops/SKILL.md +128 -128
  190. package/skills/flox-environments/SKILL.md +497 -497
  191. package/skills/flutter-dart-code-review/SKILL.md +436 -436
  192. package/skills/foundation-models-on-device/SKILL.md +243 -243
  193. package/skills/frontend-a11y/SKILL.md +446 -446
  194. package/skills/frontend-design-direction/SKILL.md +93 -93
  195. package/skills/frontend-patterns/SKILL.md +657 -657
  196. package/skills/frontend-slides/SKILL.md +185 -185
  197. package/skills/fsharp-testing/SKILL.md +281 -281
  198. package/skills/gan-style-harness/SKILL.md +279 -279
  199. package/skills/gateguard/SKILL.md +133 -133
  200. package/skills/generating-python-installer/SKILL.md +820 -820
  201. package/skills/git-workflow/SKILL.md +716 -716
  202. package/skills/github-ops/SKILL.md +145 -145
  203. package/skills/golang-patterns/SKILL.md +676 -676
  204. package/skills/golang-testing/SKILL.md +721 -721
  205. package/skills/google-workspace-ops/SKILL.md +96 -96
  206. package/skills/growth-log/SKILL.md +128 -128
  207. package/skills/healthcare-cdss-patterns/SKILL.md +246 -246
  208. package/skills/healthcare-emr-patterns/SKILL.md +160 -160
  209. package/skills/healthcare-eval-harness/SKILL.md +208 -208
  210. package/skills/healthcare-phi-compliance/SKILL.md +146 -146
  211. package/skills/hermes-imports/SKILL.md +89 -89
  212. package/skills/hexagonal-architecture/SKILL.md +277 -277
  213. package/skills/hipaa-compliance/SKILL.md +79 -79
  214. package/skills/homelab-network-readiness/SKILL.md +170 -170
  215. package/skills/homelab-network-setup/SKILL.md +130 -130
  216. package/skills/homelab-pihole-dns/SKILL.md +275 -275
  217. package/skills/homelab-vlan-segmentation/SKILL.md +312 -312
  218. package/skills/homelab-wireguard-vpn/SKILL.md +306 -306
  219. package/skills/hookify-rules/SKILL.md +128 -128
  220. package/skills/inherit-legacy-style/SKILL.md +157 -157
  221. package/skills/intent-driven-development/SKILL.md +360 -360
  222. package/skills/inventory-demand-planning/SKILL.md +247 -247
  223. package/skills/investor-materials/SKILL.md +97 -97
  224. package/skills/investor-outreach/SKILL.md +92 -92
  225. package/skills/ios-icon-gen/SKILL.md +158 -158
  226. package/skills/iterative-retrieval/SKILL.md +212 -212
  227. package/skills/ito-basket-compare/SKILL.md +64 -64
  228. package/skills/ito-compute/SKILL.md +129 -129
  229. package/skills/ito-data-atlas-agent/SKILL.md +64 -64
  230. package/skills/ito-market-intelligence/SKILL.md +61 -61
  231. package/skills/ito-trade-planner/SKILL.md +68 -68
  232. package/skills/java-coding-standards/SKILL.md +384 -384
  233. package/skills/jira-integration/SKILL.md +303 -303
  234. package/skills/jpa-patterns/SKILL.md +152 -152
  235. package/skills/knowledge-ops/SKILL.md +155 -155
  236. package/skills/kotlin-coroutines-flows/SKILL.md +285 -285
  237. package/skills/kotlin-exposed-patterns/SKILL.md +720 -720
  238. package/skills/kotlin-ktor-patterns/SKILL.md +690 -690
  239. package/skills/kotlin-patterns/SKILL.md +712 -712
  240. package/skills/kotlin-testing/SKILL.md +825 -825
  241. package/skills/kubernetes-patterns/SKILL.md +756 -756
  242. package/skills/laravel-patterns/SKILL.md +416 -416
  243. package/skills/laravel-plugin-discovery/SKILL.md +230 -230
  244. package/skills/laravel-security/SKILL.md +948 -948
  245. package/skills/laravel-tdd/SKILL.md +675 -675
  246. package/skills/laravel-verification/SKILL.md +180 -180
  247. package/skills/latency-critical-systems/SKILL.md +75 -75
  248. package/skills/lead-intelligence/SKILL.md +322 -322
  249. package/skills/liquid-glass-design/SKILL.md +279 -279
  250. package/skills/llm-trading-agent-security/SKILL.md +147 -147
  251. package/skills/logistics-exception-management/SKILL.md +222 -222
  252. package/skills/loop-design-check/SKILL.md +143 -143
  253. package/skills/mailtrap-email-integration/SKILL.md +77 -77
  254. package/skills/make-interfaces-feel-better/SKILL.md +152 -152
  255. package/skills/manim-video/SKILL.md +90 -90
  256. package/skills/market-research/SKILL.md +76 -76
  257. package/skills/marketing-campaign/SKILL.md +114 -114
  258. package/skills/mcp-server-patterns/SKILL.md +70 -70
  259. package/skills/messages-ops/SKILL.md +105 -105
  260. package/skills/ml-adoption-playbook/SKILL.md +57 -57
  261. package/skills/mle-workflow/SKILL.md +348 -348
  262. package/skills/motion-advanced/SKILL.md +596 -596
  263. package/skills/motion-foundations/SKILL.md +299 -299
  264. package/skills/motion-patterns/SKILL.md +434 -434
  265. package/skills/motion-ui/SKILL.md +576 -576
  266. package/skills/mysql-patterns/SKILL.md +413 -413
  267. package/skills/nanoclaw-repl/SKILL.md +34 -34
  268. package/skills/nestjs-patterns/SKILL.md +231 -231
  269. package/skills/netmiko-ssh-automation/SKILL.md +174 -174
  270. package/skills/network-bgp-diagnostics/SKILL.md +168 -168
  271. package/skills/network-config-validation/SKILL.md +211 -211
  272. package/skills/network-interface-health/SKILL.md +153 -153
  273. package/skills/nextjs-turbopack/SKILL.md +58 -58
  274. package/skills/nodejs-keccak256/SKILL.md +103 -103
  275. package/skills/nutrient-document-processing/SKILL.md +168 -168
  276. package/skills/nuxt4-patterns/SKILL.md +101 -101
  277. package/skills/openclaw-persona-forge/SKILL.md +289 -289
  278. package/skills/openclaw-persona-forge/references/avatar-style.md +124 -124
  279. package/skills/openclaw-persona-forge/references/boundary-rules.md +53 -53
  280. package/skills/openclaw-persona-forge/references/error-handling.md +53 -53
  281. package/skills/openclaw-persona-forge/references/identity-tension.md +48 -48
  282. package/skills/openclaw-persona-forge/references/naming-system.md +39 -39
  283. package/skills/openclaw-persona-forge/references/output-template.md +166 -166
  284. package/skills/opensource-pipeline/SKILL.md +256 -256
  285. package/skills/orch-add-feature/SKILL.md +45 -45
  286. package/skills/orch-build-mvp/SKILL.md +49 -49
  287. package/skills/orch-change-feature/SKILL.md +43 -43
  288. package/skills/orch-fix-defect/SKILL.md +43 -43
  289. package/skills/orch-pipeline/SKILL.md +121 -121
  290. package/skills/orch-refine-code/SKILL.md +44 -44
  291. package/skills/parallel-execution-optimizer/SKILL.md +74 -74
  292. package/skills/perl-patterns/SKILL.md +505 -505
  293. package/skills/perl-security/SKILL.md +504 -504
  294. package/skills/perl-testing/SKILL.md +476 -476
  295. package/skills/plan-canvas/SKILL.md +153 -153
  296. package/skills/plan-orchestrate/SKILL.md +263 -263
  297. package/skills/plankton-code-quality/SKILL.md +237 -237
  298. package/skills/postgres-patterns/SKILL.md +148 -148
  299. package/skills/prediction-market-oracle-research/SKILL.md +64 -64
  300. package/skills/prediction-market-risk-review/SKILL.md +61 -61
  301. package/skills/premium-web-design/SKILL.md +154 -154
  302. package/skills/prisma-patterns/SKILL.md +401 -401
  303. package/skills/product-capability/SKILL.md +142 -142
  304. package/skills/product-lens/SKILL.md +93 -93
  305. package/skills/production-audit/SKILL.md +207 -207
  306. package/skills/production-scheduling/SKILL.md +238 -238
  307. package/skills/project-flow-ops/SKILL.md +112 -112
  308. package/skills/prompt-optimizer/SKILL.md +398 -398
  309. package/skills/publish-website/SKILL.md +76 -0
  310. package/skills/python-patterns/SKILL.md +751 -751
  311. package/skills/python-testing/SKILL.md +817 -817
  312. package/skills/pytorch-patterns/SKILL.md +397 -397
  313. package/skills/quality-nonconformance/SKILL.md +260 -260
  314. package/skills/quarkus-patterns/SKILL.md +723 -723
  315. package/skills/quarkus-security/SKILL.md +468 -468
  316. package/skills/quarkus-tdd/SKILL.md +812 -812
  317. package/skills/quarkus-verification/SKILL.md +480 -480
  318. package/skills/ralphinho-rfc-pipeline/SKILL.md +68 -68
  319. package/skills/react-native-patterns/SKILL.md +326 -326
  320. package/skills/react-patterns/SKILL.md +342 -342
  321. package/skills/react-performance/SKILL.md +575 -575
  322. package/skills/react-testing/SKILL.md +424 -424
  323. package/skills/recsys-pipeline-architect/SKILL.md +115 -115
  324. package/skills/recursive-decision-ledger/SKILL.md +81 -81
  325. package/skills/redis-patterns/SKILL.md +404 -404
  326. package/skills/regex-vs-llm-structured-text/SKILL.md +221 -221
  327. package/skills/remotion-video-creation/SKILL.md +43 -43
  328. package/skills/repo-scan/SKILL.md +79 -79
  329. package/skills/research-ops/SKILL.md +113 -113
  330. package/skills/returns-reverse-logistics/SKILL.md +240 -240
  331. package/skills/rules-distill/SKILL.md +265 -265
  332. package/skills/rust-patterns/SKILL.md +500 -500
  333. package/skills/rust-testing/SKILL.md +501 -501
  334. package/skills/safety-guard/SKILL.md +76 -76
  335. package/skills/santa-method/SKILL.md +307 -307
  336. package/skills/scientific-db-pubmed-database/SKILL.md +176 -176
  337. package/skills/scientific-db-uspto-database/SKILL.md +178 -178
  338. package/skills/scientific-pkg-gget/SKILL.md +167 -167
  339. package/skills/scientific-thinking-literature-review/SKILL.md +193 -193
  340. package/skills/scientific-thinking-scholar-evaluation/SKILL.md +161 -161
  341. package/skills/search-first/SKILL.md +183 -183
  342. package/skills/security-bounty-hunter/SKILL.md +100 -100
  343. package/skills/security-review/SKILL.md +504 -504
  344. package/skills/security-scan/SKILL.md +166 -166
  345. package/skills/seo/SKILL.md +155 -155
  346. package/skills/skill-comply/SKILL.md +59 -59
  347. package/skills/skill-scout/SKILL.md +141 -141
  348. package/skills/skill-stocktake/SKILL.md +195 -195
  349. package/skills/social-graph-ranker/SKILL.md +155 -155
  350. package/skills/social-publisher/SKILL.md +130 -130
  351. package/skills/springboot-patterns/SKILL.md +315 -315
  352. package/skills/springboot-security/SKILL.md +273 -273
  353. package/skills/springboot-tdd/SKILL.md +159 -159
  354. package/skills/springboot-verification/SKILL.md +232 -232
  355. package/skills/strategic-compact/SKILL.md +142 -142
  356. package/skills/swift-actor-persistence/SKILL.md +144 -144
  357. package/skills/swift-concurrency-6-2/SKILL.md +216 -216
  358. package/skills/swift-protocol-di-testing/SKILL.md +191 -191
  359. package/skills/swiftui-patterns/SKILL.md +259 -259
  360. package/skills/taste/SKILL.md +264 -264
  361. package/skills/taste/references/genre-taxonomy.md +87 -87
  362. package/skills/tdd-workflow/SKILL.md +583 -583
  363. package/skills/team-agent-orchestration/SKILL.md +111 -111
  364. package/skills/team-builder/SKILL.md +169 -169
  365. package/skills/terminal-ops/SKILL.md +110 -110
  366. package/skills/tinystruct-patterns/SKILL.md +279 -279
  367. package/skills/tinystruct-patterns/references/architecture.md +90 -90
  368. package/skills/tinystruct-patterns/references/data-handling.md +60 -60
  369. package/skills/tinystruct-patterns/references/database.md +99 -99
  370. package/skills/tinystruct-patterns/references/routing.md +64 -64
  371. package/skills/tinystruct-patterns/references/system-usage.md +97 -97
  372. package/skills/tinystruct-patterns/references/testing.md +72 -72
  373. package/skills/token-budget-advisor/SKILL.md +134 -134
  374. package/skills/ui-demo/SKILL.md +466 -466
  375. package/skills/ui-to-vue/SKILL.md +135 -135
  376. package/skills/uncloud/SKILL.md +344 -344
  377. package/skills/unified-memory/SKILL.md +170 -170
  378. package/skills/unified-notifications-ops/SKILL.md +188 -188
  379. package/skills/verification-loop/SKILL.md +129 -129
  380. package/skills/video-editing/SKILL.md +311 -311
  381. package/skills/videodb/SKILL.md +375 -375
  382. package/skills/visa-doc-translate/SKILL.md +117 -117
  383. package/skills/vite-patterns/SKILL.md +450 -450
  384. package/skills/vue-patterns/SKILL.md +471 -471
  385. package/skills/windows-desktop-e2e/SKILL.md +888 -888
  386. package/skills/workspace-surface-audit/SKILL.md +126 -126
  387. package/skills/x-api/SKILL.md +235 -235
package/lib/publish.js ADDED
@@ -0,0 +1,916 @@
1
+ /**
2
+ * Codeply - end-to-end publishing.
3
+ *
4
+ * "Publish the website" turns into: a database on Supabase when the app needs
5
+ * one (project picked or created, URL + anon key written into the app the way
6
+ * its stack reads them, schema applied with RLS), a live Vercel deployment
7
+ * (project, env vars, file upload, build polled until READY) and, if the user
8
+ * wants it, a GitHub repo linked to the Vercel project so later pushes deploy
9
+ * on their own. The user only connects Vercel and Supabase (and GitHub if
10
+ * they opt in); the agent does the rest through the tools made here.
11
+ *
12
+ * Nothing in this file runs unless the user asked to publish: the tools
13
+ * refuse when the request in front of them is not a publish request, every
14
+ * step that creates something or goes live asks first, and no token is ever
15
+ * written to output, logs or files. Progress is reported as a card (Database,
16
+ * Vercel, GitHub, Live) that the desktop app draws from each result's meta.
17
+ */
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const crypto = require('crypto');
21
+ const { execFile } = require('child_process');
22
+ const oauth = require('./oauth-connectors.js');
23
+
24
+ const STATE_FILE = path.join('.codeply', 'publish.json');
25
+ const LABEL = { supabase: 'Supabase', vercel: 'Vercel', github: 'GitHub' };
26
+
27
+ // Only a request that is about putting the app online opens this flow. Checked
28
+ // against the user's own message, so a model that drifts into "let me deploy
29
+ // it for you" after an unrelated edit is stopped by the tool itself.
30
+ const PUBLISH_INTENT = /\b(publish|deploy|re-?deploy|go live|put (?:it|this|the \w+) (?:live|online|on the (?:web|internet))|make (?:it|this) live|ship (?:it|this)|host (?:it|this)|launch|vercel|live link|live url|online)\b/i;
31
+
32
+ function wantsPublish(message) {
33
+ return message == null || PUBLISH_INTENT.test(String(message));
34
+ }
35
+
36
+ const NOT_ASKED =
37
+ 'The user has not asked to publish or deploy in this request, so nothing was published. Never publish unprompted. ' +
38
+ 'Finish what they asked for; you may mention in one line that you can publish it when they want.';
39
+
40
+ // ─── Project state (.codeply/publish.json) ─────────────────────────────────
41
+ // Ids and names only, never a token or key: which Vercel project, which
42
+ // Supabase project, the GitHub repo and whether the user wanted GitHub.
43
+
44
+ function readState(root) {
45
+ try { return JSON.parse(fs.readFileSync(path.join(root, STATE_FILE), 'utf8')) || {}; } catch { return {}; }
46
+ }
47
+
48
+ function saveState(root, patch) {
49
+ const prev = readState(root);
50
+ const next = { ...prev };
51
+ for (const [k, v] of Object.entries(patch)) next[k] = v && typeof v === 'object' && !Array.isArray(v) ? { ...(prev[k] || {}), ...v } : v;
52
+ next.updatedAt = new Date().toISOString();
53
+ fs.mkdirSync(path.join(root, '.codeply'), { recursive: true });
54
+ fs.writeFileSync(path.join(root, STATE_FILE), JSON.stringify(next, null, 2) + '\n', 'utf8');
55
+ return next;
56
+ }
57
+
58
+ // ─── Stack detection ────────────────────────────────────────────────────────
59
+ // Decides three things: Vercel's framework preset, whether Vercel runs a
60
+ // build, and where the Supabase URL/key go so this stack actually reads them.
61
+
62
+ function readPackage(root) {
63
+ try { return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); } catch { return null; }
64
+ }
65
+
66
+ function depsOf(pkg) {
67
+ return { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
68
+ }
69
+
70
+ const STACKS = [
71
+ { id: 'next', label: 'Next.js', dep: 'next', framework: 'nextjs', envFile: '.env.local', prefix: 'NEXT_PUBLIC_' },
72
+ { id: 'sveltekit', label: 'SvelteKit', dep: '@sveltejs/kit', framework: 'sveltekit', envFile: '.env', prefix: 'PUBLIC_' },
73
+ { id: 'nuxt', label: 'Nuxt', dep: 'nuxt', framework: 'nuxtjs', envFile: '.env', prefix: 'NUXT_PUBLIC_' },
74
+ { id: 'astro', label: 'Astro', dep: 'astro', framework: 'astro', envFile: '.env', prefix: 'PUBLIC_' },
75
+ { id: 'remix', label: 'Remix', dep: '@remix-run/react', framework: 'remix', envFile: '.env', prefix: '' },
76
+ { id: 'vite', label: 'Vite', dep: 'vite', framework: 'vite', envFile: '.env', prefix: 'VITE_' },
77
+ { id: 'cra', label: 'Create React App', dep: 'react-scripts', framework: 'create-react-app', envFile: '.env', prefix: 'REACT_APP_' },
78
+ { id: 'angular', label: 'Angular', dep: '@angular/core', framework: 'angular', envFile: '.env', prefix: 'NG_APP_' },
79
+ ];
80
+ const SERVER_DEPS = ['express', 'fastify', 'koa', 'hono', '@hapi/hapi', 'restify'];
81
+
82
+ function detectStack(root) {
83
+ const pkg = readPackage(root);
84
+ const deps = depsOf(pkg);
85
+ for (const s of STACKS) {
86
+ if (deps[s.dep]) return { ...s, builds: true, server: false };
87
+ }
88
+ if (pkg) {
89
+ const server = SERVER_DEPS.find((d) => deps[d]);
90
+ if (server) return { id: 'node', label: `Node (${server})`, framework: null, envFile: '.env', prefix: '', builds: false, server: true };
91
+ if (pkg.scripts?.build) return { id: 'npm', label: 'npm build', framework: null, envFile: '.env', prefix: '', builds: true, server: false, buildCommand: 'npm run build', outputDirectory: 'dist' };
92
+ }
93
+ // Plain HTML/CSS/JS: nothing builds, so the browser reads the values from a
94
+ // small env.js that the pages load before their own scripts.
95
+ return { id: 'static', label: 'Static site', framework: null, envFile: null, configJs: 'env.js', prefix: '', builds: false, server: false };
96
+ }
97
+
98
+ function envNames(stack) {
99
+ return { url: `${stack.prefix}SUPABASE_URL`, key: `${stack.prefix}SUPABASE_ANON_KEY` };
100
+ }
101
+
102
+ // ─── Project files ──────────────────────────────────────────────────────────
103
+
104
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.codeply', '.vercel', '.next', '.svelte-kit', '.nuxt', '.output', '.astro',
105
+ '.cache', '.turbo', 'coverage', '__pycache__', '.venv', 'venv', '.idea', '.vscode']);
106
+ const BUILD_OUTPUT_DIRS = new Set(['dist', 'build', 'out']);
107
+ const SKIP_FILES = /^(\.env(\..*)?|\.DS_Store|Thumbs\.db|npm-debug\.log.*|yarn-error\.log.*)$/i;
108
+ const MAX_DEPLOY_FILES = 5000;
109
+ const MAX_DEPLOY_BYTES = 200 * 1024 * 1024;
110
+
111
+ /** Every file Vercel gets: no .env files, no dependencies, no local build output when Vercel builds it again. */
112
+ function listProjectFiles(root, { builds = false, max = MAX_DEPLOY_FILES } = {}) {
113
+ const out = [];
114
+ const stack = [root];
115
+ while (stack.length && out.length < max) {
116
+ const dir = stack.pop();
117
+ let entries;
118
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
119
+ for (const e of entries) {
120
+ const abs = path.join(dir, e.name);
121
+ if (e.isDirectory()) {
122
+ if (SKIP_DIRS.has(e.name)) continue;
123
+ if (e.name.startsWith('.') && e.name !== '.well-known') continue;
124
+ if (builds && dir === root && BUILD_OUTPUT_DIRS.has(e.name)) continue;
125
+ stack.push(abs);
126
+ } else if (e.isFile() && !SKIP_FILES.test(e.name)) {
127
+ out.push(abs);
128
+ if (out.length >= max) break;
129
+ }
130
+ }
131
+ }
132
+ return out.sort();
133
+ }
134
+
135
+ // ─── Does the app need a database? ──────────────────────────────────────────
136
+ // Evidence, not a verdict the agent must follow: it reads the reasons and
137
+ // makes the call with what it knows about the app.
138
+
139
+ const SCAN_EXT = /\.(html?|jsx?|tsx?|mjs|cjs|vue|svelte|astro|py|sql|prisma)$/i;
140
+ const DB_LIBS = ['@supabase/supabase-js', '@supabase/ssr', 'pg', 'mysql2', 'mongoose', 'mongodb', 'prisma', '@prisma/client', 'sqlite3',
141
+ 'better-sqlite3', 'drizzle-orm', 'firebase', 'knex', 'sequelize'];
142
+
143
+ function detectDatabaseNeed(root) {
144
+ const strong = [];
145
+ const weak = [];
146
+ let usesSupabase = false;
147
+ const schemaFiles = [];
148
+ const pkg = readPackage(root);
149
+ const deps = depsOf(pkg);
150
+ for (const lib of DB_LIBS) {
151
+ if (!deps[lib]) continue;
152
+ if (lib.startsWith('@supabase/')) usesSupabase = true;
153
+ strong.push(`package.json depends on ${lib}`);
154
+ }
155
+ const seen = new Set();
156
+ const note = (list, key, text) => { if (!seen.has(key) && list.length < 12) { seen.add(key); list.push(text); } };
157
+ for (const abs of listProjectFiles(root, { max: 800 })) {
158
+ const rel = path.relative(root, abs).replace(/\\/g, '/');
159
+ if (/\.sql$/i.test(rel) || /schema\.prisma$/i.test(rel)) {
160
+ schemaFiles.push(rel);
161
+ note(strong, `schema:${rel}`, `schema file ${rel}`);
162
+ continue;
163
+ }
164
+ if (!SCAN_EXT.test(rel)) continue;
165
+ if (/(^|\/)(api|pages\/api|app\/api)\//.test(rel)) note(weak, 'api', `server routes (${rel})`);
166
+ let text;
167
+ try {
168
+ if (fs.statSync(abs).size > 300 * 1024) continue;
169
+ text = fs.readFileSync(abs, 'utf8');
170
+ } catch { continue; }
171
+ if (/@supabase\/supabase-js|supabase\.createClient|createClient\(\s*['"`]https:\/\/[a-z0-9]+\.supabase\.co|SUPABASE_URL/i.test(text)) {
172
+ usesSupabase = true;
173
+ note(strong, 'supabase', `uses Supabase already (${rel})`);
174
+ }
175
+ if (/type\s*=\s*["']password["']|\b(signUp|signIn|signInWithPassword|signInWithOAuth|logIn|register)\s*\(/.test(text)) note(strong, 'auth', `sign-in or sign-up (${rel})`);
176
+ if (/<form[^>]*method\s*=\s*["']post/i.test(text) || /method\s*:\s*['"]POST['"]/i.test(text)) note(weak, 'post', `sends form data (${rel})`);
177
+ if (/\b(app|router)\.(post|put|patch|delete)\s*\(/.test(text)) note(weak, 'routes', `server code that accepts data (${rel})`);
178
+ if (/localStorage\.setItem/.test(text)) note(weak, 'local', `saves data only in the visitor's browser with localStorage (${rel})`);
179
+ }
180
+ const verdict = strong.length ? 'yes' : weak.length ? 'maybe' : 'no';
181
+ return { verdict, reasons: [...strong, ...weak], strong, weak, usesSupabase, schemaFiles };
182
+ }
183
+
184
+ // ─── Writing Supabase settings into the app ─────────────────────────────────
185
+
186
+ /** Add or replace KEY=value lines, leaving the rest of the file alone. */
187
+ function upsertEnv(file, vars) {
188
+ let existing = '';
189
+ try { existing = fs.readFileSync(file, 'utf8'); } catch {}
190
+ const lines = existing.length ? existing.split(/\r?\n/) : [];
191
+ for (const { key, value } of vars) {
192
+ const i = lines.findIndex((l) => l.startsWith(`${key}=`));
193
+ if (i === -1) lines.push(`${key}=${value}`);
194
+ else lines[i] = `${key}=${value}`;
195
+ }
196
+ while (lines.length && lines[lines.length - 1] === '') lines.pop();
197
+ fs.writeFileSync(file, `${lines.join('\n')}\n`, 'utf8');
198
+ }
199
+
200
+ function readEnvFile(file) {
201
+ const out = {};
202
+ try {
203
+ for (const l of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
204
+ const m = l.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
205
+ if (m) out[m[1]] = m[2].replace(/^["'](.*)["']$/, '$1');
206
+ }
207
+ } catch {}
208
+ return out;
209
+ }
210
+
211
+ /** .env files stay out of git, so a push to GitHub never carries them. */
212
+ function ensureGitignore(root, entries = ['.env', '.env.local', '.env*.local', '.vercel', 'node_modules']) {
213
+ const file = path.join(root, '.gitignore');
214
+ let text = '';
215
+ try { text = fs.readFileSync(file, 'utf8'); } catch {}
216
+ const have = new Set(text.split(/\r?\n/).map((l) => l.trim()));
217
+ const missing = entries.filter((e) => !have.has(e));
218
+ if (!missing.length) return false;
219
+ fs.writeFileSync(file, `${text}${text && !text.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`, 'utf8');
220
+ return true;
221
+ }
222
+
223
+ const ENV_JS_HEADER =
224
+ '// Written by Codeply when publishing. The anon (publishable) key is meant to be\n' +
225
+ '// public: Row Level Security on each table decides what it may read or write.\n' +
226
+ '// Never put the service role key here.\n';
227
+
228
+ /**
229
+ * Puts the project URL and anon key where this stack reads them. Returns the
230
+ * files touched and the variables, so the same values can go to Vercel.
231
+ */
232
+ function writeSupabaseConfig(root, stack, { url, anonKey }) {
233
+ const names = envNames(stack);
234
+ const vars = [{ key: names.url, value: url }, { key: names.key, value: anonKey }];
235
+ const files = [];
236
+ if (stack.configJs) {
237
+ const js = `${ENV_JS_HEADER}window.SUPABASE_URL = ${JSON.stringify(url)};\nwindow.SUPABASE_ANON_KEY = ${JSON.stringify(anonKey)};\n`;
238
+ fs.writeFileSync(path.join(root, stack.configJs), js, 'utf8');
239
+ files.push(stack.configJs);
240
+ // Load it before the page's own scripts, in every page that talks to
241
+ // Supabase (and index.html, the usual entry point).
242
+ for (const abs of listProjectFiles(root, { max: 400 })) {
243
+ if (!/\.html?$/i.test(abs)) continue;
244
+ const rel = path.relative(root, abs).replace(/\\/g, '/');
245
+ let html;
246
+ try { html = fs.readFileSync(abs, 'utf8'); } catch { continue; }
247
+ if (/<script[^>]+env\.js/i.test(html)) continue;
248
+ if (!/supabase/i.test(html) && path.basename(rel).toLowerCase() !== 'index.html') continue;
249
+ const depth = rel.split('/').length - 1;
250
+ const src = `${'../'.repeat(depth)}${stack.configJs}`;
251
+ const tag = `<script src="${src}"></script>`;
252
+ let next;
253
+ if (/<script\b/i.test(html)) next = html.replace(/<script\b/i, `${tag}\n<script`);
254
+ else if (/<\/head>/i.test(html)) next = html.replace(/<\/head>/i, `${tag}\n</head>`);
255
+ else next = `${tag}\n${html}`;
256
+ fs.writeFileSync(abs, next, 'utf8');
257
+ files.push(rel);
258
+ }
259
+ } else {
260
+ upsertEnv(path.join(root, stack.envFile), vars);
261
+ files.push(stack.envFile);
262
+ ensureGitignore(root);
263
+ }
264
+ return { files, vars, names };
265
+ }
266
+
267
+ /** The public Supabase values to hand Vercel, read back from where supabase_setup wrote them. */
268
+ function publicEnvFor(root, state, stack) {
269
+ const sb = state.supabase;
270
+ if (!sb || !sb.envFile || stack.configJs) return [];
271
+ const values = readEnvFile(path.join(root, sb.envFile));
272
+ return (sb.vars || []).filter((k) => values[k]).map((k) => ({ key: k, value: values[k] }));
273
+ }
274
+
275
+ // ─── SQL schema with RLS ────────────────────────────────────────────────────
276
+
277
+ /**
278
+ * Every table the SQL creates gets Row Level Security turned on (added when
279
+ * the SQL forgot it). Tables with RLS but no policy are reported, because the
280
+ * browser then can neither read nor write them.
281
+ */
282
+ function prepareSchemaSql(sql) {
283
+ const text = String(sql || '').trim();
284
+ const tables = [];
285
+ for (const m of text.matchAll(/create\s+table\s+(?:if\s+not\s+exists\s+)?((?:"?[\w]+"?\.)?"?[\w]+"?)/gi)) {
286
+ const full = m[1].replace(/"/g, '');
287
+ if (!tables.includes(full)) tables.push(full);
288
+ }
289
+ const bare = (t) => t.replace(/^public\./i, '');
290
+ const mentions = (t, re) => re.test(text.replace(/"/g, '')) ? true : false;
291
+ const addedRls = [];
292
+ const lines = [text.replace(/;?\s*$/, ';')];
293
+ for (const t of tables) {
294
+ const name = bare(t).replace(/[.$]/g, '\\$&');
295
+ if (!mentions(t, new RegExp(`alter\\s+table\\s+(?:only\\s+)?(?:public\\.)?${name}\\s+enable\\s+row\\s+level\\s+security`, 'i'))) {
296
+ lines.push(`alter table ${t} enable row level security;`);
297
+ addedRls.push(t);
298
+ }
299
+ }
300
+ const noPolicy = tables.filter((t) => !new RegExp(`create\\s+policy[\\s\\S]*?\\bon\\s+(?:public\\.)?${bare(t)}\\b`, 'i').test(text.replace(/"/g, '')));
301
+ return { sql: lines.join('\n'), tables, addedRls, noPolicy };
302
+ }
303
+
304
+ const DESTRUCTIVE_SQL = /\b(drop|truncate|delete\s+from)\b/i;
305
+
306
+ // ─── Vercel ─────────────────────────────────────────────────────────────────
307
+
308
+ function vercelCall(v, method, apiPath, body) {
309
+ return oauth.apiCall(oauth.API.vercel(), v.accessToken, method, apiPath, body, { teamId: v.teamId || undefined });
310
+ }
311
+
312
+ /** Vercel's error in plain words, with what the user can do about it. */
313
+ function vercelError(r, what) {
314
+ const msg = r.body?.error?.message || (typeof r.body === 'string' ? r.body.slice(0, 200) : '') || `HTTP ${r.status}`;
315
+ let hint = '';
316
+ if (r.status === 401 || r.status === 403) hint = ' The Vercel connection was refused. Reconnect Vercel in Connect Apps.';
317
+ else if (r.status === 402) hint = ' Vercel says this needs a paid plan.';
318
+ else if (r.status === 429) hint = ' Vercel is rate limiting this account. Wait a minute and try again.';
319
+ const e = new Error(`${what} failed: ${msg}.${hint}`);
320
+ e.status = r.status;
321
+ return e;
322
+ }
323
+
324
+ async function ensureVercelProject(v, name, stack) {
325
+ const got = await vercelCall(v, 'GET', `/v9/projects/${encodeURIComponent(name)}`);
326
+ if (got.ok) return { id: got.body.id, name: got.body.name, created: false };
327
+ if (got.status !== 404) throw vercelError(got, 'Looking up the Vercel project');
328
+ const made = await vercelCall(v, 'POST', '/v11/projects', { name, framework: stack.framework });
329
+ if (!made.ok) throw vercelError(made, 'Creating the Vercel project');
330
+ return { id: made.body.id, name: made.body.name, created: true };
331
+ }
332
+
333
+ async function setVercelEnv(v, projectId, vars) {
334
+ if (!vars.length) return;
335
+ const r = await vercelCall(v, 'POST', `/v10/projects/${encodeURIComponent(projectId)}/env?upsert=true`,
336
+ vars.map((x) => ({ key: x.key, value: x.value, type: 'encrypted', target: ['production', 'preview', 'development'] })));
337
+ if (!r.ok) throw vercelError(r, 'Setting the Vercel environment variables');
338
+ }
339
+
340
+ /** Uploads each file once by its SHA-1 (Vercel skips ones it already has), a few at a time. */
341
+ async function uploadFiles(v, root, files) {
342
+ const out = [];
343
+ const base = oauth.API.vercel();
344
+ const qs = v.teamId ? `?teamId=${encodeURIComponent(v.teamId)}` : '';
345
+ let next = 0;
346
+ async function worker() {
347
+ while (next < files.length) {
348
+ const abs = files[next++];
349
+ const data = fs.readFileSync(abs);
350
+ const sha = crypto.createHash('sha1').update(data).digest('hex');
351
+ const res = await fetch(`${base}/v2/files${qs}`, {
352
+ method: 'POST',
353
+ headers: { Authorization: `Bearer ${v.accessToken}`, 'Content-Type': 'application/octet-stream', 'x-vercel-digest': sha, 'Content-Length': String(data.length) },
354
+ body: data,
355
+ signal: AbortSignal.timeout(120000),
356
+ });
357
+ if (!res.ok) {
358
+ let body = null;
359
+ try { body = await res.json(); } catch {}
360
+ throw vercelError({ status: res.status, body }, `Uploading ${path.relative(root, abs)}`);
361
+ }
362
+ out.push({ file: path.relative(root, abs).replace(/\\/g, '/'), sha, size: data.length });
363
+ }
364
+ }
365
+ await Promise.all(Array.from({ length: Math.min(6, files.length) }, worker));
366
+ return out.sort((a, b) => a.file.localeCompare(b.file));
367
+ }
368
+
369
+ function projectSettingsFor(stack) {
370
+ const s = { framework: stack.framework };
371
+ if (stack.buildCommand) s.buildCommand = stack.buildCommand;
372
+ if (stack.outputDirectory) s.outputDirectory = stack.outputDirectory;
373
+ return s;
374
+ }
375
+
376
+ async function createDeployment(v, { name, projectId, files, stack }) {
377
+ const r = await vercelCall(v, 'POST', '/v13/deployments?skipAutoDetectionConfirmation=1', {
378
+ name, project: projectId, target: 'production', files, projectSettings: projectSettingsFor(stack),
379
+ });
380
+ if (!r.ok) throw vercelError(r, 'Starting the Vercel deployment');
381
+ return r.body;
382
+ }
383
+
384
+ const POLL_MS = () => Number(process.env.CODEPLY_PUBLISH_POLL_MS) || 3000;
385
+ const DEPLOY_TIMEOUT_MS = () => Number(process.env.CODEPLY_PUBLISH_TIMEOUT_MS) || 10 * 60 * 1000;
386
+ const FINAL_STATES = new Set(['READY', 'ERROR', 'CANCELED']);
387
+
388
+ async function waitForDeployment(v, id, signal) {
389
+ const deadline = Date.now() + DEPLOY_TIMEOUT_MS();
390
+ let last = null;
391
+ while (Date.now() < deadline) {
392
+ if (signal?.aborted) return { ...(last || {}), readyState: 'CANCELED' };
393
+ const r = await vercelCall(v, 'GET', `/v13/deployments/${encodeURIComponent(id)}`);
394
+ if (!r.ok) throw vercelError(r, 'Checking the deployment');
395
+ last = r.body;
396
+ const st = last.readyState || last.status;
397
+ if (FINAL_STATES.has(st)) return { ...last, readyState: st };
398
+ await new Promise((res) => setTimeout(res, POLL_MS()));
399
+ }
400
+ return { ...(last || {}), readyState: 'TIMEOUT' };
401
+ }
402
+
403
+ /** The last lines of the build log, errors first in mind. */
404
+ async function buildLog(v, id) {
405
+ const r = await vercelCall(v, 'GET', `/v3/deployments/${encodeURIComponent(id)}/events?builds=1&limit=400`);
406
+ if (!r.ok || !Array.isArray(r.body)) return '';
407
+ const lines = r.body.map((e) => String(e.payload?.text ?? e.text ?? '').trimEnd()).filter(Boolean);
408
+ return lines.slice(-40).join('\n');
409
+ }
410
+
411
+ async function liveUrl(v, project, deployment) {
412
+ const r = await vercelCall(v, 'GET', `/v9/projects/${encodeURIComponent(project.id)}/domains`);
413
+ const domains = r.ok ? (r.body.domains || []) : [];
414
+ const custom = domains.find((d) => d.verified !== false && !/\.vercel\.app$/i.test(d.name));
415
+ if (custom) return `https://${custom.name}`;
416
+ // <project>.vercel.app is public; per-deployment URLs can sit behind Vercel login.
417
+ const own = domains.filter((d) => /\.vercel\.app$/i.test(d.name)).sort((a, b) => a.name.length - b.name.length)[0];
418
+ if (own) return `https://${own.name}`;
419
+ if (project.name) return `https://${project.name}.vercel.app`;
420
+ const alias = (deployment.alias || [])[0] || deployment.url;
421
+ return alias ? `https://${String(alias).replace(/^https?:\/\//, '')}` : '';
422
+ }
423
+
424
+ async function linkGitRepo(v, projectId, repo) {
425
+ const r = await vercelCall(v, 'POST', `/v9/projects/${encodeURIComponent(projectId)}/link`, { type: 'github', repo });
426
+ if (r.ok) return { linked: true };
427
+ const msg = r.body?.error?.message || `HTTP ${r.status}`;
428
+ const needsApp = /install|permission|access|not found|integration|login connection/i.test(msg) || r.status === 400 || r.status === 404 || r.status === 403;
429
+ return {
430
+ linked: false,
431
+ why: needsApp
432
+ ? `Vercel could not reach ${repo} (${msg}). Install the Vercel app on your GitHub account at https://github.com/apps/vercel/installations/new, give it access to this repo, then publish again.`
433
+ : `Vercel would not link ${repo}: ${msg}.`,
434
+ };
435
+ }
436
+
437
+ // ─── Supabase ───────────────────────────────────────────────────────────────
438
+
439
+ function supabaseCall(token, method, apiPath, body) {
440
+ return oauth.apiCall(oauth.API.supabase(), token, method, apiPath, body);
441
+ }
442
+
443
+ function supabaseError(r, what) {
444
+ const msg = r.body?.message || r.body?.error || (typeof r.body === 'string' ? r.body.slice(0, 200) : '') || `HTTP ${r.status}`;
445
+ const hint = r.status === 401 || r.status === 403 ? ' The Supabase connection was refused. Reconnect Supabase in Connect Apps.' : '';
446
+ const e = new Error(`${what} failed: ${msg}.${hint}`);
447
+ e.status = r.status;
448
+ return e;
449
+ }
450
+
451
+ /** The project URL and its public key (publishable first, else anon). Never a secret or service role key. */
452
+ async function getPublicKeys(token, ref) {
453
+ const r = await supabaseCall(token, 'GET', `/v1/projects/${encodeURIComponent(ref)}/api-keys?reveal=true`);
454
+ if (!r.ok || !Array.isArray(r.body)) throw supabaseError(r, 'Reading the project API keys');
455
+ const safe = r.body.filter((k) => !/secret|service/i.test(`${k.type || ''} ${k.name || ''}`) && k.api_key);
456
+ const key = safe.find((k) => /publishable/i.test(k.type || '')) || safe.find((k) => /^anon$/i.test(k.name || '')) || safe.find((k) => /anon|publishable/i.test(`${k.type} ${k.name}`));
457
+ if (!key) throw new Error(`Project ${ref} has no public (anon) key yet. It may still be starting; try again in a minute.`);
458
+ return { url: `https://${ref}.supabase.co`, anonKey: key.api_key };
459
+ }
460
+
461
+ // ─── Git ────────────────────────────────────────────────────────────────────
462
+
463
+ function git(args, cwd, extraEnv) {
464
+ return new Promise((resolve, reject) => {
465
+ execFile('git', args, { cwd, windowsHide: true, timeout: 180000, maxBuffer: 8 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', ...(extraEnv || {}) } }, (err, stdout, stderr) => {
466
+ if (err) { err.output = `${stdout || ''}${stderr || ''}`; reject(err); return; }
467
+ resolve(String(stdout || '').trim());
468
+ });
469
+ });
470
+ }
471
+
472
+ /** owner/name from a GitHub remote URL (https or ssh), or null. */
473
+ function githubRepoFromUrl(url) {
474
+ const m = String(url || '').trim().match(/github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i);
475
+ return m ? `${m[1]}/${m[2]}` : null;
476
+ }
477
+
478
+ function sanitizeProjectName(raw) {
479
+ const base = String(raw || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 52);
480
+ return base || 'codeply-site';
481
+ }
482
+
483
+ function sanitizeRepoName(raw) {
484
+ return String(raw || '').trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'codeply-site';
485
+ }
486
+
487
+ function hideSecrets(text, secrets) {
488
+ let out = String(text || '');
489
+ for (const s of secrets) if (s && s.length > 6) out = out.split(s).join('[hidden]');
490
+ return out;
491
+ }
492
+
493
+ // ─── The progress card ──────────────────────────────────────────────────────
494
+
495
+ const STEPS = ['database', 'vercel', 'github', 'live'];
496
+
497
+ /** Merges a step update into this turn's card and returns a copy for meta.publish. */
498
+ function card(ctx, patch = {}) {
499
+ if (!ctx.publishCard) {
500
+ ctx.publishCard = { id: `pub-${Date.now().toString(36)}`, steps: Object.fromEntries(STEPS.map((s) => [s, { status: 'pending' }])), url: '' };
501
+ // Start from what earlier publishes already set up, so a database made last
502
+ // time reads as done instead of "Waiting".
503
+ const saved = ctx.cwd ? readState(ctx.cwd) : {};
504
+ const st = ctx.publishCard.steps;
505
+ if (saved.supabase?.ref) st.database = { status: 'done', detail: saved.supabase.name || 'Supabase', url: `https://supabase.com/dashboard/project/${saved.supabase.ref}` };
506
+ if (saved.github?.repo && saved.github.linked) st.github = { status: 'done', detail: `${saved.github.repo}, auto-deploy on`, url: `https://github.com/${saved.github.repo}` };
507
+ else if (saved.github?.choice === 'no') st.github = { status: 'skipped', detail: 'Direct deploy' };
508
+ }
509
+ const c = ctx.publishCard;
510
+ for (const [k, v] of Object.entries(patch.steps || {})) c.steps[k] = { ...c.steps[k], ...v };
511
+ if (patch.url !== undefined) c.url = patch.url;
512
+ return JSON.parse(JSON.stringify(c));
513
+ }
514
+
515
+ // ─── The tools ──────────────────────────────────────────────────────────────
516
+
517
+ /**
518
+ * `deps` comes from tools.mjs: the config module (read through so tests can
519
+ * stand in for the stored connections), a Supabase token getter that
520
+ * refreshes OAuth tokens, and resolvePath.
521
+ */
522
+ function makePublishTools({ config, getSupabaseToken, resolveDir }) {
523
+ const integration = (name) => config.getIntegration(name);
524
+ const connected = (name) => !!integration(name).accessToken;
525
+ const rootOf = (args, ctx) => (args.path ? resolveDir(args.path, ctx.cwd) : ctx.cwd);
526
+
527
+ function notConnected(service, ctx) {
528
+ const label = LABEL[service];
529
+ return {
530
+ ok: false,
531
+ output: `${label} is not connected. Call publish_connect with <service>${service}</service> so the user can connect it in one click, then carry on. Do not use a CLI instead.`,
532
+ meta: { label: `${label} not connected`, publish: card(ctx, { steps: { [service === 'supabase' ? 'database' : service]: { status: 'waiting', detail: `Connect ${label}` } } }) },
533
+ };
534
+ }
535
+
536
+ async function publish_check(args, ctx) {
537
+ const root = rootOf(args, ctx);
538
+ if (!fs.existsSync(root)) return { ok: false, output: `${args.path} does not exist.` };
539
+ const stack = detectStack(root);
540
+ const db = detectDatabaseNeed(root);
541
+ const state = readState(ctx.cwd);
542
+ const files = listProjectFiles(root, { builds: stack.builds });
543
+ let remote = '';
544
+ try { remote = await git(['remote', 'get-url', 'origin'], root); } catch {}
545
+ const lines = [
546
+ `Stack: ${stack.label}${stack.framework ? ` (Vercel preset "${stack.framework}")` : ''}${stack.builds ? ', Vercel runs the build' : ', no build step'}.`,
547
+ `Files to upload: ${files.length}.`,
548
+ `Database: ${db.verdict === 'yes' ? 'likely needed' : db.verdict === 'maybe' ? 'maybe' : 'no sign of one'}.`,
549
+ ...db.reasons.map((r) => ` - ${r}`),
550
+ db.usesSupabase ? 'The code already uses Supabase.' : '',
551
+ db.schemaFiles.length ? `Schema files: ${db.schemaFiles.join(', ')}` : '',
552
+ `Supabase settings go in: ${stack.configJs ? `${stack.configJs} (loaded by the pages, as window.SUPABASE_URL and window.SUPABASE_ANON_KEY)` : `${stack.envFile} as ${envNames(stack).url} and ${envNames(stack).key}`}.`,
553
+ `Connected: Vercel ${connected('vercel') ? 'yes' : 'no'}, Supabase ${connected('supabase') ? 'yes' : 'no'}, GitHub ${connected('github') ? 'yes' : 'no'}.`,
554
+ remote ? `Git remote: ${githubRepoFromUrl(remote) || 'not GitHub'}.` : 'No git remote yet.',
555
+ state.vercel?.projectName ? `Published before as Vercel project "${state.vercel.projectName}"${state.vercel.url ? ` at ${state.vercel.url}` : ''}.` : 'Never published from here.',
556
+ state.supabase?.ref ? `Supabase project already set up: ${state.supabase.name || state.supabase.ref} (${state.supabase.ref}).` : '',
557
+ state.github?.choice ? `GitHub auto-deploy: ${state.github.choice === 'yes' ? (state.github.linked ? `linked to ${state.github.repo}` : 'wanted, not linked yet') : 'the user said no'}.` : 'GitHub auto-deploy: not asked yet (publish_deploy asks before the first deploy).',
558
+ '',
559
+ 'Decide yourself whether the app needs a database: sign-in, data that must be saved and shared across visitors or devices, or code that already uses Supabase means yes; a brochure site or a page whose forms only need email means no.',
560
+ ].filter((l) => l !== '');
561
+ const needsDb = db.verdict !== 'no' || !!state.supabase?.ref;
562
+ return {
563
+ ok: true,
564
+ output: lines.join('\n'),
565
+ meta: {
566
+ label: `${stack.label}, database ${db.verdict}`,
567
+ publish: card(ctx, { steps: {
568
+ database: state.supabase?.ref ? { status: 'done', detail: state.supabase.name || state.supabase.ref, url: `https://supabase.com/dashboard/project/${state.supabase.ref}` } : needsDb ? { status: 'pending', detail: 'Checking' } : { status: 'skipped', detail: 'Not needed' },
569
+ github: state.github?.choice === 'no' ? { status: 'skipped', detail: 'Direct deploy' } : state.github?.linked ? { status: 'done', detail: state.github.repo, url: `https://github.com/${state.github.repo}` } : {},
570
+ } }),
571
+ },
572
+ };
573
+ }
574
+
575
+ async function publish_connect(args, ctx) {
576
+ const service = String(args.service || '').trim().toLowerCase();
577
+ if (!LABEL[service]) return { ok: false, output: 'publish_connect needs <service>: supabase, vercel or github.' };
578
+ const label = LABEL[service];
579
+ const step = service === 'supabase' ? 'database' : service;
580
+ if (connected(service)) return { ok: true, output: `${label} is already connected. Carry on.`, meta: { label: `${label} connected` } };
581
+ if (typeof ctx.ask !== 'function') {
582
+ return {
583
+ ok: false,
584
+ output: `${label} is not connected and the user cannot be asked from here. Tell them to connect ${label} in Connect Apps (account menu, Connect Apps, ${label}) and to say "continue" when it is done. Stop here.`,
585
+ meta: { label: `${label} not connected` },
586
+ };
587
+ }
588
+ const why = String(args.reason || '').trim();
589
+ const question = service === 'supabase'
590
+ ? `${why || 'This app needs a database to save its data.'} Log in to your Supabase account so Codeply can set it up.`
591
+ : service === 'vercel'
592
+ ? `${why || 'Publishing puts the site on Vercel.'} Connect your Vercel account to continue.`
593
+ : `${why || 'GitHub keeps the code and redeploys the site on every push.'} Connect your GitHub account to continue.`;
594
+ const skip = service === 'supabase' ? 'Skip the database' : service === 'github' ? 'Skip GitHub, just deploy' : 'Not now';
595
+ card(ctx, { steps: { [step]: { status: 'waiting', detail: `Waiting for you to connect ${label}` } } });
596
+ const answer = await ctx.ask({ question, options: [skip], connect: service });
597
+ if (connected(service)) {
598
+ const who = integration(service);
599
+ return {
600
+ ok: true,
601
+ output: `${label} is connected now${who.userName || who.email ? ` (${who.userName || who.email})` : ''}. Carry on with the publish right away, without asking again.`,
602
+ meta: { label: `${label} connected`, publish: card(ctx, { steps: { [step]: { status: step === 'github' ? 'active' : 'pending', detail: `${label} connected` } } }) },
603
+ };
604
+ }
605
+ const said = answer ? ` They answered: "${String(answer).slice(0, 200)}".` : '';
606
+ const next = service === 'supabase'
607
+ ? 'Publish without a database only if the app still works without one; otherwise stop and explain in plain words that the app needs Supabase to save its data.'
608
+ : service === 'github'
609
+ ? 'Deploy directly with publish_deploy instead.'
610
+ : 'Stop here and tell the user publishing needs a Vercel connection.';
611
+ return {
612
+ ok: false,
613
+ output: `The user did not connect ${label}.${said} ${next}`,
614
+ meta: { label: `${label} not connected`, publish: card(ctx, { steps: { [step]: { status: 'skipped', detail: `${label} not connected` } } }) },
615
+ };
616
+ }
617
+
618
+ async function supabase_setup(args, ctx) {
619
+ if (!wantsPublish(ctx.userMessage) && !/supabase|database/i.test(String(ctx.userMessage || ''))) return { ok: false, output: NOT_ASKED };
620
+ const token = await getSupabaseToken();
621
+ if (!token) return notConnected('supabase', ctx);
622
+ const root = ctx.cwd;
623
+ const stack = detectStack(root);
624
+ const state = readState(root);
625
+ card(ctx, { steps: { database: { status: 'active', detail: 'Setting up Supabase' } } });
626
+ const fail = (msg) => ({ ok: false, output: msg, meta: { label: 'Supabase setup failed', publish: card(ctx, { steps: { database: { status: 'error', detail: msg.slice(0, 200) } } }) } });
627
+
628
+ const list = await supabaseCall(token, 'GET', '/v1/projects');
629
+ if (!list.ok) return fail(supabaseError(list, 'Listing your Supabase projects').message);
630
+ const projects = Array.isArray(list.body) ? list.body : [];
631
+ const wanted = String(args.project || '').trim();
632
+ let chosen = null;
633
+ let create = wanted.toLowerCase() === 'new';
634
+ if (wanted && !create) {
635
+ chosen = projects.find((p) => p.ref === wanted) || projects.find((p) => p.name?.toLowerCase() === wanted.toLowerCase());
636
+ if (!chosen) return fail(`No Supabase project "${wanted}". Projects on this account: ${projects.map((p) => `${p.name} (${p.ref})`).join(', ') || 'none'}.`);
637
+ } else if (!create) {
638
+ chosen = state.supabase?.ref ? projects.find((p) => p.ref === state.supabase.ref) : null;
639
+ if (!chosen && projects.length) {
640
+ if (typeof ctx.ask !== 'function') {
641
+ return fail(`Ask the user which Supabase project to use, then call supabase_setup again with <project> set to its ref, or "new". Their projects: ${projects.map((p) => `${p.name} (${p.ref})`).join(', ')}.`);
642
+ }
643
+ const options = [...projects.slice(0, 4).map((p) => `${p.name} (${p.ref})`), 'Create a new project'];
644
+ const answer = await ctx.ask({ question: 'Which Supabase project should this app use?', options });
645
+ if (answer == null) return fail('The user did not pick a Supabase project. Stop and ask them which one to use.');
646
+ const ref = String(answer).match(/\(([a-z0-9]{6,})\)\s*$/i)?.[1];
647
+ chosen = ref ? projects.find((p) => p.ref === ref) : projects.find((p) => p.name?.toLowerCase() === String(answer).trim().toLowerCase());
648
+ if (!chosen) create = /new|create/i.test(String(answer));
649
+ if (!chosen && !create) return fail(`"${answer}" is not one of the Supabase projects. Ask again.`);
650
+ } else if (!chosen) create = true;
651
+ }
652
+
653
+ let created = false;
654
+ if (create) {
655
+ const name = sanitizeProjectName(args.name || path.basename(root));
656
+ const orgs = await supabaseCall(token, 'GET', '/v1/organizations');
657
+ if (!orgs.ok) return fail(supabaseError(orgs, 'Listing your Supabase organizations').message);
658
+ if (!Array.isArray(orgs.body) || !orgs.body.length) return fail('This Supabase account has no organization yet. Create one at supabase.com/dashboard, then publish again.');
659
+ let org = orgs.body[0];
660
+ if (orgs.body.length > 1 && typeof ctx.ask === 'function') {
661
+ const answer = await ctx.ask({ question: 'Which Supabase organization should own the new project?', options: orgs.body.slice(0, 5).map((o) => o.name) });
662
+ org = orgs.body.find((o) => o.name === answer) || org;
663
+ }
664
+ const detail = await supabaseCall(token, 'GET', `/v1/organizations/${encodeURIComponent(org.slug || org.id)}`);
665
+ const plan = String((detail.ok && detail.body?.plan) || 'unknown').toLowerCase();
666
+ const region = String(args.region || 'us-east-1').trim();
667
+ const paid = plan !== 'free';
668
+ const verdict = await ctx.approve({
669
+ tool: 'supabase_setup',
670
+ title: `Create Supabase project "${name}"`,
671
+ detail: `Organization: ${org.name} (plan: ${plan}). Region: ${region}.\n` +
672
+ (paid ? 'This organization is on a paid plan (or its plan is unknown), so a new project can add to your Supabase bill.\n' : 'Free plan: no charge. A free organization can have two active projects.\n') +
673
+ 'A strong database password is generated for you and not shown or saved in the app.',
674
+ danger: paid,
675
+ });
676
+ if (verdict === 'reject') return { ok: false, output: 'The user declined creating a Supabase project. Ask whether to use an existing project or publish without a database.', meta: { rejected: true, publish: card(ctx, { steps: { database: { status: 'skipped', detail: 'Declined' } } }) } };
677
+ try {
678
+ chosen = await oauth.supabaseCreateProject(token, {
679
+ name, organizationSlug: org.slug || org.id, region,
680
+ dbPass: crypto.randomBytes(24).toString('base64url'),
681
+ pollMs: Number(process.env.CODEPLY_PUBLISH_POLL_MS) || undefined,
682
+ });
683
+ created = true;
684
+ } catch (e) {
685
+ return fail(`Creating the Supabase project failed: ${e.message}`);
686
+ }
687
+ }
688
+
689
+ let keys;
690
+ try { keys = await getPublicKeys(token, chosen.ref); } catch (e) { return fail(e.message); }
691
+ let written;
692
+ try { written = writeSupabaseConfig(root, stack, keys); } catch (e) { return fail(`Could not write the Supabase settings into the app: ${e.message}`); }
693
+ saveState(root, { supabase: { ref: chosen.ref, name: chosen.name, url: keys.url, envFile: stack.configJs ? null : stack.envFile, configJs: stack.configJs || null, vars: written.vars.map((v) => v.key) } });
694
+
695
+ const readHow = stack.configJs
696
+ ? `The pages now load ${stack.configJs} first, so the app code reads window.SUPABASE_URL and window.SUPABASE_ANON_KEY (for example supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY)).`
697
+ : `The app code must read ${written.names.url} and ${written.names.key} (the way ${stack.label} exposes env vars to the browser). publish_deploy copies them to the Vercel project.`;
698
+ return {
699
+ ok: true,
700
+ output: `${created ? 'Created' : 'Using'} Supabase project "${chosen.name}" (ref ${chosen.ref}, ${keys.url}). Wrote its URL and anon key to ${written.files.join(', ')}. ` +
701
+ `${readHow} The anon key is public by design; Row Level Security protects the data. Never put the service role key in client code. ` +
702
+ 'If the app needs tables, write the SQL and run it with supabase_schema (the user sees it and approves first). Check that the code uses these values before deploying.',
703
+ meta: { label: `${chosen.name} (${chosen.ref})`, files: written.files, publish: card(ctx, { steps: { database: { status: 'done', detail: `${chosen.name}${created ? ' (new)' : ''}`, url: `https://supabase.com/dashboard/project/${chosen.ref}` } } }) },
704
+ };
705
+ }
706
+
707
+ async function supabase_schema(args, ctx) {
708
+ const state = readState(ctx.cwd);
709
+ const ref = String(args.ref || state.supabase?.ref || '').trim();
710
+ if (!ref) return { ok: false, output: 'No Supabase project yet. Run supabase_setup first, or pass <ref>.' };
711
+ let sql = String(args.sql || '').trim();
712
+ if (!sql && args.path) {
713
+ try { sql = fs.readFileSync(resolveDir(args.path, ctx.cwd), 'utf8'); } catch (e) { return { ok: false, output: `Could not read ${args.path}: ${e.message}` }; }
714
+ }
715
+ if (!sql.trim()) return { ok: false, output: 'supabase_schema needs <sql> (or <path> to a .sql file).' };
716
+ const token = await getSupabaseToken();
717
+ if (!token) return notConnected('supabase', ctx);
718
+ const prep = prepareSchemaSql(sql);
719
+ const notes = [
720
+ prep.addedRls.length ? `-- Codeply added: row level security on ${prep.addedRls.join(', ')}` : '',
721
+ prep.noPolicy.length ? `-- Warning: no policy on ${prep.noPolicy.join(', ')}, so the app cannot read or write ${prep.noPolicy.length === 1 ? 'it' : 'them'} yet` : '',
722
+ ].filter(Boolean);
723
+ const verdict = await ctx.approve({
724
+ tool: 'supabase_schema',
725
+ title: `Run SQL on Supabase project ${state.supabase?.name || ref}`,
726
+ detail: [...notes, prep.sql].join('\n').slice(0, 6000),
727
+ danger: DESTRUCTIVE_SQL.test(prep.sql),
728
+ });
729
+ if (verdict === 'reject') return { ok: false, output: 'The user declined running that SQL. Nothing changed in the database. Ask what they want changed.', meta: { rejected: true } };
730
+ let r;
731
+ try { r = await oauth.supabaseQuery(token, ref, prep.sql); } catch (e) { return { ok: false, output: `Running the SQL failed: ${e.message}` }; }
732
+ if (!r.ok) {
733
+ const msg = r.body?.message || r.body?.error || JSON.stringify(r.body).slice(0, 400);
734
+ return { ok: false, output: `Supabase rejected the SQL (HTTP ${r.status}): ${msg}. Fix the SQL and run supabase_schema again.`, meta: { label: 'SQL failed' } };
735
+ }
736
+ const warn = prep.noPolicy.length
737
+ ? ` Warning: ${prep.noPolicy.join(', ')} ${prep.noPolicy.length === 1 ? 'has' : 'have'} RLS on but no policy, so the app can neither read nor write ${prep.noPolicy.length === 1 ? 'it' : 'them'}. Add policies (create policy ... on <table> for select/insert using (...)) with supabase_schema.`
738
+ : '';
739
+ return {
740
+ ok: true,
741
+ output: `Applied the SQL to ${ref}.${prep.tables.length ? ` Tables: ${prep.tables.join(', ')}.` : ''}${prep.addedRls.length ? ` Turned on row level security for ${prep.addedRls.join(', ')}.` : ''}${warn}`,
742
+ meta: { label: prep.tables.length ? `tables ${prep.tables.join(', ')}` : 'SQL applied', publish: card(ctx, { steps: { database: { status: 'done', detail: prep.tables.length ? `${prep.tables.length} table(s) ready` : 'Schema applied' } } }) },
743
+ };
744
+ }
745
+
746
+ async function publish_deploy(args, ctx) {
747
+ if (!wantsPublish(ctx.userMessage)) return { ok: false, output: NOT_ASKED };
748
+ const root = rootOf(args, ctx);
749
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) return { ok: false, output: `${args.path || '.'} is not a folder.` };
750
+ const v = integration('vercel');
751
+ if (!v.accessToken) return notConnected('vercel', ctx);
752
+ let state = readState(ctx.cwd);
753
+ const stack = detectStack(root);
754
+
755
+ // Before the very first deploy: GitHub or not. Asked once per project.
756
+ if (!state.github?.choice && !state.vercel?.lastDeploymentId && typeof ctx.ask === 'function') {
757
+ const answer = await ctx.ask({ question: 'Connect GitHub so pushes deploy automatically?', options: ['Yes, connect GitHub', 'No, just deploy'] });
758
+ const yes = answer != null && /^\s*(yes|y|sure|ok)/i.test(String(answer));
759
+ state = saveState(ctx.cwd, { github: { choice: yes ? 'yes' : 'no' } });
760
+ if (yes) {
761
+ return {
762
+ ok: true,
763
+ output: 'The user wants GitHub. Run publish_github now (it connects GitHub if needed, creates or reuses the repo, pushes and links it to Vercel), then call publish_deploy again for the first deploy.',
764
+ meta: { label: 'GitHub first', publish: card(ctx, { steps: { github: { status: 'active', detail: 'Setting up GitHub' } } }) },
765
+ };
766
+ }
767
+ card(ctx, { steps: { github: { status: 'skipped', detail: 'Direct deploy' } } });
768
+ }
769
+
770
+ const files = listProjectFiles(root, { builds: stack.builds });
771
+ if (!files.length) return { ok: false, output: 'There are no files to publish in this folder.' };
772
+ let bytes = 0;
773
+ for (const f of files) bytes += fs.statSync(f).size;
774
+ if (bytes > MAX_DEPLOY_BYTES) return { ok: false, output: `The folder is ${(bytes / 1e6).toFixed(0)}MB, over the ${MAX_DEPLOY_BYTES / 1e6}MB publish limit. Remove large files (videos, archives) or publish a smaller folder.` };
775
+ const name = sanitizeProjectName(args.name || state.vercel?.projectName || path.basename(root));
776
+ const env = publicEnvFor(ctx.cwd, state, stack);
777
+
778
+ const verdict = await ctx.approve({
779
+ tool: 'publish_deploy',
780
+ title: `Publish "${name}" to Vercel`,
781
+ detail: `${files.length} file(s), ${(bytes / 1024).toFixed(0)}KB, ${stack.label}.` +
782
+ (env.length ? `\nEnvironment variables: ${env.map((e) => e.key).join(', ')}` : '') +
783
+ '\nProduction deployment: it will be live on the internet.',
784
+ danger: true,
785
+ });
786
+ if (verdict === 'reject') return { ok: false, output: 'The user declined publishing. Nothing was deployed.', meta: { rejected: true, publish: card(ctx, { steps: { vercel: { status: 'skipped', detail: 'Declined' } } }) } };
787
+
788
+ const fail = (msg, extra = {}) => ({ ok: false, output: hideSecrets(msg, [v.accessToken]), meta: { label: 'publish failed', ...extra, publish: card(ctx, { steps: { vercel: { status: 'error', detail: hideSecrets(msg, [v.accessToken]).split('\n')[0].slice(0, 220) } } }) } });
789
+ let project, deployment, final;
790
+ try {
791
+ card(ctx, { steps: { vercel: { status: 'active', detail: 'Preparing the project' } } });
792
+ project = await ensureVercelProject(v, name, stack);
793
+ state = saveState(ctx.cwd, { vercel: { projectId: project.id, projectName: project.name } });
794
+ await setVercelEnv(v, project.id, env);
795
+ const uploaded = await uploadFiles(v, root, files);
796
+ deployment = await createDeployment(v, { name: project.name, projectId: project.id, files: uploaded, stack });
797
+ final = await waitForDeployment(v, deployment.id, ctx.signal);
798
+ } catch (e) {
799
+ return fail(`Publishing to Vercel failed: ${e.message}`);
800
+ }
801
+
802
+ if (final.readyState === 'READY') {
803
+ const url = await liveUrl(v, project, final).catch(() => `https://${project.name}.vercel.app`);
804
+ saveState(ctx.cwd, { vercel: { url, lastDeploymentId: deployment.id, lastPublishedAt: new Date().toISOString() } });
805
+ ctx.publishBuildFailures = 0;
806
+ const gh = readState(ctx.cwd).github;
807
+ return {
808
+ ok: true,
809
+ output: `Published. The site is live at ${url} (Vercel project "${project.name}", deployment ${deployment.id} is READY).` +
810
+ (gh?.linked ? ` GitHub ${gh.repo} is linked, so every push to ${gh.branch || 'the main branch'} deploys on its own.` : '') +
811
+ ' Give the user this link.',
812
+ meta: { label: url, liveUrl: url, publish: card(ctx, { url, steps: { vercel: { status: 'done', detail: project.name, url: final.inspectorUrl || 'https://vercel.com/dashboard' }, live: { status: 'done', detail: url } } }) },
813
+ };
814
+ }
815
+ if (final.readyState === 'ERROR') {
816
+ ctx.publishBuildFailures = (ctx.publishBuildFailures || 0) + 1;
817
+ const log = hideSecrets(await buildLog(v, deployment.id).catch(() => ''), [v.accessToken]);
818
+ const first = ctx.publishBuildFailures === 1;
819
+ return fail(
820
+ `The build failed on Vercel (deployment ${deployment.id}).${log ? `\nBuild log, last lines:\n${log}` : ''}\n` +
821
+ (first
822
+ ? 'Find the cause in the project (the log above names it), fix it, check it locally if you can, then call publish_deploy again. You get one retry.'
823
+ : 'It failed again after a fix. Stop retrying. Tell the user in plain words what failed and what you tried.'),
824
+ { buildFailed: true },
825
+ );
826
+ }
827
+ if (final.readyState === 'TIMEOUT') return fail(`Vercel is still building deployment ${deployment.id} after ${Math.round(DEPLOY_TIMEOUT_MS() / 60000)} minutes. Check it with vercel_api GET /v13/deployments/${deployment.id} before telling the user anything.`);
828
+ return fail(`The deployment was canceled (${deployment.id}).`);
829
+ }
830
+
831
+ async function publish_github(args, ctx) {
832
+ if (!wantsPublish(ctx.userMessage) && !/github|repo|push/i.test(String(ctx.userMessage || ''))) return { ok: false, output: NOT_ASKED };
833
+ const root = rootOf(args, ctx);
834
+ const gh = integration('github');
835
+ if (!gh.accessToken) return notConnected('github', ctx);
836
+ const v = integration('vercel');
837
+ if (!v.accessToken) return notConnected('vercel', ctx);
838
+ const secrets = [gh.accessToken, v.accessToken];
839
+ const fail = (msg) => ({ ok: false, output: hideSecrets(msg, secrets), meta: { label: 'GitHub failed', publish: card(ctx, { steps: { github: { status: 'error', detail: hideSecrets(msg, secrets).slice(0, 220) } } }) } });
840
+
841
+ const isRepo = fs.existsSync(path.join(root, '.git'));
842
+ let remote = '';
843
+ if (isRepo) { try { remote = await git(['remote', 'get-url', 'origin'], root); } catch {} }
844
+ const existing = githubRepoFromUrl(remote);
845
+ const repoName = sanitizeRepoName(args.name || path.basename(root));
846
+ const isPrivate = !/^(false|no|public)$/i.test(String(args.private || ''));
847
+ const verdict = await ctx.approve({
848
+ tool: 'publish_github',
849
+ title: existing ? `Push to ${existing} and link it to Vercel` : `Create GitHub repo "${repoName}" and link it to Vercel`,
850
+ detail: (existing ? `Commits any changes and pushes to ${existing}.` : `Creates a ${isPrivate ? 'private' : 'public'} repo under ${gh.userName || 'your account'}, commits the project and pushes it.`) +
851
+ '\n.env files are kept out of git. Then the Vercel project is linked to the repo, so every push deploys on its own.',
852
+ danger: false,
853
+ });
854
+ if (verdict === 'reject') return { ok: false, output: 'The user declined the GitHub step. Deploy directly with publish_deploy.', meta: { rejected: true, publish: card(ctx, { steps: { github: { status: 'skipped', detail: 'Direct deploy' } } }) } };
855
+
856
+ card(ctx, { steps: { github: { status: 'active', detail: existing ? `Pushing to ${existing}` : 'Creating the repo' } } });
857
+ let full = existing;
858
+ let branch = 'main';
859
+ try {
860
+ ensureGitignore(root);
861
+ if (!isRepo) await git(['init', '-b', 'main'], root).catch(() => git(['init'], root));
862
+ const name = await git(['config', 'user.name'], root).catch(() => '');
863
+ const email = await git(['config', 'user.email'], root).catch(() => '');
864
+ const ident = name && email ? [] : ['-c', `user.name=${name || gh.userName || 'Codeply'}`, '-c', `user.email=${email || `${gh.userName || 'codeply'}@users.noreply.github.com`}`];
865
+ await git(['add', '-A'], root);
866
+ const status = await git(['status', '--porcelain'], root);
867
+ let hasHead = true;
868
+ try { await git(['rev-parse', 'HEAD'], root); } catch { hasHead = false; }
869
+ if (status || !hasHead) await git([...ident, 'commit', '-m', hasHead ? 'Publish with Codeply' : 'First commit', '--allow-empty'], root);
870
+ branch = (await git(['rev-parse', '--abbrev-ref', 'HEAD'], root)) || 'main';
871
+ if (!existing) {
872
+ const made = await oauth.githubCreateRepo(gh.accessToken, repoName, { private: isPrivate });
873
+ full = made.full_name;
874
+ await git(['remote', remote ? 'set-url' : 'add', 'origin', made.clone_url], root);
875
+ }
876
+ // The token rides an HTTP header for this one push only, never the
877
+ // remote URL, so it is not left in .git/config.
878
+ const basic = Buffer.from(`x-access-token:${gh.accessToken}`).toString('base64');
879
+ await git(['-c', `http.extraHeader=Authorization: Basic ${basic}`, 'push', '-u', 'origin', branch], root);
880
+ } catch (e) {
881
+ const why = /already exists/i.test(e.message) ? `A repo named "${repoName}" already exists on your GitHub. Pick another <name>.` : (e.output || e.message);
882
+ return fail(`The GitHub step failed: ${String(why).slice(0, 600)}`);
883
+ }
884
+
885
+ let project, link;
886
+ try {
887
+ const stack = detectStack(root);
888
+ const state = readState(ctx.cwd);
889
+ project = await ensureVercelProject(v, sanitizeProjectName(state.vercel?.projectName || path.basename(root)), stack);
890
+ saveState(ctx.cwd, { vercel: { projectId: project.id, projectName: project.name } });
891
+ link = await linkGitRepo(v, project.id, full);
892
+ } catch (e) {
893
+ link = { linked: false, why: e.message };
894
+ }
895
+ saveState(ctx.cwd, { github: { choice: 'yes', repo: full, branch, linked: link.linked } });
896
+ if (!link.linked) {
897
+ return {
898
+ ok: true,
899
+ output: `Pushed the code to ${full} (branch ${branch}), but linking it to Vercel did not work: ${link.why} Carry on with publish_deploy now; pushes will deploy on their own once the link works.`,
900
+ meta: { label: full, publish: card(ctx, { steps: { github: { status: 'error', detail: link.why.slice(0, 220), url: `https://github.com/${full}` } } }) },
901
+ };
902
+ }
903
+ return {
904
+ ok: true,
905
+ output: `Pushed the code to ${full} (branch ${branch}) and linked it to the Vercel project "${project.name}". From now on every push to ${branch} deploys on its own. Now call publish_deploy for the first deploy.`,
906
+ meta: { label: full, publish: card(ctx, { steps: { github: { status: 'done', detail: `${full}, auto-deploy on`, url: `https://github.com/${full}` } } }) },
907
+ };
908
+ }
909
+
910
+ return { publish_check, publish_connect, supabase_setup, supabase_schema, publish_deploy, publish_github };
911
+ }
912
+
913
+ module.exports = {
914
+ makePublishTools, wantsPublish, detectStack, detectDatabaseNeed, envNames, writeSupabaseConfig, prepareSchemaSql,
915
+ listProjectFiles, readState, saveState, githubRepoFromUrl, ensureGitignore, STATE_FILE,
916
+ };