dirpush 0.0.0-stage → 0.1.0

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 (895) hide show
  1. package/.agents/skills/dirpush/SKILL.md +92 -0
  2. package/.agents/skills/dirpush/references/assets.md +48 -0
  3. package/.agents/skills/dirpush/references/authentication.md +59 -0
  4. package/.agents/skills/dirpush/references/recovery.md +34 -0
  5. package/.agents/skills/dirpush/references/submissions.md +43 -0
  6. package/LICENSE +21 -0
  7. package/README.md +66 -2
  8. package/THIRD_PARTY_NOTICES.md +10 -0
  9. package/bin/dirpush.cjs +13 -0
  10. package/drizzle/0000_init.sql +59 -0
  11. package/drizzle/0001_jazzy_masked_marvel.sql +9 -0
  12. package/drizzle/0002_loose_may_parker.sql +9 -0
  13. package/drizzle/0003_modern_lilandra.sql +6 -0
  14. package/drizzle/0004_deep_master_chief.sql +11 -0
  15. package/drizzle/meta/0000_snapshot.json +404 -0
  16. package/drizzle/meta/0001_snapshot.json +466 -0
  17. package/drizzle/meta/0002_snapshot.json +527 -0
  18. package/drizzle/meta/0003_snapshot.json +566 -0
  19. package/drizzle/meta/0004_snapshot.json +642 -0
  20. package/drizzle/meta/_journal.json +41 -0
  21. package/examples/workspace/identity.yaml +6 -0
  22. package/examples/workspace/permissions.yaml +29 -0
  23. package/examples/workspace/product.yaml +29 -0
  24. package/package.json +76 -4
  25. package/registry/LICENSE +396 -0
  26. package/registry/directories/1000-tools/directory.yaml +55 -0
  27. package/registry/directories/1000-tools/playbook.md +5 -0
  28. package/registry/directories/10015-io/directory.yaml +66 -0
  29. package/registry/directories/10015-io/playbook.md +9 -0
  30. package/registry/directories/10words/directory.yaml +53 -0
  31. package/registry/directories/10words/playbook.md +8 -0
  32. package/registry/directories/600-tools/directory.yaml +48 -0
  33. package/registry/directories/600-tools/playbook.md +1 -0
  34. package/registry/directories/a1bookmarks/directory.yaml +42 -0
  35. package/registry/directories/aback-launch/directory.yaml +54 -0
  36. package/registry/directories/aback-launch/playbook.md +1 -0
  37. package/registry/directories/acid-tools/directory.yaml +51 -0
  38. package/registry/directories/acid-tools/playbook.md +8 -0
  39. package/registry/directories/agenthunter/directory.yaml +53 -0
  40. package/registry/directories/ai-agents-directory/directory.yaml +49 -0
  41. package/registry/directories/ai-agents-directory/playbook.md +6 -0
  42. package/registry/directories/ai-agents-list/directory.yaml +46 -0
  43. package/registry/directories/ai-agents-list/playbook.md +1 -0
  44. package/registry/directories/ai-agents-listing/directory.yaml +76 -0
  45. package/registry/directories/ai-directories/directory.yaml +45 -0
  46. package/registry/directories/ai-directories/playbook.md +7 -0
  47. package/registry/directories/ai-dust/directory.yaml +68 -0
  48. package/registry/directories/ai-dust/playbook.md +1 -0
  49. package/registry/directories/ai-nav/directory.yaml +54 -0
  50. package/registry/directories/ai-nav/playbook.md +1 -0
  51. package/registry/directories/ai-respo/directory.yaml +43 -0
  52. package/registry/directories/ai-respo/playbook.md +1 -0
  53. package/registry/directories/ai-superhub/directory.yaml +65 -0
  54. package/registry/directories/ai-tool-list/directory.yaml +44 -0
  55. package/registry/directories/ai-tool-list/playbook.md +1 -0
  56. package/registry/directories/ai-tool-saver/directory.yaml +52 -0
  57. package/registry/directories/ai-tool-saver/playbook.md +1 -0
  58. package/registry/directories/ai-toolex/directory.yaml +46 -0
  59. package/registry/directories/ai-toolex/playbook.md +1 -0
  60. package/registry/directories/ai-toolz-dir/directory.yaml +60 -0
  61. package/registry/directories/ai-website-directory/directory.yaml +66 -0
  62. package/registry/directories/ai-website-directory/playbook.md +1 -0
  63. package/registry/directories/ai-x-collection/directory.yaml +45 -0
  64. package/registry/directories/ai-x-collection/playbook.md +1 -0
  65. package/registry/directories/aiagentstore/directory.yaml +52 -0
  66. package/registry/directories/aiagentstore/playbook.md +1 -0
  67. package/registry/directories/aidirs-best/directory.yaml +46 -0
  68. package/registry/directories/aifindy/directory.yaml +44 -0
  69. package/registry/directories/aifindy/playbook.md +1 -0
  70. package/registry/directories/aihubs/directory.yaml +44 -0
  71. package/registry/directories/aihubs/playbook.md +5 -0
  72. package/registry/directories/aihuntlist/directory.yaml +43 -0
  73. package/registry/directories/aihuntlist/playbook.md +6 -0
  74. package/registry/directories/aihustle/directory.yaml +51 -0
  75. package/registry/directories/aihustle/playbook.md +5 -0
  76. package/registry/directories/ailaunch-space/directory.yaml +44 -0
  77. package/registry/directories/ailaunch-space/playbook.md +6 -0
  78. package/registry/directories/ailib/directory.yaml +64 -0
  79. package/registry/directories/ailib/playbook.md +1 -0
  80. package/registry/directories/aimatch-pro/directory.yaml +42 -0
  81. package/registry/directories/aimatch-pro/playbook.md +1 -0
  82. package/registry/directories/ainave/directory.yaml +71 -0
  83. package/registry/directories/ainave/playbook.md +10 -0
  84. package/registry/directories/aitechviral/directory.yaml +47 -0
  85. package/registry/directories/aitechviral/playbook.md +10 -0
  86. package/registry/directories/aitogrow/directory.yaml +52 -0
  87. package/registry/directories/aitogrow/playbook.md +6 -0
  88. package/registry/directories/aitoolhunt/directory.yaml +43 -0
  89. package/registry/directories/aitoolhunt/playbook.md +5 -0
  90. package/registry/directories/aitools-fyi/directory.yaml +52 -0
  91. package/registry/directories/aitools-inc/directory.yaml +48 -0
  92. package/registry/directories/aitools-inc/playbook.md +1 -0
  93. package/registry/directories/aitoolsdirectory/directory.yaml +73 -0
  94. package/registry/directories/aitoolsdirectory/playbook.md +8 -0
  95. package/registry/directories/aitoolshunt/directory.yaml +64 -0
  96. package/registry/directories/aitoolshunt/playbook.md +6 -0
  97. package/registry/directories/aitoolslist/directory.yaml +59 -0
  98. package/registry/directories/aitoolslist/playbook.md +6 -0
  99. package/registry/directories/aitoolsmagazine/directory.yaml +46 -0
  100. package/registry/directories/aitoolsmagazine/playbook.md +5 -0
  101. package/registry/directories/aitoolsmarketer/directory.yaml +56 -0
  102. package/registry/directories/aitoolsmarketer/playbook.md +7 -0
  103. package/registry/directories/aitooltrek/directory.yaml +47 -0
  104. package/registry/directories/aitooltrek/playbook.md +6 -0
  105. package/registry/directories/aitop10-tools/directory.yaml +44 -0
  106. package/registry/directories/aitop10-tools/playbook.md +5 -0
  107. package/registry/directories/aitoptools/directory.yaml +74 -0
  108. package/registry/directories/aitoptools/playbook.md +1 -0
  109. package/registry/directories/aiwithme/directory.yaml +54 -0
  110. package/registry/directories/aiwithme/playbook.md +10 -0
  111. package/registry/directories/all-in-ai-tools/directory.yaml +51 -0
  112. package/registry/directories/all-in-ai-tools/playbook.md +1 -0
  113. package/registry/directories/allmcps/directory.yaml +55 -0
  114. package/registry/directories/allmcps/playbook.md +7 -0
  115. package/registry/directories/alltopstartups/directory.yaml +43 -0
  116. package/registry/directories/altern-ai/directory.yaml +50 -0
  117. package/registry/directories/altern-ai/playbook.md +6 -0
  118. package/registry/directories/alternative-tools/directory.yaml +46 -0
  119. package/registry/directories/alternative-tools/playbook.md +4 -0
  120. package/registry/directories/alternativeto/directory.yaml +48 -0
  121. package/registry/directories/alternativeto/playbook.md +8 -0
  122. package/registry/directories/ant-directory/directory.yaml +52 -0
  123. package/registry/directories/ant-directory/playbook.md +1 -0
  124. package/registry/directories/app-scout/directory.yaml +60 -0
  125. package/registry/directories/app-scout/playbook.md +5 -0
  126. package/registry/directories/app-stack-builder/directory.yaml +71 -0
  127. package/registry/directories/appalist/directory.yaml +46 -0
  128. package/registry/directories/appalist/playbook.md +4 -0
  129. package/registry/directories/apprater/directory.yaml +65 -0
  130. package/registry/directories/apprater/playbook.md +1 -0
  131. package/registry/directories/appsumo/directory.yaml +45 -0
  132. package/registry/directories/appsumo/playbook.md +1 -0
  133. package/registry/directories/ask-ai-for-it/directory.yaml +45 -0
  134. package/registry/directories/ask-ai-for-it/playbook.md +1 -0
  135. package/registry/directories/auraplusplus/directory.yaml +56 -0
  136. package/registry/directories/auraplusplus/playbook.md +6 -0
  137. package/registry/directories/awesome-mcp-list/directory.yaml +45 -0
  138. package/registry/directories/awesome-mcp-list/playbook.md +7 -0
  139. package/registry/directories/awesome-mcp-lists/directory.yaml +44 -0
  140. package/registry/directories/awesome-mcp-lists/playbook.md +6 -0
  141. package/registry/directories/awesome-remote-mcp-servers/directory.yaml +46 -0
  142. package/registry/directories/awesome-remote-mcp-servers/playbook.md +8 -0
  143. package/registry/directories/awesome-tools/directory.yaml +55 -0
  144. package/registry/directories/awesome-tools/playbook.md +8 -0
  145. package/registry/directories/awesomeindie/directory.yaml +51 -0
  146. package/registry/directories/awesomeindie/playbook.md +7 -0
  147. package/registry/directories/axis-launch/directory.yaml +64 -0
  148. package/registry/directories/b2b-saas-market/directory.yaml +63 -0
  149. package/registry/directories/bai-tools/directory.yaml +53 -0
  150. package/registry/directories/bai-tools/playbook.md +7 -0
  151. package/registry/directories/basedtools/directory.yaml +59 -0
  152. package/registry/directories/basedtools/playbook.md +1 -0
  153. package/registry/directories/best-ai-org/directory.yaml +53 -0
  154. package/registry/directories/best-ai-org/playbook.md +1 -0
  155. package/registry/directories/best-of-ai/directory.yaml +57 -0
  156. package/registry/directories/best-of-ai/playbook.md +1 -0
  157. package/registry/directories/bestaito/directory.yaml +47 -0
  158. package/registry/directories/bestaito/playbook.md +6 -0
  159. package/registry/directories/bestofai/directory.yaml +45 -0
  160. package/registry/directories/bestofai/playbook.md +7 -0
  161. package/registry/directories/bestsky-tools/directory.yaml +44 -0
  162. package/registry/directories/bestsky-tools/playbook.md +5 -0
  163. package/registry/directories/betabound/directory.yaml +63 -0
  164. package/registry/directories/betabound/playbook.md +7 -0
  165. package/registry/directories/betalist/directory.yaml +52 -0
  166. package/registry/directories/betalist/playbook.md +11 -0
  167. package/registry/directories/betterbuys/directory.yaml +55 -0
  168. package/registry/directories/betterbuys/playbook.md +7 -0
  169. package/registry/directories/betterlaunch/directory.yaml +53 -0
  170. package/registry/directories/beyond-ai-tools/directory.yaml +43 -0
  171. package/registry/directories/beyond-ai-tools/playbook.md +1 -0
  172. package/registry/directories/billion-dollar-pitch-decks/directory.yaml +64 -0
  173. package/registry/directories/billion-dollar-pitch-decks/playbook.md +4 -0
  174. package/registry/directories/browserextension-dev/directory.yaml +55 -0
  175. package/registry/directories/browserextension-dev/playbook.md +7 -0
  176. package/registry/directories/bsdirectory/directory.yaml +68 -0
  177. package/registry/directories/bsdirectory/playbook.md +5 -0
  178. package/registry/directories/buildvoyage/directory.yaml +64 -0
  179. package/registry/directories/buildvoyage/playbook.md +1 -0
  180. package/registry/directories/built-here/directory.yaml +50 -0
  181. package/registry/directories/built-here/playbook.md +6 -0
  182. package/registry/directories/builtbyme/directory.yaml +55 -0
  183. package/registry/directories/builtbyme/playbook.md +7 -0
  184. package/registry/directories/builtineu/directory.yaml +70 -0
  185. package/registry/directories/builtineu/playbook.md +7 -0
  186. package/registry/directories/businesssoftware/directory.yaml +42 -0
  187. package/registry/directories/buy-sell-startups/directory.yaml +52 -0
  188. package/registry/directories/buy-sell-startups/playbook.md +5 -0
  189. package/registry/directories/claude-mcp-directory/directory.yaml +45 -0
  190. package/registry/directories/claude-mcp-directory/playbook.md +7 -0
  191. package/registry/directories/claude-plugin-directory/directory.yaml +48 -0
  192. package/registry/directories/claude-plugin-directory/playbook.md +7 -0
  193. package/registry/directories/cocoon-ai/directory.yaml +57 -0
  194. package/registry/directories/codehype/directory.yaml +68 -0
  195. package/registry/directories/codehype/playbook.md +7 -0
  196. package/registry/directories/collectai/directory.yaml +43 -0
  197. package/registry/directories/collectai/playbook.md +5 -0
  198. package/registry/directories/commune-launchpad/directory.yaml +51 -0
  199. package/registry/directories/confettisaas/directory.yaml +51 -0
  200. package/registry/directories/confettisaas/playbook.md +1 -0
  201. package/registry/directories/crowdstax/directory.yaml +54 -0
  202. package/registry/directories/crowdstax/playbook.md +6 -0
  203. package/registry/directories/crozdesk/directory.yaml +72 -0
  204. package/registry/directories/crozdesk/playbook.md +8 -0
  205. package/registry/directories/crypto-tools-directory/directory.yaml +60 -0
  206. package/registry/directories/crypto-tools-directory/playbook.md +6 -0
  207. package/registry/directories/ctrlaltship/directory.yaml +92 -0
  208. package/registry/directories/ctrlaltship/playbook.md +6 -0
  209. package/registry/directories/dachstartups/directory.yaml +49 -0
  210. package/registry/directories/dachstartups/playbook.md +4 -0
  211. package/registry/directories/dailypings/directory.yaml +51 -0
  212. package/registry/directories/dailypings/playbook.md +6 -0
  213. package/registry/directories/dang-ai/directory.yaml +44 -0
  214. package/registry/directories/dang-ai/playbook.md +1 -0
  215. package/registry/directories/daniellaunches/directory.yaml +47 -0
  216. package/registry/directories/daniellaunches/playbook.md +6 -0
  217. package/registry/directories/datalook/directory.yaml +60 -0
  218. package/registry/directories/dayslaunch/directory.yaml +46 -0
  219. package/registry/directories/dayslaunch/playbook.md +5 -0
  220. package/registry/directories/deeplaunch/directory.yaml +46 -0
  221. package/registry/directories/deeplaunch/playbook.md +6 -0
  222. package/registry/directories/desifounder-spotlight/directory.yaml +49 -0
  223. package/registry/directories/desifounder-spotlight/playbook.md +1 -0
  224. package/registry/directories/developers-digest-mcp/directory.yaml +46 -0
  225. package/registry/directories/developers-digest-mcp/playbook.md +7 -0
  226. package/registry/directories/devhunt/directory.yaml +53 -0
  227. package/registry/directories/devhunt/playbook.md +8 -0
  228. package/registry/directories/devpages/directory.yaml +62 -0
  229. package/registry/directories/devpages/playbook.md +7 -0
  230. package/registry/directories/devresourc-es/directory.yaml +49 -0
  231. package/registry/directories/devresourc-es/playbook.md +9 -0
  232. package/registry/directories/devtools-directory/directory.yaml +63 -0
  233. package/registry/directories/devtools-directory/playbook.md +7 -0
  234. package/registry/directories/dir-hub/directory.yaml +62 -0
  235. package/registry/directories/dir-hub/playbook.md +6 -0
  236. package/registry/directories/direct2app/directory.yaml +47 -0
  237. package/registry/directories/direct2app/playbook.md +5 -0
  238. package/registry/directories/directree/directory.yaml +56 -0
  239. package/registry/directories/directree/playbook.md +7 -0
  240. package/registry/directories/dirs-cc/directory.yaml +49 -0
  241. package/registry/directories/dirs-cc/playbook.md +1 -0
  242. package/registry/directories/dododirectory/directory.yaml +46 -0
  243. package/registry/directories/dododirectory/playbook.md +6 -0
  244. package/registry/directories/dofollowtools/directory.yaml +75 -0
  245. package/registry/directories/dofollowtools/playbook.md +7 -0
  246. package/registry/directories/dropyourai/directory.yaml +73 -0
  247. package/registry/directories/dynamite-ai/directory.yaml +59 -0
  248. package/registry/directories/dynamite-ai/playbook.md +1 -0
  249. package/registry/directories/earlyhunt/directory.yaml +44 -0
  250. package/registry/directories/earlyhunt/playbook.md +6 -0
  251. package/registry/directories/ebool/directory.yaml +72 -0
  252. package/registry/directories/ebool/playbook.md +8 -0
  253. package/registry/directories/eliteai-tools/directory.yaml +45 -0
  254. package/registry/directories/eliteai-tools/playbook.md +1 -0
  255. package/registry/directories/emoji-launch/directory.yaml +60 -0
  256. package/registry/directories/emoji-launch/playbook.md +1 -0
  257. package/registry/directories/enlisted/directory.yaml +48 -0
  258. package/registry/directories/enlisted/playbook.md +7 -0
  259. package/registry/directories/eu-software-alternatives/directory.yaml +87 -0
  260. package/registry/directories/eu-software-alternatives/playbook.md +5 -0
  261. package/registry/directories/eu-vetted/directory.yaml +64 -0
  262. package/registry/directories/eu-vetted/playbook.md +1 -0
  263. package/registry/directories/eubuilt/directory.yaml +77 -0
  264. package/registry/directories/eubuilt/playbook.md +5 -0
  265. package/registry/directories/euroalternative/directory.yaml +48 -0
  266. package/registry/directories/euroalternative/playbook.md +1 -0
  267. package/registry/directories/european-tech/directory.yaml +55 -0
  268. package/registry/directories/european-tech/playbook.md +5 -0
  269. package/registry/directories/europeanapps/directory.yaml +52 -0
  270. package/registry/directories/europeanapps/playbook.md +5 -0
  271. package/registry/directories/europeanstack/directory.yaml +66 -0
  272. package/registry/directories/europeanstack/playbook.md +7 -0
  273. package/registry/directories/eurotoolsindex/directory.yaml +68 -0
  274. package/registry/directories/eurotoolsindex/playbook.md +7 -0
  275. package/registry/directories/everydev-ai/directory.yaml +50 -0
  276. package/registry/directories/everydev-ai/playbook.md +6 -0
  277. package/registry/directories/faceless-directory/directory.yaml +52 -0
  278. package/registry/directories/fastd/directory.yaml +45 -0
  279. package/registry/directories/fastd/playbook.md +1 -0
  280. package/registry/directories/fastlaunch/directory.yaml +53 -0
  281. package/registry/directories/fastlaunch/playbook.md +10 -0
  282. package/registry/directories/favors/directory.yaml +72 -0
  283. package/registry/directories/favors/playbook.md +7 -0
  284. package/registry/directories/fazier/directory.yaml +44 -0
  285. package/registry/directories/fazier/playbook.md +8 -0
  286. package/registry/directories/featuredtool/directory.yaml +51 -0
  287. package/registry/directories/featuredtool/playbook.md +1 -0
  288. package/registry/directories/find-seo-tools/directory.yaml +72 -0
  289. package/registry/directories/find-seo-tools/playbook.md +1 -0
  290. package/registry/directories/findly-tools/directory.yaml +45 -0
  291. package/registry/directories/findly-tools/playbook.md +7 -0
  292. package/registry/directories/findstack/directory.yaml +46 -0
  293. package/registry/directories/findstack/playbook.md +1 -0
  294. package/registry/directories/findyoursaas/directory.yaml +47 -0
  295. package/registry/directories/findyoursaas/playbook.md +7 -0
  296. package/registry/directories/firsto/directory.yaml +54 -0
  297. package/registry/directories/firsto/playbook.md +10 -0
  298. package/registry/directories/fosspace/directory.yaml +49 -0
  299. package/registry/directories/founders-today/directory.yaml +52 -0
  300. package/registry/directories/francesaas/directory.yaml +49 -0
  301. package/registry/directories/francesaas/playbook.md +5 -0
  302. package/registry/directories/free-startup-directories/directory.yaml +70 -0
  303. package/registry/directories/free-startup-listing/directory.yaml +59 -0
  304. package/registry/directories/free-startup-listing/playbook.md +6 -0
  305. package/registry/directories/free-tools-directory/directory.yaml +55 -0
  306. package/registry/directories/free-tools-directory/playbook.md +7 -0
  307. package/registry/directories/freeaitools-net/directory.yaml +57 -0
  308. package/registry/directories/from-europe-with-love/directory.yaml +63 -0
  309. package/registry/directories/from-europe-with-love/playbook.md +5 -0
  310. package/registry/directories/fstlaunch/directory.yaml +57 -0
  311. package/registry/directories/fstlaunch/playbook.md +6 -0
  312. package/registry/directories/futuretools/directory.yaml +67 -0
  313. package/registry/directories/futuretools/playbook.md +10 -0
  314. package/registry/directories/glama/directory.yaml +46 -0
  315. package/registry/directories/glama/playbook.md +7 -0
  316. package/registry/directories/goodaitools/directory.yaml +46 -0
  317. package/registry/directories/goodaitools/playbook.md +7 -0
  318. package/registry/directories/gptbot/directory.yaml +56 -0
  319. package/registry/directories/gptdemo/directory.yaml +56 -0
  320. package/registry/directories/gptdemo/playbook.md +7 -0
  321. package/registry/directories/hicyou/directory.yaml +51 -0
  322. package/registry/directories/hicyou/playbook.md +1 -0
  323. package/registry/directories/hightechattic/directory.yaml +49 -0
  324. package/registry/directories/hightechattic/playbook.md +6 -0
  325. package/registry/directories/howtobuysaas/directory.yaml +59 -0
  326. package/registry/directories/howtobuysaas/playbook.md +1 -0
  327. package/registry/directories/hunt0/directory.yaml +81 -0
  328. package/registry/directories/hunt0/playbook.md +1 -0
  329. package/registry/directories/huntscreens/directory.yaml +56 -0
  330. package/registry/directories/huntscreens/playbook.md +11 -0
  331. package/registry/directories/huntstartup/directory.yaml +53 -0
  332. package/registry/directories/huntstartup/playbook.md +8 -0
  333. package/registry/directories/huzzler/directory.yaml +49 -0
  334. package/registry/directories/huzzler/playbook.md +7 -0
  335. package/registry/directories/hype-star/directory.yaml +57 -0
  336. package/registry/directories/hype-star/playbook.md +6 -0
  337. package/registry/directories/idea-kiln/directory.yaml +50 -0
  338. package/registry/directories/idea-kiln/playbook.md +6 -0
  339. package/registry/directories/ignlab-launch/directory.yaml +82 -0
  340. package/registry/directories/ignlab-launch/playbook.md +7 -0
  341. package/registry/directories/indie-deals/directory.yaml +48 -0
  342. package/registry/directories/indie-deals/playbook.md +8 -0
  343. package/registry/directories/indie-hustles/directory.yaml +61 -0
  344. package/registry/directories/indie-hustles/playbook.md +1 -0
  345. package/registry/directories/indie-makers-hub/directory.yaml +50 -0
  346. package/registry/directories/indie-makers-hub/playbook.md +5 -0
  347. package/registry/directories/indie-page/directory.yaml +47 -0
  348. package/registry/directories/indie-page/playbook.md +1 -0
  349. package/registry/directories/indiefounders/directory.yaml +48 -0
  350. package/registry/directories/indiefounders/playbook.md +5 -0
  351. package/registry/directories/indiehackers/directory.yaml +47 -0
  352. package/registry/directories/indiehackers/playbook.md +1 -0
  353. package/registry/directories/indiehackerstacks/directory.yaml +45 -0
  354. package/registry/directories/indiehackerstacks/playbook.md +5 -0
  355. package/registry/directories/indiehub/directory.yaml +45 -0
  356. package/registry/directories/indiehub/playbook.md +8 -0
  357. package/registry/directories/indiehunt/directory.yaml +45 -0
  358. package/registry/directories/indiehunt/playbook.md +6 -0
  359. package/registry/directories/indieneed/directory.yaml +78 -0
  360. package/registry/directories/indieneed/playbook.md +9 -0
  361. package/registry/directories/indieproducts/directory.yaml +47 -0
  362. package/registry/directories/indieproducts/playbook.md +4 -0
  363. package/registry/directories/indiesignal/directory.yaml +47 -0
  364. package/registry/directories/indietool/directory.yaml +44 -0
  365. package/registry/directories/indietool/playbook.md +1 -0
  366. package/registry/directories/indietools/directory.yaml +69 -0
  367. package/registry/directories/insidr-ai/directory.yaml +49 -0
  368. package/registry/directories/insidr-ai/playbook.md +5 -0
  369. package/registry/directories/intelligent-tools/directory.yaml +59 -0
  370. package/registry/directories/intelligent-tools/playbook.md +1 -0
  371. package/registry/directories/inventlist/directory.yaml +71 -0
  372. package/registry/directories/inventlist/playbook.md +5 -0
  373. package/registry/directories/joinly/directory.yaml +68 -0
  374. package/registry/directories/joinly/playbook.md +5 -0
  375. package/registry/directories/junkstartups/directory.yaml +72 -0
  376. package/registry/directories/junkstartups/playbook.md +1 -0
  377. package/registry/directories/justhunt/directory.yaml +55 -0
  378. package/registry/directories/justhunt/playbook.md +7 -0
  379. package/registry/directories/kittylaunch/directory.yaml +70 -0
  380. package/registry/directories/kittylaunch/playbook.md +7 -0
  381. package/registry/directories/lachief/directory.yaml +81 -0
  382. package/registry/directories/lachief/playbook.md +1 -0
  383. package/registry/directories/landbook/directory.yaml +48 -0
  384. package/registry/directories/landbook/playbook.md +1 -0
  385. package/registry/directories/launch-community/directory.yaml +73 -0
  386. package/registry/directories/launch-community/playbook.md +1 -0
  387. package/registry/directories/launch-list/directory.yaml +52 -0
  388. package/registry/directories/launch-list/playbook.md +6 -0
  389. package/registry/directories/launchaf/directory.yaml +51 -0
  390. package/registry/directories/launchbase-india/directory.yaml +53 -0
  391. package/registry/directories/launchboard/directory.yaml +50 -0
  392. package/registry/directories/launchboard/playbook.md +6 -0
  393. package/registry/directories/launchclash/directory.yaml +47 -0
  394. package/registry/directories/launchclash/playbook.md +6 -0
  395. package/registry/directories/launched-tools/directory.yaml +48 -0
  396. package/registry/directories/launchfree/directory.yaml +58 -0
  397. package/registry/directories/launchfree/playbook.md +1 -0
  398. package/registry/directories/launchigniter/directory.yaml +60 -0
  399. package/registry/directories/launchigniter/playbook.md +9 -0
  400. package/registry/directories/launchit/directory.yaml +48 -0
  401. package/registry/directories/launchit/playbook.md +1 -0
  402. package/registry/directories/launchllama/directory.yaml +53 -0
  403. package/registry/directories/launchllama/playbook.md +7 -0
  404. package/registry/directories/launchonit/directory.yaml +74 -0
  405. package/registry/directories/launchonit/playbook.md +7 -0
  406. package/registry/directories/launchpedia-tools/directory.yaml +43 -0
  407. package/registry/directories/launchpedia-tools/playbook.md +5 -0
  408. package/registry/directories/launchscaler/directory.yaml +53 -0
  409. package/registry/directories/launchtory/directory.yaml +47 -0
  410. package/registry/directories/launchtory/playbook.md +5 -0
  411. package/registry/directories/launchvault/directory.yaml +52 -0
  412. package/registry/directories/launchvault/playbook.md +5 -0
  413. package/registry/directories/launchvibe/directory.yaml +45 -0
  414. package/registry/directories/launchvibe/playbook.md +1 -0
  415. package/registry/directories/launchvoid/directory.yaml +45 -0
  416. package/registry/directories/launchvoid/playbook.md +6 -0
  417. package/registry/directories/letslaunch/directory.yaml +53 -0
  418. package/registry/directories/letslaunch/playbook.md +1 -0
  419. package/registry/directories/libhunt/directory.yaml +50 -0
  420. package/registry/directories/libhunt/playbook.md +1 -0
  421. package/registry/directories/linezine/directory.yaml +44 -0
  422. package/registry/directories/linezine/playbook.md +1 -0
  423. package/registry/directories/linkrena/directory.yaml +58 -0
  424. package/registry/directories/linkrena/playbook.md +6 -0
  425. package/registry/directories/list-maro/directory.yaml +74 -0
  426. package/registry/directories/list-your-tool/directory.yaml +76 -0
  427. package/registry/directories/list-your-tool/playbook.md +1 -0
  428. package/registry/directories/listbulb/directory.yaml +48 -0
  429. package/registry/directories/listbulb/playbook.md +1 -0
  430. package/registry/directories/listing-sh/directory.yaml +70 -0
  431. package/registry/directories/llm-relevance/directory.yaml +67 -0
  432. package/registry/directories/llm-relevance/playbook.md +1 -0
  433. package/registry/directories/llms-txt-hub/directory.yaml +47 -0
  434. package/registry/directories/llms-txt-hub/playbook.md +6 -0
  435. package/registry/directories/llms-txt-site/directory.yaml +53 -0
  436. package/registry/directories/llms-txt-site/playbook.md +7 -0
  437. package/registry/directories/macnative/directory.yaml +47 -0
  438. package/registry/directories/macnative/playbook.md +6 -0
  439. package/registry/directories/made-with-bolt/directory.yaml +58 -0
  440. package/registry/directories/made-with-bolt/playbook.md +1 -0
  441. package/registry/directories/made-with-lovable/directory.yaml +64 -0
  442. package/registry/directories/made-with-lovable/playbook.md +1 -0
  443. package/registry/directories/magicbox/directory.yaml +51 -0
  444. package/registry/directories/magicbox/playbook.md +16 -0
  445. package/registry/directories/maidensail/directory.yaml +54 -0
  446. package/registry/directories/makeitlast/directory.yaml +63 -0
  447. package/registry/directories/makeitlast/playbook.md +8 -0
  448. package/registry/directories/makerhunt/directory.yaml +56 -0
  449. package/registry/directories/makerhunt/playbook.md +1 -0
  450. package/registry/directories/makerlist/directory.yaml +49 -0
  451. package/registry/directories/makerlist/playbook.md +7 -0
  452. package/registry/directories/mapbid/directory.yaml +46 -0
  453. package/registry/directories/marketingdb/directory.yaml +50 -0
  454. package/registry/directories/marketingdb/playbook.md +8 -0
  455. package/registry/directories/marketraa/directory.yaml +63 -0
  456. package/registry/directories/marketraa/playbook.md +1 -0
  457. package/registry/directories/mcp-market/directory.yaml +49 -0
  458. package/registry/directories/mcp-market/playbook.md +7 -0
  459. package/registry/directories/mcp-repository/directory.yaml +45 -0
  460. package/registry/directories/mcp-repository/playbook.md +6 -0
  461. package/registry/directories/mcp-so/directory.yaml +45 -0
  462. package/registry/directories/mcp-so/playbook.md +6 -0
  463. package/registry/directories/mcpservers/directory.yaml +53 -0
  464. package/registry/directories/mcpservers/playbook.md +7 -0
  465. package/registry/directories/mf8/directory.yaml +53 -0
  466. package/registry/directories/mf8/playbook.md +5 -0
  467. package/registry/directories/microlaunch/directory.yaml +48 -0
  468. package/registry/directories/microlaunch/playbook.md +1 -0
  469. package/registry/directories/microsaasexamples/directory.yaml +50 -0
  470. package/registry/directories/must-tools/directory.yaml +68 -0
  471. package/registry/directories/must-tools/playbook.md +6 -0
  472. package/registry/directories/my-launch-stash/directory.yaml +47 -0
  473. package/registry/directories/my-launch-stash/playbook.md +8 -0
  474. package/registry/directories/nachonacho/directory.yaml +47 -0
  475. package/registry/directories/navfolders/directory.yaml +48 -0
  476. package/registry/directories/navfolders/playbook.md +5 -0
  477. package/registry/directories/navs/directory.yaml +66 -0
  478. package/registry/directories/navs/playbook.md +6 -0
  479. package/registry/directories/navtools-ai/directory.yaml +51 -0
  480. package/registry/directories/navtools-ai/playbook.md +1 -0
  481. package/registry/directories/neeed-directory/directory.yaml +60 -0
  482. package/registry/directories/neeed-directory/playbook.md +10 -0
  483. package/registry/directories/neonrev/directory.yaml +43 -0
  484. package/registry/directories/neonrev/playbook.md +1 -0
  485. package/registry/directories/newaiforyou/directory.yaml +50 -0
  486. package/registry/directories/newaiforyou/playbook.md +1 -0
  487. package/registry/directories/newtool/directory.yaml +51 -0
  488. package/registry/directories/newtool/playbook.md +1 -0
  489. package/registry/directories/nextbigwhat/directory.yaml +48 -0
  490. package/registry/directories/nextbigwhat/playbook.md +5 -0
  491. package/registry/directories/nextlaunch/directory.yaml +50 -0
  492. package/registry/directories/nextlaunch/playbook.md +8 -0
  493. package/registry/directories/nicklaunches/directory.yaml +70 -0
  494. package/registry/directories/nicklaunches/playbook.md +1 -0
  495. package/registry/directories/nocodefinder/directory.yaml +45 -0
  496. package/registry/directories/nocodefinder/playbook.md +6 -0
  497. package/registry/directories/nocodelist/directory.yaml +48 -0
  498. package/registry/directories/nocodelist/playbook.md +7 -0
  499. package/registry/directories/noonlaunch/directory.yaml +51 -0
  500. package/registry/directories/noonlaunch/playbook.md +6 -0
  501. package/registry/directories/nxgn-tools/directory.yaml +49 -0
  502. package/registry/directories/nxgn-tools/playbook.md +6 -0
  503. package/registry/directories/once-tools/directory.yaml +58 -0
  504. package/registry/directories/once-tools/playbook.md +1 -0
  505. package/registry/directories/open-launch/directory.yaml +46 -0
  506. package/registry/directories/open-launch/playbook.md +1 -0
  507. package/registry/directories/openalternative/directory.yaml +62 -0
  508. package/registry/directories/openalternative/playbook.md +8 -0
  509. package/registry/directories/openhunts/directory.yaml +52 -0
  510. package/registry/directories/openhunts/playbook.md +1 -0
  511. package/registry/directories/openresources/directory.yaml +59 -0
  512. package/registry/directories/openresources/playbook.md +6 -0
  513. package/registry/directories/opensaas-directory/directory.yaml +47 -0
  514. package/registry/directories/opensourcealternative-to/directory.yaml +63 -0
  515. package/registry/directories/opensourcealternative-to/playbook.md +6 -0
  516. package/registry/directories/opentools/directory.yaml +42 -0
  517. package/registry/directories/opentools/playbook.md +4 -0
  518. package/registry/directories/osssoftware/directory.yaml +66 -0
  519. package/registry/directories/osssoftware/playbook.md +5 -0
  520. package/registry/directories/oursoftware-eu/directory.yaml +69 -0
  521. package/registry/directories/oursoftware-eu/playbook.md +1 -0
  522. package/registry/directories/peerpush/directory.yaml +48 -0
  523. package/registry/directories/peerpush/playbook.md +8 -0
  524. package/registry/directories/phyco/directory.yaml +54 -0
  525. package/registry/directories/phygital-ai-library/directory.yaml +47 -0
  526. package/registry/directories/phygital-ai-library/playbook.md +6 -0
  527. package/registry/directories/pitchground/directory.yaml +47 -0
  528. package/registry/directories/pitchground/playbook.md +5 -0
  529. package/registry/directories/pitchwall/directory.yaml +47 -0
  530. package/registry/directories/pitchwall/playbook.md +1 -0
  531. package/registry/directories/postmake/directory.yaml +56 -0
  532. package/registry/directories/postmake/playbook.md +1 -0
  533. package/registry/directories/poweredbyai/directory.yaml +57 -0
  534. package/registry/directories/poweredbyai/playbook.md +1 -0
  535. package/registry/directories/pro-launch/directory.yaml +51 -0
  536. package/registry/directories/pro-launch/playbook.md +9 -0
  537. package/registry/directories/product-hunt/directory.yaml +54 -0
  538. package/registry/directories/product-hunt/playbook.md +1 -0
  539. package/registry/directories/product-rally/directory.yaml +50 -0
  540. package/registry/directories/product-rally/playbook.md +6 -0
  541. package/registry/directories/productivity-directory/directory.yaml +47 -0
  542. package/registry/directories/productivity-directory/playbook.md +7 -0
  543. package/registry/directories/productwatch/directory.yaml +51 -0
  544. package/registry/directories/productwatch/playbook.md +7 -0
  545. package/registry/directories/proofstories/directory.yaml +47 -0
  546. package/registry/directories/proofstories/playbook.md +1 -0
  547. package/registry/directories/prouct/directory.yaml +93 -0
  548. package/registry/directories/prouct/playbook.md +7 -0
  549. package/registry/directories/public-apis/directory.yaml +47 -0
  550. package/registry/directories/public-apis/playbook.md +6 -0
  551. package/registry/directories/publishyoursaas/directory.yaml +53 -0
  552. package/registry/directories/publishyoursaas/playbook.md +7 -0
  553. package/registry/directories/rankinpublic/directory.yaml +50 -0
  554. package/registry/directories/rankinpublic/playbook.md +1 -0
  555. package/registry/directories/rankyourai/directory.yaml +50 -0
  556. package/registry/directories/rankyourai/playbook.md +1 -0
  557. package/registry/directories/resource-fyi/directory.yaml +47 -0
  558. package/registry/directories/resource-fyi/playbook.md +5 -0
  559. package/registry/directories/roozna/directory.yaml +68 -0
  560. package/registry/directories/roozna/playbook.md +1 -0
  561. package/registry/directories/saas-fyi/directory.yaml +73 -0
  562. package/registry/directories/saas-fyi/playbook.md +7 -0
  563. package/registry/directories/saas-italia/directory.yaml +58 -0
  564. package/registry/directories/saas-launches/directory.yaml +47 -0
  565. package/registry/directories/saas-launches/playbook.md +6 -0
  566. package/registry/directories/saasbison/directory.yaml +44 -0
  567. package/registry/directories/saasbison/playbook.md +8 -0
  568. package/registry/directories/saascity/directory.yaml +50 -0
  569. package/registry/directories/saascubes/directory.yaml +49 -0
  570. package/registry/directories/saascubes/playbook.md +6 -0
  571. package/registry/directories/saasfame/directory.yaml +47 -0
  572. package/registry/directories/saasfame/playbook.md +6 -0
  573. package/registry/directories/saasgenius/directory.yaml +50 -0
  574. package/registry/directories/saasgenius/playbook.md +8 -0
  575. package/registry/directories/saasgrave-launches/directory.yaml +53 -0
  576. package/registry/directories/saasgrave-launches/playbook.md +7 -0
  577. package/registry/directories/saashammer/directory.yaml +53 -0
  578. package/registry/directories/saashammer/playbook.md +6 -0
  579. package/registry/directories/saashub/directory.yaml +51 -0
  580. package/registry/directories/saashub/playbook.md +9 -0
  581. package/registry/directories/saashunt/directory.yaml +43 -0
  582. package/registry/directories/saaslineup/directory.yaml +68 -0
  583. package/registry/directories/saaslineup/playbook.md +1 -0
  584. package/registry/directories/saaslist/directory.yaml +55 -0
  585. package/registry/directories/saaslist/playbook.md +5 -0
  586. package/registry/directories/saasmag-saas1000/directory.yaml +71 -0
  587. package/registry/directories/saaspage/directory.yaml +48 -0
  588. package/registry/directories/saasplace/directory.yaml +66 -0
  589. package/registry/directories/saasplace/playbook.md +1 -0
  590. package/registry/directories/saasrow/directory.yaml +51 -0
  591. package/registry/directories/saasrow/playbook.md +1 -0
  592. package/registry/directories/saassy-board/directory.yaml +54 -0
  593. package/registry/directories/saassy-board/playbook.md +1 -0
  594. package/registry/directories/saastore/directory.yaml +56 -0
  595. package/registry/directories/saastore/playbook.md +6 -0
  596. package/registry/directories/saastr-community-agents/directory.yaml +65 -0
  597. package/registry/directories/saastr-community-agents/playbook.md +8 -0
  598. package/registry/directories/saasvault/directory.yaml +48 -0
  599. package/registry/directories/saasvault/playbook.md +6 -0
  600. package/registry/directories/scrolllaunch/directory.yaml +54 -0
  601. package/registry/directories/scrolllaunch/playbook.md +8 -0
  602. package/registry/directories/seewhatnewai/directory.yaml +53 -0
  603. package/registry/directories/sellwithboost/directory.yaml +54 -0
  604. package/registry/directories/sellwithboost/playbook.md +5 -0
  605. package/registry/directories/serchen/directory.yaml +48 -0
  606. package/registry/directories/serchen/playbook.md +8 -0
  607. package/registry/directories/shinylaunch/directory.yaml +48 -0
  608. package/registry/directories/shinylaunch/playbook.md +6 -0
  609. package/registry/directories/shipyard-hq/directory.yaml +58 -0
  610. package/registry/directories/shipyard-hq/playbook.md +1 -0
  611. package/registry/directories/shipybara/directory.yaml +48 -0
  612. package/registry/directories/shipybara/playbook.md +8 -0
  613. package/registry/directories/shipycorn/directory.yaml +47 -0
  614. package/registry/directories/shipycorn/playbook.md +6 -0
  615. package/registry/directories/showmebest-ai/directory.yaml +50 -0
  616. package/registry/directories/showmebest-ai/playbook.md +1 -0
  617. package/registry/directories/showmysites/directory.yaml +52 -0
  618. package/registry/directories/showmysites/playbook.md +7 -0
  619. package/registry/directories/sidehunt/directory.yaml +56 -0
  620. package/registry/directories/sidehunt/playbook.md +1 -0
  621. package/registry/directories/sideprojectors/directory.yaml +55 -0
  622. package/registry/directories/sideprojectors/playbook.md +1 -0
  623. package/registry/directories/sideprojects/directory.yaml +52 -0
  624. package/registry/directories/sideprojects/playbook.md +1 -0
  625. package/registry/directories/sitepatent/directory.yaml +58 -0
  626. package/registry/directories/sitepatent/playbook.md +1 -0
  627. package/registry/directories/small-business-marketing-tools/directory.yaml +67 -0
  628. package/registry/directories/small-business-marketing-tools/playbook.md +1 -0
  629. package/registry/directories/smol-launch/directory.yaml +44 -0
  630. package/registry/directories/smol-launch/playbook.md +9 -0
  631. package/registry/directories/software-como-servicio/directory.yaml +64 -0
  632. package/registry/directories/software-como-servicio/playbook.md +6 -0
  633. package/registry/directories/software-finder/directory.yaml +47 -0
  634. package/registry/directories/software-finder/playbook.md +6 -0
  635. package/registry/directories/softwaresuggest/directory.yaml +68 -0
  636. package/registry/directories/softwaresuggest/playbook.md +6 -0
  637. package/registry/directories/solo-launches/directory.yaml +48 -0
  638. package/registry/directories/solo-launches/playbook.md +6 -0
  639. package/registry/directories/sololaunch/directory.yaml +69 -0
  640. package/registry/directories/sourceforge/directory.yaml +75 -0
  641. package/registry/directories/sourceforge/playbook.md +9 -0
  642. package/registry/directories/spacerrapps/directory.yaml +53 -0
  643. package/registry/directories/spacerrapps/playbook.md +6 -0
  644. package/registry/directories/speartip/directory.yaml +48 -0
  645. package/registry/directories/speartip/playbook.md +1 -0
  646. package/registry/directories/spiff-store/directory.yaml +46 -0
  647. package/registry/directories/spiff-store/playbook.md +6 -0
  648. package/registry/directories/spot-startups/directory.yaml +48 -0
  649. package/registry/directories/spotsaas/directory.yaml +45 -0
  650. package/registry/directories/squidir/directory.yaml +60 -0
  651. package/registry/directories/squidir/playbook.md +5 -0
  652. package/registry/directories/stackboo/directory.yaml +49 -0
  653. package/registry/directories/stackboo/playbook.md +6 -0
  654. package/registry/directories/stackledge/directory.yaml +84 -0
  655. package/registry/directories/stackledge/playbook.md +8 -0
  656. package/registry/directories/startup-alternatives/directory.yaml +68 -0
  657. package/registry/directories/startup-alternatives/playbook.md +5 -0
  658. package/registry/directories/startup-benchmarks/directory.yaml +59 -0
  659. package/registry/directories/startup-benchmarks/playbook.md +1 -0
  660. package/registry/directories/startup-collections/directory.yaml +56 -0
  661. package/registry/directories/startup-collections/playbook.md +5 -0
  662. package/registry/directories/startup-europe/directory.yaml +52 -0
  663. package/registry/directories/startup-europe/playbook.md +1 -0
  664. package/registry/directories/startup-fame/directory.yaml +51 -0
  665. package/registry/directories/startup-fame/playbook.md +8 -0
  666. package/registry/directories/startup-fast/directory.yaml +52 -0
  667. package/registry/directories/startup-fast/playbook.md +9 -0
  668. package/registry/directories/startup-list/directory.yaml +81 -0
  669. package/registry/directories/startup-list/playbook.md +8 -0
  670. package/registry/directories/startup-listing/directory.yaml +77 -0
  671. package/registry/directories/startup-listing/playbook.md +1 -0
  672. package/registry/directories/startup-og/directory.yaml +52 -0
  673. package/registry/directories/startup-ranking/directory.yaml +51 -0
  674. package/registry/directories/startup-ranking/playbook.md +1 -0
  675. package/registry/directories/startup-spotlight/directory.yaml +50 -0
  676. package/registry/directories/startup-spotlight/playbook.md +5 -0
  677. package/registry/directories/startup88/directory.yaml +46 -0
  678. package/registry/directories/startup88/playbook.md +1 -0
  679. package/registry/directories/startupaccelerator/directory.yaml +76 -0
  680. package/registry/directories/startupaccelerator/playbook.md +7 -0
  681. package/registry/directories/startupbase/directory.yaml +44 -0
  682. package/registry/directories/startupbase/playbook.md +1 -0
  683. package/registry/directories/startupbeats/directory.yaml +48 -0
  684. package/registry/directories/startupbeats/playbook.md +6 -0
  685. package/registry/directories/startupdirectory/directory.yaml +50 -0
  686. package/registry/directories/startupdirectory/playbook.md +1 -0
  687. package/registry/directories/startupinspire/directory.yaml +52 -0
  688. package/registry/directories/startupinspire/playbook.md +8 -0
  689. package/registry/directories/startuplibrary/directory.yaml +46 -0
  690. package/registry/directories/startuplibrary/playbook.md +1 -0
  691. package/registry/directories/startuplist-africa/directory.yaml +45 -0
  692. package/registry/directories/startuplist-africa/playbook.md +6 -0
  693. package/registry/directories/startups-gallery/directory.yaml +61 -0
  694. package/registry/directories/startups-gallery/playbook.md +5 -0
  695. package/registry/directories/startupsfm/directory.yaml +64 -0
  696. package/registry/directories/startupsfm/playbook.md +9 -0
  697. package/registry/directories/startupsfyi/directory.yaml +51 -0
  698. package/registry/directories/startupsfyi/playbook.md +4 -0
  699. package/registry/directories/startupslab/directory.yaml +61 -0
  700. package/registry/directories/startupslab/playbook.md +8 -0
  701. package/registry/directories/startuptile/directory.yaml +51 -0
  702. package/registry/directories/startuptile/playbook.md +5 -0
  703. package/registry/directories/startuptrusted/directory.yaml +46 -0
  704. package/registry/directories/startuptrusted/playbook.md +1 -0
  705. package/registry/directories/startuups/directory.yaml +50 -0
  706. package/registry/directories/steemhunt/directory.yaml +46 -0
  707. package/registry/directories/steemhunt/playbook.md +5 -0
  708. package/registry/directories/stellar-launch/directory.yaml +55 -0
  709. package/registry/directories/stellar-launch/playbook.md +1 -0
  710. package/registry/directories/stork-ai/directory.yaml +49 -0
  711. package/registry/directories/stork-ai/playbook.md +3 -0
  712. package/registry/directories/stork-mcp/directory.yaml +52 -0
  713. package/registry/directories/stork-mcp/playbook.md +3 -0
  714. package/registry/directories/submitaitools/directory.yaml +57 -0
  715. package/registry/directories/submitaitools/playbook.md +19 -0
  716. package/registry/directories/submithunt/directory.yaml +49 -0
  717. package/registry/directories/submithunt/playbook.md +9 -0
  718. package/registry/directories/submitmysaas/directory.yaml +51 -0
  719. package/registry/directories/submitmysaas/playbook.md +6 -0
  720. package/registry/directories/submitstartup/directory.yaml +73 -0
  721. package/registry/directories/sumodir/directory.yaml +47 -0
  722. package/registry/directories/sumodir/playbook.md +7 -0
  723. package/registry/directories/superlaunch/directory.yaml +64 -0
  724. package/registry/directories/superlaunch/playbook.md +7 -0
  725. package/registry/directories/techboard-australia/directory.yaml +54 -0
  726. package/registry/directories/techboard-australia/playbook.md +5 -0
  727. package/registry/directories/technologyadvice/directory.yaml +65 -0
  728. package/registry/directories/technologyadvice/playbook.md +8 -0
  729. package/registry/directories/tekpon/directory.yaml +44 -0
  730. package/registry/directories/tensorblock-mcp/directory.yaml +46 -0
  731. package/registry/directories/tensorblock-mcp/playbook.md +7 -0
  732. package/registry/directories/thankjohn/directory.yaml +42 -0
  733. package/registry/directories/thankjohn/playbook.md +1 -0
  734. package/registry/directories/that-ai-collection/directory.yaml +52 -0
  735. package/registry/directories/the-ai-library/directory.yaml +46 -0
  736. package/registry/directories/the-ai-library/playbook.md +1 -0
  737. package/registry/directories/the-next-ai/directory.yaml +62 -0
  738. package/registry/directories/the-popular-apps/directory.yaml +43 -0
  739. package/registry/directories/the-popular-apps/playbook.md +1 -0
  740. package/registry/directories/the-startup-inc/directory.yaml +90 -0
  741. package/registry/directories/the-startup-inc/playbook.md +4 -0
  742. package/registry/directories/the-startup-project/directory.yaml +67 -0
  743. package/registry/directories/the-startup-project/playbook.md +5 -0
  744. package/registry/directories/the-tool-directory/directory.yaml +75 -0
  745. package/registry/directories/thedevtoolsdir/directory.yaml +57 -0
  746. package/registry/directories/thedevtoolsdir/playbook.md +6 -0
  747. package/registry/directories/theres-an-ai/directory.yaml +44 -0
  748. package/registry/directories/theres-an-ai/playbook.md +1 -0
  749. package/registry/directories/theresanaiforthat/directory.yaml +51 -0
  750. package/registry/directories/theresanaiforthat/playbook.md +1 -0
  751. package/registry/directories/thesaasdir/directory.yaml +44 -0
  752. package/registry/directories/thesaasdir/playbook.md +6 -0
  753. package/registry/directories/tinylaunch/directory.yaml +50 -0
  754. package/registry/directories/tinylaunch/playbook.md +8 -0
  755. package/registry/directories/tinystartups/directory.yaml +61 -0
  756. package/registry/directories/tinystartups/playbook.md +6 -0
  757. package/registry/directories/tipseason/directory.yaml +68 -0
  758. package/registry/directories/tolodora/directory.yaml +59 -0
  759. package/registry/directories/tool-commons/directory.yaml +46 -0
  760. package/registry/directories/tool-commons/playbook.md +1 -0
  761. package/registry/directories/tool-directory-ai/directory.yaml +64 -0
  762. package/registry/directories/tool-directory-ai/playbook.md +5 -0
  763. package/registry/directories/tool-finder/directory.yaml +64 -0
  764. package/registry/directories/tool-finder/playbook.md +6 -0
  765. package/registry/directories/tooldirs/directory.yaml +64 -0
  766. package/registry/directories/tooldirs/playbook.md +7 -0
  767. package/registry/directories/toolfame/directory.yaml +47 -0
  768. package/registry/directories/toolfame/playbook.md +6 -0
  769. package/registry/directories/toolfio/directory.yaml +48 -0
  770. package/registry/directories/toolfio/playbook.md +7 -0
  771. package/registry/directories/toolfolio/directory.yaml +54 -0
  772. package/registry/directories/toolfolio/playbook.md +1 -0
  773. package/registry/directories/toolfound/directory.yaml +66 -0
  774. package/registry/directories/toolify/directory.yaml +52 -0
  775. package/registry/directories/toolify/playbook.md +8 -0
  776. package/registry/directories/toolkitly/directory.yaml +46 -0
  777. package/registry/directories/toolkitly/playbook.md +1 -0
  778. package/registry/directories/toollist-ai/directory.yaml +60 -0
  779. package/registry/directories/toollist-ai/playbook.md +1 -0
  780. package/registry/directories/toolomio/directory.yaml +55 -0
  781. package/registry/directories/toolomio/playbook.md +5 -0
  782. package/registry/directories/toolpasta/directory.yaml +46 -0
  783. package/registry/directories/toolpasta/playbook.md +1 -0
  784. package/registry/directories/toolpilot-ai/directory.yaml +77 -0
  785. package/registry/directories/toolpilot-ai/playbook.md +8 -0
  786. package/registry/directories/toolpromote/directory.yaml +81 -0
  787. package/registry/directories/toolrain/directory.yaml +48 -0
  788. package/registry/directories/toolrain/playbook.md +1 -0
  789. package/registry/directories/tools-ai-online/directory.yaml +65 -0
  790. package/registry/directories/tools-ai-online/playbook.md +7 -0
  791. package/registry/directories/tools-cafe/directory.yaml +51 -0
  792. package/registry/directories/tools-cafe/playbook.md +6 -0
  793. package/registry/directories/toolscout/directory.yaml +45 -0
  794. package/registry/directories/toolscout/playbook.md +5 -0
  795. package/registry/directories/toolsfine/directory.yaml +58 -0
  796. package/registry/directories/toolsfine/playbook.md +9 -0
  797. package/registry/directories/toolsnocode/directory.yaml +48 -0
  798. package/registry/directories/toolsnocode/playbook.md +6 -0
  799. package/registry/directories/tooltrove/directory.yaml +71 -0
  800. package/registry/directories/tooltrove/playbook.md +9 -0
  801. package/registry/directories/toolveyor/directory.yaml +44 -0
  802. package/registry/directories/toolveyor/playbook.md +1 -0
  803. package/registry/directories/toolwise/directory.yaml +67 -0
  804. package/registry/directories/toolwise/playbook.md +7 -0
  805. package/registry/directories/toools-design/directory.yaml +64 -0
  806. package/registry/directories/toools-design/playbook.md +8 -0
  807. package/registry/directories/topai-tools/directory.yaml +43 -0
  808. package/registry/directories/topai-tools/playbook.md +1 -0
  809. package/registry/directories/traffiklist/directory.yaml +55 -0
  810. package/registry/directories/traffiklist/playbook.md +6 -0
  811. package/registry/directories/trendy-startups/directory.yaml +49 -0
  812. package/registry/directories/trendy-startups/playbook.md +1 -0
  813. package/registry/directories/trillionagent/directory.yaml +57 -0
  814. package/registry/directories/trillionagent/playbook.md +1 -0
  815. package/registry/directories/truerev/directory.yaml +83 -0
  816. package/registry/directories/truerev/playbook.md +12 -0
  817. package/registry/directories/trustmrr/directory.yaml +43 -0
  818. package/registry/directories/trustradius/directory.yaml +67 -0
  819. package/registry/directories/trustradius/playbook.md +9 -0
  820. package/registry/directories/trylaunch/directory.yaml +51 -0
  821. package/registry/directories/trylaunch/playbook.md +1 -0
  822. package/registry/directories/tulimoa/directory.yaml +65 -0
  823. package/registry/directories/tulimoa/playbook.md +7 -0
  824. package/registry/directories/turbo0/directory.yaml +49 -0
  825. package/registry/directories/turbo0/playbook.md +9 -0
  826. package/registry/directories/twelve-tools/directory.yaml +47 -0
  827. package/registry/directories/twelve-tools/playbook.md +6 -0
  828. package/registry/directories/ultimate-free-tools/directory.yaml +49 -0
  829. package/registry/directories/ultimate-free-tools/playbook.md +8 -0
  830. package/registry/directories/uneed/directory.yaml +55 -0
  831. package/registry/directories/uneed/playbook.md +9 -0
  832. package/registry/directories/unitelist/directory.yaml +49 -0
  833. package/registry/directories/unitelist/playbook.md +7 -0
  834. package/registry/directories/uno-directory/directory.yaml +48 -0
  835. package/registry/directories/uno-directory/playbook.md +1 -0
  836. package/registry/directories/ventureradar/directory.yaml +47 -0
  837. package/registry/directories/ventureradar/playbook.md +5 -0
  838. package/registry/directories/vibe-coding-cloud/directory.yaml +45 -0
  839. package/registry/directories/vibe-coding-cloud/playbook.md +6 -0
  840. package/registry/directories/victrays/directory.yaml +55 -0
  841. package/registry/directories/victrays/playbook.md +8 -0
  842. package/registry/directories/visalytica/directory.yaml +59 -0
  843. package/registry/directories/wavel/directory.yaml +51 -0
  844. package/registry/directories/we-are-founders/directory.yaml +85 -0
  845. package/registry/directories/we-are-founders/playbook.md +1 -0
  846. package/registry/directories/web-store-extensions/directory.yaml +45 -0
  847. package/registry/directories/web-store-extensions/playbook.md +6 -0
  848. package/registry/directories/webcurate/directory.yaml +50 -0
  849. package/registry/directories/website-rating/directory.yaml +66 -0
  850. package/registry/directories/website-rating/playbook.md +9 -0
  851. package/registry/directories/webspot/directory.yaml +46 -0
  852. package/registry/directories/webspot/playbook.md +5 -0
  853. package/registry/directories/weekhack/directory.yaml +56 -0
  854. package/registry/directories/weekhack/playbook.md +5 -0
  855. package/registry/directories/weliketools/directory.yaml +50 -0
  856. package/registry/directories/weliketools/playbook.md +6 -0
  857. package/registry/directories/what-is-ai-tools/directory.yaml +48 -0
  858. package/registry/directories/what-is-ai-tools/playbook.md +1 -0
  859. package/registry/directories/wired-business/directory.yaml +44 -0
  860. package/registry/directories/wired-business/playbook.md +6 -0
  861. package/registry/directories/would-you-pay/directory.yaml +49 -0
  862. package/registry/directories/would-you-pay/playbook.md +6 -0
  863. package/registry/directories/xai-plugin-marketplace/directory.yaml +46 -0
  864. package/registry/directories/xai-plugin-marketplace/playbook.md +5 -0
  865. package/registry/directories/zplatform-ai/directory.yaml +75 -0
  866. package/registry/directories/zumvu/directory.yaml +50 -0
  867. package/scripts/ab +40 -0
  868. package/scripts/check-listings +8 -0
  869. package/scripts/google-login +13 -0
  870. package/scripts/show-browser +17 -0
  871. package/src/accounts.ts +81 -0
  872. package/src/audit.ts +229 -0
  873. package/src/browser.ts +123 -0
  874. package/src/capabilities.ts +62 -0
  875. package/src/cli.ts +741 -0
  876. package/src/db/index.ts +29 -0
  877. package/src/db/schema.ts +152 -0
  878. package/src/directory-kinds.ts +12 -0
  879. package/src/import-hosted.ts +156 -0
  880. package/src/jobs.ts +270 -0
  881. package/src/listings.ts +64 -0
  882. package/src/manual.ts +26 -0
  883. package/src/product-approvals.ts +31 -0
  884. package/src/public.ts +24 -0
  885. package/src/ratings.ts +71 -0
  886. package/src/registry-updates.ts +54 -0
  887. package/src/registry.ts +101 -0
  888. package/src/review.ts +58 -0
  889. package/src/server.ts +186 -0
  890. package/src/views.ts +71 -0
  891. package/src/web/app.tsx +1026 -0
  892. package/src/web/favicon.svg +4 -0
  893. package/src/web/index.html +14 -0
  894. package/src/web/style.css +301 -0
  895. package/src/workspace.ts +97 -0
