Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

sqlite db for store

authored by

nekomimi.pet and committed by
Tangled
6310df6d 1b861883

+261 -210
+32 -6
bun.lock
··· 2127 2127 2128 2128 "wisp-hosting-service/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 2129 2129 2130 - "wispctl/@atproto/identity": ["@atproto/identity@0.4.10", "", { "dependencies": { "@atproto/common-web": "^0.4.4", "@atproto/crypto": "^0.4.4" } }, "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ=="], 2130 + "wispctl/@atproto/sync": ["@atproto/sync@0.1.40", "", { "dependencies": { "@atproto/common": "^0.5.14", "@atproto/identity": "^0.4.12", "@atproto/lexicon": "^0.6.2", "@atproto/repo": "^0.8.12", "@atproto/syntax": "^0.5.0", "@atproto/xrpc-server": "^0.10.15", "multiformats": "^9.9.0", "p-queue": "^6.6.2", "ws": "^8.12.0" } }, "sha512-tnzFPqKBXPXpuGvx87sjqLHgEzIOT/QfQZUp0YOUHtpipBgvijEnNXV9XeTIgiUAv69wyRR+6YjJkLCYfHpVwQ=="], 2131 2131 2132 - "wispctl/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], 2132 + "wispctl/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], 2133 2133 2134 2134 "wispctl/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], 2135 2135 ··· 2425 2425 2426 2426 "wisp-hosting-service/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2427 2427 2428 - "wispctl/@atproto/identity/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 2428 + "wispctl/@atproto/sync/@atproto/common": ["@atproto/common@0.5.14", "", { "dependencies": { "@atproto/common-web": "^0.4.18", "@atproto/lex-cbor": "^0.0.14", "@atproto/lex-data": "^0.0.13", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-FnhTppvJw8I1AuvEkL9JREFwmM6ciYfSlQ0Zo6neiJIhTf1wf5/ONeFSYKu1/dxC63JEratGIAfVjSBJJZi7sg=="], 2429 2429 2430 - "wispctl/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], 2430 + "wispctl/@atproto/sync/@atproto/syntax": ["@atproto/syntax@0.5.0", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-UA2DSpGdOQzUQ4gi5SH+NEJz/YR3a3Fg3y2oh+xETDSiTRmA4VhHRCojhXAVsBxUT6EnItw190C/KN+DWW90kw=="], 2431 + 2432 + "wispctl/@atproto/sync/@atproto/xrpc-server": ["@atproto/xrpc-server@0.10.15", "", { "dependencies": { "@atproto/common": "^0.5.14", "@atproto/crypto": "^0.4.5", "@atproto/lex-cbor": "^0.0.14", "@atproto/lex-client": "^0.0.15", "@atproto/lex-data": "^0.0.13", "@atproto/lex-json": "^0.0.13", "@atproto/lex-schema": "^0.0.14", "@atproto/lexicon": "^0.6.2", "@atproto/ws-client": "^0.0.4", "@atproto/xrpc": "^0.7.7", "express": "^4.17.2", "http-errors": "^2.0.0", "mime-types": "^2.1.35", "rate-limiter-flexible": "^2.4.1", "ws": "^8.12.0" } }, "sha512-ryGVAKuLU0Nqkv25gsPzffJhxnCXwPOyBi+sNAfP7n+mDDwcumH6RWySEHoDDrTsGvAP2r8o2ZrLCWuzKm7vSg=="], 2433 + 2434 + "wispctl/@atproto/sync/multiformats": ["multiformats@9.9.0", "", {}, "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="], 2431 2435 2432 2436 "wispctl/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2433 2437 ··· 2505 2509 2506 2510 "wisp-hosting-service/@atproto/lexicon/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 2507 2511 2508 - "wispctl/@atproto/identity/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 2512 + "wispctl/@atproto/sync/@atproto/common/@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.14", "", { "dependencies": { "@atproto/lex-data": "^0.0.13", "tslib": "^2.8.1" } }, "sha512-zeqxaKAifR8qlFKg4A6t1RCT8TcjeDnIXLtp3QnDu0QoxslxsmcsrqNrrgmka8w+bYW2+h/rT9MPWglkT7vHyw=="], 2509 2513 2510 - "wispctl/@types/bun/bun-types/@types/node": ["@types/node@24.10.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow=="], 2514 + "wispctl/@atproto/sync/@atproto/common/@atproto/lex-data": ["@atproto/lex-data@0.0.13", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-7Z7RwZ1Y/JzBF/Tcn/I4UJ/vIGfh5zn1zjv0KX+flke2JtgFkSE8uh2hOtqgBQMNqE3zdJFM+dcSWln86hR3MQ=="], 2515 + 2516 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.14", "", { "dependencies": { "@atproto/lex-data": "^0.0.13", "tslib": "^2.8.1" } }, "sha512-zeqxaKAifR8qlFKg4A6t1RCT8TcjeDnIXLtp3QnDu0QoxslxsmcsrqNrrgmka8w+bYW2+h/rT9MPWglkT7vHyw=="], 2517 + 2518 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/lex-data": ["@atproto/lex-data@0.0.13", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-7Z7RwZ1Y/JzBF/Tcn/I4UJ/vIGfh5zn1zjv0KX+flke2JtgFkSE8uh2hOtqgBQMNqE3zdJFM+dcSWln86hR3MQ=="], 2519 + 2520 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client": ["@atproto/ws-client@0.0.4", "", { "dependencies": { "@atproto/common": "^0.5.3", "ws": "^8.12.0" } }, "sha512-dox1XIymuC7/ZRhUqKezIGgooZS45C6vHCfu0PnWjfvsLCK2kAlnvX4IBkA/WpcoijDhQ9ejChnFbo/sLmgvAg=="], 2521 + 2522 + "wispctl/@atproto/sync/@atproto/xrpc-server/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], 2511 2523 2512 2524 "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2513 2525 ··· 2533 2545 2534 2546 "webhook-service/@atproto/sync/@atproto/xrpc-server/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2535 2547 2548 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common": ["@atproto/common@0.5.9", "", { "dependencies": { "@atproto/common-web": "^0.4.13", "@atproto/lex-cbor": "0.0.9", "@atproto/lex-data": "0.0.9", "iso-datestring-validator": "^2.2.2", "multiformats": "^9.9.0", "pino": "^8.21.0" } }, "sha512-rzl8dB7ErpA/VUgCidahUtbxEph50J4g7j68bZmlwwrHlrtvTe8DjrwH5EUFEcegl9dadIhcVJ3qi0kPKEUr+g=="], 2549 + 2550 + "wispctl/@atproto/sync/@atproto/xrpc-server/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], 2551 + 2536 2552 "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 2537 2553 2538 2554 "@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], ··· 2543 2559 2544 2560 "webhook-service/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 2545 2561 2562 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web": ["@atproto/common-web@0.4.13", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "@atproto/lex-json": "0.0.9", "@atproto/syntax": "0.4.3", "zod": "^3.23.8" } }, "sha512-TewRUyB/dVJ5PtI3QmJzEgT3wDsvpnLJ+48hPl+LuUueJPamZevXKJN6dFjtbKAMFRnl2bKfdsf79qwvdSaLKQ=="], 2563 + 2564 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/lex-cbor": ["@atproto/lex-cbor@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-szkS569j1eZsIxZKh2VZHVq7pSpewy1wHh8c6nVYekHfYcJhFkevQq/DjTeatZ7YZKNReGYthQulgaZq2ytfWQ=="], 2565 + 2566 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/lex-data": ["@atproto/lex-data@0.0.9", "", { "dependencies": { "multiformats": "^9.9.0", "tslib": "^2.8.1", "uint8arrays": "3.0.0", "unicode-segmenter": "^0.14.0" } }, "sha512-1slwe4sG0cyWtsq16+rBoWIxNDqGPkkvN+PV6JuzA7dgUK9bjUmXBGQU4eZlUPSS43X1Nhmr/9VjgKmEzU9vDw=="], 2567 + 2546 2568 "webhook-service/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 2547 2569 2548 2570 "webhook-service/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 2571 + 2572 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web/@atproto/lex-json": ["@atproto/lex-json@0.0.9", "", { "dependencies": { "@atproto/lex-data": "0.0.9", "tslib": "^2.8.1" } }, "sha512-Q2v1EVZcnd+ndyZj1r2UlGikA7q6It24CFPLbxokcf5Ba4RBupH8IkkQX7mqUDSRWPgQdmZYIdW9wUln+MKDqw=="], 2573 + 2574 + "wispctl/@atproto/sync/@atproto/xrpc-server/@atproto/ws-client/@atproto/common/@atproto/common-web/@atproto/syntax": ["@atproto/syntax@0.4.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-YoZUz40YAJr5nPwvCDWgodEOlt5IftZqPJvA0JDWjuZKD8yXddTwSzXSaKQAzGOpuM+/A3uXRtPzJJqlScc+iA=="], 2549 2575 } 2550 2576 }
+12 -18
cli/commands/domain.ts
··· 16 16 siteRkey: string | undefined, 17 17 options: DomainCommandOptions, 18 18 ): Promise<void> { 19 - const nsid = 'place.wisp.v2.domain.claim' 20 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 19 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 21 20 22 21 const spinner = createSpinner(`Claiming ${domain}...`).start() 23 - const data = await callWispXrpc<DomainClaimOutput>(agent, nsid, { 22 + const data = await callWispXrpc<DomainClaimOutput>(agent, 'place.wisp.v2.domain.claim', { 24 23 serviceDid, 25 24 data: siteRkey ? { domain, siteRkey } : { domain }, 26 25 }) ··· 49 48 siteRkey: string | undefined, 50 49 options: DomainCommandOptions, 51 50 ): Promise<void> { 52 - const nsid = 'place.wisp.v2.domain.claimSubdomain' 53 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 51 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 54 52 55 53 const spinner = createSpinner(`Claiming subdomain ${subdomain}...`).start() 56 - const data = await callWispXrpc<DomainClaimSubdomainOutput>(agent, nsid, { 54 + const data = await callWispXrpc<DomainClaimSubdomainOutput>(agent, 'place.wisp.v2.domain.claimSubdomain', { 57 55 serviceDid, 58 56 data: siteRkey ? { handle: subdomain, siteRkey } : { handle: subdomain }, 59 57 }) ··· 75 73 domain: string, 76 74 options: DomainCommandOptions, 77 75 ): Promise<void> { 78 - const nsid = 'place.wisp.v2.domain.getStatus' 79 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 76 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 80 77 81 78 const spinner = createSpinner(`Fetching status for ${domain}...`).start() 82 - const data = await callWispXrpc<DomainGetStatusOutput>(agent, nsid, { 79 + const data = await callWispXrpc<DomainGetStatusOutput>(agent, 'place.wisp.v2.domain.getStatus', { 83 80 serviceDid, 84 81 params: { domain }, 85 82 }) ··· 114 111 siteRkey: string, 115 112 options: DomainCommandOptions, 116 113 ): Promise<void> { 117 - const nsid = 'place.wisp.v2.domain.addSite' 118 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 114 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 119 115 120 116 const spinner = createSpinner(`Mapping ${domain} -> ${siteRkey}...`).start() 121 - const data = await callWispXrpc<DomainAddSiteOutput>(agent, nsid, { 117 + const data = await callWispXrpc<DomainAddSiteOutput>(agent, 'place.wisp.v2.domain.addSite', { 122 118 serviceDid, 123 119 data: { domain, siteRkey }, 124 120 }) ··· 137 133 domain: string, 138 134 options: DomainCommandOptions, 139 135 ): Promise<void> { 140 - const nsid = 'place.wisp.v2.domain.delete' 141 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 136 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 142 137 143 138 const spinner = createSpinner(`Deleting ${domain}...`).start() 144 - const data = await callWispXrpc<DomainDeleteOutput>(agent, nsid, { 139 + const data = await callWispXrpc<DomainDeleteOutput>(agent, 'place.wisp.v2.domain.delete', { 145 140 serviceDid, 146 141 params: { domain }, 147 142 }) ··· 160 155 siteRkey: string, 161 156 options: DomainCommandOptions, 162 157 ): Promise<void> { 163 - const nsid = 'place.wisp.v2.site.delete' 164 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsid, options) 158 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 165 159 166 160 const spinner = createSpinner(`Deleting site ${siteRkey}...`).start() 167 - const data = await callWispXrpc<SiteDeleteOutput>(agent, nsid, { 161 + const data = await callWispXrpc<SiteDeleteOutput>(agent, 'place.wisp.v2.site.delete', { 168 162 serviceDid, 169 163 data: { siteRkey }, 170 164 })
+2 -2
cli/commands/list.ts
··· 48 48 } 49 49 50 50 export async function listDomains(identifier: string | undefined, options: ListCommandOptions): Promise<void> { 51 - const { agent, serviceDid } = await authenticateForXrpc(identifier, 'place.wisp.v2.domain.getList', options) 51 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 52 52 53 53 const fetchSpinner = createSpinner('Fetching domains...').start() 54 54 const data = await callWispXrpc<DomainListOutput>(agent, 'place.wisp.v2.domain.getList', { ··· 65 65 } 66 66 67 67 export async function listSites(identifier: string | undefined, options: ListCommandOptions): Promise<void> { 68 - const { agent, serviceDid } = await authenticateForXrpc(identifier, 'place.wisp.v2.site.getList', options) 68 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 69 69 70 70 const fetchSpinner = createSpinner('Fetching sites...').start() 71 71 const data = await callWispXrpc<SiteListOutput>(agent, 'place.wisp.v2.site.getList', {
+39 -19
cli/index.ts
··· 14 14 import { listDomains, listSites } from './commands/list.ts' 15 15 import { pull } from './commands/pull.ts' 16 16 import { serve } from './commands/serve.ts' 17 - import { authenticate, clearSessions } from './lib/auth.ts' 17 + import { authenticate, authenticateOAuth, clearDirSession, clearSessions, hasDirSession } from './lib/auth.ts' 18 18 import { 19 19 addXrpcAuthOptions, 20 20 authenticateForXrpc, ··· 85 85 identifier: string | undefined, 86 86 options: XrpcCommandOptions & { site?: string; yes?: boolean }, 87 87 ): Promise<void> { 88 - const nsids = ['place.wisp.v2.site.getList', 'place.wisp.v2.site.delete'] as const 89 - const { agent, serviceDid } = await authenticateForXrpc(identifier, nsids, options) 88 + const { agent, serviceDid } = await authenticateForXrpc(identifier, options) 90 89 91 90 let siteRkey = options.site 92 91 if (!siteRkey) { ··· 156 155 .option('-c, --concurrency <n>', 'Number of concurrent uploads (backs off to 2 on rate limit)', '3') 157 156 .option('--force-gzip', 'Force gzip compression for all files regardless of type') 158 157 .option('--password <password>', 'App password for headless authentication') 159 - .option('--store <path>', 'OAuth session store path') 158 + .option('--db <path>', 'OAuth session database path') 160 159 .option('-y, --yes', 'Skip confirmation prompts') 161 160 .action( 162 161 withExit(async (handle: string | undefined, options) => { ··· 164 163 let resolvedPath = options.path 165 164 let resolvedSite = options.site 166 165 167 - // If any required values are missing, show prompts 168 - const needsPrompts = !resolvedHandle || !resolvedPath || !resolvedSite 166 + const hasDirSess = !resolvedHandle && !options.password && (await hasDirSession(options.db)) 167 + const needsHandlePrompt = !resolvedHandle && !hasDirSess 168 + const needsPrompts = needsHandlePrompt || !resolvedPath || !resolvedSite 169 169 170 170 if (needsPrompts) { 171 171 intro(pc.cyan('wisp.place deploy')) 172 172 173 - if (!resolvedHandle) { 173 + if (needsHandlePrompt) { 174 174 resolvedHandle = await promptRequiredText('AT Protocol handle', { 175 175 placeholder: 'alice.bsky.social', 176 176 cancelMessage: 'Deploy cancelled', ··· 206 206 } 207 207 208 208 const authSpinner = createSpinner('Authenticating...').start() 209 - const { agent, did } = await authenticate(resolvedHandle!, { 209 + const { agent, did } = await authenticate(resolvedHandle, { 210 210 appPassword: options.password, 211 - storePath: options.store, 211 + dbPath: options.db, 212 212 onStatus: bindAuthStatusToSpinner(authSpinner), 213 213 }) 214 - authSpinner.succeed(`Authenticated as ${resolvedHandle} (${did})`) 214 + authSpinner.succeed(`Authenticated as ${did}`) 215 215 216 216 const result = await deploy(agent, did, { 217 217 path: resolvedPath, ··· 223 223 forceGzip: options.forceGzip, 224 224 }) 225 225 226 - const handleUrl = `https://sites.wisp.place/${resolvedHandle}/${resolvedSite}` 227 - const didUrl = result.url 228 - 229 226 console.log() 230 227 console.log(pc.dim(` URI: ${result.uri}`)) 231 - console.log(pc.cyan(` URL: ${handleUrl}`)) 232 - console.log(pc.cyan(` URL: ${didUrl}`)) 228 + if (resolvedHandle) { 229 + console.log(pc.cyan(` URL: https://sites.wisp.place/${resolvedHandle}/${resolvedSite}`)) 230 + } 231 + console.log(pc.cyan(` URL: ${result.url}`)) 233 232 234 233 if (needsPrompts) { 235 234 outro(pc.green('Deployed successfully!')) ··· 527 526 }), 528 527 ) 529 528 529 + // Login command 530 + program 531 + .command('login <handle>') 532 + .description('Authenticate and store session for the current directory') 533 + .option('--db <path>', 'OAuth session database path') 534 + .action( 535 + withExit(async (handle: string, options) => { 536 + const authSpinner = createSpinner('Authenticating...').start() 537 + const { did } = await authenticateOAuth(handle, { 538 + dbPath: options.db, 539 + onStatus: bindAuthStatusToSpinner(authSpinner), 540 + }) 541 + authSpinner.succeed(`Authenticated as ${did}`) 542 + }), 543 + ) 544 + 530 545 // Logout command 531 546 program 532 547 .command('logout') 533 - .description('Clear stored OAuth sessions') 534 - .option('--store <path>', 'OAuth session store path') 535 - .action((options) => { 536 - clearSessions(options.store) 548 + .description('Clear the stored session for the current directory') 549 + .option('--db <path>', 'OAuth session database path') 550 + .option('--all', 'Clear all stored sessions across all directories') 551 + .action(async (options) => { 552 + if (options.all) { 553 + await clearSessions(options.db) 554 + } else { 555 + await clearDirSession(options.db) 556 + } 537 557 }) 538 558 539 559 program.parse()
+154 -140
cli/lib/auth.ts
··· 1 - import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' 1 + import { mkdirSync } from 'node:fs' 2 2 import { homedir } from 'node:os' 3 - import { dirname, join, resolve } from 'node:path' 3 + import { dirname, join } from 'node:path' 4 + import { cwd } from 'node:process' 4 5 import { Agent, CredentialSession } from '@atproto/api' 5 6 import { 6 7 NodeOAuthClient, ··· 15 16 import { isBun } from '@wispplace/bun-firehose' 16 17 import { Hono } from 'hono' 17 18 import open from 'open' 18 - import { parseServiceDid } from './wisp-service.ts' 19 19 20 - const REQUIRED_BASE_SCOPE = 'atproto' 21 - const REPO_BLOB_SCOPES = [ 20 + // All scopes requested upfront so the client_id is stable across commands 21 + const OAUTH_SCOPE = [ 22 + 'atproto', 22 23 'repo:place.wisp.fs', 23 24 'repo:place.wisp.subfs', 24 25 'repo:place.wisp.settings', 25 26 'blob:*/*', 26 - ] as const 27 + 'rpc:place.wisp.v2.site.getList?aud=*', 28 + 'rpc:place.wisp.v2.site.delete?aud=*', 29 + 'rpc:place.wisp.v2.domain.getList?aud=*', 30 + 'rpc:place.wisp.v2.domain.claim?aud=*', 31 + 'rpc:place.wisp.v2.domain.claimSubdomain?aud=*', 32 + 'rpc:place.wisp.v2.domain.getStatus?aud=*', 33 + 'rpc:place.wisp.v2.domain.addSite?aud=*', 34 + 'rpc:place.wisp.v2.domain.delete?aud=*', 35 + ].join(' ') 27 36 28 - // Default session store path 29 - const DEFAULT_STORE_PATH = join(homedir(), '.wisp', 'oauth-session.json') 37 + const DEFAULT_DB_PATH = join(homedir(), '.config', 'wispctl', 'state.sqlite') 30 38 31 - // Loopback server config 32 39 const LOOPBACK_PORT = 4000 33 40 const LOOPBACK_HOST = '127.0.0.1' 34 41 35 - interface StoredData { 36 - states: Record<string, NodeSavedState> 37 - sessions: Record<string, NodeSavedSession> 42 + // Runtime-agnostic KV adapter — all SQL is baked in so return types are known 43 + 44 + interface KvRow { 45 + value: string 46 + expires_at: number | null 38 47 } 39 48 40 - function ensureDir(filePath: string) { 41 - const dir = dirname(resolve(filePath)) 42 - if (!existsSync(dir)) { 43 - mkdirSync(dir, { recursive: true }) 49 + interface KvAdapter { 50 + get(key: string): KvRow | undefined 51 + set(key: string, value: string, expiresAt: number | null): void 52 + del(key: string): void 53 + clear(): void 54 + } 55 + 56 + const SCHEMA = ` 57 + CREATE TABLE IF NOT EXISTS kv ( 58 + key TEXT PRIMARY KEY, 59 + value TEXT NOT NULL, 60 + expires_at INTEGER 61 + ) 62 + ` 63 + 64 + async function openKv(dbPath: string): Promise<KvAdapter> { 65 + mkdirSync(dirname(dbPath), { recursive: true }) 66 + 67 + if (isBun) { 68 + const { Database } = await import('bun:sqlite') 69 + const db = new Database(dbPath) 70 + db.run('PRAGMA journal_mode = WAL') 71 + db.run(SCHEMA) 72 + const getStmt = db.query<KvRow, [string]>('SELECT value, expires_at FROM kv WHERE key = ?') 73 + const setStmt = db.query('INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)') 74 + const delStmt = db.query('DELETE FROM kv WHERE key = ?') 75 + return { 76 + get: (key) => getStmt.get(key) ?? undefined, 77 + set: (key, value, expiresAt) => { setStmt.run(key, value, expiresAt) }, 78 + del: (key) => { delStmt.run(key) }, 79 + clear: () => db.run('DELETE FROM kv'), 80 + } 81 + } else { 82 + const { DatabaseSync } = await import('node:sqlite') 83 + const db = new DatabaseSync(dbPath) 84 + db.exec('PRAGMA journal_mode = WAL') 85 + db.exec(SCHEMA) 86 + const getStmt = db.prepare('SELECT value, expires_at FROM kv WHERE key = ?') 87 + const setStmt = db.prepare('INSERT OR REPLACE INTO kv (key, value, expires_at) VALUES (?, ?, ?)') 88 + const delStmt = db.prepare('DELETE FROM kv WHERE key = ?') 89 + return { 90 + get: (key) => getStmt.get(key) as KvRow | undefined, 91 + set: (key, value, expiresAt) => { setStmt.run(key, value, expiresAt) }, 92 + del: (key) => { delStmt.run(key) }, 93 + clear: () => db.exec('DELETE FROM kv'), 94 + } 44 95 } 45 96 } 46 97 47 - function loadStore(storePath: string): StoredData { 48 - if (!existsSync(storePath)) { 49 - return { states: {}, sessions: {} } 98 + function kvGet(kv: KvAdapter, key: string): string | undefined { 99 + const row = kv.get(key) 100 + if (!row) return undefined 101 + if (row.expires_at !== null && row.expires_at <= Date.now()) { 102 + kv.del(key) 103 + return undefined 50 104 } 51 - try { 52 - const content = readFileSync(storePath, 'utf-8') 53 - return JSON.parse(content) 54 - } catch { 55 - return { states: {}, sessions: {} } 56 - } 105 + return row.value 57 106 } 58 107 59 - function saveStore(storePath: string, data: StoredData) { 60 - ensureDir(storePath) 61 - writeFileSync(storePath, JSON.stringify(data, null, 2)) 108 + function kvSet(kv: KvAdapter, key: string, value: string, ttlSeconds?: number): void { 109 + const expiresAt = ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : null 110 + kv.set(key, value, expiresAt) 62 111 } 63 112 64 - function createStateStore(storePath: string): NodeSavedStateStore { 113 + function createStateStore(kv: KvAdapter): NodeSavedStateStore { 65 114 return { 66 115 async set(key: string, state: NodeSavedState) { 67 - const data = loadStore(storePath) 68 - data.states[key] = state 69 - saveStore(storePath, data) 116 + kvSet(kv, `oauth_state:${key}`, JSON.stringify(state), 600) 70 117 }, 71 118 async get(key: string) { 72 - const data = loadStore(storePath) 73 - return data.states[key] 119 + const raw = kvGet(kv, `oauth_state:${key}`) 120 + if (!raw) return undefined 121 + return JSON.parse(raw) as NodeSavedState 74 122 }, 75 123 async del(key: string) { 76 - const data = loadStore(storePath) 77 - delete data.states[key] 78 - saveStore(storePath, data) 124 + kv.del(`oauth_state:${key}`) 79 125 }, 80 126 } 81 127 } 82 128 83 - function createSessionStore(storePath: string): NodeSavedSessionStore { 129 + function createSessionStore(kv: KvAdapter): NodeSavedSessionStore { 84 130 return { 85 131 async set(sub: string, session: NodeSavedSession) { 86 - const data = loadStore(storePath) 87 - data.sessions[sub] = session 88 - saveStore(storePath, data) 132 + kvSet(kv, `oauth_session:${sub}`, JSON.stringify(session), 60 * 60 * 24 * 14) 89 133 }, 90 134 async get(sub: string) { 91 - const data = loadStore(storePath) 92 - return data.sessions[sub] 135 + const raw = kvGet(kv, `oauth_session:${sub}`) 136 + if (!raw) return undefined 137 + return JSON.parse(raw) as NodeSavedSession 93 138 }, 94 139 async del(sub: string) { 95 - const data = loadStore(storePath) 96 - delete data.sessions[sub] 97 - saveStore(storePath, data) 140 + kv.del(`oauth_session:${sub}`) 98 141 }, 99 142 } 100 143 } 101 144 102 145 export interface AuthOptions { 103 - storePath?: string 146 + dbPath?: string 104 147 appPassword?: string 105 - serviceDid?: string 106 - requiredLxms?: readonly string[] 107 - includeRepoBlobScopes?: boolean 108 148 onStatus?: (message: string) => void 109 149 } 110 150 111 - function buildOAuthScope(options: AuthOptions = {}): string { 112 - const requestedLxms = options.requiredLxms ?? [] 113 - const rpcScopes = requestedLxms.map((lxm) => `rpc:${lxm}?aud=*`) 114 - if (options.serviceDid) { 115 - parseServiceDid(options.serviceDid) 116 - } 117 - 118 - const scopes = [REQUIRED_BASE_SCOPE] 119 - if (options.includeRepoBlobScopes !== false) { 120 - scopes.push(...REPO_BLOB_SCOPES) 121 - } 122 - scopes.push(...rpcScopes) 123 - 124 - return scopes.join(' ') 125 - } 126 - 127 - function normalizeScopeToken(scope: string): string { 128 - try { 129 - return decodeURIComponent(scope) 130 - } catch { 131 - return scope 132 - } 133 - } 134 - 135 - function findMissingScopes(grantedScope: string | undefined, requiredScope: string): string[] { 136 - const granted = new Set((grantedScope || '').split(/\s+/).filter(Boolean).map(normalizeScopeToken)) 137 - const required = requiredScope.split(/\s+/).filter(Boolean).map(normalizeScopeToken) 138 - 139 - return required.filter((scope) => !granted.has(scope)) 140 - } 141 - 142 151 function emitStatus(options: AuthOptions | undefined, message: string) { 143 152 if (options?.onStatus) { 144 153 options.onStatus(message) ··· 156 165 } 157 166 158 167 /** 168 + * Check whether the current directory has a stored session 169 + */ 170 + export async function hasDirSession(dbPath?: string): Promise<boolean> { 171 + try { 172 + const kv = await openKv(dbPath || DEFAULT_DB_PATH) 173 + return kvGet(kv, `dir:${cwd()}`) !== undefined 174 + } catch { 175 + return false 176 + } 177 + } 178 + 179 + /** 159 180 * Authenticate with AT Protocol using OAuth loopback flow 160 181 */ 161 182 export async function authenticateOAuth( 162 - handle: string, 183 + handle?: string, 163 184 options: AuthOptions = {}, 164 185 ): Promise<{ agent: Agent; did: string }> { 165 - const storePath = options.storePath || DEFAULT_STORE_PATH 166 - const oauthScope = buildOAuthScope(options) 186 + const kv = await openKv(options.dbPath || DEFAULT_DB_PATH) 167 187 168 - // Build loopback client metadata 169 188 const redirectUri = `http://${LOOPBACK_HOST}:${LOOPBACK_PORT}/oauth/callback` 170 189 const clientIdParams = new URLSearchParams() 171 190 clientIdParams.append('redirect_uri', redirectUri) 172 - clientIdParams.append('scope', oauthScope) 191 + clientIdParams.append('scope', OAUTH_SCOPE) 173 192 174 193 const client = new NodeOAuthClient({ 175 194 clientMetadata: { ··· 181 200 response_types: ['code'], 182 201 application_type: 'web', 183 202 token_endpoint_auth_method: 'none', 184 - scope: oauthScope, 203 + scope: OAUTH_SCOPE, 185 204 dpop_bound_access_tokens: false, 186 205 }, 187 - stateStore: createStateStore(storePath), 188 - sessionStore: createSessionStore(storePath), 206 + stateStore: createStateStore(kv), 207 + sessionStore: createSessionStore(kv), 189 208 requestLock: requestLocalLock, 190 209 }) 191 210 192 - // Try to restore existing session 193 - const data = loadStore(storePath) 194 - const existingSessions = Object.keys(data.sessions) 195 - 196 - // Check if we have a session for this handle's DID 197 - for (const sub of existingSessions) { 211 + // Try to restore the session mapped to the current directory 212 + const dirKey = `dir:${cwd()}` 213 + const storedDid = kvGet(kv, dirKey) 214 + if (storedDid) { 198 215 try { 199 - const session = await client.restore(sub) 216 + const session = await client.restore(storedDid) 200 217 if (session) { 201 - // Verify session is still valid 202 - const agent = new Agent(session) 203 - const profile = await agent.getProfile({ actor: sub }) 204 - 205 - // Check if this is the handle we want 206 - if (profile.data.handle === handle || sub === handle) { 207 - const tokenInfo = await session.getTokenInfo(false) 208 - const missingScopes = findMissingScopes(tokenInfo.scope, oauthScope) 209 - if (missingScopes.length > 0) { 210 - continue 211 - } 212 - 213 - emitStatus(options, `Restored session for ${profile.data.handle}`) 214 - return { agent, did: sub } 215 - } 218 + emitStatus(options, `Restored session for ${storedDid}`) 219 + return { agent: new Agent(session), did: storedDid } 216 220 } 217 221 } catch { 218 - // Session invalid, continue 222 + // Session invalid or expired — clear mapping and re-auth 223 + kv.del(dirKey) 219 224 } 220 225 } 221 226 227 + // Need a handle to start a new OAuth flow 228 + if (!handle) { 229 + throw new Error('No active session for this directory. Run `wispctl login <handle>` first.') 230 + } 231 + 222 232 // Start new OAuth flow 223 233 emitStatus(options, `Starting OAuth flow for ${handle}...`) 224 234 225 - // Create loopback server to receive callback 226 235 const callbackPromise = new Promise<{ params: URLSearchParams }>((resolve, reject) => { 227 236 const app = new Hono() 228 237 let serverHandle: { close: () => void } | null = null ··· 294 303 clearTimeout(timeoutHandle) 295 304 } 296 305 settled = true 297 - 298 - // Close server after receiving callback 299 306 setTimeout(() => serverHandle?.close(), 100) 300 - 301 307 resolve({ params }) 302 - 303 308 return c.html(successHtml) 304 309 }) 305 310 306 311 app.all('*', (c) => c.text('Not found', 404)) 307 312 308 - // Start server based on runtime 309 313 if (isBun) { 310 314 const bunServer = Bun.serve({ 311 315 port: LOOPBACK_PORT, ··· 322 326 serverHandle = { close: () => nodeServer.close() } 323 327 } 324 328 325 - // Timeout after 5 minutes 326 329 timeoutHandle = setTimeout( 327 330 () => { 328 - if (settled) { 329 - return 330 - } 331 + if (settled) return 331 332 settled = true 332 333 serverHandle?.close() 333 334 reject(new Error('OAuth callback timeout')) ··· 340 341 } 341 342 }) 342 343 343 - // Get authorization URL 344 - const authUrl = await client.authorize(handle, { 345 - scope: oauthScope, 346 - }) 344 + const authUrl = await client.authorize(handle, { scope: OAUTH_SCOPE }) 347 345 348 - // Open browser 349 346 emitStatus(options, 'Opening browser for authentication...') 350 347 emitStatus(options, `If browser does not open, visit: ${authUrl}`) 351 348 await open(authUrl.toString()) 352 349 353 - // Wait for callback 354 350 const { params } = await callbackPromise 351 + const { session } = await client.callback(params) 355 352 356 - // Handle callback 357 - const { session } = await client.callback(params) 358 353 const tokenInfo = await session.getTokenInfo(false) 359 - const missingScopes = findMissingScopes(tokenInfo.scope, oauthScope) 354 + const grantedScopes = new Set((tokenInfo.scope || '').split(/\s+/).filter(Boolean)) 355 + const missingScopes = OAUTH_SCOPE.split(' ').filter((s) => !grantedScopes.has(decodeURIComponent(s))) 360 356 if (missingScopes.length > 0) { 361 357 emitWarning( 362 358 options, 363 - `OAuth token is missing requested scopes (${missingScopes.length}). First missing scope: ${missingScopes[0]}`, 359 + `OAuth token is missing ${missingScopes.length} requested scope(s). First missing: ${missingScopes[0]}`, 364 360 ) 365 361 } 366 362 367 363 const agent = new Agent(session) 368 364 const did = session.did 365 + 366 + // Map the current directory to this DID for future restores 367 + kvSet(kv, dirKey, did) 369 368 370 369 emitStatus(options, `Authenticated as ${did}`) 371 370 ··· 384 383 let serviceUrl = pdsUrl 385 384 386 385 if (!serviceUrl) { 387 - // Resolve the handle to find the correct PDS 388 386 emitStatus(options, `Resolving PDS for ${identifier}...`) 389 387 serviceUrl = await resolvePdsFromHandle(identifier) 390 388 emitStatus(options, `Found PDS: ${serviceUrl}`) ··· 404 402 /** 405 403 * Authenticate - tries OAuth if no password provided, otherwise uses app password 406 404 */ 407 - export async function authenticate(handle: string, options: AuthOptions = {}): Promise<{ agent: Agent; did: string }> { 405 + export async function authenticate(handle?: string, options: AuthOptions = {}): Promise<{ agent: Agent; did: string }> { 408 406 if (options.appPassword) { 407 + if (!handle) throw new Error('Handle required with app password authentication') 409 408 return authenticateAppPassword(handle, options.appPassword, undefined, options) 410 409 } 411 410 return authenticateOAuth(handle, options) 412 411 } 413 412 414 413 /** 415 - * Clear stored OAuth sessions 414 + * Clear the session mapping for the current directory (local logout) 416 415 */ 417 - export function clearSessions(storePath?: string) { 418 - const path = storePath || DEFAULT_STORE_PATH 419 - if (existsSync(path)) { 420 - unlinkSync(path) 421 - console.log('Cleared stored OAuth sessions') 416 + export async function clearDirSession(dbPath?: string) { 417 + try { 418 + const kv = await openKv(dbPath || DEFAULT_DB_PATH) 419 + kv.del(`dir:${cwd()}`) 420 + console.log('Cleared session for current directory') 421 + } catch { 422 + // db doesn't exist yet 423 + } 424 + } 425 + 426 + /** 427 + * Clear all stored OAuth sessions and directory mappings 428 + */ 429 + export async function clearSessions(dbPath?: string) { 430 + try { 431 + const kv = await openKv(dbPath || DEFAULT_DB_PATH) 432 + kv.clear() 433 + console.log('Cleared all stored OAuth sessions') 434 + } catch { 435 + // db doesn't exist yet 422 436 } 423 437 }
+7 -10
cli/lib/command-utils.ts
··· 1 1 import type { Agent } from '@atproto/api' 2 2 import { cancel, isCancel, text } from '@clack/prompts' 3 3 import type { Command } from 'commander' 4 - import { authenticate } from './auth.ts' 4 + import { authenticate, hasDirSession } from './auth.ts' 5 5 import { createSpinner, pc, type SpinnerLike } from './progress.ts' 6 6 import { parseServiceDid } from './wisp-service.ts' 7 7 8 8 export interface XrpcCommandOptions { 9 9 password?: string 10 - store?: string 10 + db?: string 11 11 service?: string 12 12 json?: boolean 13 13 } ··· 27 27 export function addXrpcAuthOptions<T extends Command>(command: T): T { 28 28 return command 29 29 .option('--password <password>', 'App password for headless authentication') 30 - .option('--store <path>', 'OAuth session store path') 30 + .option('--db <path>', 'OAuth session database path') 31 31 .option('--service <did:...>', 'Service DID to proxy through') 32 32 .option('--json', 'Output raw JSON') 33 33 } ··· 88 88 89 89 export async function authenticateForXrpc( 90 90 identifier: string | undefined, 91 - nsid: string | readonly string[], 92 91 options: XrpcCommandOptions, 93 92 ): Promise<{ agent: Agent; serviceDid: string; did: string }> { 94 - const requiredLxms = Array.isArray(nsid) ? [...nsid] : [nsid] 95 - const resolvedIdentifier = await resolveIdentifier(identifier) 93 + // Skip the handle prompt if the current directory already has a stored session 94 + const resolvedIdentifier = 95 + identifier ?? ((await hasDirSession(options.db)) ? undefined : await resolveIdentifier(undefined)) 96 96 const serviceDid = parseServiceDid(options.service) 97 97 98 98 const spinner = createSpinner('Authenticating...').start() 99 99 const { agent, did } = await authenticate(resolvedIdentifier, { 100 100 appPassword: options.password, 101 - storePath: options.store, 102 - serviceDid, 103 - requiredLxms, 104 - includeRepoBlobScopes: false, 101 + dbPath: options.db, 105 102 onStatus: bindAuthStatusToSpinner(spinner), 106 103 }) 107 104 spinner.succeed(`Authenticated as ${did}`)
+15 -15
cli/package.json
··· 1 1 { 2 2 "name": "wispctl", 3 3 "version": "1.0.10", 4 - "description": "CLI for wisp.place - deploy static sites to the AT Protocol", 5 - "type": "module", 6 4 "main": "./dist/index.js", 7 - "bin": { 8 - "wispctl": "dist/index.js" 9 - }, 10 - "files": [ 11 - "dist" 12 - ], 13 - "scripts": { 14 - "dev": "bun run index.ts", 15 - "build": "bun build ./index.ts --outdir ./dist --target node --sourcemap=linked && sed -i '' '1s|#!/usr/bin/env bun|#!/usr/bin/env node|' ./dist/index.js", 16 - "typecheck": "tsc --noEmit" 17 - }, 18 5 "devDependencies": { 19 6 "@atproto/api": "^0.18.17", 20 7 "@atproto/identity": "^0.4.10", ··· 31 18 "@types/mime-types": "^3.0.1", 32 19 "@types/node": "^22.0.0", 33 20 "@wispplace/atproto-utils": "workspace:*", 34 - "@wispplace/page-generators": "workspace:*", 35 21 "@wispplace/bun-firehose": "workspace:*", 36 22 "@wispplace/constants": "workspace:*", 37 23 "@wispplace/fs-utils": "workspace:*", 38 24 "@wispplace/lexicons": "workspace:*", 25 + "@wispplace/page-generators": "workspace:*", 39 26 "commander": "^14.0.2", 40 27 "hono": "^4.7.4", 41 28 "ignore": "^7.0.5", ··· 44 31 "open": "^11.0.0", 45 32 "picocolors": "^1.1.1", 46 33 "typescript": "^5" 47 - } 34 + }, 35 + "bin": { 36 + "wispctl": "dist/index.js" 37 + }, 38 + "description": "CLI for wisp.place - deploy static sites to the AT Protocol", 39 + "files": [ 40 + "dist" 41 + ], 42 + "scripts": { 43 + "dev": "bun run index.ts", 44 + "build": "bun build ./index.ts --outdir ./dist --target node --sourcemap=linked && sed -i '' '1s|#!/usr/bin/env bun|#!/usr/bin/env node|' ./dist/index.js", 45 + "typecheck": "tsc --noEmit" 46 + }, 47 + "type": "module" 48 48 }