emdash 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* emdash plugin init
|
|
3
3
|
*
|
|
4
|
-
* Scaffold a new EmDash plugin. Generates the
|
|
4
|
+
* Scaffold a new EmDash plugin. Generates the sandboxed-format boilerplate:
|
|
5
5
|
* src/index.ts -- descriptor factory
|
|
6
6
|
* src/sandbox-entry.ts -- definePlugin({ hooks, routes })
|
|
7
7
|
* package.json
|
|
8
8
|
* tsconfig.json
|
|
9
9
|
*
|
|
10
|
-
* Use --native to generate native-format boilerplate
|
|
10
|
+
* Use --format=native (or --native) to generate native-format boilerplate
|
|
11
|
+
* instead (createPlugin + React admin). When neither is passed and stdout
|
|
12
|
+
* is a TTY, the user is prompted to choose.
|
|
11
13
|
*
|
|
12
14
|
*/
|
|
13
15
|
|
|
@@ -22,6 +24,8 @@ import { fileExists } from "./bundle-utils.js";
|
|
|
22
24
|
const SLUG_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
23
25
|
const SCOPE_RE = /^@[^/]+\//;
|
|
24
26
|
|
|
27
|
+
type PluginFormat = "standard" | "native";
|
|
28
|
+
|
|
25
29
|
export const pluginInitCommand = defineCommand({
|
|
26
30
|
meta: {
|
|
27
31
|
name: "init",
|
|
@@ -37,15 +41,27 @@ export const pluginInitCommand = defineCommand({
|
|
|
37
41
|
type: "string",
|
|
38
42
|
description: "Plugin name/id (e.g. my-plugin or @org/my-plugin)",
|
|
39
43
|
},
|
|
44
|
+
format: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description:
|
|
47
|
+
"Plugin format: sandboxed or native. Prompts when running interactively if not set.",
|
|
48
|
+
valueHint: "sandboxed|native",
|
|
49
|
+
},
|
|
40
50
|
native: {
|
|
41
51
|
type: "boolean",
|
|
42
|
-
description: "
|
|
52
|
+
description: "Shortcut for --format=native",
|
|
43
53
|
default: false,
|
|
44
54
|
},
|
|
45
55
|
},
|
|
46
56
|
async run({ args }) {
|
|
47
57
|
const targetDir = resolve(args.dir);
|
|
48
|
-
|
|
58
|
+
|
|
59
|
+
const format = await resolveFormat(args.format, args.native);
|
|
60
|
+
if (!format) {
|
|
61
|
+
consola.info("Cancelled");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const isNative = format === "native";
|
|
49
65
|
|
|
50
66
|
// Derive plugin name from --name or directory name
|
|
51
67
|
let pluginName = args.name || basename(targetDir);
|
|
@@ -71,7 +87,7 @@ export const pluginInitCommand = defineCommand({
|
|
|
71
87
|
process.exit(1);
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
consola.start(`Scaffolding ${isNative ? "native" : "
|
|
90
|
+
consola.start(`Scaffolding ${isNative ? "native" : "sandboxed"} plugin: ${pluginName}`);
|
|
75
91
|
|
|
76
92
|
await mkdir(srcDir, { recursive: true });
|
|
77
93
|
|
|
@@ -83,22 +99,99 @@ export const pluginInitCommand = defineCommand({
|
|
|
83
99
|
|
|
84
100
|
consola.success(`Plugin scaffolded in ${targetDir}`);
|
|
85
101
|
consola.info("Next steps:");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
const steps: string[] = [];
|
|
103
|
+
if (args.dir !== ".") steps.push(`cd ${args.dir}`);
|
|
104
|
+
steps.push("pnpm install");
|
|
105
|
+
steps.push(
|
|
106
|
+
isNative
|
|
107
|
+
? "Edit src/index.ts to add hooks and routes"
|
|
108
|
+
: "Edit src/sandbox-entry.ts to add hooks and routes",
|
|
109
|
+
);
|
|
110
|
+
steps.push("pnpm build");
|
|
111
|
+
if (!isNative) steps.push("emdash plugin validate --dir .");
|
|
112
|
+
steps.forEach((step, i) => consola.info(` ${i + 1}. ${step}`));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
async function resolveFormat(
|
|
117
|
+
formatArg: string | undefined,
|
|
118
|
+
nativeFlag: boolean,
|
|
119
|
+
): Promise<PluginFormat | null> {
|
|
120
|
+
if (formatArg) {
|
|
121
|
+
const normalized = formatArg.toLowerCase();
|
|
122
|
+
let parsed: PluginFormat;
|
|
123
|
+
if (normalized === "native") {
|
|
124
|
+
parsed = "native";
|
|
125
|
+
} else if (normalized === "sandboxed" || normalized === "standard") {
|
|
126
|
+
parsed = "standard";
|
|
92
127
|
} else {
|
|
93
|
-
consola.
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
consola.error(`Invalid --format "${formatArg}". Use "sandboxed" or "native".`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
if (nativeFlag && parsed !== "native") {
|
|
132
|
+
consola.error(`Conflicting flags: --native and --format=${formatArg}. Pass only one.`);
|
|
133
|
+
process.exit(1);
|
|
96
134
|
}
|
|
97
|
-
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
if (nativeFlag) return "native";
|
|
138
|
+
|
|
139
|
+
if (!process.stdout.isTTY) return "standard";
|
|
140
|
+
|
|
141
|
+
const choice = await consola.prompt("Which plugin format?", {
|
|
142
|
+
type: "select",
|
|
143
|
+
initial: "standard",
|
|
144
|
+
options: [
|
|
145
|
+
{
|
|
146
|
+
label: "Sandboxed",
|
|
147
|
+
value: "standard",
|
|
148
|
+
hint: "runs in an isolated sandbox; safe to install from the marketplace",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
label: "Native",
|
|
152
|
+
value: "native",
|
|
153
|
+
hint: "full runtime access; install from npm",
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
cancel: "null",
|
|
157
|
+
});
|
|
158
|
+
if (choice === null) return null;
|
|
159
|
+
return choice as PluginFormat;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function camelCase(slug: string): string {
|
|
163
|
+
return slug
|
|
164
|
+
.split("-")
|
|
165
|
+
.map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
|
|
166
|
+
.join("");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pascalCase(slug: string): string {
|
|
170
|
+
return slug
|
|
171
|
+
.split("-")
|
|
172
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
173
|
+
.join("");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const TSCONFIG = {
|
|
177
|
+
compilerOptions: {
|
|
178
|
+
target: "ES2022",
|
|
179
|
+
module: "preserve",
|
|
180
|
+
moduleResolution: "bundler",
|
|
181
|
+
strict: true,
|
|
182
|
+
esModuleInterop: true,
|
|
183
|
+
declaration: true,
|
|
184
|
+
outDir: "./dist",
|
|
185
|
+
rootDir: "./src",
|
|
98
186
|
},
|
|
99
|
-
|
|
187
|
+
include: ["src/**/*"],
|
|
188
|
+
exclude: ["node_modules", "dist"],
|
|
189
|
+
} as const;
|
|
190
|
+
|
|
191
|
+
const TSDOWN_VERSION = "^0.20.0";
|
|
192
|
+
const TYPESCRIPT_VERSION = "^5.9.0";
|
|
100
193
|
|
|
101
|
-
// ──
|
|
194
|
+
// ── Sandboxed format scaffolding ─────────────────────────────────
|
|
102
195
|
|
|
103
196
|
async function scaffoldStandard(
|
|
104
197
|
targetDir: string,
|
|
@@ -106,13 +199,8 @@ async function scaffoldStandard(
|
|
|
106
199
|
pluginName: string,
|
|
107
200
|
slug: string,
|
|
108
201
|
): Promise<void> {
|
|
109
|
-
|
|
110
|
-
const fnName = slug
|
|
111
|
-
.split("-")
|
|
112
|
-
.map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
|
|
113
|
-
.join("");
|
|
202
|
+
const fnName = camelCase(slug);
|
|
114
203
|
|
|
115
|
-
// package.json
|
|
116
204
|
await writeFile(
|
|
117
205
|
join(targetDir, "package.json"),
|
|
118
206
|
JSON.stringify(
|
|
@@ -120,74 +208,98 @@ async function scaffoldStandard(
|
|
|
120
208
|
name: pluginName,
|
|
121
209
|
version: "0.1.0",
|
|
122
210
|
type: "module",
|
|
211
|
+
main: "./dist/index.mjs",
|
|
123
212
|
exports: {
|
|
124
|
-
".":
|
|
125
|
-
|
|
213
|
+
".": {
|
|
214
|
+
types: "./dist/index.d.mts",
|
|
215
|
+
import: "./dist/index.mjs",
|
|
216
|
+
},
|
|
217
|
+
"./sandbox": {
|
|
218
|
+
types: "./dist/sandbox-entry.d.mts",
|
|
219
|
+
import: "./dist/sandbox-entry.mjs",
|
|
220
|
+
},
|
|
126
221
|
},
|
|
127
|
-
files: ["
|
|
222
|
+
files: ["dist"],
|
|
223
|
+
scripts: {
|
|
224
|
+
build: "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean",
|
|
225
|
+
dev: "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --watch",
|
|
226
|
+
typecheck: "tsc --noEmit",
|
|
227
|
+
},
|
|
228
|
+
keywords: ["emdash", "emdash-plugin"],
|
|
229
|
+
license: "MIT",
|
|
128
230
|
peerDependencies: {
|
|
129
231
|
emdash: "*",
|
|
130
232
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// tsconfig.json
|
|
138
|
-
await writeFile(
|
|
139
|
-
join(targetDir, "tsconfig.json"),
|
|
140
|
-
JSON.stringify(
|
|
141
|
-
{
|
|
142
|
-
compilerOptions: {
|
|
143
|
-
target: "ES2022",
|
|
144
|
-
module: "preserve",
|
|
145
|
-
moduleResolution: "bundler",
|
|
146
|
-
strict: true,
|
|
147
|
-
esModuleInterop: true,
|
|
148
|
-
declaration: true,
|
|
149
|
-
outDir: "./dist",
|
|
150
|
-
rootDir: "./src",
|
|
233
|
+
devDependencies: {
|
|
234
|
+
emdash: "*",
|
|
235
|
+
tsdown: TSDOWN_VERSION,
|
|
236
|
+
typescript: TYPESCRIPT_VERSION,
|
|
151
237
|
},
|
|
152
|
-
include: ["src/**/*"],
|
|
153
|
-
exclude: ["node_modules", "dist"],
|
|
154
238
|
},
|
|
155
239
|
null,
|
|
156
240
|
"\t",
|
|
157
241
|
) + "\n",
|
|
158
242
|
);
|
|
159
243
|
|
|
160
|
-
|
|
244
|
+
await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify(TSCONFIG, null, "\t") + "\n");
|
|
245
|
+
|
|
161
246
|
await writeFile(
|
|
162
247
|
join(srcDir, "index.ts"),
|
|
163
248
|
`import type { PluginDescriptor } from "emdash";
|
|
164
249
|
|
|
165
250
|
export function ${fnName}Plugin(): PluginDescriptor {
|
|
166
251
|
\treturn {
|
|
167
|
-
\t\tid: "${
|
|
252
|
+
\t\tid: "${slug}",
|
|
168
253
|
\t\tversion: "0.1.0",
|
|
169
254
|
\t\tformat: "standard",
|
|
170
255
|
\t\tentrypoint: "${pluginName}/sandbox",
|
|
171
|
-
|
|
256
|
+
|
|
257
|
+
\t\tcapabilities: ["content:read"],
|
|
258
|
+
\t\tstorage: {
|
|
259
|
+
\t\t\tevents: { indexes: ["timestamp"] },
|
|
260
|
+
\t\t},
|
|
172
261
|
\t};
|
|
173
262
|
}
|
|
174
263
|
`,
|
|
175
264
|
);
|
|
176
265
|
|
|
177
|
-
// src/sandbox-entry.ts -- plugin definition
|
|
178
266
|
await writeFile(
|
|
179
267
|
join(srcDir, "sandbox-entry.ts"),
|
|
180
268
|
`import { definePlugin } from "emdash";
|
|
181
269
|
import type { PluginContext } from "emdash";
|
|
182
270
|
|
|
271
|
+
interface ContentSaveEvent {
|
|
272
|
+
\tcollection: string;
|
|
273
|
+
\tcontent: { id: string };
|
|
274
|
+
\tisNew: boolean;
|
|
275
|
+
}
|
|
276
|
+
|
|
183
277
|
export default definePlugin({
|
|
184
278
|
\thooks: {
|
|
185
279
|
\t\t"content:afterSave": {
|
|
186
|
-
\t\t\thandler: async (event:
|
|
280
|
+
\t\t\thandler: async (event: ContentSaveEvent, ctx: PluginContext) => {
|
|
187
281
|
\t\t\t\tctx.log.info("Content saved", {
|
|
188
282
|
\t\t\t\t\tcollection: event.collection,
|
|
189
283
|
\t\t\t\t\tid: event.content.id,
|
|
190
284
|
\t\t\t\t});
|
|
285
|
+
|
|
286
|
+
\t\t\t\tawait ctx.storage.events.put(\`save-\${Date.now()}\`, {
|
|
287
|
+
\t\t\t\t\ttimestamp: new Date().toISOString(),
|
|
288
|
+
\t\t\t\t\tcollection: event.collection,
|
|
289
|
+
\t\t\t\t\tcontentId: event.content.id,
|
|
290
|
+
\t\t\t\t});
|
|
291
|
+
\t\t\t},
|
|
292
|
+
\t\t},
|
|
293
|
+
\t},
|
|
294
|
+
|
|
295
|
+
\troutes: {
|
|
296
|
+
\t\trecent: {
|
|
297
|
+
\t\t\thandler: async (_routeCtx, ctx: PluginContext) => {
|
|
298
|
+
\t\t\t\tconst result = await ctx.storage.events.query({
|
|
299
|
+
\t\t\t\t\torderBy: { timestamp: "desc" },
|
|
300
|
+
\t\t\t\t\tlimit: 10,
|
|
301
|
+
\t\t\t\t});
|
|
302
|
+
\t\t\t\treturn { events: result.items };
|
|
191
303
|
\t\t\t},
|
|
192
304
|
\t\t},
|
|
193
305
|
\t},
|
|
@@ -204,12 +316,9 @@ async function scaffoldNative(
|
|
|
204
316
|
pluginName: string,
|
|
205
317
|
slug: string,
|
|
206
318
|
): Promise<void> {
|
|
207
|
-
const fnName = slug
|
|
208
|
-
|
|
209
|
-
.map((s, i) => (i === 0 ? s : s[0].toUpperCase() + s.slice(1)))
|
|
210
|
-
.join("");
|
|
319
|
+
const fnName = camelCase(slug);
|
|
320
|
+
const typeName = pascalCase(slug);
|
|
211
321
|
|
|
212
|
-
// package.json
|
|
213
322
|
await writeFile(
|
|
214
323
|
join(targetDir, "package.json"),
|
|
215
324
|
JSON.stringify(
|
|
@@ -217,71 +326,88 @@ async function scaffoldNative(
|
|
|
217
326
|
name: pluginName,
|
|
218
327
|
version: "0.1.0",
|
|
219
328
|
type: "module",
|
|
329
|
+
main: "./dist/index.mjs",
|
|
220
330
|
exports: {
|
|
221
|
-
".":
|
|
331
|
+
".": {
|
|
332
|
+
types: "./dist/index.d.mts",
|
|
333
|
+
import: "./dist/index.mjs",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
files: ["dist"],
|
|
337
|
+
scripts: {
|
|
338
|
+
build: "tsdown src/index.ts --format esm --dts --clean",
|
|
339
|
+
dev: "tsdown src/index.ts --format esm --dts --watch",
|
|
340
|
+
typecheck: "tsc --noEmit",
|
|
222
341
|
},
|
|
223
|
-
|
|
342
|
+
keywords: ["emdash", "emdash-plugin"],
|
|
343
|
+
license: "MIT",
|
|
224
344
|
peerDependencies: {
|
|
225
345
|
emdash: "*",
|
|
226
346
|
},
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
// tsconfig.json
|
|
234
|
-
await writeFile(
|
|
235
|
-
join(targetDir, "tsconfig.json"),
|
|
236
|
-
JSON.stringify(
|
|
237
|
-
{
|
|
238
|
-
compilerOptions: {
|
|
239
|
-
target: "ES2022",
|
|
240
|
-
module: "preserve",
|
|
241
|
-
moduleResolution: "bundler",
|
|
242
|
-
strict: true,
|
|
243
|
-
esModuleInterop: true,
|
|
244
|
-
declaration: true,
|
|
245
|
-
outDir: "./dist",
|
|
246
|
-
rootDir: "./src",
|
|
347
|
+
devDependencies: {
|
|
348
|
+
emdash: "*",
|
|
349
|
+
tsdown: TSDOWN_VERSION,
|
|
350
|
+
typescript: TYPESCRIPT_VERSION,
|
|
247
351
|
},
|
|
248
|
-
include: ["src/**/*"],
|
|
249
|
-
exclude: ["node_modules", "dist"],
|
|
250
352
|
},
|
|
251
353
|
null,
|
|
252
354
|
"\t",
|
|
253
355
|
) + "\n",
|
|
254
356
|
);
|
|
255
357
|
|
|
256
|
-
|
|
358
|
+
await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify(TSCONFIG, null, "\t") + "\n");
|
|
359
|
+
|
|
257
360
|
await writeFile(
|
|
258
361
|
join(srcDir, "index.ts"),
|
|
259
362
|
`import { definePlugin } from "emdash";
|
|
260
363
|
import type { PluginDescriptor } from "emdash";
|
|
261
364
|
|
|
262
|
-
export
|
|
365
|
+
export interface ${typeName}Options {
|
|
366
|
+
\tenabled?: boolean;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function ${fnName}Plugin(options: ${typeName}Options = {}): PluginDescriptor<${typeName}Options> {
|
|
263
370
|
\treturn {
|
|
264
|
-
\t\tid: "${
|
|
371
|
+
\t\tid: "${slug}",
|
|
265
372
|
\t\tversion: "0.1.0",
|
|
266
373
|
\t\tformat: "native",
|
|
267
374
|
\t\tentrypoint: "${pluginName}",
|
|
268
|
-
\t\toptions
|
|
375
|
+
\t\toptions,
|
|
269
376
|
\t};
|
|
270
377
|
}
|
|
271
378
|
|
|
272
|
-
export function createPlugin() {
|
|
379
|
+
export function createPlugin(options: ${typeName}Options = {}) {
|
|
273
380
|
\treturn definePlugin({
|
|
274
|
-
\t\tid: "${
|
|
381
|
+
\t\tid: "${slug}",
|
|
275
382
|
\t\tversion: "0.1.0",
|
|
276
383
|
|
|
384
|
+
\t\tcapabilities: ["content:read"],
|
|
385
|
+
\t\tstorage: {
|
|
386
|
+
\t\t\tevents: { indexes: ["createdAt"] },
|
|
387
|
+
\t\t},
|
|
388
|
+
|
|
277
389
|
\t\thooks: {
|
|
278
390
|
\t\t\t"content:afterSave": async (event, ctx) => {
|
|
279
|
-
\t\t\t\
|
|
391
|
+
\t\t\t\tif (options.enabled === false) return;
|
|
392
|
+
\t\t\t\tawait ctx.storage.events.put(\`evt_\${Date.now()}\`, {
|
|
280
393
|
\t\t\t\t\tcollection: event.collection,
|
|
281
|
-
\t\t\t\t\
|
|
394
|
+
\t\t\t\t\tcontentId: event.content.id,
|
|
395
|
+
\t\t\t\t\tcreatedAt: new Date().toISOString(),
|
|
282
396
|
\t\t\t\t});
|
|
283
397
|
\t\t\t},
|
|
284
398
|
\t\t},
|
|
399
|
+
|
|
400
|
+
\t\troutes: {
|
|
401
|
+
\t\t\trecent: {
|
|
402
|
+
\t\t\t\thandler: async (ctx) => {
|
|
403
|
+
\t\t\t\t\tconst result = await ctx.storage.events.query({
|
|
404
|
+
\t\t\t\t\t\torderBy: { createdAt: "desc" },
|
|
405
|
+
\t\t\t\t\t\tlimit: 10,
|
|
406
|
+
\t\t\t\t\t});
|
|
407
|
+
\t\t\t\t\treturn { events: result.items };
|
|
408
|
+
\t\t\t\t},
|
|
409
|
+
\t\t\t},
|
|
410
|
+
\t\t},
|
|
285
411
|
\t});
|
|
286
412
|
}
|
|
287
413
|
|
|
@@ -21,6 +21,7 @@ import { createGzipDecoder, unpackTar } from "modern-tar";
|
|
|
21
21
|
import pc from "picocolors";
|
|
22
22
|
|
|
23
23
|
import { pluginManifestSchema } from "../../plugins/manifest-schema.js";
|
|
24
|
+
import { CAPABILITY_RENAMES, isDeprecatedCapability } from "../../plugins/types.js";
|
|
24
25
|
import {
|
|
25
26
|
getMarketplaceCredential,
|
|
26
27
|
saveMarketplaceCredential,
|
|
@@ -440,6 +441,29 @@ export const publishCommand = defineCommand({
|
|
|
440
441
|
}
|
|
441
442
|
console.log();
|
|
442
443
|
|
|
444
|
+
// ── Step 2.5: Hard-fail on deprecated capability names ──
|
|
445
|
+
//
|
|
446
|
+
// Refusing to publish manifests that use deprecated capability names
|
|
447
|
+
// keeps the marketplace clean while the deprecation window is open.
|
|
448
|
+
// The fix is mechanical and entirely in the author's hands — they
|
|
449
|
+
// rename, re-bundle, and republish. Better to refuse 5 publishes
|
|
450
|
+
// than ship 500 deprecated manifests. We check before authentication
|
|
451
|
+
// so authors don't burn a device-flow login on a doomed publish.
|
|
452
|
+
const deprecatedCaps = manifest.capabilities.filter(isDeprecatedCapability);
|
|
453
|
+
if (deprecatedCaps.length > 0) {
|
|
454
|
+
consola.error(
|
|
455
|
+
"Plugin declares deprecated capability names. Rename them and re-bundle before publishing:",
|
|
456
|
+
);
|
|
457
|
+
for (const cap of deprecatedCaps) {
|
|
458
|
+
const replacement = CAPABILITY_RENAMES[cap];
|
|
459
|
+
consola.error(` ${cap} → ${replacement}`);
|
|
460
|
+
}
|
|
461
|
+
consola.error(
|
|
462
|
+
"See https://emdashcms.com/docs/plugins/overview#capabilities for the full rename table.",
|
|
463
|
+
);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
|
|
443
467
|
// ── Step 3: Authenticate ──
|
|
444
468
|
//
|
|
445
469
|
// Priority: EMDASH_MARKETPLACE_TOKEN env var > stored credential > interactive device flow.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets CLI commands
|
|
3
|
+
*
|
|
4
|
+
* Pure (no-DB) commands for working with EmDash secrets:
|
|
5
|
+
*
|
|
6
|
+
* - `emdash secrets generate` — emits a fresh `EMDASH_ENCRYPTION_KEY`.
|
|
7
|
+
* Optionally writes it to `.dev.vars` (Workers) or `.env` (Node).
|
|
8
|
+
* - `emdash secrets fingerprint <key>` — prints the kid for a key,
|
|
9
|
+
* useful in CI for verifying what's been deployed without exposing
|
|
10
|
+
* the raw value.
|
|
11
|
+
*
|
|
12
|
+
* DB-touching commands (`status`, `migrate`, `rotate`) live elsewhere:
|
|
13
|
+
* the CLI process can't open the production D1/Postgres binding from
|
|
14
|
+
* the operator's machine, so those operations ship as admin HTTP
|
|
15
|
+
* endpoints in a later PR. A thin `--site <url>` wrapper for those
|
|
16
|
+
* endpoints can land alongside.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { resolve } from "node:path";
|
|
21
|
+
|
|
22
|
+
import { defineCommand } from "citty";
|
|
23
|
+
import { consola } from "consola";
|
|
24
|
+
import pc from "picocolors";
|
|
25
|
+
|
|
26
|
+
import { EmDashSecretsError, fingerprintKey, generateEncryptionKey } from "../../config/secrets.js";
|
|
27
|
+
|
|
28
|
+
const KEY_VAR_NAME = "EMDASH_ENCRYPTION_KEY";
|
|
29
|
+
/** Matches a populated entry — `KEY=<at least one char>`. */
|
|
30
|
+
const POPULATED_KEY_LINE_PATTERN = /^EMDASH_ENCRYPTION_KEY=.+$/m;
|
|
31
|
+
/**
|
|
32
|
+
* Matches any line starting `KEY=` including `KEY=` with empty value.
|
|
33
|
+
* Used for in-place replacement when the entry exists but has no value.
|
|
34
|
+
*/
|
|
35
|
+
const ANY_KEY_LINE_PATTERN = /^EMDASH_ENCRYPTION_KEY=.*$/m;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Append (or replace) `EMDASH_ENCRYPTION_KEY` in a dotenv-style file.
|
|
39
|
+
*
|
|
40
|
+
* Idempotent: if the entry exists with a populated value, leaves it alone
|
|
41
|
+
* (returns `"skipped"`) unless `force` is set. An entry with an empty
|
|
42
|
+
* value (`EMDASH_ENCRYPTION_KEY=`) is treated as "not set" and gets
|
|
43
|
+
* replaced — placeholder lines aren't a reason to refuse.
|
|
44
|
+
*
|
|
45
|
+
* Always ends the resulting file with a trailing newline. Doesn't touch
|
|
46
|
+
* other variables.
|
|
47
|
+
*
|
|
48
|
+
* Exported for tests.
|
|
49
|
+
*/
|
|
50
|
+
export function writeEncryptionKeyToFile(
|
|
51
|
+
targetPath: string,
|
|
52
|
+
value: string,
|
|
53
|
+
force: boolean,
|
|
54
|
+
): "wrote" | "skipped" {
|
|
55
|
+
const exists = existsSync(targetPath);
|
|
56
|
+
const existing = exists ? readFileSync(targetPath, "utf-8") : "";
|
|
57
|
+
|
|
58
|
+
const hasPopulatedKey = POPULATED_KEY_LINE_PATTERN.test(existing);
|
|
59
|
+
if (hasPopulatedKey && !force) {
|
|
60
|
+
return "skipped";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const newLine = `${KEY_VAR_NAME}=${value}`;
|
|
64
|
+
let next: string;
|
|
65
|
+
if (ANY_KEY_LINE_PATTERN.test(existing)) {
|
|
66
|
+
// In-place replace handles both populated-and-forced and empty-value
|
|
67
|
+
// cases. Then ensure trailing newline.
|
|
68
|
+
next = existing.replace(ANY_KEY_LINE_PATTERN, newLine);
|
|
69
|
+
if (!next.endsWith("\n")) next += "\n";
|
|
70
|
+
} else {
|
|
71
|
+
// Append. Insert a separating newline only if the file has content
|
|
72
|
+
// not already ending in one.
|
|
73
|
+
const sep = existing.length === 0 ? "" : existing.endsWith("\n") ? "" : "\n";
|
|
74
|
+
next = `${existing}${sep}${newLine}\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
writeFileSync(targetPath, next);
|
|
78
|
+
return "wrote";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const generateCommand = defineCommand({
|
|
82
|
+
meta: {
|
|
83
|
+
name: "generate",
|
|
84
|
+
description: "Generate a new EmDash encryption key",
|
|
85
|
+
},
|
|
86
|
+
args: {
|
|
87
|
+
write: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description:
|
|
90
|
+
"Optional path to write the key to (e.g. .dev.vars or .env). " +
|
|
91
|
+
"Won't overwrite an existing entry without --force.",
|
|
92
|
+
},
|
|
93
|
+
force: {
|
|
94
|
+
type: "boolean",
|
|
95
|
+
description: "When used with --write, overwrite an existing entry",
|
|
96
|
+
default: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
run({ args }) {
|
|
100
|
+
const value = generateEncryptionKey();
|
|
101
|
+
|
|
102
|
+
if (args.write) {
|
|
103
|
+
const targetPath = resolve(process.cwd(), args.write);
|
|
104
|
+
const result = writeEncryptionKeyToFile(targetPath, value, args.force);
|
|
105
|
+
if (result === "skipped") {
|
|
106
|
+
// Idempotent no-op: entry already populated. Exit 0 so chained
|
|
107
|
+
// scripts (`emdash secrets generate --write && pnpm dev`) don't
|
|
108
|
+
// break. Pass --force to replace, with full awareness that
|
|
109
|
+
// existing encrypted secrets become unreadable.
|
|
110
|
+
consola.info(
|
|
111
|
+
`${KEY_VAR_NAME} already set in ${pc.cyan(args.write)}; leaving it alone. ` +
|
|
112
|
+
`Pass ${pc.bold("--force")} to replace it.`,
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
consola.log("");
|
|
117
|
+
consola.log(`${pc.bold("Wrote")} ${pc.cyan(KEY_VAR_NAME)} to ${pc.cyan(args.write)}`);
|
|
118
|
+
consola.log("");
|
|
119
|
+
consola.log(
|
|
120
|
+
pc.yellow(
|
|
121
|
+
"Keep this file out of version control. Losing the key means losing every secret encrypted with it.",
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
consola.log("");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Print the key to stdout (one line, no decoration) so it can be
|
|
129
|
+
// piped into env files or secret-management tools without scraping.
|
|
130
|
+
// Explanatory text goes to stderr so it doesn't pollute the pipe.
|
|
131
|
+
process.stdout.write(`${value}\n`);
|
|
132
|
+
const guidance = [
|
|
133
|
+
"",
|
|
134
|
+
pc.bold("EmDash encryption key generated."),
|
|
135
|
+
"",
|
|
136
|
+
`Set ${pc.cyan(KEY_VAR_NAME)} in your environment.`,
|
|
137
|
+
"For Cloudflare deployments, push it to your Worker's secrets.",
|
|
138
|
+
"For Node deployments, add it to your process environment or .env file.",
|
|
139
|
+
"",
|
|
140
|
+
pc.yellow("Keep this value secret. Losing it means losing every secret encrypted with it."),
|
|
141
|
+
"",
|
|
142
|
+
].join("\n");
|
|
143
|
+
process.stderr.write(`${guidance}\n`);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const fingerprintCommand = defineCommand({
|
|
148
|
+
meta: {
|
|
149
|
+
name: "fingerprint",
|
|
150
|
+
description: "Print the kid (8-char fingerprint) for an encryption key",
|
|
151
|
+
},
|
|
152
|
+
args: {
|
|
153
|
+
key: {
|
|
154
|
+
type: "positional",
|
|
155
|
+
description: "The full key value (with the emdash_enc_v1_ prefix)",
|
|
156
|
+
required: true,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
async run({ args }) {
|
|
160
|
+
try {
|
|
161
|
+
const kid = await fingerprintKey(args.key);
|
|
162
|
+
// Newline-only on stdout so it pipes cleanly into env/CI logs
|
|
163
|
+
// without leaking the raw key.
|
|
164
|
+
process.stdout.write(`${kid}\n`);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
consola.error(
|
|
167
|
+
error instanceof EmDashSecretsError ? error.message : "Failed to fingerprint key",
|
|
168
|
+
);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const secretsCommand = defineCommand({
|
|
175
|
+
meta: {
|
|
176
|
+
name: "secrets",
|
|
177
|
+
description: "Manage EmDash secrets (generate, inspect)",
|
|
178
|
+
},
|
|
179
|
+
subCommands: {
|
|
180
|
+
generate: generateCommand,
|
|
181
|
+
fingerprint: fingerprintCommand,
|
|
182
|
+
},
|
|
183
|
+
});
|
package/src/cli/credentials.ts
CHANGED
|
@@ -130,7 +130,7 @@ function readStore(): CredentialStore {
|
|
|
130
130
|
|
|
131
131
|
function writeStore(store: CredentialStore): void {
|
|
132
132
|
const dir = getConfigDir();
|
|
133
|
-
mkdirSync(dir, { recursive: true });
|
|
133
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
134
134
|
|
|
135
135
|
const path = getCredentialPath();
|
|
136
136
|
writeFileSync(path, JSON.stringify(store, null, "\t"), {
|