emdash 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/dist/{adapters-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
  2. package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
  3. package/dist/{apply-BzltprvY.mjs → apply-Ded_1vng.mjs} +167 -254
  4. package/dist/apply-Ded_1vng.mjs.map +1 -0
  5. package/dist/astro/index.d.mts +6 -6
  6. package/dist/astro/index.mjs +10 -2
  7. package/dist/astro/index.mjs.map +1 -1
  8. package/dist/astro/middleware/auth.d.mts +5 -5
  9. package/dist/astro/middleware/auth.mjs +5 -5
  10. package/dist/astro/middleware/redirect.mjs +5 -5
  11. package/dist/astro/middleware/request-context.mjs +4 -4
  12. package/dist/astro/middleware/setup.mjs +1 -1
  13. package/dist/astro/middleware.d.mts.map +1 -1
  14. package/dist/astro/middleware.mjs +94 -43
  15. package/dist/astro/middleware.mjs.map +1 -1
  16. package/dist/astro/types.d.mts +12 -11
  17. package/dist/astro/types.d.mts.map +1 -1
  18. package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
  19. package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
  20. package/dist/{byline-BSaNL1w7.mjs → byline-gFn1r0vA.mjs} +4 -4
  21. package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
  22. package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
  23. package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
  24. package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
  25. package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
  26. package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
  27. package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
  28. package/dist/cli/index.mjs +342 -95
  29. package/dist/cli/index.mjs.map +1 -1
  30. package/dist/client/cf-access.d.mts +1 -1
  31. package/dist/client/index.d.mts +1 -1
  32. package/dist/client/index.mjs +1 -1
  33. package/dist/{config-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
  34. package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
  35. package/dist/{content-8lOYF0pr.mjs → content-CERxPUN0.mjs} +14 -3
  36. package/dist/content-CERxPUN0.mjs.map +1 -0
  37. package/dist/database/instrumentation.d.mts +6 -4
  38. package/dist/database/instrumentation.d.mts.map +1 -1
  39. package/dist/database/instrumentation.mjs +19 -7
  40. package/dist/database/instrumentation.mjs.map +1 -1
  41. package/dist/db/index.d.mts +3 -3
  42. package/dist/db/index.mjs +1 -1
  43. package/dist/db/libsql.d.mts +1 -1
  44. package/dist/db/postgres.d.mts +1 -1
  45. package/dist/db/sqlite.d.mts +1 -1
  46. package/dist/{db-errors-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
  47. package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
  48. package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
  49. package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
  50. package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
  51. package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
  52. package/dist/{index-BFRaVcD6.d.mts → index-Cg-rC4Gj.d.mts} +110 -87
  53. package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
  54. package/dist/index.d.mts +11 -11
  55. package/dist/index.mjs +29 -28
  56. package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
  57. package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
  58. package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
  59. package/dist/loader-ou_PXAjg.mjs.map +1 -0
  60. package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
  61. package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.mjs.map} +1 -1
  62. package/dist/media/index.d.mts +1 -1
  63. package/dist/media/index.mjs +1 -1
  64. package/dist/media/local-runtime.d.mts +7 -7
  65. package/dist/media/local-runtime.mjs +3 -3
  66. package/dist/{media-BW32b4gi.mjs → media-1fFhub9c.mjs} +22 -10
  67. package/dist/media-1fFhub9c.mjs.map +1 -0
  68. package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
  69. package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
  70. package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
  71. package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
  72. package/dist/page/index.d.mts +2 -2
  73. package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
  74. package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
  75. package/dist/{placeholder-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
  76. package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
  77. package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
  78. package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
  79. package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
  80. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  81. package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
  82. package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
  83. package/dist/{query-Cg9ZKRQ0.mjs → query-8c_meo_K.mjs} +13 -13
  84. package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.mjs.map} +1 -1
  85. package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
  86. package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
  87. package/dist/{registry-Dw70ChxB.mjs → registry-Do34mz_P.mjs} +7 -6
  88. package/dist/registry-Do34mz_P.mjs.map +1 -0
  89. package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
  90. package/dist/request-cache-D4I69LeL.mjs.map +1 -0
  91. package/dist/request-context.d.mts +27 -1
  92. package/dist/request-context.d.mts.map +1 -1
  93. package/dist/request-context.mjs +16 -3
  94. package/dist/request-context.mjs.map +1 -1
  95. package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
  96. package/dist/runner-DIcU2UCC.mjs.map +1 -0
  97. package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
  98. package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
  99. package/dist/runtime.d.mts +6 -6
  100. package/dist/runtime.mjs +3 -3
  101. package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
  102. package/dist/search-DuWhx4NG.mjs.map +1 -0
  103. package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
  104. package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
  105. package/dist/seed/index.d.mts +2 -2
  106. package/dist/seed/index.mjs +15 -14
  107. package/dist/seo/index.d.mts +1 -1
  108. package/dist/storage/local.d.mts +1 -1
  109. package/dist/storage/local.mjs +1 -1
  110. package/dist/storage/s3.d.mts +1 -1
  111. package/dist/storage/s3.mjs +1 -1
  112. package/dist/taxonomies-Bw76xAxo.mjs +407 -0
  113. package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
  114. package/dist/taxonomy-D6NvlKo8.mjs +218 -0
  115. package/dist/taxonomy-D6NvlKo8.mjs.map +1 -0
  116. package/dist/{tokens-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
  117. package/dist/{tokens-D7zMmWi2.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
  118. package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
  119. package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
  120. package/dist/{transport-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
  121. package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
  122. package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
  123. package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
  124. package/dist/{types-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
  125. package/dist/types-56BKbld_.mjs.map +1 -0
  126. package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
  127. package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
  128. package/dist/{types-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
  129. package/dist/types-BQx6ZXpR.d.mts.map +1 -0
  130. package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
  131. package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
  132. package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
  133. package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
  134. package/dist/types-DiI8NOG_.mjs +16 -0
  135. package/dist/types-DiI8NOG_.mjs.map +1 -0
  136. package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
  137. package/dist/types-IN5z_S3P.d.mts.map +1 -0
  138. package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
  139. package/dist/types-IZSZfEwv.d.mts.map +1 -0
  140. package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
  141. package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
  142. package/dist/{validate-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
  143. package/dist/validate-CO3JjFV5.d.mts.map +1 -0
  144. package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
  145. package/dist/validate-UK4Ja1uo.mjs.map +1 -0
  146. package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
  147. package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
  148. package/dist/version-Bg31I_Ff.mjs +7 -0
  149. package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
  150. package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
  151. package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
  152. package/package.json +9 -8
  153. package/src/api/errors.ts +5 -0
  154. package/src/api/handlers/content.ts +20 -0
  155. package/src/api/handlers/dashboard.ts +29 -36
  156. package/src/api/handlers/media-allowlist.ts +40 -0
  157. package/src/api/handlers/media.ts +1 -1
  158. package/src/api/handlers/menus.ts +400 -89
  159. package/src/api/handlers/taxonomies.ts +273 -97
  160. package/src/api/handlers/validate-media-fields.ts +125 -0
  161. package/src/api/schemas/common.ts +7 -0
  162. package/src/api/schemas/media.ts +23 -3
  163. package/src/api/schemas/menus.ts +23 -0
  164. package/src/api/schemas/schema.ts +11 -2
  165. package/src/api/schemas/taxonomies.ts +39 -0
  166. package/src/astro/integration/routes.ts +10 -0
  167. package/src/astro/middleware.ts +46 -11
  168. package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
  169. package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
  170. package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
  171. package/src/astro/routes/api/media/upload-url.ts +10 -4
  172. package/src/astro/routes/api/media.ts +12 -4
  173. package/src/astro/routes/api/menus/[name]/items.ts +16 -6
  174. package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
  175. package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
  176. package/src/astro/routes/api/menus/[name].ts +19 -10
  177. package/src/astro/routes/api/menus/index.ts +9 -6
  178. package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
  179. package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
  180. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
  181. package/src/astro/routes/api/taxonomies/index.ts +9 -6
  182. package/src/astro/types.ts +5 -1
  183. package/src/auth/rate-limit.ts +3 -3
  184. package/src/cli/commands/bundle-utils.ts +81 -6
  185. package/src/cli/commands/bundle.ts +18 -15
  186. package/src/cli/commands/export-seed.ts +139 -24
  187. package/src/cli/commands/plugin-init.ts +216 -90
  188. package/src/database/instrumentation.ts +22 -8
  189. package/src/database/migrations/016_api_tokens.ts +18 -3
  190. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
  191. package/src/database/migrations/037_credential_algorithm.ts +18 -0
  192. package/src/database/migrations/runner.ts +4 -0
  193. package/src/database/repositories/content.ts +11 -0
  194. package/src/database/repositories/media.ts +40 -10
  195. package/src/database/repositories/taxonomy.ts +193 -89
  196. package/src/database/types.ts +12 -3
  197. package/src/emdash-runtime.ts +16 -3
  198. package/src/fields/file.ts +7 -6
  199. package/src/fields/image.ts +12 -11
  200. package/src/fields/types.ts +3 -0
  201. package/src/i18n/resolve.ts +37 -0
  202. package/src/index.ts +1 -1
  203. package/src/loader.ts +49 -2
  204. package/src/mcp/server.ts +114 -26
  205. package/src/media/mime.ts +75 -0
  206. package/src/menus/index.ts +143 -124
  207. package/src/menus/types.ts +15 -1
  208. package/src/plugins/types.ts +81 -191
  209. package/src/request-cache.ts +6 -2
  210. package/src/request-context.ts +42 -2
  211. package/src/schema/registry.ts +5 -5
  212. package/src/schema/types.ts +3 -2
  213. package/src/schema/zod-generator.ts +12 -2
  214. package/src/seed/apply.ts +157 -54
  215. package/src/seed/types.ts +18 -1
  216. package/src/seed/validate.ts +27 -13
  217. package/src/taxonomies/index.ts +230 -213
  218. package/src/taxonomies/types.ts +10 -0
  219. package/dist/apply-BzltprvY.mjs.map +0 -1
  220. package/dist/content-8lOYF0pr.mjs.map +0 -1
  221. package/dist/index-BFRaVcD6.d.mts.map +0 -1
  222. package/dist/loader-CKLbBnhK.mjs.map +0 -1
  223. package/dist/media-BW32b4gi.mjs.map +0 -1
  224. package/dist/registry-Dw70ChxB.mjs.map +0 -1
  225. package/dist/request-cache-B-bmkipQ.mjs.map +0 -1
  226. package/dist/runner-C7ADox5q.mjs.map +0 -1
  227. package/dist/search-dOGEccMa.mjs.map +0 -1
  228. package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
  229. package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
  230. package/dist/types-4fVtCIm0.mjs +0 -68
  231. package/dist/types-4fVtCIm0.mjs.map +0 -1
  232. package/dist/types-BSyXeCFW.d.mts.map +0 -1
  233. package/dist/types-BuBIptGk.d.mts.map +0 -1
  234. package/dist/types-CIOg5AR8.mjs.map +0 -1
  235. package/dist/types-CrtWgIvl.d.mts.map +0 -1
  236. package/dist/validate-Baqf0slj.mjs.map +0 -1
  237. package/dist/validate-BfQh_C_y.d.mts.map +0 -1
  238. package/dist/version-DoxrVdYf.mjs +0 -7
  239. package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * emdash plugin init
3
3
  *
4
- * Scaffold a new EmDash plugin. Generates the standard-format boilerplate:
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 instead (createPlugin + React admin).
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: "Generate native-format plugin (createPlugin + React admin)",
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
- const isNative = args.native;
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" : "standard"} plugin: ${pluginName}`);
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
- if (args.dir !== ".") {
87
- consola.info(` 1. cd ${args.dir}`);
88
- }
89
- consola.info(` ${args.dir !== "." ? "2" : "1"}. pnpm install`);
90
- if (isNative) {
91
- consola.info(` ${args.dir !== "." ? "3" : "2"}. Edit src/index.ts to add hooks and routes`);
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.info(
94
- ` ${args.dir !== "." ? "3" : "2"}. Edit src/sandbox-entry.ts to add hooks and routes`,
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
- consola.info(` ${args.dir !== "." ? "4" : "3"}. emdash plugin validate --dir .`);
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
- // ── Standard format scaffolding ──────────────────────────────────
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
- // Derive the camelCase function name from slug
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
- ".": "./src/index.ts",
125
- "./sandbox": "./src/sandbox-entry.ts",
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: ["src"],
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
- null,
133
- "\t",
134
- ) + "\n",
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
- // src/index.ts -- descriptor factory
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: "${pluginName}",
252
+ \t\tid: "${slug}",
168
253
  \t\tversion: "0.1.0",
169
254
  \t\tformat: "standard",
170
255
  \t\tentrypoint: "${pluginName}/sandbox",
171
- \t\tcapabilities: [],
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: any, ctx: PluginContext) => {
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
- .split("-")
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
- ".": "./src/index.ts",
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
- files: ["src"],
342
+ keywords: ["emdash", "emdash-plugin"],
343
+ license: "MIT",
224
344
  peerDependencies: {
225
345
  emdash: "*",
226
346
  },
227
- },
228
- null,
229
- "\t",
230
- ) + "\n",
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
- // src/index.ts -- descriptor + createPlugin
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 function ${fnName}Plugin(): PluginDescriptor {
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: "${pluginName}",
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: "${pluginName}",
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\tctx.log.info("Content saved", {
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\tid: event.content.id,
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
 
@@ -83,16 +83,30 @@ export function isInstrumentationEnabled(): boolean {
83
83
 
84
84
  function kyselyLog(event: LogEvent): void {
85
85
  if (event.level !== "query") return;
86
- const rec = getRequestContext()?.queryRecorder;
87
- if (!rec) return;
88
- recordEvent(rec, event.query.sql, event.query.parameters, event.queryDurationMillis);
86
+ const ctx = getRequestContext();
87
+ if (!ctx) return;
88
+ const dur = event.queryDurationMillis;
89
+ if (ctx.metrics) {
90
+ const m = ctx.metrics;
91
+ m.dbCount += 1;
92
+ m.dbTotalMs += dur;
93
+ const finishedAt = performance.now() - m.start;
94
+ const startedAt = finishedAt - dur;
95
+ if (m.dbFirstOffset === null) m.dbFirstOffset = startedAt;
96
+ m.dbLastOffset = finishedAt;
97
+ }
98
+ if (ctx.queryRecorder) {
99
+ recordEvent(ctx.queryRecorder, event.query.sql, event.query.parameters, dur);
100
+ }
89
101
  }
90
102
 
91
103
  /**
92
- * Returns a Kysely `log` option when instrumentation is enabled, or undefined.
93
- * Pass as `new Kysely({ dialect, log: kyselyLogOption() })` so disabled mode
94
- * has zero overhead Kysely skips query timing entirely when `log` is absent.
104
+ * Returns a Kysely `log` callback. Always returns a function so per-request
105
+ * counters (db.count, db.total, db.first, db.last) and the optional NDJSON
106
+ * recorder both get fed. The cost over the previous "undefined when off"
107
+ * behaviour is one `performance.now()` pair per query inside Kysely, which
108
+ * is in the noise compared to any real query.
95
109
  */
96
- export function kyselyLogOption(): Logger | undefined {
97
- return isInstrumentationEnabled() ? kyselyLog : undefined;
110
+ export function kyselyLogOption(): Logger {
111
+ return kyselyLog;
98
112
  }
@@ -9,11 +9,20 @@ import { currentTimestamp } from "../dialect-helpers.js";
9
9
  * 1. _emdash_api_tokens — Personal Access Tokens (ec_pat_...)
10
10
  * 2. _emdash_oauth_tokens — OAuth access/refresh tokens (ec_oat_/ec_ort_...)
11
11
  * 3. _emdash_device_codes — OAuth Device Flow state (RFC 8628)
12
+ *
13
+ * Every CREATE is guarded with `.ifNotExists()` so the migration is safe to
14
+ * re-run against a partially-applied schema. See #954 for the failure mode:
15
+ * if `up()` crashes mid-way (D1 subrequest limit, isolate cancellation,
16
+ * transient connection error), the migration record never gets inserted
17
+ * into `_emdash_migrations`, and the next request retries `up()` from the
18
+ * top. Without these guards, the retry crashed with `table ... already
19
+ * exists` and blocked every subsequent boot of the Worker.
12
20
  */
13
21
  export async function up(db: Kysely<unknown>): Promise<void> {
14
22
  // ── Personal Access Tokens ───────────────────────────────────────
15
23
  await db.schema
16
24
  .createTable("_emdash_api_tokens")
25
+ .ifNotExists()
17
26
  .addColumn("id", "text", (col) => col.primaryKey())
18
27
  .addColumn("name", "text", (col) => col.notNull())
19
28
  .addColumn("token_hash", "text", (col) => col.notNull().unique())
@@ -30,12 +39,14 @@ export async function up(db: Kysely<unknown>): Promise<void> {
30
39
 
31
40
  await db.schema
32
41
  .createIndex("idx_api_tokens_token_hash")
42
+ .ifNotExists()
33
43
  .on("_emdash_api_tokens")
34
44
  .column("token_hash")
35
45
  .execute();
36
46
 
37
47
  await db.schema
38
48
  .createIndex("idx_api_tokens_user_id")
49
+ .ifNotExists()
39
50
  .on("_emdash_api_tokens")
40
51
  .column("user_id")
41
52
  .execute();
@@ -43,6 +54,7 @@ export async function up(db: Kysely<unknown>): Promise<void> {
43
54
  // ── OAuth Tokens ─────────────────────────────────────────────────
44
55
  await db.schema
45
56
  .createTable("_emdash_oauth_tokens")
57
+ .ifNotExists()
46
58
  .addColumn("token_hash", "text", (col) => col.primaryKey())
47
59
  .addColumn("token_type", "text", (col) => col.notNull()) // 'access' | 'refresh'
48
60
  .addColumn("user_id", "text", (col) => col.notNull())
@@ -58,12 +70,14 @@ export async function up(db: Kysely<unknown>): Promise<void> {
58
70
 
59
71
  await db.schema
60
72
  .createIndex("idx_oauth_tokens_user_id")
73
+ .ifNotExists()
61
74
  .on("_emdash_oauth_tokens")
62
75
  .column("user_id")
63
76
  .execute();
64
77
 
65
78
  await db.schema
66
79
  .createIndex("idx_oauth_tokens_expires")
80
+ .ifNotExists()
67
81
  .on("_emdash_oauth_tokens")
68
82
  .column("expires_at")
69
83
  .execute();
@@ -71,6 +85,7 @@ export async function up(db: Kysely<unknown>): Promise<void> {
71
85
  // ── Device Codes (OAuth Device Flow, RFC 8628) ───────────────────
72
86
  await db.schema
73
87
  .createTable("_emdash_device_codes")
88
+ .ifNotExists()
74
89
  .addColumn("device_code", "text", (col) => col.primaryKey())
75
90
  .addColumn("user_code", "text", (col) => col.notNull().unique())
76
91
  .addColumn("scopes", "text", (col) => col.notNull()) // JSON array
@@ -83,7 +98,7 @@ export async function up(db: Kysely<unknown>): Promise<void> {
83
98
  }
84
99
 
85
100
  export async function down(db: Kysely<unknown>): Promise<void> {
86
- await db.schema.dropTable("_emdash_device_codes").execute();
87
- await db.schema.dropTable("_emdash_oauth_tokens").execute();
88
- await db.schema.dropTable("_emdash_api_tokens").execute();
101
+ await db.schema.dropTable("_emdash_device_codes").ifExists().execute();
102
+ await db.schema.dropTable("_emdash_oauth_tokens").ifExists().execute();
103
+ await db.schema.dropTable("_emdash_api_tokens").ifExists().execute();
89
104
  }