dirpush 0.0.0 → 0.1.1
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 +64 -5
- 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 +61 -6
- 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 +780 -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/cli.ts
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { HOME, openDb, PKG } from "./db";
|
|
9
|
+
import { events, galleries, jobs, productApprovals, registryChanges, todos, type GalleryItem } from "./db/schema";
|
|
10
|
+
import * as J from "./jobs";
|
|
11
|
+
import { checkListing, closeBrowser, findInSitemap } from "./listings";
|
|
12
|
+
import { Directory, loadDirectories, loadDirectory, writeDirectory, normalizeUrl } from "./registry";
|
|
13
|
+
import { addTodo, closeTodo, reviewGallery, reviewRegistryChange } from "./review";
|
|
14
|
+
import { consumeProductApproval, requestProductApproval } from "./product-approvals";
|
|
15
|
+
import { importHosted, readHostedExport } from "./import-hosted";
|
|
16
|
+
import { addManualSubmission } from "./manual";
|
|
17
|
+
import { present } from "./views";
|
|
18
|
+
import { auditDirectory, auditDirectoryBrowser } from "./audit";
|
|
19
|
+
import { browserStatus, newPassword, startBrowser, stopBrowser, vaultLogin } from "./browser";
|
|
20
|
+
import { accountsFor, backfillAccounts, listAccounts } from "./accounts";
|
|
21
|
+
import { detect, loadCapabilities, saveCapabilities } from "./capabilities";
|
|
22
|
+
import { ratingDomain, ratingHistory, refreshRatings } from "./ratings";
|
|
23
|
+
import { fetchRegistryFile, registrySource, registryUpdates } from "./registry-updates";
|
|
24
|
+
import { loadIdentity, loadPermissions, loadProduct, permission, productIds, resolveProductId, PRODUCTS, utm, WORKSPACE } from "./workspace";
|
|
25
|
+
|
|
26
|
+
// Browser helpers run as shell scripts with their arguments untouched (`dirpush ab snapshot -i`), against your launch home.
|
|
27
|
+
const SCRIPTS: Record<string, string> = { ab: "ab", "google-login": "google-login", "show-browser": "show-browser", "check-listings": "check-listings" };
|
|
28
|
+
const first = process.argv[2];
|
|
29
|
+
if (first && SCRIPTS[first]) {
|
|
30
|
+
const p = Bun.spawn([join(PKG, "scripts", SCRIPTS[first]), ...process.argv.slice(3)], { stdio: ["inherit", "inherit", "inherit"], env: { ...process.env, LAUNCH_HOME: HOME } });
|
|
31
|
+
process.exit(await p.exited);
|
|
32
|
+
}
|
|
33
|
+
if (first === "--version" || first === "-v") {
|
|
34
|
+
console.log((await Bun.file(join(PKG, "package.json")).json()).version);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
if (first === "--help" || first === "-h") process.argv.splice(2, 1, "help");
|
|
38
|
+
|
|
39
|
+
const { values: flags, positionals } = parseArgs({
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
options: {
|
|
42
|
+
worker: { type: "string" },
|
|
43
|
+
kind: { type: "string" },
|
|
44
|
+
status: { type: "string" },
|
|
45
|
+
limit: { type: "string" },
|
|
46
|
+
claim: { type: "boolean" },
|
|
47
|
+
skill: { type: "boolean" },
|
|
48
|
+
free: { type: "boolean" },
|
|
49
|
+
unsubmitted: { type: "boolean" },
|
|
50
|
+
badge: { type: "boolean" },
|
|
51
|
+
retryable: { type: "boolean" },
|
|
52
|
+
reason: { type: "string" },
|
|
53
|
+
type: { type: "string" },
|
|
54
|
+
message: { type: "string" },
|
|
55
|
+
note: { type: "string" },
|
|
56
|
+
input: { type: "string" },
|
|
57
|
+
result: { type: "string" },
|
|
58
|
+
data: { type: "string" },
|
|
59
|
+
outcome: { type: "string" },
|
|
60
|
+
"listing-url": { type: "string" },
|
|
61
|
+
evidence: { type: "string" },
|
|
62
|
+
provider: { type: "string" },
|
|
63
|
+
port: { type: "string" },
|
|
64
|
+
host: { type: "string" }, // serve: bind address (default 127.0.0.1)
|
|
65
|
+
product: { type: "string" },
|
|
66
|
+
all: { type: "boolean" },
|
|
67
|
+
source: { type: "string" },
|
|
68
|
+
job: { type: "string" },
|
|
69
|
+
url: { type: "string" },
|
|
70
|
+
username: { type: "string" },
|
|
71
|
+
rel: { type: "string" }, // registry set-link
|
|
72
|
+
samples: { type: "string" },
|
|
73
|
+
noindex: { type: "string" },
|
|
74
|
+
"bot-check": { type: "string" },
|
|
75
|
+
why: { type: "string" }, // registry set-link --rel unknown --why <reason>
|
|
76
|
+
timeout: { type: "string" },
|
|
77
|
+
force: { type: "boolean" },
|
|
78
|
+
browser: { type: "boolean" }, // registry audit: real Chrome for pages a fetch can't read
|
|
79
|
+
apply: { type: "boolean" },
|
|
80
|
+
full: { type: "boolean" }, // raw rows instead of compact views
|
|
81
|
+
pretty: { type: "boolean" }, // indented JSON (default when stdout is a terminal)
|
|
82
|
+
json: { type: "boolean" }, // accepted for readability; output is always JSON
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const [cmd, sub, ...args] = positionals;
|
|
87
|
+
const db = openDb();
|
|
88
|
+
const limit = Number(flags.limit ?? 20);
|
|
89
|
+
|
|
90
|
+
// Agents read stdout: compact rows and unindented JSON unless --full / --pretty (or a human at a terminal).
|
|
91
|
+
const out = (v: unknown) => {
|
|
92
|
+
const shown = flags.full ? v : present(v);
|
|
93
|
+
console.log(flags.pretty || process.stdout.isTTY ? JSON.stringify(shown, null, 2) : JSON.stringify(shown));
|
|
94
|
+
};
|
|
95
|
+
/** Side notes for the agent (why a list is empty or cut short) go to stderr, so stdout stays plain JSON. */
|
|
96
|
+
const note = (msg: string) => console.error(`note: ${msg}`);
|
|
97
|
+
function worker() {
|
|
98
|
+
const w = flags.worker ?? process.env.LAUNCH_WORKER;
|
|
99
|
+
if (!w) throw new Error("pass --worker <name> or set LAUNCH_WORKER (e.g. claude-code, codex)");
|
|
100
|
+
return w;
|
|
101
|
+
}
|
|
102
|
+
function arg(i: number, name: string) {
|
|
103
|
+
const v = args[i];
|
|
104
|
+
if (!v) throw new Error(`missing <${name}>`);
|
|
105
|
+
return v;
|
|
106
|
+
}
|
|
107
|
+
/** --input/--result/--data: inline JSON, a .json/.yaml file path, or '-' for stdin. */
|
|
108
|
+
async function readInput(v: string | undefined, name = "input"): Promise<any> {
|
|
109
|
+
if (!v) throw new Error(`missing --${name}`);
|
|
110
|
+
if (v.trim().startsWith("{") || v.trim().startsWith("[")) return JSON.parse(v);
|
|
111
|
+
const text = v === "-" ? await Bun.stdin.text() : await Bun.file(v).text();
|
|
112
|
+
return v.endsWith(".yaml") || v.endsWith(".yml") ? Bun.YAML.parse(text) : JSON.parse(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
const BROAD_AUDIENCES = new Set(["startups", "saas", "products", "software", "indie-makers", "side-projects", "business-software", "tools"]);
|
|
117
|
+
const GOOGLE_IDENTITY_SCOPES = new Set([
|
|
118
|
+
"openid", "email", "profile",
|
|
119
|
+
"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile",
|
|
120
|
+
]);
|
|
121
|
+
// Google's exact consent lines for openid/email/profile (Italian and English UI). Whole-line matches only,
|
|
122
|
+
// so "see your contacts' email addresses" or anything Drive/Gmail never passes.
|
|
123
|
+
const GOOGLE_IDENTITY_TEXT = [
|
|
124
|
+
/^Visualizzare l'indirizzo email principale del tuo Account Google\.?$/,
|
|
125
|
+
/^See your primary Google Account email address\.?$/,
|
|
126
|
+
/^Associarti alle tue informazioni personali su Google\.?$/,
|
|
127
|
+
/^Associate you with your personal info on Google\.?$/,
|
|
128
|
+
/^Visualizzare le tue informazioni personali, incluse quelle che hai reso (pubblicamente disponibili|disponibili pubblicamente)\.?$/,
|
|
129
|
+
/^See your personal info, including any personal info you've made publicly available\.?$/,
|
|
130
|
+
// Newer "Accedi a <site>" layout: each item is a value line (the identity's name/email) plus one of these labels.
|
|
131
|
+
/^Nome e immagine del profilo$/, /^Indirizzo email$/, /^Name and profile picture$/, /^Email address$/,
|
|
132
|
+
];
|
|
133
|
+
/** Block types where a worker is waiting on the human right now (dashboard shows them first, desktop ping). */
|
|
134
|
+
const URGENT = new Set(["google_login", "captcha"]);
|
|
135
|
+
/** Scopes of a Google sign-in URL; throws unless they are identity-only. */
|
|
136
|
+
function googleScopes(url: string) {
|
|
137
|
+
const u = new URL(url);
|
|
138
|
+
if (u.hostname !== "accounts.google.com") throw new Error(`not a Google sign-in page: ${u.hostname}`);
|
|
139
|
+
// "Sign in with Google" button/One Tap (Google Identity Services) can only return an ID token.
|
|
140
|
+
if (u.pathname.startsWith("/gsi/")) return { ok: true, scopes: ["openid", "email", "profile"], via: "google-identity-services" };
|
|
141
|
+
// The scope can sit in the query or inside an encoded continue/redirect parameter.
|
|
142
|
+
const raw = decodeURIComponent(decodeURIComponent(u.search));
|
|
143
|
+
const scopes = [...new Set([...raw.matchAll(/[?&]scope=([^&]+)/g)].flatMap((m) => m[1]!.replaceAll("+", " ").split(/[ ,]+/)).filter(Boolean))];
|
|
144
|
+
if (!scopes.length) throw new Error("no scope in the URL: can't verify what is requested; block for the human");
|
|
145
|
+
const extra = scopes.filter((sc) => !GOOGLE_IDENTITY_SCOPES.has(sc));
|
|
146
|
+
if (extra.length) throw new Error(`asks for more than identity: ${extra.join(", ")}. Do not continue; block for the human`);
|
|
147
|
+
return { ok: true, scopes };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const SubmittableCost = ["VERIFIED_FREE", "FREEMIUM", "CONDITIONAL_FREE"];
|
|
151
|
+
|
|
152
|
+
/** Installs the skill for every agent on this computer. A git checkout already has it in .agents/skills (--skill installs it anyway). */
|
|
153
|
+
function installSkill() {
|
|
154
|
+
if (HOME === PKG && !flags.skill) return { installed: [], note: "git checkout: the skill is in .agents/skills/dirpush; pass --skill to install it for every agent" };
|
|
155
|
+
const src = join(PKG, ".agents/skills/dirpush");
|
|
156
|
+
const to = [join(homedir(), ".claude/skills/dirpush"), join(homedir(), ".agents/skills/dirpush")];
|
|
157
|
+
for (const d of to) {
|
|
158
|
+
rmSync(d, { recursive: true, force: true });
|
|
159
|
+
cpSync(src, d, { recursive: true });
|
|
160
|
+
}
|
|
161
|
+
return { installed: to };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const commands: Record<string, (() => Promise<unknown> | unknown) | undefined> = {
|
|
165
|
+
/** Sets up your launch home and the agent skill; with a product id, also scaffolds that product. Safe to re-run. */
|
|
166
|
+
async "init"() {
|
|
167
|
+
const id = sub;
|
|
168
|
+
if (id && !/^[a-z0-9-]+$/.test(id)) throw new Error("usage: dirpush init [product-id] (a lowercase slug, e.g. acme)");
|
|
169
|
+
const created: string[] = [];
|
|
170
|
+
const copy = (from: string, to: string) => {
|
|
171
|
+
if (existsSync(to)) return;
|
|
172
|
+
cpSync(join(PKG, "examples/workspace", from), to);
|
|
173
|
+
created.push(to.slice(HOME.length + 1));
|
|
174
|
+
};
|
|
175
|
+
mkdirSync(WORKSPACE, { recursive: true });
|
|
176
|
+
copy("identity.yaml", join(WORKSPACE, "identity.yaml"));
|
|
177
|
+
copy("permissions.yaml", join(WORKSPACE, "permissions.yaml"));
|
|
178
|
+
if (id) {
|
|
179
|
+
for (const d of ["assets/brand", "assets/screenshots"]) mkdirSync(join(PRODUCTS, id, d), { recursive: true });
|
|
180
|
+
copy("product.yaml", join(PRODUCTS, id, "product.yaml"));
|
|
181
|
+
}
|
|
182
|
+
const skill = installSkill();
|
|
183
|
+
const caps = await loadCapabilities();
|
|
184
|
+
const detected = detect();
|
|
185
|
+
return {
|
|
186
|
+
home: HOME, product: id ?? null, created, skill, detected,
|
|
187
|
+
next: [
|
|
188
|
+
...(id ? [`fill in ${join(PRODUCTS, id, "product.yaml")}, then check it with 'dirpush product show --product ${id}'`]
|
|
189
|
+
: productIds().length ? [] : ["add your product: 'dirpush init <product-id>' (a lowercase slug, e.g. acme)"]),
|
|
190
|
+
...(detected.agentBrowser ? [] : ["install agent-browser, which agents use to drive Chrome: 'npm i -g agent-browser'"]),
|
|
191
|
+
...(caps ? [] : [`open your agent and say "set up dirpush": it checks which tools it has (email, browser) and records them`]),
|
|
192
|
+
`then ask your agent to "continue my launch"; 'dirpush serve' opens the dashboard`,
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
"products list": async () => Promise.all(productIds().map(async (id) => {
|
|
198
|
+
const p = await loadProduct(id).catch((e: Error) => ({ id, error: e.message }));
|
|
199
|
+
return "error" in p ? p : { id, name: p.name, url: p.url };
|
|
200
|
+
})),
|
|
201
|
+
"product show": () => loadProduct(flags.product),
|
|
202
|
+
"permission": async () => {
|
|
203
|
+
let pid: string | undefined;
|
|
204
|
+
try {
|
|
205
|
+
pid = resolveProductId(flags.product);
|
|
206
|
+
} catch {} // no single product: shared permissions.yaml applies
|
|
207
|
+
return { action: sub, product: pid ?? null, mode: await permission(sub ?? "", pid) };
|
|
208
|
+
},
|
|
209
|
+
"utm": async () => ({ url: utm((await loadProduct(flags.product)).url, sub ?? "") }),
|
|
210
|
+
|
|
211
|
+
// ---- work protocol --------------------------------------------------------
|
|
212
|
+
"work next"() {
|
|
213
|
+
const job = J.next(db, "submission");
|
|
214
|
+
if (!job) return { job: null, message: "no claimable work" };
|
|
215
|
+
return { job: flags.claim ? J.claim(db, job.id, worker()) : job };
|
|
216
|
+
},
|
|
217
|
+
"work list": () =>
|
|
218
|
+
db.select().from(jobs).where(and(eq(jobs.kind, "submission"), flags.status ? eq(jobs.status, flags.status as any) : undefined)).orderBy(desc(jobs.updatedAt)).limit(limit).all(),
|
|
219
|
+
"work show": () => {
|
|
220
|
+
const job = db.select().from(jobs).where(eq(jobs.id, arg(0, "job"))).get();
|
|
221
|
+
if (!job) throw new Error(`no such job: ${arg(0, "job")}`);
|
|
222
|
+
const evs = J.jobEvents(db, job.id);
|
|
223
|
+
if (evs.length > 20 && !flags.all) note(`showing the last 20 of ${evs.length} events; --all for every one`);
|
|
224
|
+
return { job, events: flags.all ? evs : evs.slice(-20) };
|
|
225
|
+
},
|
|
226
|
+
"work claim": () => J.claim(db, arg(0, "job"), worker()),
|
|
227
|
+
"work heartbeat": () => J.heartbeat(db, arg(0, "job"), worker()),
|
|
228
|
+
"work checkpoint": async () => J.checkpoint(db, arg(0, "job"), worker(), arg(1, "step"), flags.data ? await readInput(flags.data, "data") : {}),
|
|
229
|
+
async "work begin-submit"() {
|
|
230
|
+
// Enforced here, not just in the playbook: a "confirm" directory needs the human's Approve first.
|
|
231
|
+
const id = arg(0, "job");
|
|
232
|
+
const job = db.select().from(jobs).where(eq(jobs.id, id)).get();
|
|
233
|
+
if (!job) throw new Error(`no such job: ${id}`);
|
|
234
|
+
const perms = await loadPermissions(job.productId);
|
|
235
|
+
const mode = perms.directories?.[job.subject] ?? perms.submissions?.submit ?? "confirm";
|
|
236
|
+
if (mode === "never") throw new Error(`permissions say never submit to ${job.subject}: work fail ${id} --reason "submission not permitted"`);
|
|
237
|
+
if (mode === "confirm" && !J.approved(db, id))
|
|
238
|
+
throw new Error(`${job.subject} needs the human's approval before submitting: work block ${id} --type permission --message "Ready to submit to ${job.subject} — approve in the dashboard"`);
|
|
239
|
+
return J.beginSubmit(db, id, worker());
|
|
240
|
+
},
|
|
241
|
+
"work submitted": () => J.recordSubmitted(db, arg(0, "job"), worker(), (flags.outcome ?? "submitted") as J.Outcome, flags["listing-url"]),
|
|
242
|
+
"work not-submitted": () => J.recordNotSubmitted(db, arg(0, "job"), worker(), flags.evidence ?? ""),
|
|
243
|
+
"work block"() {
|
|
244
|
+
const job = J.block(db, arg(0, "job"), worker(), flags.type ?? "user", flags.message ?? "");
|
|
245
|
+
// Urgent blocks hold a worker that is waiting right now: ping the human's desktop (macOS; no-op elsewhere).
|
|
246
|
+
if (URGENT.has(flags.type ?? "") && process.platform === "darwin" && !process.env.LAUNCH_QUIET)
|
|
247
|
+
Bun.spawn(["osascript", "-e", `display notification ${JSON.stringify(`${job.subject}: open the dashboard (Needs you)`)} with title "dirpush: needs you" sound name "Glass"`]);
|
|
248
|
+
return job;
|
|
249
|
+
},
|
|
250
|
+
"work unblock": () => J.unblock(db, arg(0, "job"), flags.note),
|
|
251
|
+
"work fail": () => J.fail(db, arg(0, "job"), worker(), flags.reason ?? "unspecified", !!flags.retryable),
|
|
252
|
+
"work release": () => J.release(db, arg(0, "job"), worker()),
|
|
253
|
+
"work requeue": () => J.requeue(db, arg(0, "job")),
|
|
254
|
+
/** For "already listed" discoveries; real submits go through begin-submit/submitted. */
|
|
255
|
+
async "work complete"() {
|
|
256
|
+
const id = arg(0, "job");
|
|
257
|
+
const raw = await readInput(flags.result, "result");
|
|
258
|
+
J.complete(db, id, worker(), raw.outcome ?? "live", raw);
|
|
259
|
+
return raw.listing_url ? J.setOutcome(db, id, raw.outcome ?? "live", raw.listing_url) : db.select().from(jobs).where(eq(jobs.id, id)).get();
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// ---- registry -------------------------------------------------------------
|
|
263
|
+
/** Measure link rel, noindex and bot check for every directory (or the ids given). No AI; safe for cron. */
|
|
264
|
+
async "registry audit"() {
|
|
265
|
+
if (flags.browser) {
|
|
266
|
+
// One directory at a time in a throwaway logged-out session; only directories still unmeasured unless ids are given.
|
|
267
|
+
let todo = (await loadDirectories()).filter((d) => (args.length ? args.includes(d.id) : (d.link?.rel ?? "unknown") === "unknown"));
|
|
268
|
+
if (flags.limit) todo = todo.slice(0, limit);
|
|
269
|
+
const session = `audit-${process.pid}`, out: { id: string; rel: string; note: string }[] = [];
|
|
270
|
+
try {
|
|
271
|
+
for (const d of todo) {
|
|
272
|
+
const { link, note: why } = auditDirectoryBrowser(d, session);
|
|
273
|
+
if (link.rel !== "unknown" || !d.link) await writeDirectory({ ...d, link }, undefined);
|
|
274
|
+
out.push({ id: d.id, rel: link.rel, note: why });
|
|
275
|
+
if (!process.env.LAUNCH_QUIET) console.error(`${out.length}/${todo.length} ${d.id}: ${link.rel} (${why})`);
|
|
276
|
+
}
|
|
277
|
+
} finally {
|
|
278
|
+
closeBrowser(session);
|
|
279
|
+
}
|
|
280
|
+
return { audited: out.length, measured: out.filter((r) => r.rel !== "unknown").length, results: out };
|
|
281
|
+
}
|
|
282
|
+
let dirs = await loadDirectories();
|
|
283
|
+
if (args.length) dirs = dirs.filter((d) => args.includes(d.id));
|
|
284
|
+
if (flags.limit) dirs = dirs.slice(0, limit);
|
|
285
|
+
// Our own listings are the best evidence: the exact link a directory gave us.
|
|
286
|
+
const hosts = new Map((await Promise.all(productIds().map((id) => loadProduct(id).then((p) => [id, new URL(p.url).hostname.replace(/^www\./, "")] as const).catch(() => null)))).filter((x) => x !== null));
|
|
287
|
+
const own = new Map<string, { url: string; productHost: string }[]>();
|
|
288
|
+
for (const j of db.select().from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.status, "done"))).all())
|
|
289
|
+
if (j.listingUrl && hosts.has(j.productId) && /^https?:/.test(j.listingUrl)) own.set(j.subject, [...(own.get(j.subject) ?? []), { url: j.listingUrl, productHost: hosts.get(j.productId)! }]);
|
|
290
|
+
const results: { id: string; rel: string; method: string; claimed: string | null; bot_check: string; noindex: boolean | null }[] = [];
|
|
291
|
+
let next = 0;
|
|
292
|
+
await Promise.all(Array.from({ length: 12 }, async () => {
|
|
293
|
+
while (next < dirs.length) {
|
|
294
|
+
const d = dirs[next++]!;
|
|
295
|
+
const { playbook } = await loadDirectory(d.id);
|
|
296
|
+
const link = await auditDirectory(d, own.get(d.id) ?? [], playbook ?? "");
|
|
297
|
+
await writeDirectory({ ...d, link }, undefined);
|
|
298
|
+
results.push({ id: d.id, rel: link.rel, method: link.method, claimed: link.claimed, bot_check: link.bot_check, noindex: link.noindex });
|
|
299
|
+
if (!process.env.LAUNCH_QUIET) console.error(`${results.length}/${dirs.length} ${d.id}: ${link.rel} (${link.method})`);
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
const count = (k: keyof (typeof results)[number]) => results.reduce((m, r) => ((m[String(r[k])] = (m[String(r[k])] ?? 0) + 1), m), {} as Record<string, number>);
|
|
303
|
+
return { audited: results.length, rel: count("rel"), method: count("method"), bot_check: count("bot_check"), noindex: count("noindex"),
|
|
304
|
+
claimMismatch: results.filter((r) => r.claimed && r.rel !== "unknown" && r.rel !== "mixed" && r.claimed !== r.rel).map((r) => `${r.id}: claims ${r.claimed}, measured ${r.rel}`) };
|
|
305
|
+
},
|
|
306
|
+
/** Record a link measurement an agent made by hand on real listing pages (when `registry audit` couldn't). */
|
|
307
|
+
"registry set-link": async () => {
|
|
308
|
+
const { directory: d } = await loadDirectory(arg(0, "directory-id"));
|
|
309
|
+
const rel = flags.rel as NonNullable<Directory["link"]>["rel"] | undefined;
|
|
310
|
+
if (!rel || !["dofollow", "nofollow", "ugc", "sponsored", "mixed", "unknown"].includes(rel)) throw new Error("--rel dofollow|nofollow|ugc|sponsored|mixed|unknown");
|
|
311
|
+
const evidence = flags.evidence?.trim() || null;
|
|
312
|
+
const base = (h: string) => h.replace(/^www\./, "").split(".").slice(-2).join(".");
|
|
313
|
+
if (rel !== "unknown") {
|
|
314
|
+
if (!evidence) throw new Error("--evidence <listing page URL> is required: the page whose outbound product link you read");
|
|
315
|
+
if (base(new URL(evidence).hostname) !== base(new URL(d.website).hostname)) throw new Error(`evidence ${evidence} is not on ${new URL(d.website).hostname}`);
|
|
316
|
+
}
|
|
317
|
+
const bot = flags["bot-check"] as NonNullable<Directory["link"]>["bot_check"] | undefined;
|
|
318
|
+
// Measured on one of our own listings: keep the result, never publish the URL (the public site must not name our products).
|
|
319
|
+
const ours = evidence && (await Promise.all(productIds().map((id) => loadProduct(id).catch(() => null)))).some((p) =>
|
|
320
|
+
p && [p.id, p.name, new URL(p.url).hostname.replace(/^www\./, "").split(".")[0]!].some((w) => w && evidence.toLowerCase().includes(w.toLowerCase())));
|
|
321
|
+
if (ours) note("evidence is one of our own listings: recorded as own_listing, URL not stored");
|
|
322
|
+
const link = {
|
|
323
|
+
rel, method: (rel === "unknown" ? "none" : ours ? "own_listing" : "sample_listing") as NonNullable<Directory["link"]>["method"], evidence: ours ? null : evidence,
|
|
324
|
+
samples: Number(flags.samples ?? (rel === "unknown" ? 0 : 1)),
|
|
325
|
+
noindex: flags.noindex === "yes" ? true : flags.noindex === "no" ? false : (d.link?.noindex ?? null),
|
|
326
|
+
claimed: d.link?.claimed ?? null,
|
|
327
|
+
bot_check: bot ?? d.link?.bot_check ?? "unknown",
|
|
328
|
+
checked_at: new Date().toISOString().slice(0, 10),
|
|
329
|
+
...(rel === "unknown" && flags.why ? { unmeasured: flags.why as NonNullable<NonNullable<Directory["link"]>["unmeasured"]> } : {}),
|
|
330
|
+
};
|
|
331
|
+
if (rel === "unknown" && !flags.why) throw new Error("--rel unknown needs --why no_listings|no_outbound_link|bot_wall|login_required|site_down");
|
|
332
|
+
await writeDirectory({ ...d, link }, undefined);
|
|
333
|
+
return { id: d.id, link };
|
|
334
|
+
},
|
|
335
|
+
"registry list": async () => {
|
|
336
|
+
const dirs = (await loadDirectories()).filter((d) => (!flags.free || SubmittableCost.includes(d.cost.classification)) && (!flags.kind || d.kind === flags.kind));
|
|
337
|
+
const cap = flags.limit ? limit : 50;
|
|
338
|
+
if (dirs.length > cap) note(`showing ${cap} of ${dirs.length}; narrow with --kind/--free or raise --limit. For one directory: registry show <id>`);
|
|
339
|
+
return dirs.slice(0, cap);
|
|
340
|
+
},
|
|
341
|
+
"registry show": () => loadDirectory(arg(0, "directory-id")),
|
|
342
|
+
"registry report-change": async () => {
|
|
343
|
+
const id = arg(0, "directory-id");
|
|
344
|
+
await loadDirectory(id);
|
|
345
|
+
const observation = await readInput(flags.input);
|
|
346
|
+
// Agents fix the YAML themselves; this is the record, not a request for review.
|
|
347
|
+
const row = db.insert(registryChanges).values({ directoryId: id, observation, status: "accepted" }).returning().get();
|
|
348
|
+
J.logEvent(db, null, "registry_change_accepted", flags.worker ?? process.env.LAUNCH_WORKER ?? null, { directoryId: id, changeId: row.id });
|
|
349
|
+
return row;
|
|
350
|
+
},
|
|
351
|
+
"registry accept": () => reviewRegistryChange(db, Number(arg(0, "change-id")), "accepted"),
|
|
352
|
+
"registry reject": () => reviewRegistryChange(db, Number(arg(0, "change-id")), "rejected"),
|
|
353
|
+
"registry changes": () => db.select().from(registryChanges).where(eq(registryChanges.status, (flags.status ?? "proposed") as any)).all(),
|
|
354
|
+
|
|
355
|
+
// ---- submissions ----------------------------------------------------------
|
|
356
|
+
"directories recommend": async () => {
|
|
357
|
+
const product = await loadProduct(flags.product);
|
|
358
|
+
const done = new Set(
|
|
359
|
+
db.select({ s: jobs.subject }).from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.productId, product.id))).all().map((r) => r.s),
|
|
360
|
+
);
|
|
361
|
+
let dirs = await loadDirectories();
|
|
362
|
+
if (flags.kind) dirs = dirs.filter((d) => d.kind === flags.kind);
|
|
363
|
+
if (flags.free || (await permission("money.spend", product.id)) === "never") dirs = dirs.filter((d) => SubmittableCost.includes(d.cost.classification));
|
|
364
|
+
if (!flags.badge && (await permission("external.add_backlink", product.id)) === "never") dirs = dirs.filter((d) => d.cost.classification !== "CONDITIONAL_FREE");
|
|
365
|
+
if (flags.unsubmitted) dirs = dirs.filter((d) => !done.has(d.id));
|
|
366
|
+
// Launch boards, campaigns, lead-gen sites and human-only directories need a decision; show them only with --all.
|
|
367
|
+
if (!flags.all) dirs = dirs.filter((d) => (!d.kind || d.kind === "listing") && !d.human_only);
|
|
368
|
+
// Exact category match scores 2; general startup/software audiences fit any product (1).
|
|
369
|
+
// Niche directories with no overlap (e.g. AI-only for a non-AI product) are hidden unless --all.
|
|
370
|
+
const fit = (d: Directory) =>
|
|
371
|
+
d.audience.reduce((n, a) => n + (product.categories.includes(a) ? 2 : BROAD_AUDIENCES.has(a) ? 1 : 0), 0);
|
|
372
|
+
const picked = dirs
|
|
373
|
+
.map((d) => ({ ...d, fit: fit(d) }))
|
|
374
|
+
.filter((d) => flags.all || d.fit > 0)
|
|
375
|
+
.sort((a, b) => b.fit - a.fit)
|
|
376
|
+
.slice(0, limit);
|
|
377
|
+
if (!picked.length && !flags.all) note("no plain listings left that fit; launch sites, MCP registries and other kinds need --all (and a human decision for launch boards)");
|
|
378
|
+
return picked;
|
|
379
|
+
},
|
|
380
|
+
"submissions enqueue": async () => {
|
|
381
|
+
const product = await loadProduct(flags.product);
|
|
382
|
+
const ids = args;
|
|
383
|
+
if (!ids.length) throw new Error("missing <directory-id...>");
|
|
384
|
+
const noBadges = (await permission("external.add_backlink", product.id)) === "never";
|
|
385
|
+
for (const id of ids) {
|
|
386
|
+
const { directory } = await loadDirectory(id); // must exist in the verified registry
|
|
387
|
+
if (noBadges && directory.cost.classification === "CONDITIONAL_FREE")
|
|
388
|
+
throw new Error(`${id} is free only with a badge/backlink, and permissions say external.add_backlink: never`);
|
|
389
|
+
}
|
|
390
|
+
return ids.map((id) => J.enqueue(db, "submission", id, product.id));
|
|
391
|
+
},
|
|
392
|
+
"submissions list": () => {
|
|
393
|
+
const rows = db.select().from(jobs).where(and(
|
|
394
|
+
eq(jobs.kind, "submission"),
|
|
395
|
+
flags.product ? eq(jobs.productId, flags.product) : undefined,
|
|
396
|
+
flags.outcome ? eq(jobs.outcome, flags.outcome) : undefined,
|
|
397
|
+
flags.status ? eq(jobs.status, flags.status as any) : undefined,
|
|
398
|
+
)).orderBy(desc(jobs.updatedAt)).all();
|
|
399
|
+
const cap = flags.limit ? limit : 50;
|
|
400
|
+
if (rows.length > cap) note(`showing the ${cap} most recent of ${rows.length}; filter with --outcome live|pending|rejected or --status blocked|failed|queued, or raise --limit`);
|
|
401
|
+
return rows.slice(0, cap);
|
|
402
|
+
},
|
|
403
|
+
/** No AI: fetch each pending listing's expected URL (registry listing_url_pattern); live if the page mentions the product's domain. Safe for cron. */
|
|
404
|
+
async "submissions check"() {
|
|
405
|
+
// `submissions check [job]`: one job (any outcome) or every pending one.
|
|
406
|
+
const pending = args[0]
|
|
407
|
+
? db.select().from(jobs).where(eq(jobs.id, args[0])).all()
|
|
408
|
+
: db.select().from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.status, "done"), eq(jobs.outcome, "pending"))).all();
|
|
409
|
+
const results = [];
|
|
410
|
+
const session = `check-${process.pid}`;
|
|
411
|
+
try {
|
|
412
|
+
for (const job of pending) {
|
|
413
|
+
const { directory } = await loadDirectory(job.subject).catch(() => ({ directory: undefined }));
|
|
414
|
+
const product = await loadProduct(job.productId).catch(() => undefined);
|
|
415
|
+
if (!product) {
|
|
416
|
+
results.push({ job: job.id, directory: job.subject, result: "can't check: product not found" });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const toSlug = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
420
|
+
const host = new URL(product.url).hostname.replace(/^www\./, "");
|
|
421
|
+
const slugs = [...new Set([toSlug(product.name), toSlug(host)])]; // "ledgerline", "ledgerline-com"
|
|
422
|
+
// The worker's recorded listing URL wins; else the directory's pattern; else its sitemap.
|
|
423
|
+
const url = job.listingUrl ?? directory?.listing_url_pattern?.replace("{slug}", slugs[0]!)
|
|
424
|
+
?? (directory && await findInSitemap(directory.website, slugs).catch(() => undefined));
|
|
425
|
+
if (!url) {
|
|
426
|
+
results.push({ job: job.id, directory: job.subject, result: "can't check automatically (no listing URL, not in sitemap)" });
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const { live, how } = await checkListing(url, host, slugs, session);
|
|
430
|
+
if (live) J.setOutcome(db, job.id, "live", url);
|
|
431
|
+
results.push({ job: job.id, directory: job.subject, url, result: live ? `live (${how})` : how });
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
closeBrowser(session);
|
|
435
|
+
}
|
|
436
|
+
return results;
|
|
437
|
+
},
|
|
438
|
+
/** A listing you made yourself: `submissions add <directory-id|URL> [--listing-url u] [--outcome pending|live|rejected]`. */
|
|
439
|
+
"submissions add": async () => {
|
|
440
|
+
const product = await loadProduct(flags.product);
|
|
441
|
+
const outcome = (flags.outcome ?? (flags["listing-url"] ? "live" : "pending")) as J.Outcome;
|
|
442
|
+
return addManualSubmission(db, product.id, arg(0, "directory-id or URL"), outcome, flags["listing-url"]);
|
|
443
|
+
},
|
|
444
|
+
"submissions outcome": () => {
|
|
445
|
+
const outcome = arg(1, "outcome");
|
|
446
|
+
if (!["pending", "live", "rejected", "needs_badge"].includes(outcome)) throw new Error("outcome must be pending, live, rejected or needs_badge");
|
|
447
|
+
return J.setOutcome(db, arg(0, "job"), outcome as J.Outcome, flags["listing-url"]);
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
// ---- galleries ------------------------------------------------------------
|
|
451
|
+
"gallery show": async () => {
|
|
452
|
+
const product = await loadProduct(flags.product);
|
|
453
|
+
return db.select().from(galleries).where(eq(galleries.productId, product.id)).orderBy(desc(galleries.revision)).all();
|
|
454
|
+
},
|
|
455
|
+
"gallery propose": async () => {
|
|
456
|
+
const product = await loadProduct(flags.product);
|
|
457
|
+
const items = z.array(z.object({ id: z.string(), path: z.string(), description: z.string() })).parse(await readInput(flags.input));
|
|
458
|
+
for (const i of items) if (!existsSync(join(product.dir, i.path))) throw new Error(`screenshot not found: workspace/products/${product.id}/${i.path}`);
|
|
459
|
+
const row = db.insert(galleries).values({ productId: product.id, items: items as GalleryItem[] }).returning().get();
|
|
460
|
+
J.logEvent(db, null, "gallery_proposed", flags.worker ?? null, { revision: row.revision });
|
|
461
|
+
// Park the job so its lease can't expire while the human reviews; approve/reject re-queues it.
|
|
462
|
+
if (flags.job) J.block(db, flags.job, worker(), "gallery_approval", `Review gallery revision ${row.revision} for ${product.id}`);
|
|
463
|
+
return row;
|
|
464
|
+
},
|
|
465
|
+
"gallery approved": async () => {
|
|
466
|
+
const product = await loadProduct(flags.product);
|
|
467
|
+
return (
|
|
468
|
+
db.select().from(galleries).where(and(eq(galleries.productId, product.id), eq(galleries.status, "approved"))).orderBy(desc(galleries.revision)).get() ?? null
|
|
469
|
+
);
|
|
470
|
+
},
|
|
471
|
+
"gallery approve": () => reviewGallery(db, Number(arg(0, "revision")), "approved"),
|
|
472
|
+
"gallery reject": () => reviewGallery(db, Number(arg(0, "revision")), "rejected"),
|
|
473
|
+
|
|
474
|
+
// ---- Google sign-in guard ----------------------------------------------------
|
|
475
|
+
/** Run on the Google consent/sign-in URL before clicking anything. Fails unless it only asks for identity. */
|
|
476
|
+
async "oauth check"() {
|
|
477
|
+
const url = arg(0, "url");
|
|
478
|
+
// OAuth URLs contain short-lived state tokens. Accept stdin so callers can
|
|
479
|
+
// inspect scopes without putting those tokens in process arguments/logs.
|
|
480
|
+
return googleScopes(url === "-" ? (await Bun.stdin.text()).trim() : url);
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/** Consent pages don't carry the scope in the URL: pipe the page text in; only identity items pass. */
|
|
484
|
+
async "oauth consent"() {
|
|
485
|
+
const text = await Bun.stdin.text();
|
|
486
|
+
// Google renders each requested permission as a tab-indented line.
|
|
487
|
+
let items = text.split("\n").map((l) => l.trim()).filter((l, i, all) => text.split("\n")[i]!.startsWith("\t") && l);
|
|
488
|
+
if (!items.length) {
|
|
489
|
+
// Newer layout: "Google consentirà a <site> di accedere a queste informazioni su di te", items, then the privacy line.
|
|
490
|
+
const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
491
|
+
const start = lines.findIndex((l) => /queste informazioni su di te\.?$|this info about you\.?$/i.test(l));
|
|
492
|
+
const end = lines.findIndex((l, i) => i > start && /^(Leggi le norme|Review |Read |Before using)/.test(l));
|
|
493
|
+
const me = await loadIdentity();
|
|
494
|
+
if (start >= 0 && end > start + 1) items = lines.slice(start + 1, end).filter((l) => l !== me.email && l !== me.display_name);
|
|
495
|
+
}
|
|
496
|
+
if (!items.length) {
|
|
497
|
+
// "Signing in again" to a site this account already approved: Google shows no list. The scopes of the
|
|
498
|
+
// flow's first Google URL (--url, the one `oauth check` passed) are then the only thing to verify.
|
|
499
|
+
if (flags.url) return { ...googleScopes(flags.url), reSignIn: true };
|
|
500
|
+
throw new Error("no permission list found on the page; pass --url <the Google URL that passed oauth check>, or block for the human");
|
|
501
|
+
}
|
|
502
|
+
const extra = items.filter((i) => !GOOGLE_IDENTITY_TEXT.some((re) => re.test(i)));
|
|
503
|
+
if (extra.length) throw new Error(`consent asks for more than identity: ${extra.join(" | ")}. Do not click Allow; block for the human`);
|
|
504
|
+
return { ok: true, items };
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
// ---- human surface -------------------------------------------------------
|
|
508
|
+
"assets request-capture": async () => {
|
|
509
|
+
const product = await loadProduct(flags.product);
|
|
510
|
+
const mode = await permission("assets.capture_new", product.id);
|
|
511
|
+
if (mode === "never") throw new Error(`assets.capture_new is never for ${product.id}`);
|
|
512
|
+
if (mode === "automatic") return { productId: product.id, action: "assets.capture_new", status: "approved", permission: mode };
|
|
513
|
+
return requestProductApproval(db, {
|
|
514
|
+
productId: product.id,
|
|
515
|
+
action: "assets.capture_new",
|
|
516
|
+
message: `Approve new marketing screenshot captures for ${product.name ?? product.id}. This is product-wide and does not authorize any directory submission.`,
|
|
517
|
+
});
|
|
518
|
+
},
|
|
519
|
+
"assets capture-approval": async () => {
|
|
520
|
+
const product = await loadProduct(flags.product);
|
|
521
|
+
return db.select().from(productApprovals)
|
|
522
|
+
.where(and(eq(productApprovals.productId, product.id), eq(productApprovals.action, "assets.capture_new")))
|
|
523
|
+
.orderBy(desc(productApprovals.id)).get() ?? null;
|
|
524
|
+
},
|
|
525
|
+
"assets capture-complete": () => consumeProductApproval(db, Number(arg(0, "approval-id"))),
|
|
526
|
+
/**
|
|
527
|
+
* The human's side of bot checks, one job at a time in the terminal: opens the page in the agents' Chrome, waits,
|
|
528
|
+
* then hands the job back to the agents (or records that the human submitted it). Agents never solve these.
|
|
529
|
+
*/
|
|
530
|
+
async "checks"() {
|
|
531
|
+
const types = (flags.type ?? "captcha,bot_protection").split(",").map((t) => t.trim());
|
|
532
|
+
const queue = db.select().from(jobs)
|
|
533
|
+
.where(and(eq(jobs.kind, "submission"), eq(jobs.status, "blocked"), inArray(jobs.blockType, types))).all()
|
|
534
|
+
.filter((j) => !flags.product || j.productId === flags.product);
|
|
535
|
+
if (!queue.length) return { done: 0, note: `nothing blocked on ${types.join(" or ")}` };
|
|
536
|
+
if (!process.stdin.isTTY) throw new Error("checks is interactive: run it yourself in a terminal (it's the human's part)");
|
|
537
|
+
const ab = (...a: string[]) => Bun.spawnSync([join(PKG, "scripts/ab"), ...a], { env: { ...process.env, LAUNCH_HOME: HOME }, stdin: "ignore", stdout: "ignore", stderr: "ignore" }).exitCode;
|
|
538
|
+
const lines = console[Symbol.asyncIterator]();
|
|
539
|
+
const ask = async (q: string) => { process.stdout.write(q); return ((await lines.next()).value ?? "q").trim().toLowerCase(); };
|
|
540
|
+
const tally = { handedBack: [] as string[], submittedByYou: [] as string[], skipped: [] as string[] };
|
|
541
|
+
for (const [i, job] of queue.entries()) {
|
|
542
|
+
const dir = await loadDirectory(job.subject).then((d) => d.directory).catch(() => null);
|
|
543
|
+
const url = job.blockMessage?.match(/https?:\/\/[^\s)'"]+/)?.[0]?.replace(/[.,;]$/, "") ?? dir?.submission_url ?? dir?.website;
|
|
544
|
+
console.log(`\n[${i + 1}/${queue.length}] ${dir?.name ?? job.subject} (${job.blockType})\n${job.blockMessage ?? ""}\n`);
|
|
545
|
+
if (url && ab("open", url) === 0) Bun.spawnSync([join(PKG, "scripts/show-browser")], { env: { ...process.env, LAUNCH_HOME: HOME }, stdin: "ignore", stdout: "ignore", stderr: "ignore" });
|
|
546
|
+
else if (url) console.log(`couldn't open the agents' Chrome; open it yourself: ${url}`);
|
|
547
|
+
console.log("Clear the check in that window (and anything the message asks for).");
|
|
548
|
+
let a = "";
|
|
549
|
+
while (!["d", "s", "n", "q"].includes(a)) a = await ask("[d] cleared, agents take it from here [s] I submitted it myself [n] skip [q] quit: ");
|
|
550
|
+
if (a === "q") break;
|
|
551
|
+
if (a === "n") { tally.skipped.push(job.subject); continue; }
|
|
552
|
+
if (a === "d") {
|
|
553
|
+
J.unblock(db, job.id, "human cleared the bot check in the agents' Chrome; continue in the same browser session");
|
|
554
|
+
tally.handedBack.push(job.subject);
|
|
555
|
+
} else {
|
|
556
|
+
const link = await ask("listing URL, if there is one (Enter to skip): ");
|
|
557
|
+
J.recordManual(db, job.id, "pending", link && link !== "q" ? link : undefined);
|
|
558
|
+
tally.submittedByYou.push(job.subject);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { ...tally, next: tally.handedBack.length ? `ask your agent to "continue my launch": ${tally.handedBack.length} job(s) are queued again` : undefined };
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/** Everything waiting on the human, grouped. --product narrows it. */
|
|
565
|
+
"actions": () => {
|
|
566
|
+
const mine = <T extends { productId: string }>(xs: T[]) => xs.filter((x) => !flags.product || !x.productId || x.productId === flags.product);
|
|
567
|
+
const blocked = mine(db.select().from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.status, "blocked"))).all());
|
|
568
|
+
const hold = blocked.filter((j) => j.blockType === "hold");
|
|
569
|
+
return {
|
|
570
|
+
needsYou: blocked.filter((j) => j.blockType !== "hold" && j.blockType !== "gallery_approval"),
|
|
571
|
+
onHold: hold.map((j) => ({ id: j.id, directory: j.subject })),
|
|
572
|
+
verifying: mine(db.select().from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.status, "unknown"))).all()),
|
|
573
|
+
galleries: mine(db.select().from(galleries).where(eq(galleries.status, "proposed")).all()).map((g) => ({ revision: g.revision, product: g.productId, screenshots: g.items.length })),
|
|
574
|
+
productApprovals: mine(db.select().from(productApprovals).where(eq(productApprovals.status, "pending")).all()).map((a) => ({ id: a.id, product: a.productId, action: a.action, message: a.message })),
|
|
575
|
+
registryChanges: db.select().from(registryChanges).where(eq(registryChanges.status, "proposed")).all().map((c) => ({ id: c.id, directory: c.directoryId })),
|
|
576
|
+
todos: mine(db.select().from(todos).where(eq(todos.status, "open")).all()).map((t) => ({ id: t.id, job: t.jobId ?? undefined, message: t.message, url: t.url ?? undefined })),
|
|
577
|
+
};
|
|
578
|
+
},
|
|
579
|
+
/** Block until a human acts on a blocked job. Run it in the background; it exits with the job. */
|
|
580
|
+
async "wait"() {
|
|
581
|
+
const id = sub;
|
|
582
|
+
if (!id) throw new Error("missing <job>");
|
|
583
|
+
const deadline = flags.timeout ? Date.now() + Number(flags.timeout) * 60_000 : Infinity;
|
|
584
|
+
for (;;) {
|
|
585
|
+
const job = db.select().from(jobs).where(eq(jobs.id, id)).get();
|
|
586
|
+
if (!job) throw new Error(`no job ${id}`);
|
|
587
|
+
if (job.status !== "blocked") return { resolved: true, job, events: J.jobEvents(db, id).slice(-3) };
|
|
588
|
+
if (Date.now() > deadline) return { resolved: false, job };
|
|
589
|
+
await Bun.sleep(2000);
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
"todo add": () => {
|
|
593
|
+
if (!flags.message) throw new Error("missing --message");
|
|
594
|
+
const job = flags.job ? db.select().from(jobs).where(eq(jobs.id, flags.job)).get() : undefined;
|
|
595
|
+
if (flags.job && !job) throw new Error(`no job ${flags.job}`);
|
|
596
|
+
return addTodo(db, { productId: job?.productId ?? flags.product ?? "", jobId: flags.job, message: flags.message, url: flags.url });
|
|
597
|
+
},
|
|
598
|
+
"todo list": () => db.select().from(todos).where(eq(todos.status, flags.status === "done" ? "done" : "open")).all(),
|
|
599
|
+
"todo done": () => closeTodo(db, Number(arg(0, "todo"))),
|
|
600
|
+
"events": () => db.select().from(events).orderBy(desc(events.id)).limit(limit).all(),
|
|
601
|
+
"overview": () => ({
|
|
602
|
+
jobs: db.select({ kind: jobs.kind, status: jobs.status, n: sql<number>`count(*)` }).from(jobs).groupBy(jobs.kind, jobs.status).all(),
|
|
603
|
+
outcomes: db
|
|
604
|
+
.select({ outcome: jobs.outcome, n: sql<number>`count(*)` })
|
|
605
|
+
.from(jobs)
|
|
606
|
+
.where(and(eq(jobs.kind, "submission"), inArray(jobs.status, ["done"])))
|
|
607
|
+
.groupBy(jobs.outcome)
|
|
608
|
+
.all(),
|
|
609
|
+
}),
|
|
610
|
+
// ---- registry updates -----------------------------------------------------
|
|
611
|
+
/** Diffs the published registry (dirpush repo, LAUNCH_REGISTRY_URL or --url; https or a local path) against yours. --apply writes it. */
|
|
612
|
+
async "registry updates"() {
|
|
613
|
+
return registryUpdates(await fetchRegistryFile(flags.url ?? registrySource()), Boolean(flags.apply));
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
// ---- domain rating (optional Ahrefs) ---------------------------------------
|
|
617
|
+
/** Weekly DR for your products (and with --all, every registry directory). Needs AHREFS_API_KEY in the environment. */
|
|
618
|
+
async "dr refresh"() {
|
|
619
|
+
const apiKey = process.env.AHREFS_API_KEY?.trim();
|
|
620
|
+
if (!apiKey) throw new Error("set AHREFS_API_KEY in the environment (free key from ahrefs.com/api); DR tracking is optional");
|
|
621
|
+
const sites = (await Promise.all(productIds().map((id) => loadProduct(id).then((p) => p.url).catch(() => null)))).filter((u): u is string => !!u);
|
|
622
|
+
if (flags.all) sites.push(...(await loadDirectories()).map((d) => d.website));
|
|
623
|
+
return refreshRatings(db, sites, { apiKey, force: Boolean(flags.force) });
|
|
624
|
+
},
|
|
625
|
+
/** DR history per product: first and latest reading, change, and the live-listing count beside it. */
|
|
626
|
+
async "dr show"() {
|
|
627
|
+
const ids = flags.product ? [resolveProductId(flags.product)] : productIds();
|
|
628
|
+
return Promise.all(ids.map(async (id) => {
|
|
629
|
+
const product = await loadProduct(id);
|
|
630
|
+
const history = ratingHistory(db, [product.url]).get(ratingDomain(product.url)) ?? [];
|
|
631
|
+
const live = db.select({ n: sql<number>`count(*)` }).from(jobs)
|
|
632
|
+
.where(and(eq(jobs.kind, "submission"), eq(jobs.productId, id), eq(jobs.outcome, "live"))).get()!.n;
|
|
633
|
+
const first = history.at(-1), latest = history[0];
|
|
634
|
+
return { product: id, domain: ratingDomain(product.url), latest: latest ?? null, first: first ?? null,
|
|
635
|
+
change: latest && first ? latest.rating - first.rating : null, liveListings: live, history };
|
|
636
|
+
}));
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
// ---- one-off import ---------------------------------------------------------
|
|
640
|
+
/** Imports the retired dirpush.com D1 export (--input dir, default data/hosted-export). Safe to re-run. */
|
|
641
|
+
async "import hosted"() {
|
|
642
|
+
const dir = flags.input ?? join(HOME, "data/hosted-export");
|
|
643
|
+
return importHosted(db, await readHostedExport(dir), productIds());
|
|
644
|
+
},
|
|
645
|
+
|
|
646
|
+
// ---- capabilities: what the working agent can use (browser, email…), recorded during setup ----
|
|
647
|
+
async "capabilities"() {
|
|
648
|
+
const caps = await loadCapabilities();
|
|
649
|
+
if (!caps) note("not checked yet: run the capability check in SKILL.md (Setup), then 'dirpush capabilities set --input json'");
|
|
650
|
+
return { recorded: caps, detected: detect() };
|
|
651
|
+
},
|
|
652
|
+
async "capabilities set"() {
|
|
653
|
+
if (!flags.input) throw new Error(`capabilities set --input '{"email":{"available":true,"tool":"…","address":"…","read_only":true}}'`);
|
|
654
|
+
return saveCapabilities(await readInput(flags.input, "input"), process.env.LAUNCH_WORKER);
|
|
655
|
+
},
|
|
656
|
+
|
|
657
|
+
// ---- accounts: which directories we already have a login on (filled from checkpoints) ----
|
|
658
|
+
"accounts list": () => {
|
|
659
|
+
const rows = listAccounts(db, flags.product ? resolveProductId(flags.product) : undefined);
|
|
660
|
+
if (!rows.length) note("no accounts recorded yet; they appear when a job checkpoints --data '{\"account\":{…}}' (run `accounts backfill` once on an old database)");
|
|
661
|
+
return rows.map((r) => ({ directory: r.directoryId, product: r.productId || "shared", method: r.method, username: r.username ?? undefined, vault: r.vault ?? undefined, lastLogin: r.lastLoginAt ? new Date(r.lastLoginAt).toISOString().slice(0, 10) : undefined }));
|
|
662
|
+
},
|
|
663
|
+
"accounts show": () => {
|
|
664
|
+
const id = arg(0, "directory-id");
|
|
665
|
+
const rows = accountsFor(db, id, flags.product ? resolveProductId(flags.product) : undefined);
|
|
666
|
+
return { directory: id, hasAccount: rows.length > 0, accounts: rows, signIn: rows[0] ? (rows[0].vault ? `dirpush ab auth login ${rows[0].vault}` : rows[0].method === "google" || rows[0].method === "github" ? `use the site's "Sign in with ${rows[0].method === "google" ? "Google" : "GitHub"}" button` : "magic link / email code to the identity email") : "no account: follow authentication.md (new account only if permitted)" };
|
|
667
|
+
},
|
|
668
|
+
async "accounts backfill"() {
|
|
669
|
+
const p = Bun.spawnSync(["agent-browser", "auth", "list", "--json"], { stdout: "pipe", stderr: "ignore" });
|
|
670
|
+
let vault: { name: string; username: string }[] = [];
|
|
671
|
+
try { vault = (JSON.parse(p.stdout.toString()).data?.profiles ?? []).map((v: { name: string; username: string }) => ({ name: v.name, username: v.username })); } catch { note("agent-browser vault not readable; filling from jobs only"); }
|
|
672
|
+
return backfillAccounts(db, vault, new Set((await loadDirectories()).map((d) => d.id)), productIds());
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
// ---- browser: the dedicated Chrome agents drive with agent-browser (scripts/ab) ----
|
|
676
|
+
"browser start": () => startBrowser(),
|
|
677
|
+
"browser status": () => browserStatus(),
|
|
678
|
+
"browser stop": () => stopBrowser(),
|
|
679
|
+
"browser login": () => vaultLogin(arg(0, "vault-name")),
|
|
680
|
+
"browser password": () => {
|
|
681
|
+
if (!flags.url || !flags.username) throw new Error("browser password <vault-name> --url <login-url> --username <email> <field-selector…>");
|
|
682
|
+
return newPassword(arg(0, "vault-name"), flags.url, flags.username, args.slice(1));
|
|
683
|
+
},
|
|
684
|
+
async "serve"() {
|
|
685
|
+
const { serve } = await import("./server");
|
|
686
|
+
// --host 0.0.0.0 shares it on the local network. The dashboard has no login: only do that on a network you trust.
|
|
687
|
+
const host = flags.host ?? process.env.HOST ?? "127.0.0.1";
|
|
688
|
+
if (host !== "127.0.0.1" && host !== "localhost") note(`dashboard reachable from the network on ${host}; it has no login, so anyone on this network can use it`);
|
|
689
|
+
serve(Number(flags.port ?? 4321), host);
|
|
690
|
+
return new Promise(() => {}); // keep running
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
const HELP: [string, [string, string][]][] = [
|
|
696
|
+
["Start", [
|
|
697
|
+
["init [product-id] [--skill]", "set up your launch home (~/.dirpush, or the repo for a git checkout) and install the agent skill; with an id, scaffold that product. Safe to re-run"],
|
|
698
|
+
["product show [--product id]", "the product's canonical facts (never invent beyond these)"],
|
|
699
|
+
["products list", "all products in this workspace"],
|
|
700
|
+
["permission <group.action>", "automatic | confirm | never for an action, e.g. accounts.oauth_google"],
|
|
701
|
+
["actions [--product id]", "everything waiting on the human, grouped"],
|
|
702
|
+
["checks [--type captcha,bot_protection] [--product id]", "for the human: clear CAPTCHAs and bot checks one by one in the agents' Chrome, then hand each job back"],
|
|
703
|
+
]],
|
|
704
|
+
["Work loop", [
|
|
705
|
+
["work next [--claim]", "next submission job (unknown ones first); --claim leases it to you"],
|
|
706
|
+
["work show <job> [--all]", "a job, its saved data and its last 20 events"],
|
|
707
|
+
["work checkpoint <job> <step> [--data json]", "record progress; extends the lease"],
|
|
708
|
+
["work heartbeat <job>", "extend the lease on a long page"],
|
|
709
|
+
["work begin-submit <job>", "right before the final submit click"],
|
|
710
|
+
["work submitted <job> [--outcome pending|live] [--listing-url u]", "right after the submit result"],
|
|
711
|
+
["work not-submitted <job> --evidence text", "an unknown job was verified as not received"],
|
|
712
|
+
["work complete <job> --result json", "the product was already listed there"],
|
|
713
|
+
["work block <job> --type t --message text", "hand a step to the human (captcha, email_verification, permission, missing_info, manual, hold)"],
|
|
714
|
+
["work fail <job> --reason text [--retryable]", "give up on a directory, with the reason"],
|
|
715
|
+
["work release <job>", "stop cleanly and give the job back"],
|
|
716
|
+
["wait <job> [--timeout min]", "block until the human resolves a blocked job (run in background)"],
|
|
717
|
+
["work list [--status s] [--limit n]", "recent submission jobs"],
|
|
718
|
+
]],
|
|
719
|
+
["Directories", [
|
|
720
|
+
["directories recommend --product id [--free] [--unsubmitted] [--all] [--kind k] [--limit n]", "directories that fit and are allowed"],
|
|
721
|
+
["submissions enqueue <directory-id…> --product id", "queue submissions"],
|
|
722
|
+
["registry show <id>", "full definition and playbook for one directory"],
|
|
723
|
+
["registry list [--free] [--kind k] [--limit n]", "registry rows (50 by default)"],
|
|
724
|
+
["registry report-change <id> --input json", "record a fix you made to a directory's YAML"],
|
|
725
|
+
["registry updates [--apply]", "compare with the published registry (dirpush.com)"],
|
|
726
|
+
["registry audit [ids…]", "measure link rel, noindex and bot check (no AI)"],
|
|
727
|
+
["registry set-link <id> --rel r --evidence <listing url> [--samples n] [--noindex yes|no] [--bot-check t] | --rel unknown --why reason", "record a link type you measured by hand, or why it can't be measured"],
|
|
728
|
+
["utm <directory-id>", "the product URL with UTM parameters for that directory"],
|
|
729
|
+
]],
|
|
730
|
+
["Submissions", [
|
|
731
|
+
["submissions list [--product id] [--outcome o] [--status s] [--limit n]", "submissions, most recent first (50 by default)"],
|
|
732
|
+
["submissions add <id|url> --product id [--listing-url u] [--outcome live|pending]", "a listing made outside the agents"],
|
|
733
|
+
["submissions outcome <job> live|rejected|pending|needs_badge [--listing-url u]", "moderation result"],
|
|
734
|
+
["submissions check [job]", "re-check listings in review, mark live ones (no AI)"],
|
|
735
|
+
]],
|
|
736
|
+
["Human hand-offs", [
|
|
737
|
+
["todo add --job id --message text [--url u]", "something for the human that doesn't block the job"],
|
|
738
|
+
["todo list | todo done <id>", "open to-dos"],
|
|
739
|
+
["gallery approved --product id | gallery propose --job id --input json", "screenshots"],
|
|
740
|
+
["oauth check <url> | oauth consent --url u", "verify Google asks only for identity scopes before clicking Allow"],
|
|
741
|
+
]],
|
|
742
|
+
["Setup", [
|
|
743
|
+
["capabilities", "what the agent recorded it can use (email tool, browser) plus what the CLI detects"],
|
|
744
|
+
["capabilities set --input json", "record your tools after the setup check, e.g. an email tool for verification links"],
|
|
745
|
+
]],
|
|
746
|
+
["Accounts", [
|
|
747
|
+
["accounts show <directory-id> [--product id]", "do we have a login there, and how to sign in (check before any sign-up)"],
|
|
748
|
+
["accounts list [--product id]", "every directory account (no secrets; passwords stay in the vault)"],
|
|
749
|
+
["accounts backfill", "one-time: fill accounts from old job data and the agent-browser vault"],
|
|
750
|
+
]],
|
|
751
|
+
["Browser", [
|
|
752
|
+
["ab <agent-browser command…>", "drive the agents' Chrome: dirpush ab open <url>, ab snapshot -i, ab fill @e3 \"text\", ab click @e5…"],
|
|
753
|
+
["google-login", "the human signs into Google once in the agents' Chrome profile"],
|
|
754
|
+
["browser start | status | stop", "the dedicated Chrome (real profile, Google services blocked) agents drive"],
|
|
755
|
+
["browser login <vault-name>", "fill a saved login into the open tab on that site (password never shown)"],
|
|
756
|
+
["browser password <vault-name> --url u --username e <selector…>", "new account: generate, fill, verify and save a password (never shown)"],
|
|
757
|
+
]],
|
|
758
|
+
["Other", [
|
|
759
|
+
["dr show | dr refresh [--all]", "Domain Rating history (refresh needs AHREFS_API_KEY)"],
|
|
760
|
+
["serve [--port n] [--host 0.0.0.0]", "the local dashboard (localhost only unless --host)"],
|
|
761
|
+
]],
|
|
762
|
+
];
|
|
763
|
+
if (!cmd || cmd === "help") {
|
|
764
|
+
console.log(`dirpush: durable directory submissions. Output is JSON on stdout (compact; --pretty to indent, --full for raw rows).\n\n${HELP.map(([group, rows]) => `${group}\n${rows.map(([c, d]) => ` ${c}\n ${d}`).join("\n")}`).join("\n\n")}\n`);
|
|
765
|
+
process.exit(0);
|
|
766
|
+
}
|
|
767
|
+
const run = commands[`${cmd} ${sub}`] ?? commands[cmd ?? ""];
|
|
768
|
+
if (!run) {
|
|
769
|
+
const tried = sub ? `${cmd} ${sub}` : cmd;
|
|
770
|
+
const close = Object.keys(commands).filter((c) => c.startsWith(cmd) || c.includes(tried) || (sub && c.endsWith(sub)));
|
|
771
|
+
console.error(JSON.stringify({ error: `unknown command: ${tried}`, ...(close.length && { didYouMean: close.slice(0, 5) }), help: "dirpush help" }));
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const result = await run();
|
|
776
|
+
if (result !== undefined) out(result);
|
|
777
|
+
} catch (e) {
|
|
778
|
+
console.error(JSON.stringify({ error: e instanceof z.ZodError ? z.prettifyError(e) : (e as Error).message }));
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|