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.
- package/.agents/skills/dirpush/SKILL.md +92 -0
- package/.agents/skills/dirpush/references/assets.md +48 -0
- package/.agents/skills/dirpush/references/authentication.md +59 -0
- package/.agents/skills/dirpush/references/recovery.md +34 -0
- package/.agents/skills/dirpush/references/submissions.md +43 -0
- package/LICENSE +21 -0
- package/README.md +66 -2
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/dirpush.cjs +13 -0
- package/drizzle/0000_init.sql +59 -0
- package/drizzle/0001_jazzy_masked_marvel.sql +9 -0
- package/drizzle/0002_loose_may_parker.sql +9 -0
- package/drizzle/0003_modern_lilandra.sql +6 -0
- package/drizzle/0004_deep_master_chief.sql +11 -0
- package/drizzle/meta/0000_snapshot.json +404 -0
- package/drizzle/meta/0001_snapshot.json +466 -0
- package/drizzle/meta/0002_snapshot.json +527 -0
- package/drizzle/meta/0003_snapshot.json +566 -0
- package/drizzle/meta/0004_snapshot.json +642 -0
- package/drizzle/meta/_journal.json +41 -0
- package/examples/workspace/identity.yaml +6 -0
- package/examples/workspace/permissions.yaml +29 -0
- package/examples/workspace/product.yaml +29 -0
- package/package.json +76 -4
- package/registry/LICENSE +396 -0
- package/registry/directories/1000-tools/directory.yaml +55 -0
- package/registry/directories/1000-tools/playbook.md +5 -0
- package/registry/directories/10015-io/directory.yaml +66 -0
- package/registry/directories/10015-io/playbook.md +9 -0
- package/registry/directories/10words/directory.yaml +53 -0
- package/registry/directories/10words/playbook.md +8 -0
- package/registry/directories/600-tools/directory.yaml +48 -0
- package/registry/directories/600-tools/playbook.md +1 -0
- package/registry/directories/a1bookmarks/directory.yaml +42 -0
- package/registry/directories/aback-launch/directory.yaml +54 -0
- package/registry/directories/aback-launch/playbook.md +1 -0
- package/registry/directories/acid-tools/directory.yaml +51 -0
- package/registry/directories/acid-tools/playbook.md +8 -0
- package/registry/directories/agenthunter/directory.yaml +53 -0
- package/registry/directories/ai-agents-directory/directory.yaml +49 -0
- package/registry/directories/ai-agents-directory/playbook.md +6 -0
- package/registry/directories/ai-agents-list/directory.yaml +46 -0
- package/registry/directories/ai-agents-list/playbook.md +1 -0
- package/registry/directories/ai-agents-listing/directory.yaml +76 -0
- package/registry/directories/ai-directories/directory.yaml +45 -0
- package/registry/directories/ai-directories/playbook.md +7 -0
- package/registry/directories/ai-dust/directory.yaml +68 -0
- package/registry/directories/ai-dust/playbook.md +1 -0
- package/registry/directories/ai-nav/directory.yaml +54 -0
- package/registry/directories/ai-nav/playbook.md +1 -0
- package/registry/directories/ai-respo/directory.yaml +43 -0
- package/registry/directories/ai-respo/playbook.md +1 -0
- package/registry/directories/ai-superhub/directory.yaml +65 -0
- package/registry/directories/ai-tool-list/directory.yaml +44 -0
- package/registry/directories/ai-tool-list/playbook.md +1 -0
- package/registry/directories/ai-tool-saver/directory.yaml +52 -0
- package/registry/directories/ai-tool-saver/playbook.md +1 -0
- package/registry/directories/ai-toolex/directory.yaml +46 -0
- package/registry/directories/ai-toolex/playbook.md +1 -0
- package/registry/directories/ai-toolz-dir/directory.yaml +60 -0
- package/registry/directories/ai-website-directory/directory.yaml +66 -0
- package/registry/directories/ai-website-directory/playbook.md +1 -0
- package/registry/directories/ai-x-collection/directory.yaml +45 -0
- package/registry/directories/ai-x-collection/playbook.md +1 -0
- package/registry/directories/aiagentstore/directory.yaml +52 -0
- package/registry/directories/aiagentstore/playbook.md +1 -0
- package/registry/directories/aidirs-best/directory.yaml +46 -0
- package/registry/directories/aifindy/directory.yaml +44 -0
- package/registry/directories/aifindy/playbook.md +1 -0
- package/registry/directories/aihubs/directory.yaml +44 -0
- package/registry/directories/aihubs/playbook.md +5 -0
- package/registry/directories/aihuntlist/directory.yaml +43 -0
- package/registry/directories/aihuntlist/playbook.md +6 -0
- package/registry/directories/aihustle/directory.yaml +51 -0
- package/registry/directories/aihustle/playbook.md +5 -0
- package/registry/directories/ailaunch-space/directory.yaml +44 -0
- package/registry/directories/ailaunch-space/playbook.md +6 -0
- package/registry/directories/ailib/directory.yaml +64 -0
- package/registry/directories/ailib/playbook.md +1 -0
- package/registry/directories/aimatch-pro/directory.yaml +42 -0
- package/registry/directories/aimatch-pro/playbook.md +1 -0
- package/registry/directories/ainave/directory.yaml +71 -0
- package/registry/directories/ainave/playbook.md +10 -0
- package/registry/directories/aitechviral/directory.yaml +47 -0
- package/registry/directories/aitechviral/playbook.md +10 -0
- package/registry/directories/aitogrow/directory.yaml +52 -0
- package/registry/directories/aitogrow/playbook.md +6 -0
- package/registry/directories/aitoolhunt/directory.yaml +43 -0
- package/registry/directories/aitoolhunt/playbook.md +5 -0
- package/registry/directories/aitools-fyi/directory.yaml +52 -0
- package/registry/directories/aitools-inc/directory.yaml +48 -0
- package/registry/directories/aitools-inc/playbook.md +1 -0
- package/registry/directories/aitoolsdirectory/directory.yaml +73 -0
- package/registry/directories/aitoolsdirectory/playbook.md +8 -0
- package/registry/directories/aitoolshunt/directory.yaml +64 -0
- package/registry/directories/aitoolshunt/playbook.md +6 -0
- package/registry/directories/aitoolslist/directory.yaml +59 -0
- package/registry/directories/aitoolslist/playbook.md +6 -0
- package/registry/directories/aitoolsmagazine/directory.yaml +46 -0
- package/registry/directories/aitoolsmagazine/playbook.md +5 -0
- package/registry/directories/aitoolsmarketer/directory.yaml +56 -0
- package/registry/directories/aitoolsmarketer/playbook.md +7 -0
- package/registry/directories/aitooltrek/directory.yaml +47 -0
- package/registry/directories/aitooltrek/playbook.md +6 -0
- package/registry/directories/aitop10-tools/directory.yaml +44 -0
- package/registry/directories/aitop10-tools/playbook.md +5 -0
- package/registry/directories/aitoptools/directory.yaml +74 -0
- package/registry/directories/aitoptools/playbook.md +1 -0
- package/registry/directories/aiwithme/directory.yaml +54 -0
- package/registry/directories/aiwithme/playbook.md +10 -0
- package/registry/directories/all-in-ai-tools/directory.yaml +51 -0
- package/registry/directories/all-in-ai-tools/playbook.md +1 -0
- package/registry/directories/allmcps/directory.yaml +55 -0
- package/registry/directories/allmcps/playbook.md +7 -0
- package/registry/directories/alltopstartups/directory.yaml +43 -0
- package/registry/directories/altern-ai/directory.yaml +50 -0
- package/registry/directories/altern-ai/playbook.md +6 -0
- package/registry/directories/alternative-tools/directory.yaml +46 -0
- package/registry/directories/alternative-tools/playbook.md +4 -0
- package/registry/directories/alternativeto/directory.yaml +48 -0
- package/registry/directories/alternativeto/playbook.md +8 -0
- package/registry/directories/ant-directory/directory.yaml +52 -0
- package/registry/directories/ant-directory/playbook.md +1 -0
- package/registry/directories/app-scout/directory.yaml +60 -0
- package/registry/directories/app-scout/playbook.md +5 -0
- package/registry/directories/app-stack-builder/directory.yaml +71 -0
- package/registry/directories/appalist/directory.yaml +46 -0
- package/registry/directories/appalist/playbook.md +4 -0
- package/registry/directories/apprater/directory.yaml +65 -0
- package/registry/directories/apprater/playbook.md +1 -0
- package/registry/directories/appsumo/directory.yaml +45 -0
- package/registry/directories/appsumo/playbook.md +1 -0
- package/registry/directories/ask-ai-for-it/directory.yaml +45 -0
- package/registry/directories/ask-ai-for-it/playbook.md +1 -0
- package/registry/directories/auraplusplus/directory.yaml +56 -0
- package/registry/directories/auraplusplus/playbook.md +6 -0
- package/registry/directories/awesome-mcp-list/directory.yaml +45 -0
- package/registry/directories/awesome-mcp-list/playbook.md +7 -0
- package/registry/directories/awesome-mcp-lists/directory.yaml +44 -0
- package/registry/directories/awesome-mcp-lists/playbook.md +6 -0
- package/registry/directories/awesome-remote-mcp-servers/directory.yaml +46 -0
- package/registry/directories/awesome-remote-mcp-servers/playbook.md +8 -0
- package/registry/directories/awesome-tools/directory.yaml +55 -0
- package/registry/directories/awesome-tools/playbook.md +8 -0
- package/registry/directories/awesomeindie/directory.yaml +51 -0
- package/registry/directories/awesomeindie/playbook.md +7 -0
- package/registry/directories/axis-launch/directory.yaml +64 -0
- package/registry/directories/b2b-saas-market/directory.yaml +63 -0
- package/registry/directories/bai-tools/directory.yaml +53 -0
- package/registry/directories/bai-tools/playbook.md +7 -0
- package/registry/directories/basedtools/directory.yaml +59 -0
- package/registry/directories/basedtools/playbook.md +1 -0
- package/registry/directories/best-ai-org/directory.yaml +53 -0
- package/registry/directories/best-ai-org/playbook.md +1 -0
- package/registry/directories/best-of-ai/directory.yaml +57 -0
- package/registry/directories/best-of-ai/playbook.md +1 -0
- package/registry/directories/bestaito/directory.yaml +47 -0
- package/registry/directories/bestaito/playbook.md +6 -0
- package/registry/directories/bestofai/directory.yaml +45 -0
- package/registry/directories/bestofai/playbook.md +7 -0
- package/registry/directories/bestsky-tools/directory.yaml +44 -0
- package/registry/directories/bestsky-tools/playbook.md +5 -0
- package/registry/directories/betabound/directory.yaml +63 -0
- package/registry/directories/betabound/playbook.md +7 -0
- package/registry/directories/betalist/directory.yaml +52 -0
- package/registry/directories/betalist/playbook.md +11 -0
- package/registry/directories/betterbuys/directory.yaml +55 -0
- package/registry/directories/betterbuys/playbook.md +7 -0
- package/registry/directories/betterlaunch/directory.yaml +53 -0
- package/registry/directories/beyond-ai-tools/directory.yaml +43 -0
- package/registry/directories/beyond-ai-tools/playbook.md +1 -0
- package/registry/directories/billion-dollar-pitch-decks/directory.yaml +64 -0
- package/registry/directories/billion-dollar-pitch-decks/playbook.md +4 -0
- package/registry/directories/browserextension-dev/directory.yaml +55 -0
- package/registry/directories/browserextension-dev/playbook.md +7 -0
- package/registry/directories/bsdirectory/directory.yaml +68 -0
- package/registry/directories/bsdirectory/playbook.md +5 -0
- package/registry/directories/buildvoyage/directory.yaml +64 -0
- package/registry/directories/buildvoyage/playbook.md +1 -0
- package/registry/directories/built-here/directory.yaml +50 -0
- package/registry/directories/built-here/playbook.md +6 -0
- package/registry/directories/builtbyme/directory.yaml +55 -0
- package/registry/directories/builtbyme/playbook.md +7 -0
- package/registry/directories/builtineu/directory.yaml +70 -0
- package/registry/directories/builtineu/playbook.md +7 -0
- package/registry/directories/businesssoftware/directory.yaml +42 -0
- package/registry/directories/buy-sell-startups/directory.yaml +52 -0
- package/registry/directories/buy-sell-startups/playbook.md +5 -0
- package/registry/directories/claude-mcp-directory/directory.yaml +45 -0
- package/registry/directories/claude-mcp-directory/playbook.md +7 -0
- package/registry/directories/claude-plugin-directory/directory.yaml +48 -0
- package/registry/directories/claude-plugin-directory/playbook.md +7 -0
- package/registry/directories/cocoon-ai/directory.yaml +57 -0
- package/registry/directories/codehype/directory.yaml +68 -0
- package/registry/directories/codehype/playbook.md +7 -0
- package/registry/directories/collectai/directory.yaml +43 -0
- package/registry/directories/collectai/playbook.md +5 -0
- package/registry/directories/commune-launchpad/directory.yaml +51 -0
- package/registry/directories/confettisaas/directory.yaml +51 -0
- package/registry/directories/confettisaas/playbook.md +1 -0
- package/registry/directories/crowdstax/directory.yaml +54 -0
- package/registry/directories/crowdstax/playbook.md +6 -0
- package/registry/directories/crozdesk/directory.yaml +72 -0
- package/registry/directories/crozdesk/playbook.md +8 -0
- package/registry/directories/crypto-tools-directory/directory.yaml +60 -0
- package/registry/directories/crypto-tools-directory/playbook.md +6 -0
- package/registry/directories/ctrlaltship/directory.yaml +92 -0
- package/registry/directories/ctrlaltship/playbook.md +6 -0
- package/registry/directories/dachstartups/directory.yaml +49 -0
- package/registry/directories/dachstartups/playbook.md +4 -0
- package/registry/directories/dailypings/directory.yaml +51 -0
- package/registry/directories/dailypings/playbook.md +6 -0
- package/registry/directories/dang-ai/directory.yaml +44 -0
- package/registry/directories/dang-ai/playbook.md +1 -0
- package/registry/directories/daniellaunches/directory.yaml +47 -0
- package/registry/directories/daniellaunches/playbook.md +6 -0
- package/registry/directories/datalook/directory.yaml +60 -0
- package/registry/directories/dayslaunch/directory.yaml +46 -0
- package/registry/directories/dayslaunch/playbook.md +5 -0
- package/registry/directories/deeplaunch/directory.yaml +46 -0
- package/registry/directories/deeplaunch/playbook.md +6 -0
- package/registry/directories/desifounder-spotlight/directory.yaml +49 -0
- package/registry/directories/desifounder-spotlight/playbook.md +1 -0
- package/registry/directories/developers-digest-mcp/directory.yaml +46 -0
- package/registry/directories/developers-digest-mcp/playbook.md +7 -0
- package/registry/directories/devhunt/directory.yaml +53 -0
- package/registry/directories/devhunt/playbook.md +8 -0
- package/registry/directories/devpages/directory.yaml +62 -0
- package/registry/directories/devpages/playbook.md +7 -0
- package/registry/directories/devresourc-es/directory.yaml +49 -0
- package/registry/directories/devresourc-es/playbook.md +9 -0
- package/registry/directories/devtools-directory/directory.yaml +63 -0
- package/registry/directories/devtools-directory/playbook.md +7 -0
- package/registry/directories/dir-hub/directory.yaml +62 -0
- package/registry/directories/dir-hub/playbook.md +6 -0
- package/registry/directories/direct2app/directory.yaml +47 -0
- package/registry/directories/direct2app/playbook.md +5 -0
- package/registry/directories/directree/directory.yaml +56 -0
- package/registry/directories/directree/playbook.md +7 -0
- package/registry/directories/dirs-cc/directory.yaml +49 -0
- package/registry/directories/dirs-cc/playbook.md +1 -0
- package/registry/directories/dododirectory/directory.yaml +46 -0
- package/registry/directories/dododirectory/playbook.md +6 -0
- package/registry/directories/dofollowtools/directory.yaml +75 -0
- package/registry/directories/dofollowtools/playbook.md +7 -0
- package/registry/directories/dropyourai/directory.yaml +73 -0
- package/registry/directories/dynamite-ai/directory.yaml +59 -0
- package/registry/directories/dynamite-ai/playbook.md +1 -0
- package/registry/directories/earlyhunt/directory.yaml +44 -0
- package/registry/directories/earlyhunt/playbook.md +6 -0
- package/registry/directories/ebool/directory.yaml +72 -0
- package/registry/directories/ebool/playbook.md +8 -0
- package/registry/directories/eliteai-tools/directory.yaml +45 -0
- package/registry/directories/eliteai-tools/playbook.md +1 -0
- package/registry/directories/emoji-launch/directory.yaml +60 -0
- package/registry/directories/emoji-launch/playbook.md +1 -0
- package/registry/directories/enlisted/directory.yaml +48 -0
- package/registry/directories/enlisted/playbook.md +7 -0
- package/registry/directories/eu-software-alternatives/directory.yaml +87 -0
- package/registry/directories/eu-software-alternatives/playbook.md +5 -0
- package/registry/directories/eu-vetted/directory.yaml +64 -0
- package/registry/directories/eu-vetted/playbook.md +1 -0
- package/registry/directories/eubuilt/directory.yaml +77 -0
- package/registry/directories/eubuilt/playbook.md +5 -0
- package/registry/directories/euroalternative/directory.yaml +48 -0
- package/registry/directories/euroalternative/playbook.md +1 -0
- package/registry/directories/european-tech/directory.yaml +55 -0
- package/registry/directories/european-tech/playbook.md +5 -0
- package/registry/directories/europeanapps/directory.yaml +52 -0
- package/registry/directories/europeanapps/playbook.md +5 -0
- package/registry/directories/europeanstack/directory.yaml +66 -0
- package/registry/directories/europeanstack/playbook.md +7 -0
- package/registry/directories/eurotoolsindex/directory.yaml +68 -0
- package/registry/directories/eurotoolsindex/playbook.md +7 -0
- package/registry/directories/everydev-ai/directory.yaml +50 -0
- package/registry/directories/everydev-ai/playbook.md +6 -0
- package/registry/directories/faceless-directory/directory.yaml +52 -0
- package/registry/directories/fastd/directory.yaml +45 -0
- package/registry/directories/fastd/playbook.md +1 -0
- package/registry/directories/fastlaunch/directory.yaml +53 -0
- package/registry/directories/fastlaunch/playbook.md +10 -0
- package/registry/directories/favors/directory.yaml +72 -0
- package/registry/directories/favors/playbook.md +7 -0
- package/registry/directories/fazier/directory.yaml +44 -0
- package/registry/directories/fazier/playbook.md +8 -0
- package/registry/directories/featuredtool/directory.yaml +51 -0
- package/registry/directories/featuredtool/playbook.md +1 -0
- package/registry/directories/find-seo-tools/directory.yaml +72 -0
- package/registry/directories/find-seo-tools/playbook.md +1 -0
- package/registry/directories/findly-tools/directory.yaml +45 -0
- package/registry/directories/findly-tools/playbook.md +7 -0
- package/registry/directories/findstack/directory.yaml +46 -0
- package/registry/directories/findstack/playbook.md +1 -0
- package/registry/directories/findyoursaas/directory.yaml +47 -0
- package/registry/directories/findyoursaas/playbook.md +7 -0
- package/registry/directories/firsto/directory.yaml +54 -0
- package/registry/directories/firsto/playbook.md +10 -0
- package/registry/directories/fosspace/directory.yaml +49 -0
- package/registry/directories/founders-today/directory.yaml +52 -0
- package/registry/directories/francesaas/directory.yaml +49 -0
- package/registry/directories/francesaas/playbook.md +5 -0
- package/registry/directories/free-startup-directories/directory.yaml +70 -0
- package/registry/directories/free-startup-listing/directory.yaml +59 -0
- package/registry/directories/free-startup-listing/playbook.md +6 -0
- package/registry/directories/free-tools-directory/directory.yaml +55 -0
- package/registry/directories/free-tools-directory/playbook.md +7 -0
- package/registry/directories/freeaitools-net/directory.yaml +57 -0
- package/registry/directories/from-europe-with-love/directory.yaml +63 -0
- package/registry/directories/from-europe-with-love/playbook.md +5 -0
- package/registry/directories/fstlaunch/directory.yaml +57 -0
- package/registry/directories/fstlaunch/playbook.md +6 -0
- package/registry/directories/futuretools/directory.yaml +67 -0
- package/registry/directories/futuretools/playbook.md +10 -0
- package/registry/directories/glama/directory.yaml +46 -0
- package/registry/directories/glama/playbook.md +7 -0
- package/registry/directories/goodaitools/directory.yaml +46 -0
- package/registry/directories/goodaitools/playbook.md +7 -0
- package/registry/directories/gptbot/directory.yaml +56 -0
- package/registry/directories/gptdemo/directory.yaml +56 -0
- package/registry/directories/gptdemo/playbook.md +7 -0
- package/registry/directories/hicyou/directory.yaml +51 -0
- package/registry/directories/hicyou/playbook.md +1 -0
- package/registry/directories/hightechattic/directory.yaml +49 -0
- package/registry/directories/hightechattic/playbook.md +6 -0
- package/registry/directories/howtobuysaas/directory.yaml +59 -0
- package/registry/directories/howtobuysaas/playbook.md +1 -0
- package/registry/directories/hunt0/directory.yaml +81 -0
- package/registry/directories/hunt0/playbook.md +1 -0
- package/registry/directories/huntscreens/directory.yaml +56 -0
- package/registry/directories/huntscreens/playbook.md +11 -0
- package/registry/directories/huntstartup/directory.yaml +53 -0
- package/registry/directories/huntstartup/playbook.md +8 -0
- package/registry/directories/huzzler/directory.yaml +49 -0
- package/registry/directories/huzzler/playbook.md +7 -0
- package/registry/directories/hype-star/directory.yaml +57 -0
- package/registry/directories/hype-star/playbook.md +6 -0
- package/registry/directories/idea-kiln/directory.yaml +50 -0
- package/registry/directories/idea-kiln/playbook.md +6 -0
- package/registry/directories/ignlab-launch/directory.yaml +82 -0
- package/registry/directories/ignlab-launch/playbook.md +7 -0
- package/registry/directories/indie-deals/directory.yaml +48 -0
- package/registry/directories/indie-deals/playbook.md +8 -0
- package/registry/directories/indie-hustles/directory.yaml +61 -0
- package/registry/directories/indie-hustles/playbook.md +1 -0
- package/registry/directories/indie-makers-hub/directory.yaml +50 -0
- package/registry/directories/indie-makers-hub/playbook.md +5 -0
- package/registry/directories/indie-page/directory.yaml +47 -0
- package/registry/directories/indie-page/playbook.md +1 -0
- package/registry/directories/indiefounders/directory.yaml +48 -0
- package/registry/directories/indiefounders/playbook.md +5 -0
- package/registry/directories/indiehackers/directory.yaml +47 -0
- package/registry/directories/indiehackers/playbook.md +1 -0
- package/registry/directories/indiehackerstacks/directory.yaml +45 -0
- package/registry/directories/indiehackerstacks/playbook.md +5 -0
- package/registry/directories/indiehub/directory.yaml +45 -0
- package/registry/directories/indiehub/playbook.md +8 -0
- package/registry/directories/indiehunt/directory.yaml +45 -0
- package/registry/directories/indiehunt/playbook.md +6 -0
- package/registry/directories/indieneed/directory.yaml +78 -0
- package/registry/directories/indieneed/playbook.md +9 -0
- package/registry/directories/indieproducts/directory.yaml +47 -0
- package/registry/directories/indieproducts/playbook.md +4 -0
- package/registry/directories/indiesignal/directory.yaml +47 -0
- package/registry/directories/indietool/directory.yaml +44 -0
- package/registry/directories/indietool/playbook.md +1 -0
- package/registry/directories/indietools/directory.yaml +69 -0
- package/registry/directories/insidr-ai/directory.yaml +49 -0
- package/registry/directories/insidr-ai/playbook.md +5 -0
- package/registry/directories/intelligent-tools/directory.yaml +59 -0
- package/registry/directories/intelligent-tools/playbook.md +1 -0
- package/registry/directories/inventlist/directory.yaml +71 -0
- package/registry/directories/inventlist/playbook.md +5 -0
- package/registry/directories/joinly/directory.yaml +68 -0
- package/registry/directories/joinly/playbook.md +5 -0
- package/registry/directories/junkstartups/directory.yaml +72 -0
- package/registry/directories/junkstartups/playbook.md +1 -0
- package/registry/directories/justhunt/directory.yaml +55 -0
- package/registry/directories/justhunt/playbook.md +7 -0
- package/registry/directories/kittylaunch/directory.yaml +70 -0
- package/registry/directories/kittylaunch/playbook.md +7 -0
- package/registry/directories/lachief/directory.yaml +81 -0
- package/registry/directories/lachief/playbook.md +1 -0
- package/registry/directories/landbook/directory.yaml +48 -0
- package/registry/directories/landbook/playbook.md +1 -0
- package/registry/directories/launch-community/directory.yaml +73 -0
- package/registry/directories/launch-community/playbook.md +1 -0
- package/registry/directories/launch-list/directory.yaml +52 -0
- package/registry/directories/launch-list/playbook.md +6 -0
- package/registry/directories/launchaf/directory.yaml +51 -0
- package/registry/directories/launchbase-india/directory.yaml +53 -0
- package/registry/directories/launchboard/directory.yaml +50 -0
- package/registry/directories/launchboard/playbook.md +6 -0
- package/registry/directories/launchclash/directory.yaml +47 -0
- package/registry/directories/launchclash/playbook.md +6 -0
- package/registry/directories/launched-tools/directory.yaml +48 -0
- package/registry/directories/launchfree/directory.yaml +58 -0
- package/registry/directories/launchfree/playbook.md +1 -0
- package/registry/directories/launchigniter/directory.yaml +60 -0
- package/registry/directories/launchigniter/playbook.md +9 -0
- package/registry/directories/launchit/directory.yaml +48 -0
- package/registry/directories/launchit/playbook.md +1 -0
- package/registry/directories/launchllama/directory.yaml +53 -0
- package/registry/directories/launchllama/playbook.md +7 -0
- package/registry/directories/launchonit/directory.yaml +74 -0
- package/registry/directories/launchonit/playbook.md +7 -0
- package/registry/directories/launchpedia-tools/directory.yaml +43 -0
- package/registry/directories/launchpedia-tools/playbook.md +5 -0
- package/registry/directories/launchscaler/directory.yaml +53 -0
- package/registry/directories/launchtory/directory.yaml +47 -0
- package/registry/directories/launchtory/playbook.md +5 -0
- package/registry/directories/launchvault/directory.yaml +52 -0
- package/registry/directories/launchvault/playbook.md +5 -0
- package/registry/directories/launchvibe/directory.yaml +45 -0
- package/registry/directories/launchvibe/playbook.md +1 -0
- package/registry/directories/launchvoid/directory.yaml +45 -0
- package/registry/directories/launchvoid/playbook.md +6 -0
- package/registry/directories/letslaunch/directory.yaml +53 -0
- package/registry/directories/letslaunch/playbook.md +1 -0
- package/registry/directories/libhunt/directory.yaml +50 -0
- package/registry/directories/libhunt/playbook.md +1 -0
- package/registry/directories/linezine/directory.yaml +44 -0
- package/registry/directories/linezine/playbook.md +1 -0
- package/registry/directories/linkrena/directory.yaml +58 -0
- package/registry/directories/linkrena/playbook.md +6 -0
- package/registry/directories/list-maro/directory.yaml +74 -0
- package/registry/directories/list-your-tool/directory.yaml +76 -0
- package/registry/directories/list-your-tool/playbook.md +1 -0
- package/registry/directories/listbulb/directory.yaml +48 -0
- package/registry/directories/listbulb/playbook.md +1 -0
- package/registry/directories/listing-sh/directory.yaml +70 -0
- package/registry/directories/llm-relevance/directory.yaml +67 -0
- package/registry/directories/llm-relevance/playbook.md +1 -0
- package/registry/directories/llms-txt-hub/directory.yaml +47 -0
- package/registry/directories/llms-txt-hub/playbook.md +6 -0
- package/registry/directories/llms-txt-site/directory.yaml +53 -0
- package/registry/directories/llms-txt-site/playbook.md +7 -0
- package/registry/directories/macnative/directory.yaml +47 -0
- package/registry/directories/macnative/playbook.md +6 -0
- package/registry/directories/made-with-bolt/directory.yaml +58 -0
- package/registry/directories/made-with-bolt/playbook.md +1 -0
- package/registry/directories/made-with-lovable/directory.yaml +64 -0
- package/registry/directories/made-with-lovable/playbook.md +1 -0
- package/registry/directories/magicbox/directory.yaml +51 -0
- package/registry/directories/magicbox/playbook.md +16 -0
- package/registry/directories/maidensail/directory.yaml +54 -0
- package/registry/directories/makeitlast/directory.yaml +63 -0
- package/registry/directories/makeitlast/playbook.md +8 -0
- package/registry/directories/makerhunt/directory.yaml +56 -0
- package/registry/directories/makerhunt/playbook.md +1 -0
- package/registry/directories/makerlist/directory.yaml +49 -0
- package/registry/directories/makerlist/playbook.md +7 -0
- package/registry/directories/mapbid/directory.yaml +46 -0
- package/registry/directories/marketingdb/directory.yaml +50 -0
- package/registry/directories/marketingdb/playbook.md +8 -0
- package/registry/directories/marketraa/directory.yaml +63 -0
- package/registry/directories/marketraa/playbook.md +1 -0
- package/registry/directories/mcp-market/directory.yaml +49 -0
- package/registry/directories/mcp-market/playbook.md +7 -0
- package/registry/directories/mcp-repository/directory.yaml +45 -0
- package/registry/directories/mcp-repository/playbook.md +6 -0
- package/registry/directories/mcp-so/directory.yaml +45 -0
- package/registry/directories/mcp-so/playbook.md +6 -0
- package/registry/directories/mcpservers/directory.yaml +53 -0
- package/registry/directories/mcpservers/playbook.md +7 -0
- package/registry/directories/mf8/directory.yaml +53 -0
- package/registry/directories/mf8/playbook.md +5 -0
- package/registry/directories/microlaunch/directory.yaml +48 -0
- package/registry/directories/microlaunch/playbook.md +1 -0
- package/registry/directories/microsaasexamples/directory.yaml +50 -0
- package/registry/directories/must-tools/directory.yaml +68 -0
- package/registry/directories/must-tools/playbook.md +6 -0
- package/registry/directories/my-launch-stash/directory.yaml +47 -0
- package/registry/directories/my-launch-stash/playbook.md +8 -0
- package/registry/directories/nachonacho/directory.yaml +47 -0
- package/registry/directories/navfolders/directory.yaml +48 -0
- package/registry/directories/navfolders/playbook.md +5 -0
- package/registry/directories/navs/directory.yaml +66 -0
- package/registry/directories/navs/playbook.md +6 -0
- package/registry/directories/navtools-ai/directory.yaml +51 -0
- package/registry/directories/navtools-ai/playbook.md +1 -0
- package/registry/directories/neeed-directory/directory.yaml +60 -0
- package/registry/directories/neeed-directory/playbook.md +10 -0
- package/registry/directories/neonrev/directory.yaml +43 -0
- package/registry/directories/neonrev/playbook.md +1 -0
- package/registry/directories/newaiforyou/directory.yaml +50 -0
- package/registry/directories/newaiforyou/playbook.md +1 -0
- package/registry/directories/newtool/directory.yaml +51 -0
- package/registry/directories/newtool/playbook.md +1 -0
- package/registry/directories/nextbigwhat/directory.yaml +48 -0
- package/registry/directories/nextbigwhat/playbook.md +5 -0
- package/registry/directories/nextlaunch/directory.yaml +50 -0
- package/registry/directories/nextlaunch/playbook.md +8 -0
- package/registry/directories/nicklaunches/directory.yaml +70 -0
- package/registry/directories/nicklaunches/playbook.md +1 -0
- package/registry/directories/nocodefinder/directory.yaml +45 -0
- package/registry/directories/nocodefinder/playbook.md +6 -0
- package/registry/directories/nocodelist/directory.yaml +48 -0
- package/registry/directories/nocodelist/playbook.md +7 -0
- package/registry/directories/noonlaunch/directory.yaml +51 -0
- package/registry/directories/noonlaunch/playbook.md +6 -0
- package/registry/directories/nxgn-tools/directory.yaml +49 -0
- package/registry/directories/nxgn-tools/playbook.md +6 -0
- package/registry/directories/once-tools/directory.yaml +58 -0
- package/registry/directories/once-tools/playbook.md +1 -0
- package/registry/directories/open-launch/directory.yaml +46 -0
- package/registry/directories/open-launch/playbook.md +1 -0
- package/registry/directories/openalternative/directory.yaml +62 -0
- package/registry/directories/openalternative/playbook.md +8 -0
- package/registry/directories/openhunts/directory.yaml +52 -0
- package/registry/directories/openhunts/playbook.md +1 -0
- package/registry/directories/openresources/directory.yaml +59 -0
- package/registry/directories/openresources/playbook.md +6 -0
- package/registry/directories/opensaas-directory/directory.yaml +47 -0
- package/registry/directories/opensourcealternative-to/directory.yaml +63 -0
- package/registry/directories/opensourcealternative-to/playbook.md +6 -0
- package/registry/directories/opentools/directory.yaml +42 -0
- package/registry/directories/opentools/playbook.md +4 -0
- package/registry/directories/osssoftware/directory.yaml +66 -0
- package/registry/directories/osssoftware/playbook.md +5 -0
- package/registry/directories/oursoftware-eu/directory.yaml +69 -0
- package/registry/directories/oursoftware-eu/playbook.md +1 -0
- package/registry/directories/peerpush/directory.yaml +48 -0
- package/registry/directories/peerpush/playbook.md +8 -0
- package/registry/directories/phyco/directory.yaml +54 -0
- package/registry/directories/phygital-ai-library/directory.yaml +47 -0
- package/registry/directories/phygital-ai-library/playbook.md +6 -0
- package/registry/directories/pitchground/directory.yaml +47 -0
- package/registry/directories/pitchground/playbook.md +5 -0
- package/registry/directories/pitchwall/directory.yaml +47 -0
- package/registry/directories/pitchwall/playbook.md +1 -0
- package/registry/directories/postmake/directory.yaml +56 -0
- package/registry/directories/postmake/playbook.md +1 -0
- package/registry/directories/poweredbyai/directory.yaml +57 -0
- package/registry/directories/poweredbyai/playbook.md +1 -0
- package/registry/directories/pro-launch/directory.yaml +51 -0
- package/registry/directories/pro-launch/playbook.md +9 -0
- package/registry/directories/product-hunt/directory.yaml +54 -0
- package/registry/directories/product-hunt/playbook.md +1 -0
- package/registry/directories/product-rally/directory.yaml +50 -0
- package/registry/directories/product-rally/playbook.md +6 -0
- package/registry/directories/productivity-directory/directory.yaml +47 -0
- package/registry/directories/productivity-directory/playbook.md +7 -0
- package/registry/directories/productwatch/directory.yaml +51 -0
- package/registry/directories/productwatch/playbook.md +7 -0
- package/registry/directories/proofstories/directory.yaml +47 -0
- package/registry/directories/proofstories/playbook.md +1 -0
- package/registry/directories/prouct/directory.yaml +93 -0
- package/registry/directories/prouct/playbook.md +7 -0
- package/registry/directories/public-apis/directory.yaml +47 -0
- package/registry/directories/public-apis/playbook.md +6 -0
- package/registry/directories/publishyoursaas/directory.yaml +53 -0
- package/registry/directories/publishyoursaas/playbook.md +7 -0
- package/registry/directories/rankinpublic/directory.yaml +50 -0
- package/registry/directories/rankinpublic/playbook.md +1 -0
- package/registry/directories/rankyourai/directory.yaml +50 -0
- package/registry/directories/rankyourai/playbook.md +1 -0
- package/registry/directories/resource-fyi/directory.yaml +47 -0
- package/registry/directories/resource-fyi/playbook.md +5 -0
- package/registry/directories/roozna/directory.yaml +68 -0
- package/registry/directories/roozna/playbook.md +1 -0
- package/registry/directories/saas-fyi/directory.yaml +73 -0
- package/registry/directories/saas-fyi/playbook.md +7 -0
- package/registry/directories/saas-italia/directory.yaml +58 -0
- package/registry/directories/saas-launches/directory.yaml +47 -0
- package/registry/directories/saas-launches/playbook.md +6 -0
- package/registry/directories/saasbison/directory.yaml +44 -0
- package/registry/directories/saasbison/playbook.md +8 -0
- package/registry/directories/saascity/directory.yaml +50 -0
- package/registry/directories/saascubes/directory.yaml +49 -0
- package/registry/directories/saascubes/playbook.md +6 -0
- package/registry/directories/saasfame/directory.yaml +47 -0
- package/registry/directories/saasfame/playbook.md +6 -0
- package/registry/directories/saasgenius/directory.yaml +50 -0
- package/registry/directories/saasgenius/playbook.md +8 -0
- package/registry/directories/saasgrave-launches/directory.yaml +53 -0
- package/registry/directories/saasgrave-launches/playbook.md +7 -0
- package/registry/directories/saashammer/directory.yaml +53 -0
- package/registry/directories/saashammer/playbook.md +6 -0
- package/registry/directories/saashub/directory.yaml +51 -0
- package/registry/directories/saashub/playbook.md +9 -0
- package/registry/directories/saashunt/directory.yaml +43 -0
- package/registry/directories/saaslineup/directory.yaml +68 -0
- package/registry/directories/saaslineup/playbook.md +1 -0
- package/registry/directories/saaslist/directory.yaml +55 -0
- package/registry/directories/saaslist/playbook.md +5 -0
- package/registry/directories/saasmag-saas1000/directory.yaml +71 -0
- package/registry/directories/saaspage/directory.yaml +48 -0
- package/registry/directories/saasplace/directory.yaml +66 -0
- package/registry/directories/saasplace/playbook.md +1 -0
- package/registry/directories/saasrow/directory.yaml +51 -0
- package/registry/directories/saasrow/playbook.md +1 -0
- package/registry/directories/saassy-board/directory.yaml +54 -0
- package/registry/directories/saassy-board/playbook.md +1 -0
- package/registry/directories/saastore/directory.yaml +56 -0
- package/registry/directories/saastore/playbook.md +6 -0
- package/registry/directories/saastr-community-agents/directory.yaml +65 -0
- package/registry/directories/saastr-community-agents/playbook.md +8 -0
- package/registry/directories/saasvault/directory.yaml +48 -0
- package/registry/directories/saasvault/playbook.md +6 -0
- package/registry/directories/scrolllaunch/directory.yaml +54 -0
- package/registry/directories/scrolllaunch/playbook.md +8 -0
- package/registry/directories/seewhatnewai/directory.yaml +53 -0
- package/registry/directories/sellwithboost/directory.yaml +54 -0
- package/registry/directories/sellwithboost/playbook.md +5 -0
- package/registry/directories/serchen/directory.yaml +48 -0
- package/registry/directories/serchen/playbook.md +8 -0
- package/registry/directories/shinylaunch/directory.yaml +48 -0
- package/registry/directories/shinylaunch/playbook.md +6 -0
- package/registry/directories/shipyard-hq/directory.yaml +58 -0
- package/registry/directories/shipyard-hq/playbook.md +1 -0
- package/registry/directories/shipybara/directory.yaml +48 -0
- package/registry/directories/shipybara/playbook.md +8 -0
- package/registry/directories/shipycorn/directory.yaml +47 -0
- package/registry/directories/shipycorn/playbook.md +6 -0
- package/registry/directories/showmebest-ai/directory.yaml +50 -0
- package/registry/directories/showmebest-ai/playbook.md +1 -0
- package/registry/directories/showmysites/directory.yaml +52 -0
- package/registry/directories/showmysites/playbook.md +7 -0
- package/registry/directories/sidehunt/directory.yaml +56 -0
- package/registry/directories/sidehunt/playbook.md +1 -0
- package/registry/directories/sideprojectors/directory.yaml +55 -0
- package/registry/directories/sideprojectors/playbook.md +1 -0
- package/registry/directories/sideprojects/directory.yaml +52 -0
- package/registry/directories/sideprojects/playbook.md +1 -0
- package/registry/directories/sitepatent/directory.yaml +58 -0
- package/registry/directories/sitepatent/playbook.md +1 -0
- package/registry/directories/small-business-marketing-tools/directory.yaml +67 -0
- package/registry/directories/small-business-marketing-tools/playbook.md +1 -0
- package/registry/directories/smol-launch/directory.yaml +44 -0
- package/registry/directories/smol-launch/playbook.md +9 -0
- package/registry/directories/software-como-servicio/directory.yaml +64 -0
- package/registry/directories/software-como-servicio/playbook.md +6 -0
- package/registry/directories/software-finder/directory.yaml +47 -0
- package/registry/directories/software-finder/playbook.md +6 -0
- package/registry/directories/softwaresuggest/directory.yaml +68 -0
- package/registry/directories/softwaresuggest/playbook.md +6 -0
- package/registry/directories/solo-launches/directory.yaml +48 -0
- package/registry/directories/solo-launches/playbook.md +6 -0
- package/registry/directories/sololaunch/directory.yaml +69 -0
- package/registry/directories/sourceforge/directory.yaml +75 -0
- package/registry/directories/sourceforge/playbook.md +9 -0
- package/registry/directories/spacerrapps/directory.yaml +53 -0
- package/registry/directories/spacerrapps/playbook.md +6 -0
- package/registry/directories/speartip/directory.yaml +48 -0
- package/registry/directories/speartip/playbook.md +1 -0
- package/registry/directories/spiff-store/directory.yaml +46 -0
- package/registry/directories/spiff-store/playbook.md +6 -0
- package/registry/directories/spot-startups/directory.yaml +48 -0
- package/registry/directories/spotsaas/directory.yaml +45 -0
- package/registry/directories/squidir/directory.yaml +60 -0
- package/registry/directories/squidir/playbook.md +5 -0
- package/registry/directories/stackboo/directory.yaml +49 -0
- package/registry/directories/stackboo/playbook.md +6 -0
- package/registry/directories/stackledge/directory.yaml +84 -0
- package/registry/directories/stackledge/playbook.md +8 -0
- package/registry/directories/startup-alternatives/directory.yaml +68 -0
- package/registry/directories/startup-alternatives/playbook.md +5 -0
- package/registry/directories/startup-benchmarks/directory.yaml +59 -0
- package/registry/directories/startup-benchmarks/playbook.md +1 -0
- package/registry/directories/startup-collections/directory.yaml +56 -0
- package/registry/directories/startup-collections/playbook.md +5 -0
- package/registry/directories/startup-europe/directory.yaml +52 -0
- package/registry/directories/startup-europe/playbook.md +1 -0
- package/registry/directories/startup-fame/directory.yaml +51 -0
- package/registry/directories/startup-fame/playbook.md +8 -0
- package/registry/directories/startup-fast/directory.yaml +52 -0
- package/registry/directories/startup-fast/playbook.md +9 -0
- package/registry/directories/startup-list/directory.yaml +81 -0
- package/registry/directories/startup-list/playbook.md +8 -0
- package/registry/directories/startup-listing/directory.yaml +77 -0
- package/registry/directories/startup-listing/playbook.md +1 -0
- package/registry/directories/startup-og/directory.yaml +52 -0
- package/registry/directories/startup-ranking/directory.yaml +51 -0
- package/registry/directories/startup-ranking/playbook.md +1 -0
- package/registry/directories/startup-spotlight/directory.yaml +50 -0
- package/registry/directories/startup-spotlight/playbook.md +5 -0
- package/registry/directories/startup88/directory.yaml +46 -0
- package/registry/directories/startup88/playbook.md +1 -0
- package/registry/directories/startupaccelerator/directory.yaml +76 -0
- package/registry/directories/startupaccelerator/playbook.md +7 -0
- package/registry/directories/startupbase/directory.yaml +44 -0
- package/registry/directories/startupbase/playbook.md +1 -0
- package/registry/directories/startupbeats/directory.yaml +48 -0
- package/registry/directories/startupbeats/playbook.md +6 -0
- package/registry/directories/startupdirectory/directory.yaml +50 -0
- package/registry/directories/startupdirectory/playbook.md +1 -0
- package/registry/directories/startupinspire/directory.yaml +52 -0
- package/registry/directories/startupinspire/playbook.md +8 -0
- package/registry/directories/startuplibrary/directory.yaml +46 -0
- package/registry/directories/startuplibrary/playbook.md +1 -0
- package/registry/directories/startuplist-africa/directory.yaml +45 -0
- package/registry/directories/startuplist-africa/playbook.md +6 -0
- package/registry/directories/startups-gallery/directory.yaml +61 -0
- package/registry/directories/startups-gallery/playbook.md +5 -0
- package/registry/directories/startupsfm/directory.yaml +64 -0
- package/registry/directories/startupsfm/playbook.md +9 -0
- package/registry/directories/startupsfyi/directory.yaml +51 -0
- package/registry/directories/startupsfyi/playbook.md +4 -0
- package/registry/directories/startupslab/directory.yaml +61 -0
- package/registry/directories/startupslab/playbook.md +8 -0
- package/registry/directories/startuptile/directory.yaml +51 -0
- package/registry/directories/startuptile/playbook.md +5 -0
- package/registry/directories/startuptrusted/directory.yaml +46 -0
- package/registry/directories/startuptrusted/playbook.md +1 -0
- package/registry/directories/startuups/directory.yaml +50 -0
- package/registry/directories/steemhunt/directory.yaml +46 -0
- package/registry/directories/steemhunt/playbook.md +5 -0
- package/registry/directories/stellar-launch/directory.yaml +55 -0
- package/registry/directories/stellar-launch/playbook.md +1 -0
- package/registry/directories/stork-ai/directory.yaml +49 -0
- package/registry/directories/stork-ai/playbook.md +3 -0
- package/registry/directories/stork-mcp/directory.yaml +52 -0
- package/registry/directories/stork-mcp/playbook.md +3 -0
- package/registry/directories/submitaitools/directory.yaml +57 -0
- package/registry/directories/submitaitools/playbook.md +19 -0
- package/registry/directories/submithunt/directory.yaml +49 -0
- package/registry/directories/submithunt/playbook.md +9 -0
- package/registry/directories/submitmysaas/directory.yaml +51 -0
- package/registry/directories/submitmysaas/playbook.md +6 -0
- package/registry/directories/submitstartup/directory.yaml +73 -0
- package/registry/directories/sumodir/directory.yaml +47 -0
- package/registry/directories/sumodir/playbook.md +7 -0
- package/registry/directories/superlaunch/directory.yaml +64 -0
- package/registry/directories/superlaunch/playbook.md +7 -0
- package/registry/directories/techboard-australia/directory.yaml +54 -0
- package/registry/directories/techboard-australia/playbook.md +5 -0
- package/registry/directories/technologyadvice/directory.yaml +65 -0
- package/registry/directories/technologyadvice/playbook.md +8 -0
- package/registry/directories/tekpon/directory.yaml +44 -0
- package/registry/directories/tensorblock-mcp/directory.yaml +46 -0
- package/registry/directories/tensorblock-mcp/playbook.md +7 -0
- package/registry/directories/thankjohn/directory.yaml +42 -0
- package/registry/directories/thankjohn/playbook.md +1 -0
- package/registry/directories/that-ai-collection/directory.yaml +52 -0
- package/registry/directories/the-ai-library/directory.yaml +46 -0
- package/registry/directories/the-ai-library/playbook.md +1 -0
- package/registry/directories/the-next-ai/directory.yaml +62 -0
- package/registry/directories/the-popular-apps/directory.yaml +43 -0
- package/registry/directories/the-popular-apps/playbook.md +1 -0
- package/registry/directories/the-startup-inc/directory.yaml +90 -0
- package/registry/directories/the-startup-inc/playbook.md +4 -0
- package/registry/directories/the-startup-project/directory.yaml +67 -0
- package/registry/directories/the-startup-project/playbook.md +5 -0
- package/registry/directories/the-tool-directory/directory.yaml +75 -0
- package/registry/directories/thedevtoolsdir/directory.yaml +57 -0
- package/registry/directories/thedevtoolsdir/playbook.md +6 -0
- package/registry/directories/theres-an-ai/directory.yaml +44 -0
- package/registry/directories/theres-an-ai/playbook.md +1 -0
- package/registry/directories/theresanaiforthat/directory.yaml +51 -0
- package/registry/directories/theresanaiforthat/playbook.md +1 -0
- package/registry/directories/thesaasdir/directory.yaml +44 -0
- package/registry/directories/thesaasdir/playbook.md +6 -0
- package/registry/directories/tinylaunch/directory.yaml +50 -0
- package/registry/directories/tinylaunch/playbook.md +8 -0
- package/registry/directories/tinystartups/directory.yaml +61 -0
- package/registry/directories/tinystartups/playbook.md +6 -0
- package/registry/directories/tipseason/directory.yaml +68 -0
- package/registry/directories/tolodora/directory.yaml +59 -0
- package/registry/directories/tool-commons/directory.yaml +46 -0
- package/registry/directories/tool-commons/playbook.md +1 -0
- package/registry/directories/tool-directory-ai/directory.yaml +64 -0
- package/registry/directories/tool-directory-ai/playbook.md +5 -0
- package/registry/directories/tool-finder/directory.yaml +64 -0
- package/registry/directories/tool-finder/playbook.md +6 -0
- package/registry/directories/tooldirs/directory.yaml +64 -0
- package/registry/directories/tooldirs/playbook.md +7 -0
- package/registry/directories/toolfame/directory.yaml +47 -0
- package/registry/directories/toolfame/playbook.md +6 -0
- package/registry/directories/toolfio/directory.yaml +48 -0
- package/registry/directories/toolfio/playbook.md +7 -0
- package/registry/directories/toolfolio/directory.yaml +54 -0
- package/registry/directories/toolfolio/playbook.md +1 -0
- package/registry/directories/toolfound/directory.yaml +66 -0
- package/registry/directories/toolify/directory.yaml +52 -0
- package/registry/directories/toolify/playbook.md +8 -0
- package/registry/directories/toolkitly/directory.yaml +46 -0
- package/registry/directories/toolkitly/playbook.md +1 -0
- package/registry/directories/toollist-ai/directory.yaml +60 -0
- package/registry/directories/toollist-ai/playbook.md +1 -0
- package/registry/directories/toolomio/directory.yaml +55 -0
- package/registry/directories/toolomio/playbook.md +5 -0
- package/registry/directories/toolpasta/directory.yaml +46 -0
- package/registry/directories/toolpasta/playbook.md +1 -0
- package/registry/directories/toolpilot-ai/directory.yaml +77 -0
- package/registry/directories/toolpilot-ai/playbook.md +8 -0
- package/registry/directories/toolpromote/directory.yaml +81 -0
- package/registry/directories/toolrain/directory.yaml +48 -0
- package/registry/directories/toolrain/playbook.md +1 -0
- package/registry/directories/tools-ai-online/directory.yaml +65 -0
- package/registry/directories/tools-ai-online/playbook.md +7 -0
- package/registry/directories/tools-cafe/directory.yaml +51 -0
- package/registry/directories/tools-cafe/playbook.md +6 -0
- package/registry/directories/toolscout/directory.yaml +45 -0
- package/registry/directories/toolscout/playbook.md +5 -0
- package/registry/directories/toolsfine/directory.yaml +58 -0
- package/registry/directories/toolsfine/playbook.md +9 -0
- package/registry/directories/toolsnocode/directory.yaml +48 -0
- package/registry/directories/toolsnocode/playbook.md +6 -0
- package/registry/directories/tooltrove/directory.yaml +71 -0
- package/registry/directories/tooltrove/playbook.md +9 -0
- package/registry/directories/toolveyor/directory.yaml +44 -0
- package/registry/directories/toolveyor/playbook.md +1 -0
- package/registry/directories/toolwise/directory.yaml +67 -0
- package/registry/directories/toolwise/playbook.md +7 -0
- package/registry/directories/toools-design/directory.yaml +64 -0
- package/registry/directories/toools-design/playbook.md +8 -0
- package/registry/directories/topai-tools/directory.yaml +43 -0
- package/registry/directories/topai-tools/playbook.md +1 -0
- package/registry/directories/traffiklist/directory.yaml +55 -0
- package/registry/directories/traffiklist/playbook.md +6 -0
- package/registry/directories/trendy-startups/directory.yaml +49 -0
- package/registry/directories/trendy-startups/playbook.md +1 -0
- package/registry/directories/trillionagent/directory.yaml +57 -0
- package/registry/directories/trillionagent/playbook.md +1 -0
- package/registry/directories/truerev/directory.yaml +83 -0
- package/registry/directories/truerev/playbook.md +12 -0
- package/registry/directories/trustmrr/directory.yaml +43 -0
- package/registry/directories/trustradius/directory.yaml +67 -0
- package/registry/directories/trustradius/playbook.md +9 -0
- package/registry/directories/trylaunch/directory.yaml +51 -0
- package/registry/directories/trylaunch/playbook.md +1 -0
- package/registry/directories/tulimoa/directory.yaml +65 -0
- package/registry/directories/tulimoa/playbook.md +7 -0
- package/registry/directories/turbo0/directory.yaml +49 -0
- package/registry/directories/turbo0/playbook.md +9 -0
- package/registry/directories/twelve-tools/directory.yaml +47 -0
- package/registry/directories/twelve-tools/playbook.md +6 -0
- package/registry/directories/ultimate-free-tools/directory.yaml +49 -0
- package/registry/directories/ultimate-free-tools/playbook.md +8 -0
- package/registry/directories/uneed/directory.yaml +55 -0
- package/registry/directories/uneed/playbook.md +9 -0
- package/registry/directories/unitelist/directory.yaml +49 -0
- package/registry/directories/unitelist/playbook.md +7 -0
- package/registry/directories/uno-directory/directory.yaml +48 -0
- package/registry/directories/uno-directory/playbook.md +1 -0
- package/registry/directories/ventureradar/directory.yaml +47 -0
- package/registry/directories/ventureradar/playbook.md +5 -0
- package/registry/directories/vibe-coding-cloud/directory.yaml +45 -0
- package/registry/directories/vibe-coding-cloud/playbook.md +6 -0
- package/registry/directories/victrays/directory.yaml +55 -0
- package/registry/directories/victrays/playbook.md +8 -0
- package/registry/directories/visalytica/directory.yaml +59 -0
- package/registry/directories/wavel/directory.yaml +51 -0
- package/registry/directories/we-are-founders/directory.yaml +85 -0
- package/registry/directories/we-are-founders/playbook.md +1 -0
- package/registry/directories/web-store-extensions/directory.yaml +45 -0
- package/registry/directories/web-store-extensions/playbook.md +6 -0
- package/registry/directories/webcurate/directory.yaml +50 -0
- package/registry/directories/website-rating/directory.yaml +66 -0
- package/registry/directories/website-rating/playbook.md +9 -0
- package/registry/directories/webspot/directory.yaml +46 -0
- package/registry/directories/webspot/playbook.md +5 -0
- package/registry/directories/weekhack/directory.yaml +56 -0
- package/registry/directories/weekhack/playbook.md +5 -0
- package/registry/directories/weliketools/directory.yaml +50 -0
- package/registry/directories/weliketools/playbook.md +6 -0
- package/registry/directories/what-is-ai-tools/directory.yaml +48 -0
- package/registry/directories/what-is-ai-tools/playbook.md +1 -0
- package/registry/directories/wired-business/directory.yaml +44 -0
- package/registry/directories/wired-business/playbook.md +6 -0
- package/registry/directories/would-you-pay/directory.yaml +49 -0
- package/registry/directories/would-you-pay/playbook.md +6 -0
- package/registry/directories/xai-plugin-marketplace/directory.yaml +46 -0
- package/registry/directories/xai-plugin-marketplace/playbook.md +5 -0
- package/registry/directories/zplatform-ai/directory.yaml +75 -0
- package/registry/directories/zumvu/directory.yaml +50 -0
- package/scripts/ab +40 -0
- package/scripts/check-listings +8 -0
- package/scripts/google-login +13 -0
- package/scripts/show-browser +17 -0
- package/src/accounts.ts +81 -0
- package/src/audit.ts +229 -0
- package/src/browser.ts +123 -0
- package/src/capabilities.ts +62 -0
- package/src/cli.ts +741 -0
- package/src/db/index.ts +29 -0
- package/src/db/schema.ts +152 -0
- package/src/directory-kinds.ts +12 -0
- package/src/import-hosted.ts +156 -0
- package/src/jobs.ts +270 -0
- package/src/listings.ts +64 -0
- package/src/manual.ts +26 -0
- package/src/product-approvals.ts +31 -0
- package/src/public.ts +24 -0
- package/src/ratings.ts +71 -0
- package/src/registry-updates.ts +54 -0
- package/src/registry.ts +101 -0
- package/src/review.ts +58 -0
- package/src/server.ts +186 -0
- package/src/views.ts +71 -0
- package/src/web/app.tsx +1026 -0
- package/src/web/favicon.svg +4 -0
- package/src/web/index.html +14 -0
- package/src/web/style.css +301 -0
- package/src/workspace.ts +97 -0
package/src/web/app.tsx
ADDED
|
@@ -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 <product-id></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 />);
|