@@ -0,0 +1,1026 @@
1
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { DIRECTORY_KINDS } from "../directory-kinds";
4
+
5
+ // ---- types ------------------------------------------------------------------
6
+
7
+ type Job = {
8
+ id: string; kind: string; subject: string; productId: string; status: string; step: string; worker: string | null;
9
+ attempts: number; blockType: string | null; blockMessage: string | null; lastError: string | null;
10
+ outcome: string | null; listingUrl: string | null; updatedAt: number; data: Record<string, unknown>;
11
+ };
12
+ type Event = { id: number; jobId: string | null; type: string; worker: string | null; data: Record<string, any>; at: number };
13
+ type Todo = { id: number; productId: string; jobId: string | null; message: string; url: string | null; createdAt: number };
14
+ type ProductApproval = { id: number; productId: string; action: string; message: string; createdAt: number };
15
+ type Gallery = { revision: number; productId: string; status: string; items: { path: string; description: string }[]; createdAt: number };
16
+ type Change = { id: number; directoryId: string; observation: Record<string, any>; createdAt: number };
17
+ type Actions = { blocked: Job[]; unknown: Job[]; failed: Job[]; todos: Todo[]; galleries: Gallery[]; productApprovals: ProductApproval[]; registryChanges: Change[] };
18
+ type Dir = {
19
+ id: string; name: string; website: string; submission_url: string | null;
20
+ cost: { classification: string; notes?: string }; audience: string[];
21
+ authentication: { required: boolean; methods?: string[] };
22
+ submission: { conditions: string[]; fields?: { name: string; required: boolean; limit?: number }[] };
23
+ verification: { checked_at: string; sources?: string[] };
24
+ kind?: keyof typeof DIRECTORY_KINDS; human_only?: string | null; review_time?: string;
25
+ };
26
+ type Product = { id: string; name?: string; url?: string; tagline?: string; error?: string; [k: string]: any };
27
+ type Ratings = { products: Record<string, { measuredAt: number; rating: number }[]>; directories: Record<string, number | null> };
28
+ type Meta = { registrySource: string; ahrefs: boolean };
29
+ type Updates = { source: string; checkedAt: number; applied: boolean; added: string[]; changed: string[]; localNewer: string[]; onlyLocal: string[]; generatedAt: string };
30
+
31
+ const VIEWS = { overview: "Overview", needs: "Needs you", submissions: "Submissions", directories: "Directories", product: "Product" } as const;
32
+ type View = keyof typeof VIEWS;
33
+
34
+ // ---- plumbing ---------------------------------------------------------------
35
+
36
+ /** Polls `path`; any action fires "refresh" so every view reloads at once. */
37
+ function useApi<T>(path: string | null, every = 5000): T | undefined {
38
+ const [data, setData] = useState<T>();
39
+ useEffect(() => {
40
+ if (!path) return;
41
+ const load = () => fetch(path).then((r) => r.json() as Promise<T>).then(setData, () => {});
42
+ load();
43
+ const t = setInterval(load, every);
44
+ addEventListener("refresh", load);
45
+ return () => (clearInterval(t), removeEventListener("refresh", load));
46
+ }, [path]);
47
+ return data;
48
+ }
49
+
50
+ async function post<T = unknown>(path: string, body?: object): Promise<T & { error?: string }> {
51
+ const r = await fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body ?? {}) });
52
+ const j = (await r.json().catch(() => ({}))) as T & { error?: string };
53
+ dispatchEvent(new Event("refresh"));
54
+ return j;
55
+ }
56
+ const act = async (path: string, body?: object) => {
57
+ const j = await post(path, body);
58
+ if (j.error) alert(j.error);
59
+ return !j.error;
60
+ };
61
+ const unblock = (job: Job, note: string, approve = false) => act(`/api/jobs/${job.id}/unblock`, { note, ...(approve && { approve: "yes" }) });
62
+
63
+ const ago = (ms: number) => {
64
+ const s = Math.round((Date.now() - ms) / 1000);
65
+ return s < 60 ? "just now" : s < 3600 ? `${Math.round(s / 60)}m ago` : s < 86400 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago`;
66
+ };
67
+ const When = ({ at }: { at: number }) => <time dateTime={new Date(at).toISOString()} title={new Date(at).toLocaleString("en-US")}>{ago(at)}</time>;
68
+ const day = (ms: number) => new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric" });
69
+ const words = (s: string) => s.replaceAll("_", " ").toLowerCase();
70
+ const host = (url: string) => url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
71
+ const Ext = ({ href, children }: { href: string; children?: ReactNode }) => <a href={href} target="_blank" rel="noreferrer">{children ?? host(href)}</a>;
72
+ const Linkify = ({ text }: { text: string }) => (
73
+ <>{text.split(/(https?:\/\/[^\s)]+)/).map((part, i) => (i % 2 ? <a key={i} href={part} target="_blank" rel="noreferrer">{host(part)}</a> : part))}</>
74
+ );
75
+ const Loading = () => <p className="quiet">Loading…</p>;
76
+ const store = {
77
+ get: (k: string) => { try { return localStorage.getItem(k); } catch { return null; } },
78
+ set: (k: string, v: string) => { try { localStorage.setItem(k, v); } catch {} },
79
+ };
80
+ const parseHash = () => {
81
+ const [view, arg = ""] = decodeURIComponent(location.hash.slice(1)).split("/");
82
+ return { view: (view && view in VIEWS ? view : "overview") as View, arg };
83
+ };
84
+
85
+ // ---- vocabulary ---------------------------------------------------------------
86
+
87
+ type Tone = "green" | "yellow" | "red" | "blue" | "gray";
88
+ type State = "live" | "review" | "badge" | "needs" | "hold" | "queued" | "running" | "checking" | "failed" | "rejected";
89
+ const STATE: Record<State, [string, Tone]> = {
90
+ live: ["Live", "green"], review: ["In review", "blue"], badge: ["Needs badge", "gray"], needs: ["Needs you", "yellow"], hold: ["On hold", "gray"],
91
+ queued: ["Queued", "gray"], running: ["Working", "blue"], checking: ["Verifying", "blue"], failed: ["Didn't work", "red"], rejected: ["Rejected", "red"],
92
+ };
93
+ function stateOf(j: Job): State {
94
+ if (j.status === "blocked") return j.blockType === "hold" ? "hold" : "needs";
95
+ if (j.status === "failed") return "failed";
96
+ if (j.status === "unknown") return "checking";
97
+ if (j.status === "running") return "running";
98
+ if (j.status === "done") return j.outcome === "live" ? "live" : j.outcome === "rejected" ? "rejected" : j.outcome === "needs_badge" ? "badge" : "review";
99
+ return "queued";
100
+ }
101
+ const Tag = ({ tone = "gray", children }: { tone?: Tone; children: ReactNode }) => <span className={`tag ${tone}`}>{children}</span>;
102
+ const StateTag = ({ job }: { job: Job }) => { const [label, tone] = STATE[stateOf(job)]; return <Tag tone={tone}>{label}</Tag>; };
103
+
104
+ const COST: Record<string, [string, Tone]> = {
105
+ VERIFIED_FREE: ["Free", "green"], FREEMIUM: ["Freemium", "blue"], CONDITIONAL_FREE: ["Conditions", "yellow"], PAID: ["Paid", "red"],
106
+ };
107
+ const CostTag = ({ d }: { d: Dir }) => { const [label, tone] = COST[d.cost.classification] ?? [words(d.cost.classification), "gray" as Tone]; return <Tag tone={tone}>{label}</Tag>; };
108
+ const signIn = (d: Dir) => (d.authentication.required ? (d.authentication.methods?.length ? d.authentication.methods.join(", ") : "account") : "none");
109
+
110
+ /** Why a job ended where it did, in one line. */
111
+ const reasonOf = (j: Job) =>
112
+ j.status === "blocked" ? (j.blockMessage ?? "") : j.status === "failed" ? (j.lastError ?? "").replace(/^(\w+): /, (_, r) => `${words(r)}: `) : j.status === "done" ? "" : j.step === "queued" ? "" : `Step: ${words(j.step)}`;
113
+
114
+ function describe(e: Event): string {
115
+ const d = e.data;
116
+ const type = e.type.replace(/^hosted\./, "");
117
+ switch (type) {
118
+ case "job_created": return "Queued";
119
+ case "claimed": return "An agent picked it up";
120
+ case "checkpoint": return `Step: ${words(String(d.step ?? ""))}`;
121
+ case "blocked": return `Needed you: ${d.message ?? words(d.type ?? "")}`;
122
+ case "needs_user": return `Needed you: ${d.detail ?? words(d.block_type ?? "")}`;
123
+ case "unblocked": return `You answered${d.note ? `: ${d.note}` : ""}`;
124
+ case "reply_to_target": return `You answered${d.decision ? `: ${d.decision}` : ""}`;
125
+ case "failed": return `Didn't work: ${d.reason}`;
126
+ case "failed_retryable": return `Will retry: ${d.reason}`;
127
+ case "submit_attempted": case "begin_submit": return "Clicked submit";
128
+ case "submission_recorded": return `Submitted (${words(String(d.outcome))})`;
129
+ case "report": return `${STATE[(d.state === "pending" ? "review" : d.state) as State]?.[0] ?? words(String(d.state ?? "updated"))}${d.detail ? `: ${d.detail}` : ""}`;
130
+ case "submission_state_unknown": return "Unclear whether it went through; will verify";
131
+ case "verified_not_submitted": return "Verified it didn't go through";
132
+ case "outcome_changed": return `Now ${words(String(d.to))}`;
133
+ case "requeued": case "retry_target": return "Sent back to the queue";
134
+ case "resume": return "Resumed";
135
+ case "lease_expired": return "Agent went quiet; job freed";
136
+ case "released": return "Agent handed it back";
137
+ case "todo_added": return `To-do: ${d.message}`;
138
+ case "hosted_import": return "Imported from dirpush.com";
139
+ case "added_manually": return `You recorded it by hand${d.listingUrl ? `: ${d.listingUrl}` : ""}`;
140
+ default: return words(type);
141
+ }
142
+ }
143
+
144
+ // ---- app --------------------------------------------------------------------
145
+
146
+ function App() {
147
+ const products = useApi<Product[]>("/api/products", 30000);
148
+ const actions = useApi<Actions>("/api/actions");
149
+ const [chosen, setChosen] = useState(() => store.get("product") ?? "");
150
+ const product = products?.find((p) => p.id === chosen) ?? products?.[0];
151
+ const [route, setRoute] = useState(parseHash);
152
+ const [panel, setPanel] = useState<{ job?: string; dir?: string } | null>(null);
153
+ useEffect(() => {
154
+ const on = () => (setRoute(parseHash()), setPanel(null));
155
+ addEventListener("hashchange", on);
156
+ return () => removeEventListener("hashchange", on);
157
+ }, []);
158
+ const pid = product?.id ?? "";
159
+ const needs = actions ? needsCount(actions, pid) : 0;
160
+ const open = { job: (id: string) => setPanel({ job: id }), dir: (id: string) => setPanel({ dir: id }) };
161
+
162
+ return (
163
+ <div className="shell">
164
+ <aside className="side">
165
+ <div className="brand">dirpush</div>
166
+ {products && products.length > 1 && (
167
+ <select className="switch" value={pid} onChange={(e) => (setChosen(e.target.value), store.set("product", e.target.value))} aria-label="Product">
168
+ {products.map((p) => <option key={p.id} value={p.id}>{p.name ?? p.id}</option>)}
169
+ </select>
170
+ )}
171
+ {products?.length === 1 && <div className="switch static">{product?.name ?? pid}</div>}
172
+ <nav aria-label="Views">
173
+ {(Object.keys(VIEWS) as View[]).map((v) => (
174
+ <a key={v} href={`#${v}`} aria-current={route.view === v ? "page" : undefined}>
175
+ {VIEWS[v]}
176
+ {v === "needs" && needs > 0 && <span className="count">{needs}</span>}
177
+ </a>
178
+ ))}
179
+ </nav>
180
+ <p className="side-foot">Runs on this computer.<br />Agents use the <code>launch</code> CLI.</p>
181
+ </aside>
182
+ <main>
183
+ {!products ? <Loading /> : !product ? <Empty /> : (
184
+ <>
185
+ {route.view === "overview" && <Overview product={product} actions={actions} open={open} />}
186
+ {route.view === "needs" && (actions ? <NeedsYou d={actions} pid={pid} open={open} /> : <Loading />)}
187
+ {route.view === "submissions" && <Submissions pid={pid} filter={route.arg} open={open} />}
188
+ {route.view === "directories" && <Directories pid={pid} open={open} />}
189
+ {route.view === "product" && <ProductView product={product} />}
190
+ </>
191
+ )}
192
+ </main>
193
+ {panel?.job && <JobPanel id={panel.job} close={() => setPanel(null)} openDir={open.dir} />}
194
+ {panel?.dir && <DirectoryPanel id={panel.dir} pid={pid} close={() => setPanel(null)} openJob={open.job} />}
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function Empty() {
200
+ return (
201
+ <section className="page">
202
+ <h1>No product yet</h1>
203
+ <p className="lede">Run <code>dirpush init &lt;product-id&gt;</code>, fill in <code>product.yaml</code>, then ask your agent to “continue my launch”.</p>
204
+ </section>
205
+ );
206
+ }
207
+
208
+ function Head({ title, children, aside }: { title: string; children?: ReactNode; aside?: ReactNode }) {
209
+ return (
210
+ <header className="head">
211
+ <div>
212
+ <h1>{title}</h1>
213
+ {children && <p className="lede">{children}</p>}
214
+ </div>
215
+ {aside && <div className="head-aside">{aside}</div>}
216
+ </header>
217
+ );
218
+ }
219
+
220
+ // ---- overview ---------------------------------------------------------------
221
+
222
+ function Overview({ product, actions, open }: { product: Product; actions?: Actions; open: { job: (id: string) => void } }) {
223
+ const jobs = useApi<Job[]>("/api/jobs?kind=submission");
224
+ const ratings = useApi<Ratings>("/api/ratings", 60000);
225
+ const meta = useApi<Meta>("/api/meta", 60000);
226
+ const feed = useApi<{ activity: Event[] }>("/api/overview", 10000);
227
+ if (!jobs) return <Loading />;
228
+ const mine = jobs.filter((j) => j.productId === product.id);
229
+ const n = (...s: State[]) => mine.filter((j) => s.includes(stateOf(j))).length;
230
+ const submitted = n("live", "review", "badge", "rejected");
231
+ const parts = [
232
+ { label: "Live", n: n("live"), tone: "green", href: "#submissions/live" },
233
+ { label: "In review", n: n("review", "badge"), tone: "blue", href: "#submissions/review" },
234
+ { label: "Needs you", n: n("needs"), tone: "yellow", href: "#needs" },
235
+ { label: "Queued", n: n("queued", "running", "checking"), tone: "gray", href: "#submissions/queued" },
236
+ { label: "On hold", n: n("hold"), tone: "faint", href: "#submissions/hold" },
237
+ ];
238
+ const active = mine.length - n("failed", "rejected");
239
+ const subject = new Map(jobs.map((j) => [j.id, j]));
240
+ const needsTotal = actions ? needsCount(actions, product.id) : 0;
241
+ const needsList = actions ? actions.blocked.filter((j) => j.productId === product.id && j.kind === "submission" && j.blockType !== "hold" && j.blockType !== "gallery_approval") : [];
242
+ const activity = (feed?.activity ?? []).filter((e) => e.jobId && subject.get(e.jobId)?.productId === product.id && !["hosted_import", "job_created"].includes(e.type)).slice(0, 8);
243
+
244
+ return (
245
+ <section className="page">
246
+ <Head title={product.name ?? product.id}>{product.url && <Ext href={product.url} />}{product.tagline && <> · {product.tagline}</>}</Head>
247
+
248
+ <div className="stats">
249
+ <Stat href="#submissions/live" n={n("live")} label="Live" tone="green" />
250
+ <Stat href="#submissions/review" n={n("review", "badge")} label="In review" />
251
+ <Stat href="#needs" n={needsTotal} label="Need you" tone={needsTotal ? "yellow" : undefined} />
252
+ <Stat href="#submissions/queued" n={n("queued", "running", "checking")} label="Queued" />
253
+ </div>
254
+
255
+ <div className="grid-2 stretch">
256
+ <section className="card">
257
+ <h2>Progress</h2>
258
+ <p className="big-line"><strong>{submitted}</strong> of {active} directories submitted</p>
259
+ <div className="segments" aria-hidden="true">
260
+ {parts.map((p) => p.n > 0 && <span key={p.label} className={p.tone} style={{ flexGrow: p.n }} />)}
261
+ </div>
262
+ <ul className="legend">
263
+ {parts.map((p) => (
264
+ <li key={p.label}>
265
+ <a href={p.href}><span className={`dot ${p.tone}`} />{p.label}<span className="legend-n">{p.n}</span></a>
266
+ </li>
267
+ ))}
268
+ </ul>
269
+ <p className="quiet small">{n("failed", "rejected")} more didn't work: paid, badge walls or not a fit.</p>
270
+ </section>
271
+ <DomainRating history={ratings?.products[product.id]} meta={meta} tall />
272
+ </div>
273
+
274
+ <section className="block">
275
+ <div className="block-head"><h2>Needs you</h2>{needsTotal > 0 && <a href="#needs">See all {needsTotal}</a>}</div>
276
+ {needsList.length === 0 ? <p className="quiet">Nothing is waiting on you. Agents can keep going.</p> : (
277
+ <ul className="list">
278
+ {needsList.slice(0, 5).map((j) => (
279
+ <li key={j.id}>
280
+ <button className="row-button" onClick={() => open.job(j.id)}>
281
+ <span className="row-title">{j.subject}</span>
282
+ <span className="row-text">{j.blockMessage}</span>
283
+ <Tag tone="yellow">{GROUP_LABEL[groupOf(j)]}</Tag>
284
+ </button>
285
+ </li>
286
+ ))}
287
+ </ul>
288
+ )}
289
+ </section>
290
+
291
+ <section className="block">
292
+ <h2>Recent activity</h2>
293
+ <ol className="feed">
294
+ {activity.map((e) => (
295
+ <li key={e.id}>
296
+ <span className="quiet"><When at={e.at} /></span>
297
+ {e.jobId ? <button className="link" onClick={() => open.job(e.jobId!)}>{subject.get(e.jobId)?.subject ?? e.jobId}</button> : <span />}
298
+ <span className="clip" title={describe(e)}>{describe(e)}</span>
299
+ </li>
300
+ ))}
301
+ </ol>
302
+ </section>
303
+ </section>
304
+ );
305
+ }
306
+
307
+ function Stat({ href, n, label, tone }: { href: string; n: number; label: string; tone?: Tone }) {
308
+ return (
309
+ <a className="stat" href={href}>
310
+ <span className={`stat-n ${tone ?? ""}`}>{n}</span>
311
+ <span className="stat-label">{label}</span>
312
+ </a>
313
+ );
314
+ }
315
+
316
+ function DomainRating({ history, meta, tall }: { history?: { measuredAt: number; rating: number }[]; meta?: Meta; tall?: boolean }) {
317
+ const [busy, setBusy] = useState(false);
318
+ const readings = [...(history ?? [])].reverse(); // oldest first
319
+ const latest = readings.at(-1), first = readings[0];
320
+ const change = latest && first ? latest.rating - first.rating : 0;
321
+ return (
322
+ <section className="card">
323
+ <div className="block-head">
324
+ <h2>Domain Rating</h2>
325
+ {meta?.ahrefs && <button className="ghost" disabled={busy} onClick={async () => { setBusy(true); const r = await post("/api/ratings/refresh"); setBusy(false); if (r.error) alert(r.error); }}>{busy ? "Measuring…" : "Measure now"}</button>}
326
+ </div>
327
+ <p className="big-line">
328
+ <strong className="dr">{latest?.rating ?? "—"}</strong>
329
+ {latest && <span className={change > 0 ? "up" : change < 0 ? "down" : "quiet"}>{change > 0 ? `+${change}` : change} since {day(first!.measuredAt)}</span>}
330
+ </p>
331
+ <DrChart readings={readings} height={tall ? 236 : 110} />
332
+ <p className="quiet small">
333
+ {!latest ? (meta?.ahrefs ? "No readings yet. Press Measure now." : <>Optional: set <code>AHREFS_API_KEY</code> to measure weekly.</>)
334
+ : <>Ahrefs, {readings.length} reading{readings.length === 1 ? "" : "s"}. Dotted where there's no data. DR moves weeks after listings go live.</>}
335
+ </p>
336
+ </section>
337
+ );
338
+ }
339
+
340
+ /** DR over time. Solid between readings a week or so apart; dotted across gaps and from the last reading to today. */
341
+ function DrChart({ readings, height }: { readings: { measuredAt: number; rating: number }[]; height: number }) {
342
+ const W = 480, H = height, padL = 28, padR = 8, padT = 10, padB = 22;
343
+ const now = Date.now(), DAY = 86400000, GAP = 9 * DAY;
344
+ const start = readings[0]?.measuredAt ?? now - 30 * DAY;
345
+ const end = Math.max(now, start + 7 * DAY);
346
+ const values = readings.map((r) => r.rating);
347
+ const lo = Math.max(0, Math.floor(Math.min(...values, 100) - 5)), hi = Math.min(100, Math.ceil(Math.max(...values, 0) + 5));
348
+ const [y0, y1] = readings.length ? [lo, Math.max(hi, lo + 10)] : [0, 100];
349
+ const x = (t: number) => padL + ((t - start) / (end - start)) * (W - padL - padR);
350
+ const y = (v: number) => padT + (1 - (v - y0) / (y1 - y0)) * (H - padT - padB);
351
+ const segments: { a: [number, number]; b: [number, number]; dotted: boolean }[] = [];
352
+ readings.forEach((r, i) => {
353
+ const next = readings[i + 1];
354
+ if (next) segments.push({ a: [r.measuredAt, r.rating], b: [next.measuredAt, next.rating], dotted: next.measuredAt - r.measuredAt > GAP });
355
+ });
356
+ if (latest(readings) && now - latest(readings)!.measuredAt > DAY) segments.push({ a: [latest(readings)!.measuredAt, latest(readings)!.rating], b: [end, latest(readings)!.rating], dotted: true });
357
+ const ticks = [y0, Math.round((y0 + y1) / 2), y1];
358
+ return (
359
+ <svg className="chart" viewBox={`0 0 ${W} ${H}`} role="img" aria-label={readings.length ? `Domain Rating from ${readings[0]!.rating} to ${latest(readings)!.rating}` : "No Domain Rating readings yet"}>
360
+ {ticks.map((v) => (
361
+ <g key={v}>
362
+ <line className="grid" x1={padL} x2={W - padR} y1={y(v)} y2={y(v)} />
363
+ <text className="axis" x={padL - 6} y={y(v) + 3} textAnchor="end">{v}</text>
364
+ </g>
365
+ ))}
366
+ <text className="axis" x={padL} y={H - 4}>{day(start)}</text>
367
+ <text className="axis" x={W - padR} y={H - 4} textAnchor="end">Today</text>
368
+ {!readings.length && <line className="line dotted" x1={padL} x2={W - padR} y1={y((y0 + y1) / 2)} y2={y((y0 + y1) / 2)} />}
369
+ {segments.map((s, i) => <line key={i} className={`line ${s.dotted ? "dotted" : ""}`} x1={x(s.a[0])} y1={y(s.a[1])} x2={x(s.b[0])} y2={y(s.b[1])} />)}
370
+ {readings.map((r) => (
371
+ <circle key={r.measuredAt} className="point" cx={x(r.measuredAt)} cy={y(r.rating)} r={3}><title>{`${r.rating} on ${day(r.measuredAt)}`}</title></circle>
372
+ ))}
373
+ </svg>
374
+ );
375
+ }
376
+ const latest = <T,>(xs: T[]) => xs.at(-1);
377
+
378
+ // ---- needs you --------------------------------------------------------------
379
+
380
+ type Group = "answer" | "yourself" | "email" | "hold";
381
+ const GROUP_LABEL: Record<Group, string> = { answer: "Question", yourself: "Do it yourself", email: "Verify email", hold: "On hold" };
382
+ const YOURSELF = new Set(["captcha", "bot_protection", "human_verification", "manual", "google_login"]);
383
+ const groupOf = (j: Job): Group => (j.blockType === "hold" ? "hold" : j.blockType === "email_verification" ? "email" : YOURSELF.has(j.blockType ?? "") ? "yourself" : "answer");
384
+
385
+ function needsCount(d: Actions, pid: string) {
386
+ const subs = d.blocked.filter((j) => j.productId === pid && j.kind === "submission" && j.blockType !== "hold" && j.blockType !== "gallery_approval");
387
+ const mine = <T extends { productId: string }>(xs: T[]) => xs.filter((x) => !x.productId || x.productId === pid);
388
+ return subs.length + mine(d.galleries).length + mine(d.productApprovals).length + mine(d.todos).length + d.registryChanges.length;
389
+ }
390
+
391
+ /** Only an explicit dashboard press may approve a final submission. */
392
+ const wantsApproval = (j: Job) => j.blockType === "permission" && ["ready", "submitting"].includes(j.step) &&
393
+ /submit|send|final action/i.test(j.blockMessage ?? "") && /approve.{0,100}dashboard|dashboard.{0,100}approve/i.test(j.blockMessage ?? "");
394
+
395
+ function NeedsYou({ d, pid, open }: { d: Actions; pid: string; open: { job: (id: string) => void } }) {
396
+ const dirs = useApi<Dir[]>("/api/registry", 60000);
397
+ const submitUrl = new Map((dirs ?? []).map((x) => [x.id, x.submission_url ?? x.website]));
398
+ const mine = <T extends { productId: string }>(xs: T[]) => xs.filter((x) => !x.productId || x.productId === pid);
399
+ const blocked = d.blocked.filter((j) => j.productId === pid && j.kind === "submission" && j.blockType !== "gallery_approval");
400
+ const by = (g: Group) => blocked.filter((j) => groupOf(j) === g);
401
+ const approvals = mine(d.galleries).length + mine(d.productApprovals).length + d.registryChanges.length;
402
+ const total = blocked.length - by("hold").length + approvals + mine(d.todos).length;
403
+ const unknown = d.unknown.filter((j) => j.productId === pid);
404
+
405
+ return (
406
+ <section className="page">
407
+ <Head title="Needs you">{total ? `${total} things only you can do. Agents pick each job back up the moment you act.` : "Nothing is waiting on you."}</Head>
408
+
409
+ {approvals > 0 && (
410
+ <Section title="Approvals" n={approvals}>
411
+ {mine(d.galleries).map((g) => (
412
+ <Item key={`g${g.revision}`} title="Screenshots" kind="Approve">
413
+ <p>Agents will use these on directories that take screenshots.</p>
414
+ <ul className="shots">{g.items.map((i) => <li key={i.path}><img src={`/workspace/products/${g.productId}/${i.path}`} alt={i.description} loading="lazy" /><span>{i.description}</span></li>)}</ul>
415
+ <div className="actions">
416
+ <button onClick={() => act(`/api/galleries/${g.revision}/approve`)}>Approve</button>
417
+ <button className="ghost" onClick={() => act(`/api/galleries/${g.revision}/reject`)}>Reject</button>
418
+ </div>
419
+ </Item>
420
+ ))}
421
+ {mine(d.productApprovals).map((a) => (
422
+ <Item key={`p${a.id}`} title={words(a.action)} kind="Approve">
423
+ <p>{a.message}</p>
424
+ <div className="actions">
425
+ <button onClick={() => act(`/api/product-approvals/${a.id}/approve`)}>Approve</button>
426
+ <button className="ghost" onClick={() => act(`/api/product-approvals/${a.id}/reject`)}>Reject</button>
427
+ </div>
428
+ </Item>
429
+ ))}
430
+ {d.registryChanges.map((c) => (
431
+ <Item key={`r${c.id}`} title={c.directoryId} kind="Registry change">
432
+ <p>An agent saw the live site differ from the registry.</p>
433
+ <div className="actions">
434
+ <button onClick={() => act(`/api/registry-changes/${c.id}/accept`)}>Accept</button>
435
+ <button className="ghost" onClick={() => act(`/api/registry-changes/${c.id}/reject`)}>Reject</button>
436
+ </div>
437
+ </Item>
438
+ ))}
439
+ </Section>
440
+ )}
441
+
442
+ <Section title="Questions" n={by("answer").length} note="The agent needs a fact or a yes/no. Your answer goes straight to it.">
443
+ {by("answer").map((j) => <QuestionItem key={j.id} job={j} open={open.job} />)}
444
+ </Section>
445
+
446
+ <Section title="Do it yourself" n={by("yourself").length} note="These sites block automated browsers. Open the page in your normal browser, submit, then mark it done.">
447
+ {by("yourself").map((j) => (
448
+ <Item key={j.id} title={j.subject} kind={words(j.blockType ?? "")} onTitle={() => open.job(j.id)} at={j.updatedAt}>
449
+ <p><Linkify text={j.blockMessage ?? ""} /></p>
450
+ <div className="actions">
451
+ {j.blockType === "google_login"
452
+ ? <button onClick={() => act("/api/google-login")}>Open Google sign-in</button>
453
+ : submitUrl.get(j.subject) && <a className="button" href={submitUrl.get(j.subject)!} target="_blank" rel="noreferrer">Open submit page</a>}
454
+ <button className="ghost" onClick={() => act(`/api/jobs/${j.id}/manual`, { outcome: "pending" })}>I submitted it</button>
455
+ <button className="ghost" onClick={() => unblock(j, "skip")}>Skip</button>
456
+ </div>
457
+ </Item>
458
+ ))}
459
+ </Section>
460
+
461
+ <Section title="Verify email" n={by("email").length} note="Click the link in the email the directory sent, then confirm here.">
462
+ {by("email").map((j) => (
463
+ <Item key={j.id} title={j.subject} kind="Email" onTitle={() => open.job(j.id)} at={j.updatedAt}>
464
+ <p><Linkify text={j.blockMessage ?? ""} /></p>
465
+ <div className="actions">
466
+ <button onClick={() => unblock(j, "verified")}>Verified</button>
467
+ <button className="ghost" onClick={() => unblock(j, "No email arrived: resend the verification email, then block again.")}>Didn't arrive</button>
468
+ <button className="ghost" onClick={() => unblock(j, "skip")}>Skip</button>
469
+ </div>
470
+ </Item>
471
+ ))}
472
+ </Section>
473
+
474
+ <Section title="To-dos" n={mine(d.todos).length} note="Things to check when you have a minute. They don't hold up any submission.">
475
+ {mine(d.todos).map((t) => (
476
+ <li key={`t${t.id}`} className="todo">
477
+ <p>{t.message}</p>
478
+ {t.url && <Ext href={t.url} />}
479
+ <button className="ghost" onClick={() => act(`/api/todos/${t.id}/done`)}>Done</button>
480
+ </li>
481
+ ))}
482
+ </Section>
483
+
484
+ {unknown.length > 0 && (
485
+ <Section title="Being verified" n={unknown.length} note="An agent stopped mid-submit. The next run checks for the listing before trying again. Nothing to do.">
486
+ {unknown.map((j) => <Item key={j.id} title={j.subject} kind="Checking" onTitle={() => open.job(j.id)} at={j.updatedAt} />)}
487
+ </Section>
488
+ )}
489
+
490
+ {by("hold").length > 0 && (
491
+ <details className="fold">
492
+ <summary>On hold <span className="quiet">{by("hold").length}</span><span className="quiet small"> · Parked on purpose, for example until a release.</span></summary>
493
+ <ul className="items">
494
+ {by("hold").map((j) => (
495
+ <Item key={j.id} title={j.subject} kind="Hold" onTitle={() => open.job(j.id)} at={j.updatedAt}>
496
+ <p><Linkify text={j.blockMessage ?? ""} /></p>
497
+ <div className="actions">
498
+ <button className="ghost" onClick={() => unblock(j, "Resume now.")}>Resume</button>
499
+ <button className="ghost" onClick={() => unblock(j, "skip")}>Skip</button>
500
+ </div>
501
+ </Item>
502
+ ))}
503
+ </ul>
504
+ </details>
505
+ )}
506
+ </section>
507
+ );
508
+ }
509
+
510
+ function Section({ title, n, note, children }: { title: string; n: number; note?: string; children: ReactNode }) {
511
+ if (!n) return null;
512
+ return (
513
+ <section className="block">
514
+ <div className="block-head"><h2>{title} <span className="quiet">{n}</span></h2></div>
515
+ {note && <p className="quiet small note">{note}</p>}
516
+ <ul className="items">{children}</ul>
517
+ </section>
518
+ );
519
+ }
520
+
521
+ function Item({ title, kind, at, onTitle, children }: { title: string; kind: string; at?: number; onTitle?: () => void; children?: ReactNode }) {
522
+ return (
523
+ <li className="item">
524
+ <div className="item-head">
525
+ {onTitle ? <button className="link strong" onClick={onTitle}>{title}</button> : <strong>{title}</strong>}
526
+ <span className="quiet small">{kind}{at && <> · <When at={at} /></>}</span>
527
+ </div>
528
+ {children}
529
+ </li>
530
+ );
531
+ }
532
+
533
+ function QuestionItem({ job, open }: { job: Job; open: (id: string) => void }) {
534
+ const [note, setNote] = useState("");
535
+ return (
536
+ <Item title={job.subject} kind={words(job.blockType ?? "question")} onTitle={() => open(job.id)} at={job.updatedAt}>
537
+ <p><Linkify text={job.blockMessage ?? ""} /></p>
538
+ <form className="answer" onSubmit={(e) => (e.preventDefault(), unblock(job, note).then((ok) => ok && setNote("")))}>
539
+ <input value={note} onChange={(e) => setNote(e.target.value)} placeholder="Your answer" aria-label={`Answer for ${job.subject}`} />
540
+ <button type="submit" disabled={!note.trim()}>Send</button>
541
+ {wantsApproval(job) && <button type="button" onClick={() => unblock(job, "approved", true)}>Approve submission</button>}
542
+ <button type="button" className="ghost" onClick={() => unblock(job, "skip")}>Skip</button>
543
+ </form>
544
+ </Item>
545
+ );
546
+ }
547
+
548
+ // ---- the one table ----------------------------------------------------------
549
+
550
+ type Col<T> = { label: string; value: (r: T) => string | number | null | undefined; render?: (r: T) => ReactNode; className?: string };
551
+ type Chip<T> = { id: string; label: string; test: (r: T) => boolean };
552
+
553
+ function Table<T>({ rows, cols, rowKey, onRow, chips, initialChip = "", sort: initialSort = [0, 1], label, empty = "Nothing here.", tools }: {
554
+ rows: T[]; cols: Col<T>[]; rowKey: (r: T) => string; onRow?: (r: T) => void; chips?: Chip<T>[]; initialChip?: string;
555
+ sort?: [number, 1 | -1]; label: string; empty?: string; tools?: ReactNode;
556
+ }) {
557
+ const [q, setQ] = useState("");
558
+ const [chip, setChip] = useState(chips?.some((c) => c.id === initialChip) ? initialChip : "");
559
+ const [[by, dir], setSort] = useState(initialSort);
560
+ const [all, setAll] = useState(false);
561
+ const shown = useMemo(() => {
562
+ const test = chips?.find((c) => c.id === chip)?.test;
563
+ const needle = q.trim().toLowerCase();
564
+ const key = (r: T) => cols[by]!.value(r) ?? "";
565
+ return rows
566
+ .filter((r) => (!test || test(r)) && (!needle || cols.some((c) => String(c.value(r) ?? "").toLowerCase().includes(needle))))
567
+ .sort((a, b) => {
568
+ const x = key(a), y = key(b);
569
+ return (typeof x === "number" && typeof y === "number" ? x - y : String(x).localeCompare(String(y))) * dir;
570
+ });
571
+ }, [rows, q, chip, by, dir]);
572
+ const cap = all ? shown.length : 150;
573
+
574
+ return (
575
+ <div className="table">
576
+ <div className="toolbar">
577
+ <input type="search" placeholder={`Search ${label}`} value={q} onChange={(e) => setQ(e.target.value)} aria-label={`Search ${label}`} />
578
+ {tools}
579
+ <span className="quiet small count-label">{shown.length === rows.length ? rows.length : `${shown.length} of ${rows.length}`}</span>
580
+ </div>
581
+ {chips && (
582
+ <div className="chips" role="group" aria-label={`Filter ${label}`}>
583
+ {[{ id: "", label: "All", test: () => true } as Chip<T>, ...chips].map((c) => (
584
+ <button key={c.id} className="chip" aria-pressed={chip === c.id} onClick={() => setChip(c.id)}>
585
+ {c.label}<span>{rows.filter(c.test).length}</span>
586
+ </button>
587
+ ))}
588
+ </div>
589
+ )}
590
+ {shown.length === 0 ? <p className="quiet pad">{rows.length ? "No matches." : empty}</p> : (
591
+ <div className="scroll">
592
+ <table>
593
+ <thead>
594
+ <tr>
595
+ {cols.map((c, i) => (
596
+ <th key={c.label} className={c.className} aria-sort={by === i ? (dir > 0 ? "ascending" : "descending") : undefined}>
597
+ <button className="sort" onClick={() => setSort([i, by === i ? (-dir as 1 | -1) : 1])}>
598
+ {c.label}<span aria-hidden="true">{by === i ? (dir > 0 ? " ↑" : " ↓") : ""}</span>
599
+ </button>
600
+ </th>
601
+ ))}
602
+ </tr>
603
+ </thead>
604
+ <tbody>
605
+ {shown.slice(0, cap).map((r) => (
606
+ <tr key={rowKey(r)} className={onRow ? "clickable" : undefined} tabIndex={onRow ? 0 : undefined}
607
+ onClick={(e) => onRow && !(e.target as Element).closest("a,button,input") && onRow(r)}
608
+ onKeyDown={(e) => onRow && e.key === "Enter" && e.target === e.currentTarget && onRow(r)}>
609
+ {cols.map((c) => <td key={c.label} className={c.className}>{c.render ? c.render(r) : (c.value(r) ?? "—")}</td>)}
610
+ </tr>
611
+ ))}
612
+ </tbody>
613
+ </table>
614
+ {shown.length > cap && <button className="ghost more" onClick={() => setAll(true)}>Show all {shown.length}</button>}
615
+ </div>
616
+ )}
617
+ </div>
618
+ );
619
+ }
620
+
621
+ // ---- submissions ------------------------------------------------------------
622
+
623
+ const SUB_CHIPS: Chip<Job>[] = [
624
+ { id: "live", label: "Live", test: (j) => stateOf(j) === "live" },
625
+ { id: "review", label: "In review", test: (j) => ["review", "badge"].includes(stateOf(j)) },
626
+ { id: "needs", label: "Needs you", test: (j) => stateOf(j) === "needs" },
627
+ { id: "queued", label: "Queued", test: (j) => ["queued", "running", "checking"].includes(stateOf(j)) },
628
+ { id: "hold", label: "On hold", test: (j) => stateOf(j) === "hold" },
629
+ { id: "failed", label: "Didn't work", test: (j) => ["failed", "rejected"].includes(stateOf(j)) },
630
+ ];
631
+
632
+ function Submissions({ pid, filter, open }: { pid: string; filter: string; open: { job: (id: string) => void } }) {
633
+ const jobs = useApi<Job[]>("/api/jobs?kind=submission");
634
+ const [checking, setChecking] = useState(false);
635
+ const [adding, setAdding] = useState(false);
636
+ if (!jobs) return <Loading />;
637
+ const rows = jobs.filter((j) => j.productId === pid);
638
+ const cols: Col<Job>[] = [
639
+ { label: "Directory", value: (j) => j.subject, render: (j) => <span className="strong">{j.subject}</span> },
640
+ { label: "Status", value: (j) => STATE[stateOf(j)][0], render: (j) => <StateTag job={j} /> },
641
+ { label: "Detail", value: reasonOf, render: (j) => <span className="clip quiet" title={reasonOf(j)}>{reasonOf(j) || "—"}</span>, className: "wide" },
642
+ { label: "Listing", value: (j) => j.listingUrl, render: (j) => (j.listingUrl ? <Ext href={j.listingUrl}>Open</Ext> : <span className="quiet">—</span>) },
643
+ { label: "Updated", value: (j) => j.updatedAt, render: (j) => <span className="quiet"><When at={j.updatedAt} /></span>, className: "num" },
644
+ ];
645
+ const check = async () => {
646
+ setChecking(true);
647
+ const r = await post<{ results: { directory: string; result: string }[] }>("/api/submissions/check");
648
+ setChecking(false);
649
+ const live = (r.results ?? []).filter((x) => x.result.startsWith("live"));
650
+ alert(live.length ? `Now live: ${live.map((x) => x.directory).join(", ")}` : `Checked ${(r.results ?? []).length} listings. None newly live.`);
651
+ };
652
+ return (
653
+ <section className="page">
654
+ <Head title="Submissions" aside={<div className="actions tight">
655
+ <button className="ghost" onClick={() => setAdding(!adding)}>Add a listing</button>
656
+ <button className="ghost" disabled={checking} onClick={check}>{checking ? "Checking…" : "Check listings in review"}</button>
657
+ </div>}>
658
+ Every directory your agent has worked on, plus listings you made yourself. Select one for its timeline.
659
+ </Head>
660
+ {adding && <AddListing pid={pid} done={() => setAdding(false)} />}
661
+ <Table label="submissions" rows={rows} cols={cols} rowKey={(j) => j.id} onRow={(j) => open.job(j.id)} chips={SUB_CHIPS} initialChip={filter}
662
+ sort={[cols.length - 1, -1]} empty="Nothing yet. Ask your agent to “continue my launch”." />
663
+ </section>
664
+ );
665
+ }
666
+
667
+ function AddListing({ pid, done }: { pid: string; done: () => void }) {
668
+ const [target, setTarget] = useState("");
669
+ const [url, setUrl] = useState("");
670
+ const [outcome, setOutcome] = useState("pending");
671
+ return (
672
+ <form className="card add" onSubmit={async (e) => { e.preventDefault(); if (await act("/api/submissions/add", { product: pid, target, outcome, listingUrl: url })) done(); }}>
673
+ <h2>Add a listing you made yourself</h2>
674
+ <p className="quiet small">Agents won't submit there again, and “Check listings in review” tracks it like the rest. Use a registry directory id or any site's URL.</p>
675
+ <div className="answer">
676
+ <input value={target} onChange={(e) => setTarget(e.target.value)} placeholder="Directory id or site URL" aria-label="Directory" required />
677
+ <input type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="Listing URL (optional)" aria-label="Listing URL" />
678
+ <select value={outcome} onChange={(e) => setOutcome(e.target.value)} aria-label="Status">
679
+ <option value="pending">In review</option>
680
+ <option value="live">Live</option>
681
+ </select>
682
+ <button type="submit" disabled={!target.trim()}>Add</button>
683
+ <button type="button" className="ghost" onClick={done}>Cancel</button>
684
+ </div>
685
+ </form>
686
+ );
687
+ }
688
+
689
+ // ---- directories ------------------------------------------------------------
690
+
691
+ function Directories({ pid, open }: { pid: string; open: { job: (id: string) => void; dir: (id: string) => void } }) {
692
+ const dirs = useApi<Dir[]>("/api/registry", 60000);
693
+ const jobs = useApi<Job[]>("/api/jobs?kind=submission", 15000);
694
+ const ratings = useApi<Ratings>("/api/ratings", 60000);
695
+ const [kind, setKind] = useState("");
696
+ if (!dirs || !jobs) return <Loading />;
697
+ const sub = new Map(jobs.filter((j) => j.productId === pid).map((j) => [j.subject, j]));
698
+ const dr = (d: Dir) => ratings?.directories[d.id] ?? null;
699
+ const cols: Col<Dir>[] = [
700
+ { label: "Directory", value: (d) => d.name, className: "wide", render: (d) => (
701
+ <span className="stack"><span className="strong clip">{d.name}</span><span className="quiet small clip">{d.kind ? DIRECTORY_KINDS[d.kind] : "Unclassified"} · {host(d.website)}</span></span>
702
+ ) },
703
+ { label: "Cost", value: (d) => COST[d.cost.classification]?.[0] ?? d.cost.classification, render: (d) => <CostTag d={d} /> },
704
+ { label: "Sign-in", value: signIn, render: (d) => <span className="clip short quiet" title={signIn(d)}>{signIn(d)}</span> },
705
+ { label: "DR", value: (d) => dr(d) ?? -1, render: (d) => <span className="mono">{dr(d) ?? "—"}</span>, className: "num" },
706
+ { label: "Your status", value: (d) => (sub.get(d.id) ? STATE[stateOf(sub.get(d.id)!)][0] : "Not tried"), render: (d) => (sub.get(d.id) ? <StateTag job={sub.get(d.id)!} /> : <span className="quiet">Not tried</span>) },
707
+ { label: "Verified", value: (d) => d.verification.checked_at, render: (d) => <span className="quiet mono">{d.verification.checked_at}</span>, className: "num" },
708
+ ];
709
+ const chips: Chip<Dir>[] = [
710
+ ...Object.entries(COST).map(([id, [label]]) => ({ id, label, test: (d: Dir) => d.cost.classification === id })),
711
+ { id: "untried", label: "Not tried", test: (d) => !sub.has(d.id) },
712
+ ];
713
+ const rows = dirs.filter((d) => !kind || (d.kind ?? "unclassified") === kind);
714
+ return (
715
+ <section className="page">
716
+ <Head title="Directories" aside={<UpdatesButton />}>
717
+ The verified registry: {dirs.length} places to list a product, with cost, sign-in and the fields each form asks for.
718
+ </Head>
719
+ <UpdatesPanel />
720
+ <Table label="directories" rows={rows} cols={cols} rowKey={(d) => d.id} chips={chips} sort={[3, -1]} onRow={(d) => open.dir(d.id)}
721
+ tools={
722
+ <select value={kind} onChange={(e) => setKind(e.target.value)} aria-label="Type">
723
+ <option value="">All types</option>
724
+ {Object.entries(DIRECTORY_KINDS).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
725
+ <option value="unclassified">Unclassified</option>
726
+ </select>
727
+ } />
728
+ </section>
729
+ );
730
+ }
731
+
732
+ /** Registry update state shared by the header button and the panel under it. */
733
+ type UpdatesState = { busy: boolean; result: Updates | null; error: string };
734
+ const updates = {
735
+ state: { busy: false, result: null, error: "" } as UpdatesState,
736
+ set(patch: Partial<UpdatesState>) { Object.assign(updates.state, patch); dispatchEvent(new Event("registry-updates")); },
737
+ async run(apply: boolean) {
738
+ updates.set({ busy: true, error: "" });
739
+ const r = await post<Updates>("/api/registry/updates", { apply });
740
+ updates.set({ busy: false, ...(r.error ? { error: r.error, result: null } : { result: r }) });
741
+ },
742
+ };
743
+ function useUpdates() {
744
+ const [, tick] = useState(0);
745
+ useEffect(() => {
746
+ const on = () => tick((n) => n + 1);
747
+ addEventListener("registry-updates", on);
748
+ return () => removeEventListener("registry-updates", on);
749
+ }, []);
750
+ return updates.state;
751
+ }
752
+
753
+ function UpdatesButton() {
754
+ const s = useUpdates();
755
+ return <button disabled={s.busy} onClick={() => updates.run(false)}>{s.busy ? "Checking…" : "Check for updates"}</button>;
756
+ }
757
+
758
+ function UpdatesPanel() {
759
+ const s = useUpdates();
760
+ const meta = useApi<Meta>("/api/meta", 60000);
761
+ if (!s.result && !s.error) return null;
762
+ const source = meta?.registrySource ?? "";
763
+ if (s.error)
764
+ return (
765
+ <div className="notice red">
766
+ <p><strong>Couldn't reach the dirpush registry.</strong> {s.error}</p>
767
+ <p className="quiet small">Source: <code>{source}</code>. It's published from the dirpush repository; set <code>LAUNCH_REGISTRY_URL</code> to use another copy.</p>
768
+ <button className="ghost" onClick={() => updates.set({ error: "" })}>Dismiss</button>
769
+ </div>
770
+ );
771
+ const r = s.result!;
772
+ const pending = r.added.length + r.changed.length;
773
+ return (
774
+ <div className={`notice ${pending && !r.applied ? "blue" : "green"}`}>
775
+ <p>
776
+ <strong>{r.applied ? `Applied ${pending} update${pending === 1 ? "" : "s"}.` : pending ? `${pending} update${pending === 1 ? "" : "s"} available.` : "Your registry is up to date."}</strong>{" "}
777
+ <span className="quiet">Published {day(Date.parse(r.generatedAt))} · checked <When at={r.checkedAt} /></span>
778
+ </p>
779
+ {!r.applied && (r.added.length > 0 || r.changed.length > 0 || r.localNewer.length > 0) && (
780
+ <dl className="diff">
781
+ {r.added.length > 0 && <div><dt>New</dt><dd>{r.added.join(", ")}</dd></div>}
782
+ {r.changed.length > 0 && <div><dt>Changed</dt><dd>{r.changed.join(", ")}</dd></div>}
783
+ {r.localNewer.length > 0 && <div><dt>Kept yours</dt><dd>{r.localNewer.join(", ")} <span className="quiet">(you verified these more recently)</span></dd></div>}
784
+ </dl>
785
+ )}
786
+ <div className="actions">
787
+ {pending > 0 && !r.applied && <button disabled={s.busy} onClick={() => updates.run(true)}>Apply {pending} update{pending === 1 ? "" : "s"}</button>}
788
+ <button className="ghost" onClick={() => updates.set({ result: null })}>Dismiss</button>
789
+ </div>
790
+ </div>
791
+ );
792
+ }
793
+
794
+ // ---- product ----------------------------------------------------------------
795
+
796
+ function ProductView({ product }: { product: Product }) {
797
+ const d = useApi<{ product: Product; galleries: Gallery[] }>(`/api/products/${encodeURIComponent(product.id)}`, 30000);
798
+ const settings = useApi<{ identity: Record<string, string> & { error?: string }; permissions: Record<string, Record<string, string>> & { error?: string } }>("/api/settings", 60000);
799
+ const ratings = useApi<Ratings>("/api/ratings", 60000);
800
+ const meta = useApi<Meta>("/api/meta", 60000);
801
+ if (!d) return <Loading />;
802
+ const p = d.product;
803
+ if (p.error) return <section className="page"><p className="notice red">{p.error}</p></section>;
804
+ const list = (xs?: string[]) => (xs?.length ? xs.join(", ") : "—");
805
+ const perms = settings && !settings.permissions.error ? Object.entries(settings.permissions).flatMap(([g, xs]) => Object.entries(xs).map(([k, mode]) => ({ action: `${g}.${k}`, mode }))) : [];
806
+ const MODE: Record<string, Tone> = { automatic: "green", confirm: "yellow", never: "red" };
807
+ return (
808
+ <section className="page">
809
+ <Head title={p.name ?? product.id}>{p.tagline}</Head>
810
+ <p className="quiet small">Agents copy these facts into every form. Edit <code>workspace/products/{product.id}/product.yaml</code> to change them.</p>
811
+ <DomainRating history={ratings?.products[product.id]} meta={meta} />
812
+ <div className="gap-top">
813
+ <section className="card">
814
+ <h2>Facts</h2>
815
+ <dl className="facts">
816
+ <div><dt>Website</dt><dd><Ext href={p.url!} /></dd></div>
817
+ <div><dt>Short</dt><dd>{p.descriptions?.short}</dd></div>
818
+ {p.descriptions?.medium && <div><dt>Medium</dt><dd>{p.descriptions.medium}</dd></div>}
819
+ <div><dt>Categories</dt><dd>{list(p.categories)}</dd></div>
820
+ <div><dt>Pricing</dt><dd>{p.pricing?.free_plan ? "Free plan. " : ""}{p.pricing?.notes}</dd></div>
821
+ {(p.country || p.launched) && <div><dt>Company</dt><dd>{[p.city, p.country].filter(Boolean).join(", ")}{p.launched && ` · launched ${p.launched}`}</dd></div>}
822
+ {Object.entries((p.social ?? {}) as Record<string, string | null>).filter(([, v]) => v).map(([k, v]) => <div key={k}><dt>{k}</dt><dd><Ext href={v!} /></dd></div>)}
823
+ </dl>
824
+ </section>
825
+ </div>
826
+ {(p.features as string[] | undefined)?.length ? (
827
+ <section className="block">
828
+ <h2>Features</h2>
829
+ <ul className="plain">{(p.features as string[]).map((f) => <li key={f}>{f}</li>)}</ul>
830
+ </section>
831
+ ) : null}
832
+ <section className="block">
833
+ <h2>Screenshots</h2>
834
+ {d.galleries.length === 0 ? <p className="quiet">No screenshots proposed yet.</p> : d.galleries.slice(0, 1).map((g) => (
835
+ <div key={g.revision}>
836
+ <p className="quiet small">Revision {g.revision} · {g.status}</p>
837
+ <ul className="shots">{g.items.map((i) => <li key={i.path}><img src={`/workspace/products/${product.id}/${i.path}`} alt={i.description} loading="lazy" /><span>{i.description}</span></li>)}</ul>
838
+ </div>
839
+ ))}
840
+ </section>
841
+ {settings && (
842
+ <section className="block">
843
+ <h2>Who agents sign up as</h2>
844
+ {settings.identity.error ? <p className="quiet">{settings.identity.error}</p> : (
845
+ <dl className="facts">{Object.entries(settings.identity).map(([k, v]) => <div key={k}><dt>{words(k)}</dt><dd>{v}</dd></div>)}</dl>
846
+ )}
847
+ <h2 className="gap">What agents may do</h2>
848
+ <p className="quiet small">Automatic: just do it. Confirm: ask first. Never: stop. Edit <code>workspace/permissions.yaml</code>.</p>
849
+ <ul className="perms">{perms.map((x) => <li key={x.action}><code>{x.action}</code><Tag tone={MODE[x.mode]}>{x.mode}</Tag></li>)}</ul>
850
+ </section>
851
+ )}
852
+ </section>
853
+ );
854
+ }
855
+
856
+ // ---- side panels ------------------------------------------------------------
857
+
858
+ function Panel({ title, sub, close, children }: { title: string; sub?: ReactNode; close: () => void; children: ReactNode }) {
859
+ useEffect(() => {
860
+ const k = (e: KeyboardEvent) => e.key === "Escape" && close();
861
+ addEventListener("keydown", k);
862
+ return () => removeEventListener("keydown", k);
863
+ }, []);
864
+ return (
865
+ <>
866
+ <div className="backdrop" onClick={close} />
867
+ <aside className="panel" role="dialog" aria-modal="true" aria-label={title}>
868
+ <div className="panel-head">
869
+ <div><h2>{title}</h2>{sub && <p className="quiet small">{sub}</p>}</div>
870
+ <button className="ghost" onClick={close} autoFocus>Close</button>
871
+ </div>
872
+ {children}
873
+ </aside>
874
+ </>
875
+ );
876
+ }
877
+
878
+ function JobPanel({ id, close, openDir }: { id: string; close: () => void; openDir: (id: string) => void }) {
879
+ const d = useApi<{ job?: Job; events: Event[]; evidence: string[]; directory: Dir | { error: string } | null; error?: string }>(`/api/jobs/${id}`);
880
+ const [url, setUrl] = useState("");
881
+ const j = d?.job;
882
+ const dir = d?.directory && !("error" in d.directory) ? d.directory : null;
883
+ return (
884
+ <Panel title={dir?.name ?? j?.subject ?? id} sub={j && <>{j.productId} · <When at={j.updatedAt} /></>} close={close}>
885
+ {!d ? <Loading /> : d.error ? <p className="notice red">{d.error}</p> : j && (
886
+ <>
887
+ <p className="links">
888
+ <StateTag job={j} />
889
+ {dir && <button className="link" onClick={() => openDir(dir.id)}>Directory details</button>}
890
+ {dir?.submission_url && <Ext href={dir.submission_url}>Submit page</Ext>}
891
+ {j.listingUrl && <Ext href={j.listingUrl}>Your listing</Ext>}
892
+ </p>
893
+ {j.status === "blocked" && <div className="notice yellow"><p><Linkify text={j.blockMessage ?? ""} /></p></div>}
894
+ {j.status === "failed" && (
895
+ <div className="notice red">
896
+ <p>{reasonOf(j)}</p>
897
+ <button className="ghost" onClick={() => act(`/api/jobs/${j.id}/requeue`)}>Try again</button>
898
+ </div>
899
+ )}
900
+ {j.status !== "done" && j.status !== "running" && (
901
+ <details className="manual">
902
+ <summary className="quiet small">Listed it yourself?</summary>
903
+ <ManualForm compact onSave={(outcome, listingUrl) => act(`/api/jobs/${j.id}/manual`, { outcome, listingUrl })} />
904
+ </details>
905
+ )}
906
+ {j.status === "done" && (
907
+ <form className="answer" onSubmit={(e) => (e.preventDefault(), act(`/api/jobs/${j.id}/outcome`, { outcome: "live", listingUrl: url || j.listingUrl }))}>
908
+ <input type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder={j.listingUrl ?? "Listing URL"} aria-label="Listing URL" />
909
+ <button type="submit" disabled={j.outcome === "live" && !url}>Mark live</button>
910
+ <button type="button" className="ghost" disabled={j.outcome === "rejected"} onClick={() => act(`/api/jobs/${j.id}/outcome`, { outcome: "rejected" })}>Rejected</button>
911
+ </form>
912
+ )}
913
+ {d.evidence.length > 0 && (
914
+ <>
915
+ <h3>Evidence</h3>
916
+ <ul className="shots">{d.evidence.map((f) => <li key={f}><a href={`/evidence/${j.id}/${encodeURIComponent(f)}`} target="_blank" rel="noreferrer"><img src={`/evidence/${j.id}/${encodeURIComponent(f)}`} alt={f} loading="lazy" /></a><span>{f}</span></li>)}</ul>
917
+ </>
918
+ )}
919
+ <h3>Timeline</h3>
920
+ <p className="quiet small">Everything that happened on this directory, newest first: what agents did, what you answered, and moderation results.</p>
921
+ <Timeline events={d.events} />
922
+ {Object.keys(j.data).length > 0 && (
923
+ <details className="raw"><summary className="quiet small">Agent notes</summary><pre>{JSON.stringify(j.data, null, 2)}</pre></details>
924
+ )}
925
+ </>
926
+ )}
927
+ </Panel>
928
+ );
929
+ }
930
+
931
+ function Timeline({ events }: { events: Event[] }) {
932
+ const byDay = new Map<string, Event[]>();
933
+ for (const e of [...events].sort((a, b) => b.at - a.at || b.id - a.id)) {
934
+ const k = new Date(e.at).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
935
+ byDay.set(k, [...(byDay.get(k) ?? []), e]);
936
+ }
937
+ return (
938
+ <div className="timeline">
939
+ {[...byDay].map(([label, es]) => (
940
+ <section key={label}>
941
+ <h4>{label}</h4>
942
+ <ol>
943
+ {es.map((e) => (
944
+ <li key={e.id} className={e.worker === "human" || /unblocked|reply_to_target|added_manually/.test(e.type) ? "you" : ""}>
945
+ <time className="quiet small">{new Date(e.at).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" })}</time>
946
+ <span><Linkify text={describe(e)} /></span>
947
+ </li>
948
+ ))}
949
+ </ol>
950
+ </section>
951
+ ))}
952
+ </div>
953
+ );
954
+ }
955
+
956
+ /** "I listed it myself": for a directory (or any site) the agents haven't finished. */
957
+ function ManualForm({ onSave, compact }: { onSave: (outcome: string, listingUrl: string) => Promise<boolean>; compact?: boolean }) {
958
+ const [url, setUrl] = useState("");
959
+ const [outcome, setOutcome] = useState("pending");
960
+ return (
961
+ <form className="answer" onSubmit={(e) => (e.preventDefault(), onSave(outcome, url).then((ok) => ok && setUrl("")))}>
962
+ <input type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="Listing URL (optional)" aria-label="Listing URL" />
963
+ <select value={outcome} onChange={(e) => setOutcome(e.target.value)} aria-label="Status">
964
+ <option value="pending">In review</option>
965
+ <option value="live">Live</option>
966
+ </select>
967
+ <button type="submit">{compact ? "Save" : "I listed it myself"}</button>
968
+ </form>
969
+ );
970
+ }
971
+
972
+ function DirectoryPanel({ id, pid, close, openJob }: { id: string; pid: string; close: () => void; openJob: (id: string) => void }) {
973
+ const d = useApi<{ directory: Dir; playbook: string | null; error?: string }>(`/api/registry/${encodeURIComponent(id)}`, 60000);
974
+ const jobs = useApi<Job[]>("/api/jobs?kind=submission", 15000);
975
+ const ratings = useApi<Ratings>("/api/ratings", 60000);
976
+ const x = d?.directory;
977
+ const job = jobs?.find((j) => j.productId === pid && j.subject === id);
978
+ return (
979
+ <Panel title={x?.name ?? id} sub={x && <Ext href={x.website} />} close={close}>
980
+ {!d ? <Loading /> : d.error ? <p className="notice red">{d.error}</p> : x && (
981
+ <>
982
+ <p className="links">
983
+ <CostTag d={x} />
984
+ {job ? <button className="link" onClick={() => openJob(job.id)}>Your submission: {STATE[stateOf(job)][0]}</button> : <span className="quiet">Not tried yet</span>}
985
+ {x.submission_url && <Ext href={x.submission_url}>Submit page</Ext>}
986
+ </p>
987
+ {!job && <ManualForm onSave={(outcome, listingUrl) => act("/api/submissions/add", { product: pid, target: id, outcome, listingUrl })} />}
988
+ <dl className="facts gap-top">
989
+ <div><dt>Type</dt><dd>{x.kind ? DIRECTORY_KINDS[x.kind] : "Unclassified"}</dd></div>
990
+ {x.cost.notes && <div><dt>Cost</dt><dd>{x.cost.notes}</dd></div>}
991
+ <div><dt>Sign-in</dt><dd>{signIn(x)}</dd></div>
992
+ {x.review_time && <div><dt>Review</dt><dd>{x.review_time}</dd></div>}
993
+ <div><dt>DR</dt><dd>{ratings?.directories[id] ?? "—"}</dd></div>
994
+ <div><dt>Audience</dt><dd>{x.audience.join(", ") || "—"}</dd></div>
995
+ {x.human_only && <div><dt>Human only</dt><dd>{x.human_only}</dd></div>}
996
+ <div><dt>Verified</dt><dd>{x.verification.checked_at}</dd></div>
997
+ </dl>
998
+ {(x.submission.fields?.length ?? 0) > 0 && (
999
+ <>
1000
+ <h3>Form fields</h3>
1001
+ <ul className="fields">
1002
+ {x.submission.fields!.map((f) => (
1003
+ <li key={f.name}><span>{f.name}</span><span className="quiet small">{f.required ? "required" : "optional"}{f.limit ? ` · max ${f.limit}` : ""}</span></li>
1004
+ ))}
1005
+ </ul>
1006
+ </>
1007
+ )}
1008
+ {x.submission.conditions.length > 0 && (
1009
+ <>
1010
+ <h3>Conditions</h3>
1011
+ <ul className="plain">{x.submission.conditions.map((c) => <li key={c}>{c}</li>)}</ul>
1012
+ </>
1013
+ )}
1014
+ {d.playbook && (
1015
+ <>
1016
+ <h3>Playbook</h3>
1017
+ <div className="playbook">{d.playbook.replace(/^# .*\n+/, "")}</div>
1018
+ </>
1019
+ )}
1020
+ </>
1021
+ )}
1022
+ </Panel>
1023
+ );
1024
+ }
1025
+
1026
+ createRoot(document.getElementById("root")!).render(<App />);