@samva/cli 0.18.0 → 0.19.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 (3) hide show
  1. package/AGENTS.md +8 -0
  2. package/dist/bin.mjs +295 -32
  3. package/package.json +9 -9
package/AGENTS.md CHANGED
@@ -79,6 +79,14 @@ uploads again. `--dry-run` lists each attachment and uploads nothing.
79
79
  or the newest publication that still validates input identically. They are mutually exclusive and
80
80
  each requires `--template-id`, because a reference is a statement about one template's history.
81
81
 
82
+ `samva templates list --shared true|false` filters by whether the template's project is shared with
83
+ every namespace, and every listed template reports `shared`. Sharing belongs to an organization-level
84
+ project: `templates create --shared` shares the new project and refuses a namespace (exit `2`), and
85
+ `templates update <id> --data '{}' --shared true|false` shares or unshares the project for every
86
+ entry in it. `--shared` that disagrees with `shared` in `--data` exits `2`. A namespace-scoped key
87
+ lists, reads, and sends shared templates but gets 403 changing them, and the API refuses moving a
88
+ shared project into a namespace.
89
+
82
90
  `samva templates workspace <open|diff|patch|add-asset|check|fixture|save|history|publish>` is the
83
91
  hosted project workspace over the CLI, for a caller with no clone: an agent in a sandbox, a CI job,
84
92
  a script. It reaches the same operations the hosted MCP tools reach, so an agent can start a journey in one
package/dist/bin.mjs CHANGED
@@ -14106,7 +14106,7 @@ var layer12 = /* @__PURE__ */ layer.pipe(/* @__PURE__ */ provideMerge(/* @__PURE
14106
14106
  // package.json
14107
14107
  var package_default = {
14108
14108
  name: "@samva/cli",
14109
- version: "0.18.0",
14109
+ version: "0.19.0",
14110
14110
  description: "Samva CLI \u2014 email and platform operations from your terminal",
14111
14111
  keywords: [
14112
14112
  "cli",
@@ -14157,21 +14157,21 @@ var package_default = {
14157
14157
  "@samva/contracts": "workspace:*",
14158
14158
  "@samva/core": "workspace:*",
14159
14159
  "@samva/mailbox": "workspace:*",
14160
- "@samva/markup": "^0.13.1",
14161
- "@samva/vite": "^0.13.1",
14160
+ "@samva/markup": "catalog:",
14161
+ "@samva/vite": "catalog:",
14162
14162
  "@types/node": "catalog:",
14163
14163
  "better-auth": "catalog:",
14164
14164
  effect: "catalog:effect",
14165
- samva: "workspace:0.21.0",
14165
+ samva: "workspace:0.22.0",
14166
14166
  typescript: "catalog:",
14167
14167
  vite: "~8.3.0",
14168
14168
  vitest: "catalog:"
14169
14169
  },
14170
14170
  optionalDependencies: {
14171
- "@samva/cli-darwin-arm64": "workspace:0.18.0",
14172
- "@samva/cli-darwin-x64": "workspace:0.18.0",
14173
- "@samva/cli-linux-arm64": "workspace:0.18.0",
14174
- "@samva/cli-linux-x64": "workspace:0.18.0"
14171
+ "@samva/cli-darwin-arm64": "workspace:0.19.0",
14172
+ "@samva/cli-darwin-x64": "workspace:0.19.0",
14173
+ "@samva/cli-linux-arm64": "workspace:0.19.0",
14174
+ "@samva/cli-linux-x64": "workspace:0.19.0"
14175
14175
  },
14176
14176
  engines: {
14177
14177
  node: ">=18"
@@ -24189,6 +24189,9 @@ var CreateWebhookPayload = /* @__PURE__ */ Struct({
24189
24189
  Literal2("webhook.test"),
24190
24190
  Literal2("domain.updated"),
24191
24191
  Literal2("domain.warming.updated"),
24192
+ Literal2("template.registration.updated"),
24193
+ Literal2("flow.registration.updated"),
24194
+ Literal2("whatsapp.number.connected"),
24192
24195
  Literal2("mailbox.message.received"),
24193
24196
  Literal2("mailbox.message.sent"),
24194
24197
  Literal2("mailbox.action.pending_approval"),
@@ -24233,6 +24236,9 @@ var UpdateWebhookPayload = /* @__PURE__ */ Struct({
24233
24236
  Literal2("webhook.test"),
24234
24237
  Literal2("domain.updated"),
24235
24238
  Literal2("domain.warming.updated"),
24239
+ Literal2("template.registration.updated"),
24240
+ Literal2("flow.registration.updated"),
24241
+ Literal2("whatsapp.number.connected"),
24236
24242
  Literal2("mailbox.message.received"),
24237
24243
  Literal2("mailbox.message.sent"),
24238
24244
  Literal2("mailbox.action.pending_approval"),
@@ -25093,7 +25099,7 @@ var __exportAll = (all, no_symbols) => {
25093
25099
  };
25094
25100
 
25095
25101
  // ../sdk/dist/package.js
25096
- var version = "0.21.0";
25102
+ var version = "0.22.0";
25097
25103
 
25098
25104
  // ../sdk/dist/client-attribution.js
25099
25105
  var SDK_VERSION = version;
@@ -25432,6 +25438,7 @@ var commandTree = {
25432
25438
  list: {},
25433
25439
  get: {},
25434
25440
  create: {},
25441
+ "from-website": {},
25435
25442
  update: {},
25436
25443
  remove: {}
25437
25444
  },
@@ -27062,6 +27069,7 @@ var SDK_OPERATION_CALLS = {
27062
27069
  },
27063
27070
  templatesList: { kind: "params" },
27064
27071
  templatesCreate: { kind: "payload" },
27072
+ templatesCreateFromWebsite: { kind: "payload" },
27065
27073
  templatesAddEntry: {
27066
27074
  kind: "pathPayload",
27067
27075
  paths: ["id"]
@@ -28553,6 +28561,7 @@ var SamvaOperations = {
28553
28561
  templatesAttachFamilyMember: operationMetadata85,
28554
28562
  templatesDetachFamilyMember: operationMetadata87,
28555
28563
  templatesCreateFamilyFromTemplate: operationMetadata85,
28564
+ templatesCreateFromWebsite: operationMetadata85,
28556
28565
  templatesAddEntry: operationMetadata85,
28557
28566
  unsubscribeList: operationMetadata90,
28558
28567
  unsubscribeCreate: operationMetadata91,
@@ -29451,6 +29460,10 @@ var apiKeysCommand = make29("api-keys").pipe(withDescription3("Create, inspect,
29451
29460
 
29452
29461
  // src/commands/brands.ts
29453
29462
  import { join as join3, relative, resolve as resolve5 } from "path";
29463
+ // ../../node_modules/.bun/@samva+markup@0.15.0+a19bcd7db259aaf7/node_modules/@samva/markup/dist/dark-scheme-CbCwGDWH.js
29464
+ var DARK_SCHEME_QUERY = /^(?:only\s+)?(?:screen\s+and\s+)?\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
29465
+ var isDarkSchemeQuery = (prelude) => DARK_SCHEME_QUERY.test(prelude.trim());
29466
+
29454
29467
  // ../../node_modules/.bun/css-tree@3.2.1/node_modules/css-tree/lib/tokenizer/types.js
29455
29468
  var EOF = 0;
29456
29469
  var Ident = 1;
@@ -53223,21 +53236,21 @@ var $ = (e) => {
53223
53236
  };
53224
53237
  var ae = { ok: l, isOk: u, err: d, isError: f, try: T, tryPromise: E, map: D, mapError: O, tryRecover: k, andThen: A, tryRecoverAsync: ee, andThenAsync: te, match: ne, tap: j, tapAsync: M, tapError: N6, tapErrorAsync: P, tapBoth: F4, tapBothAsync: I, unwrap: L, unwrapOr: z, gen: B, await: V, codec: re, all: Q, allAsync: ie, partition: $, partitionAsync: async (e) => $(await Z(e, `Result.partitionAsync input promise rejected`)), flatten: (e) => e.status === `ok` ? e.value : e };
53225
53238
 
53226
- // ../../node_modules/.bun/@samva+markup@0.13.1+a19bcd7db259aaf7/node_modules/@samva/markup/dist/theme-css-CEUrDpqq.js
53239
+ // ../../node_modules/.bun/@samva+markup@0.15.0+a19bcd7db259aaf7/node_modules/@samva/markup/dist/theme-css-s9QwD5Ko.js
53227
53240
  var BRAND_NAMESPACES = [
53228
53241
  "--color-",
53229
53242
  "--font-",
53230
53243
  "--text-",
53231
53244
  "--leading-",
53232
53245
  "--tracking-",
53233
- "--radius-",
53246
+ "--radius",
53234
53247
  "--spacing",
53235
53248
  "--container-",
53236
53249
  "--shadow-"
53237
53250
  ];
53251
+ var inNamespace = (property, namespace) => namespace.endsWith("-") ? property.startsWith(namespace) : property === namespace || property.startsWith(`${namespace}-`);
53238
53252
  var BRAND_PROPERTY = /^--[a-z][a-z0-9-]*$/;
53239
53253
  var BRAND_UNSAFE_VALUE = /[{};<>\\@]|url\s*\(|expression\s*\(|\/\*/i;
53240
- var BRAND_DARK_MEDIA = /^\(\s*prefers-color-scheme\s*:\s*dark\s*\)$/i;
53241
53254
  var BRAND_FONT_DESCRIPTORS = /* @__PURE__ */ new Set([
53242
53255
  "font-family",
53243
53256
  "src",
@@ -53246,7 +53259,6 @@ var BRAND_FONT_DESCRIPTORS = /* @__PURE__ */ new Set([
53246
53259
  "font-display",
53247
53260
  "unicode-range"
53248
53261
  ]);
53249
- var darkVariableName = (property) => `${property}-dark`;
53250
53262
  var spanOfNode = (node) => ({
53251
53263
  start: node.loc?.start.offset ?? 0,
53252
53264
  end: node.loc?.end.offset ?? 0
@@ -53256,6 +53268,7 @@ var unquoteFamily = (value) => {
53256
53268
  const quoted = /^(['"])(.*)\1$/s.exec(trimmed);
53257
53269
  return quoted === null ? trimmed : quoted[2].replace(/\\(.)/g, "$1");
53258
53270
  };
53271
+ var quoteFamily = (family) => `"${family.replace(/["\\]/g, "\\$&")}"`;
53259
53272
  var parseBrandCss = (css) => {
53260
53273
  const diagnostics = [];
53261
53274
  const report = (code, message, span) => {
@@ -53297,7 +53310,7 @@ var parseBrandCss = (css) => {
53297
53310
  report("brand-css-property", `${property} is not a theme variable; name theme variables --namespace-name.`, span);
53298
53311
  continue;
53299
53312
  }
53300
- if (!BRAND_NAMESPACES.some((namespace) => property.startsWith(namespace))) {
53313
+ if (!BRAND_NAMESPACES.some((namespace) => inNamespace(property, namespace))) {
53301
53314
  report("brand-css-namespace", `${property} is outside the brand namespaces (${BRAND_NAMESPACES.map((namespace) => namespace.endsWith("-") ? `${namespace}*` : namespace).join(", ")}).`, span);
53302
53315
  continue;
53303
53316
  }
@@ -53393,7 +53406,7 @@ var parseBrandCss = (css) => {
53393
53406
  readFontFace(node);
53394
53407
  continue;
53395
53408
  }
53396
- if (name === "media" && node.prelude !== null && BRAND_DARK_MEDIA.test(generate55(node.prelude).trim())) {
53409
+ if (name === "media" && node.prelude !== null && isDarkSchemeQuery(generate55(node.prelude))) {
53397
53410
  for (const child of node.block.children)
53398
53411
  if (child.type === "Atrule" && child.name.toLowerCase() === "theme")
53399
53412
  readTheme(child, dark, "dark");
@@ -53404,19 +53417,103 @@ var parseBrandCss = (css) => {
53404
53417
  }
53405
53418
  report("brand-css-unsupported", "A brand theme contains only @theme, a dark-mode @theme (@media (prefers-color-scheme: dark)), and @font-face.", spanOfNode(node));
53406
53419
  }
53407
- for (const property of Object.keys(dark))
53408
- if (darkVariableName(property) in variables)
53409
- report("brand-css-dark-conflict", `${darkVariableName(property)} and the dark-mode ${property} name the same theme variable; keep one.`, {
53410
- start: 0,
53411
- end: 0
53412
- });
53413
53420
  return diagnostics.length > 0 ? ae.err(diagnostics) : ae.ok({
53414
53421
  variables,
53415
53422
  dark,
53416
53423
  fontFaces
53417
53424
  });
53418
53425
  };
53419
- // ../../node_modules/.bun/@samva+markup@0.13.1+a19bcd7db259aaf7/node_modules/@samva/markup/dist/diagnostic-codes-DdZ4tka5.js
53426
+ var printDeclarations = (entries, indent) => Object.entries(entries).map(([property, value]) => `${indent}${property}: ${value};`).join(`
53427
+ `);
53428
+ var printFontFace = (face) => [
53429
+ "@font-face {",
53430
+ ` font-family: ${quoteFamily(face.family)};`,
53431
+ ` src: ${face.src};`,
53432
+ ...face.weight === undefined ? [] : [` font-weight: ${face.weight};`],
53433
+ ...face.style === undefined ? [] : [` font-style: ${face.style};`],
53434
+ ...face.display === undefined ? [] : [` font-display: ${face.display};`],
53435
+ ...face.unicodeRange === undefined ? [] : [` unicode-range: ${face.unicodeRange};`],
53436
+ "}"
53437
+ ].join(`
53438
+ `);
53439
+ var printBrandCss = (brand) => {
53440
+ const blocks = [
53441
+ ...brand.fontFaces.map(printFontFace),
53442
+ ...Object.keys(brand.variables).length === 0 ? [] : [`@theme {
53443
+ ${printDeclarations(brand.variables, " ")}
53444
+ }`],
53445
+ ...Object.keys(brand.dark).length === 0 ? [] : [`@media (prefers-color-scheme: dark) {
53446
+ @theme {
53447
+ ${printDeclarations(brand.dark, " ")}
53448
+ }
53449
+ }`]
53450
+ ];
53451
+ return blocks.length === 0 ? "" : `${blocks.join(`
53452
+
53453
+ `)}
53454
+ `;
53455
+ };
53456
+ var THEME_COLOR_ROLES = {
53457
+ background: "The page, behind everything.",
53458
+ foreground: "Text on the page.",
53459
+ card: "A surface that groups the message's content.",
53460
+ "card-foreground": "Text on a card.",
53461
+ primary: "The main action's fill (a button), and links.",
53462
+ "primary-foreground": "Text on a primary fill.",
53463
+ secondary: "A quieter action's fill.",
53464
+ "secondary-foreground": "Text on a secondary fill.",
53465
+ muted: "A subdued surface: a code block, a table header.",
53466
+ "muted-foreground": "Secondary text on the page, a card or a muted surface: labels, captions, the footer.",
53467
+ accent: "A highlighted surface: a callout, a selected row.",
53468
+ "accent-foreground": "Text on an accent surface.",
53469
+ border: "Rules, dividers and outlines."
53470
+ };
53471
+ var THEME_COLORS = Object.keys(THEME_COLOR_ROLES);
53472
+ var colorVariables = (colors) => Object.fromEntries(THEME_COLORS.map((name) => [`--color-${name}`, colors[name]]));
53473
+ var DEFAULT_THEME = {
53474
+ variables: {
53475
+ ...colorVariables({
53476
+ background: "#ffffff",
53477
+ foreground: "#0a0a0a",
53478
+ card: "#ffffff",
53479
+ "card-foreground": "#0a0a0a",
53480
+ primary: "#171717",
53481
+ "primary-foreground": "#fafafa",
53482
+ secondary: "#f5f5f5",
53483
+ "secondary-foreground": "#171717",
53484
+ muted: "#f5f5f5",
53485
+ "muted-foreground": "#737373",
53486
+ accent: "#f5f5f5",
53487
+ "accent-foreground": "#171717",
53488
+ border: "#e5e5e5"
53489
+ }),
53490
+ "--radius": "8px",
53491
+ "--radius-sm": "calc(var(--radius) - 4px)",
53492
+ "--radius-md": "calc(var(--radius) - 2px)",
53493
+ "--radius-lg": "var(--radius)",
53494
+ "--radius-xl": "calc(var(--radius) + 4px)",
53495
+ "--font-sans": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
53496
+ "--font-heading": "var(--font-sans)"
53497
+ },
53498
+ dark: colorVariables({
53499
+ background: "#0a0a0a",
53500
+ foreground: "#fafafa",
53501
+ card: "#171717",
53502
+ "card-foreground": "#fafafa",
53503
+ primary: "#fafafa",
53504
+ "primary-foreground": "#171717",
53505
+ secondary: "#262626",
53506
+ "secondary-foreground": "#fafafa",
53507
+ muted: "#262626",
53508
+ "muted-foreground": "#a3a3a3",
53509
+ accent: "#262626",
53510
+ "accent-foreground": "#fafafa",
53511
+ border: "#2e2e2e"
53512
+ }),
53513
+ fontFaces: []
53514
+ };
53515
+ var DEFAULT_THEME_CSS = printBrandCss(DEFAULT_THEME);
53516
+ // ../../node_modules/.bun/@samva+markup@0.15.0+a19bcd7db259aaf7/node_modules/@samva/markup/dist/diagnostic-codes-UXwOO3A-.js
53420
53517
  var buildLineMap = (source) => {
53421
53518
  const lineStarts = [0];
53422
53519
  for (let index = 0;index < source.length; index++) {
@@ -53641,10 +53738,22 @@ var DIAGNOSTIC_CODES = codes2({
53641
53738
  summary: "<Columns per> is not a positive integer literal.",
53642
53739
  fix: "Write per={2}."
53643
53740
  },
53741
+ "invalid-columns-gap": {
53742
+ summary: "<Columns gap> is set on a Columns whose cells are repeated with each or rendered conditionally.",
53743
+ fix: "Write each Column out unconditionally, or pad the cells instead of using gap."
53744
+ },
53745
+ "invalid-columns-stack": {
53746
+ summary: "<Columns stack> is not a true or false literal.",
53747
+ fix: "Write stack={false}, or leave it out to stack on narrow screens."
53748
+ },
53644
53749
  "invalid-button-number": {
53645
53750
  summary: "A Button size prop is not a finite number.",
53646
53751
  fix: "Pass a finite number."
53647
53752
  },
53753
+ "invalid-number-prop": {
53754
+ summary: "A primitive's numeric prop, such as <Email gutter>, is not a finite number in range.",
53755
+ fix: "Pass a finite number the message allows, for example gutter={16}."
53756
+ },
53648
53757
  "unsupported-element": {
53649
53758
  summary: "An element outside the supported email vocabulary.",
53650
53759
  fix: "Use an element from the contract's element table."
@@ -53745,6 +53854,18 @@ var DIAGNOSTIC_CODES = codes2({
53745
53854
  summary: "A custom property is redefined inside a conditional at-rule.",
53746
53855
  fix: "Keep the override on a class rule."
53747
53856
  },
53857
+ "email-contrast": {
53858
+ summary: "Text, or a filled button against its surroundings, falls below the contrast ratio in light or dark mode (below 1.5:1 a button is indistinguishable from its surface; WCAG 1.4.11 asks 3:1).",
53859
+ fix: "Pair background and text tokens (bg-primary with text-primary-foreground), give the theme's dark-mode @theme a value for the token, or choose colors that reach the required ratio.",
53860
+ upgrade: [
53861
+ "Move the project's theme onto the theme vocabulary with one dark-mode block. In starter.css and theme.css, rename each color token to the vocabulary name for its role (reference.md, Theme): a page color to --color-background, the main text to --color-foreground, a surface to --color-card with --color-card-foreground, the brand or action color to --color-primary with --color-primary-foreground, subdued text to --color-muted-foreground, a highlight to --color-accent with --color-accent-foreground, rules to --color-border, body and heading fonts to --font-sans and --font-heading, and a corner radius to --radius. Keep every value.",
53862
+ "Move each --color-<name>-dark value into @media (prefers-color-scheme: dark) { @theme { --color-<name>: <value>; } } under the renamed token, and delete the -dark tokens. In every template and partial, rename the utilities to match (bg-surface to bg-card, text-muted to text-muted-foreground, and so on) and delete each dark:\u2026-dark class whose token now has a dark value; keep a dark: class only where it picks a different token in dark mode.",
53863
+ "Then fix what email-contrast still reports at its location: pair each fill with its -foreground token, or give the dark block a value that reaches the ratio. Do not weaken the design to pass: keep the brand's colors and adjust the paired token.",
53864
+ "Compile every entry and render every fixture in light and dark mode; confirm the light render is unchanged apart from fixes. Resolve errors before requesting one saved version for the whole project, approved by the user; do not publish."
53865
+ ].join(`
53866
+
53867
+ `)
53868
+ },
53748
53869
  "email-font-fallback": {
53749
53870
  summary: "A font stack names a project font without a generic fallback.",
53750
53871
  fix: "End the stack with sans-serif, serif or monospace."
@@ -54689,10 +54810,10 @@ var listRecipients = /* @__PURE__ */ make42("campaignsListRecipients");
54689
54810
 
54690
54811
  // src/lib/run-hold.ts
54691
54812
  var WHATSAPP_MESSAGING_LIMIT_HOLD = "whatsapp_messaging_limit";
54692
- var holdLabel = (reason) => reason === null || reason === undefined ? null : reason === WHATSAPP_MESSAGING_LIMIT_HOLD ? `${reason} (the number's messaging limit; the run resumes on its own)` : reason;
54813
+ var holdLabel = (reason) => reason === null || reason === undefined ? null : reason === WHATSAPP_MESSAGING_LIMIT_HOLD ? `${reason} (the messaging limit its business portfolio shares across numbers; the run resumes on its own, sooner if Meta raises the limit)` : reason;
54693
54814
  var holdFields = (run) => [
54694
54815
  ["Held", holdLabel(run.holdReason)],
54695
- ["Resumes at", run.holdUntil]
54816
+ ["Resumes by", run.holdUntil]
54696
54817
  ];
54697
54818
  var campaignPacingFields = (run, options) => [
54698
54819
  ...holdFields(run),
@@ -74045,6 +74166,14 @@ var TemplatePage = /* @__PURE__ */ Struct({
74045
74166
  items: ArraySchema(TemplateSummary),
74046
74167
  nextCursor: Union2([String4, Null2])
74047
74168
  }).annotate({ identifier: "TemplatePage" });
74169
+ var TemplateFromWebsite = /* @__PURE__ */ Struct({
74170
+ template: Template,
74171
+ website: Struct({
74172
+ url: String4.annotate({ description: "The address Samva checked, normalized." }),
74173
+ host: String4
74174
+ }),
74175
+ ask: String4.annotate({ description: "The request to send the template assistant in the editor. The assistant reads the site, rebuilds the project's theme and blocks to match it, and proposes a brand that changes nothing until the customer applies it." })
74176
+ }).annotate({ identifier: "TemplateFromWebsite" });
74048
74177
  var TemplateFamilyPage = /* @__PURE__ */ Struct({
74049
74178
  items: ArraySchema(TemplateFamily),
74050
74179
  nextCursor: Union2([String4, Null2])
@@ -75692,6 +75821,20 @@ var TemplatesCreateFamilyFromTemplate409 = ConflictError2;
75692
75821
  var TemplatesCreateFamilyFromTemplate422 = ValidationError2;
75693
75822
  var TemplatesCreateFamilyFromTemplate500 = InternalError2;
75694
75823
  var TemplatesCreateFamilyFromTemplate504 = GatewayTimeoutError2;
75824
+ var TemplatesCreateFromWebsiteRequestJson = /* @__PURE__ */ Struct({
75825
+ url: String4.annotate({ description: "The website whose look the template should match: a public https address, or a bare domain such as `example.com`." }).check(isMinLength(1).annotate({ expected: "a value with a length of at least 1" })).check(isMaxCodePoints(2048).annotate({ expected: "a string with at most 2048 code points" })).check(isMaxLength(2048).annotate({ expected: "a value with a length of at most 2048" })),
75826
+ name: optionalKey2(Union2([String4.check(isMinLength(1).annotate({ expected: "a value with a length of at least 1" })).check(isMaxCodePoints(200).annotate({ expected: "a string with at most 200 code points" })).check(isMaxLength(200).annotate({ expected: "a value with a length of at most 200" })), Null2])),
75827
+ namespaceId: optionalKey2(Union2([NamespaceId2, Null2]).annotate({ description: "The namespace the new template project belongs to. Omit for organization level. A namespace-scoped key always creates in its own namespace." })),
75828
+ idempotencyKey: String4.annotate({ description: "Repeating a request with the same key, address, name, and namespace returns the project the first one created, even if the site has since stopped answering. The same key with another address, name, or namespace is refused with a 409." }).check(isMinLength(1).annotate({ expected: "a value with a length of at least 1" })).check(isMaxCodePoints(255).annotate({ expected: "a string with at most 255 code points" })).check(isMaxLength(255).annotate({ expected: "a value with a length of at most 255" }))
75829
+ });
75830
+ var TemplatesCreateFromWebsite201 = TemplateFromWebsite;
75831
+ var TemplatesCreateFromWebsite401 = UnauthorizedError2;
75832
+ var TemplatesCreateFromWebsite403 = ForbiddenError2;
75833
+ var TemplatesCreateFromWebsite404 = ResourceNotFoundError2;
75834
+ var TemplatesCreateFromWebsite409 = ConflictError2;
75835
+ var TemplatesCreateFromWebsite422 = ValidationError2;
75836
+ var TemplatesCreateFromWebsite500 = InternalError2;
75837
+ var TemplatesCreateFromWebsite504 = GatewayTimeoutError2;
75695
75838
  var TemplatesAddEntryPathParams = /* @__PURE__ */ Struct({ id: TemplateProjectId2 });
75696
75839
  var TemplatesAddEntryRequestJson = /* @__PURE__ */ Struct({
75697
75840
  entryPath: String4.check(isMaxCodePoints(1024).annotate({ expected: "a string with at most 1024 code points" })).check(isPattern2(/* @__PURE__ */ new RegExp("^(?!\\/)(?!.*\\\\)(?!.*(?:^|\\/)\\.{1,2}(?:\\/|$))(?!.*(?:^|\\/)\\.git(?:\\/|$))(?:templates|emails)\\/.+\\.tsx$", "u")).annotate({ expected: "a string matching the RegExp ^(?!\\/)(?!.*\\\\)(?!.*(?:^|\\/)\\.{1,2}(?:\\/|$))(?!.*(?:^|\\/)\\.git(?:\\/|$))(?:templates|emails)\\/.+\\.tsx$" })).check(isMaxLength(1024).annotate({ expected: "a value with a length of at most 1024" })),
@@ -76104,7 +76247,19 @@ var TemplatesGroup = class extends make37("Templates").add(get5("templatesList",
76104
76247
  TemplatesCreateFamilyFromTemplate500,
76105
76248
  TemplatesCreateFamilyFromTemplate504.pipe(status(504))
76106
76249
  ]
76107
- }).middleware(ApiKeySecurityMiddleware).annotate(Identifier, "templates.createFamilyFromTemplate").annotate(Summary, "Create a family from a template").annotate(Description, "Creates a template family containing the specified template and returns the new family with status 201."), post("templatesAddEntry", "/v1/templates/projects/:id/entries", {
76250
+ }).middleware(ApiKeySecurityMiddleware).annotate(Identifier, "templates.createFamilyFromTemplate").annotate(Summary, "Create a family from a template").annotate(Description, "Creates a template family containing the specified template and returns the new family with status 201."), post("templatesCreateFromWebsite", "/v1/templates/from-website", {
76251
+ payload: TemplatesCreateFromWebsiteRequestJson,
76252
+ success: TemplatesCreateFromWebsite201.pipe(status(201)),
76253
+ error: [
76254
+ TemplatesCreateFromWebsite401.pipe(status(401)),
76255
+ TemplatesCreateFromWebsite403.pipe(status(403)),
76256
+ TemplatesCreateFromWebsite404.pipe(status(404)),
76257
+ TemplatesCreateFromWebsite409.pipe(status(409)),
76258
+ TemplatesCreateFromWebsite422.pipe(status(422)),
76259
+ TemplatesCreateFromWebsite500,
76260
+ TemplatesCreateFromWebsite504.pipe(status(504))
76261
+ ]
76262
+ }).middleware(ApiKeySecurityMiddleware).annotate(Identifier, "templates.createFromWebsite").annotate(Summary, "Create a template from a website").annotate(Description, "Checks that the website is a public page Samva can read, creates an email template project from Samva's canonical TSX starter, and returns the template with status 201 and the request that makes the template assistant match the site. The assistant runs when the request is sent in the editor; it rebuilds the project's theme and blocks from the site and proposes a brand that applies only after the customer approves it. A 422 names why the site cannot be matched: not a web address, not public, no answer, a missing page, refused automated visits, or not a web page."), post("templatesAddEntry", "/v1/templates/projects/:id/entries", {
76108
76263
  params: TemplatesAddEntryPathParams,
76109
76264
  payload: TemplatesAddEntryRequestJson,
76110
76265
  success: TemplatesAddEntry201.pipe(status(201)),
@@ -76124,6 +76279,7 @@ var TemplatesGroup = class extends make37("Templates").add(get5("templatesList",
76124
76279
  var make54 = /* @__PURE__ */ forGroup(TemplatesGroup);
76125
76280
  var list26 = /* @__PURE__ */ make54("templatesList");
76126
76281
  var create15 = /* @__PURE__ */ make54("templatesCreate");
76282
+ var createFromWebsite = /* @__PURE__ */ make54("templatesCreateFromWebsite");
76127
76283
  var addEntry = /* @__PURE__ */ make54("templatesAddEntry");
76128
76284
  var getById = /* @__PURE__ */ make54("templatesGetById");
76129
76285
  var remove13 = /* @__PURE__ */ make54("templatesRemove");
@@ -76723,6 +76879,66 @@ var formatReportedDiagnostic = (diagnostic) => {
76723
76879
  Upgrade: ${diagnostic.upgrade}`;
76724
76880
  };
76725
76881
 
76882
+ // src/commands/templates/from-website-command.ts
76883
+ var webOriginForApi = (apiUrl) => {
76884
+ if (!URL.canParse(apiUrl))
76885
+ return null;
76886
+ const url = new URL(apiUrl);
76887
+ if (!url.hostname.startsWith("api."))
76888
+ return null;
76889
+ url.hostname = url.hostname.slice("api.".length);
76890
+ return url.origin;
76891
+ };
76892
+ var editorPath = (orgSlug, templateId, ask) => `/dashboard/${encodeURIComponent(orgSlug)}/templates/${encodeURIComponent(templateId)}/edit?ask=${encodeURIComponent(ask)}`;
76893
+ var urlArgument = String10("url").pipe(withDescription4("The website to match: a public https address or a bare domain"));
76894
+ var fromWebsiteCommand = make29("from-website", {
76895
+ url: urlArgument,
76896
+ name: String9("name").pipe(withDescription2("Name for the new template; defaults to one derived from the site"), optional4),
76897
+ namespace: namespaceFlag("create"),
76898
+ idempotencyKey: String9("idempotency-key").pipe(withDescription2("Key (1-255 characters) so a retry returns the original template; generated per run when omitted"), optional4),
76899
+ json: jsonFlag
76900
+ }).pipe(withDescription3("Create a template project from a website, then open the editor to have the assistant match its brand"), withExamples([
76901
+ {
76902
+ command: "samva templates from-website example.com",
76903
+ description: "Check the site, create the project, and print the editor link"
76904
+ },
76905
+ {
76906
+ command: 'samva templates from-website https://example.com --name "Example welcome" --json',
76907
+ description: "Machine-readable result with the template, website, ask, and editor link"
76908
+ }
76909
+ ]), withHandler(({ url, name, namespace, idempotencyKey, json }) => gen2(function* () {
76910
+ const key = getOrUndefined(idempotencyKey);
76911
+ if (key !== undefined && (key.length < 1 || key.length > 255))
76912
+ return yield* new SamvaCliError({
76913
+ message: "--idempotency-key must contain between 1 and 255 characters."
76914
+ });
76915
+ const client = yield* getClientProvider;
76916
+ const context = yield* resolveExecutionContext;
76917
+ const orgSlug = context.apiKey ? (yield* get25().pipe(client.provide, mapError2((error) => toCliApiError("Could not look up the organization", error)))).organization.slug : context.orgSlug;
76918
+ const result = yield* createFromWebsite({
76919
+ url,
76920
+ ...isSome2(name) ? { name: name.value } : {},
76921
+ ...namespaceField(namespace),
76922
+ idempotencyKey: key ?? crypto.randomUUID()
76923
+ }).pipe(client.provide, mapError2((error) => toCliApiError("Could not create a template from the website", error)));
76924
+ const path = orgSlug === undefined ? null : editorPath(orgSlug, result.template.id, result.ask);
76925
+ const origin = webOriginForApi(context.apiUrl);
76926
+ const editorUrl = path !== null && origin !== null ? `${origin}${path}` : null;
76927
+ if (json)
76928
+ return yield* printJson({
76929
+ template: { id: result.template.id, name: result.template.name },
76930
+ website: result.website,
76931
+ ask: result.ask,
76932
+ editorUrl
76933
+ });
76934
+ yield* printFields([
76935
+ ["Template", result.template.id],
76936
+ ["Website", result.website.host],
76937
+ ...editorUrl !== null ? [["Editor", editorUrl]] : [["Ask", result.ask]]
76938
+ ]);
76939
+ yield* log(editorUrl !== null ? "The brand proposal applies only when you approve it in the editor." : "Open the template in the dashboard editor and send the ask above; the brand proposal applies only when you approve it there.");
76940
+ }).pipe(json ? provideService2(MinimumLogLevel2, "None") : identity)));
76941
+
76726
76942
  // src/commands/templates/local-build.ts
76727
76943
  import { resolve as resolve9 } from "path";
76728
76944
  var buildDir = (dir, assetBase) => runToolchain({
@@ -78847,13 +79063,18 @@ var build3 = make29("build", {
78847
79063
  yield* logInfo(`Built ${built.files.length} template file(s) with compiler ${artifact.compilerVersion} to ${target}`);
78848
79064
  }
78849
79065
  }).pipe(json ? provideService2(MinimumLogLevel2, "None") : identity)));
79066
+ var sharedFilterFlag = Literals3("shared", ["true", "false"]).pipe(withDescription2("Only templates shared with every namespace (true), or only unshared ones (false)"), optional4);
78850
79067
  var listRemote = make29("list", {
78851
79068
  cursor: String9("cursor").pipe(withDescription2("Resume after this nextCursor from a previous page"), optional4),
78852
79069
  limit: Int5("limit").pipe(withDescription2("Items per page"), optional4),
78853
79070
  namespace: namespaceFlag("list"),
79071
+ shared: sharedFilterFlag,
78854
79072
  all: Boolean6("all").pipe(withDescription2("Traverse every page (requires --jsonl)"), withDefault5(false)),
78855
79073
  json: jsonFlag
78856
- }).pipe(withDescription3("List remote templates"), withExamples([{ command: "samva templates list --json" }]), withHandler(({ cursor, limit, namespace, all, json }) => gen2(function* () {
79074
+ }).pipe(withDescription3("List remote templates"), withExamples([
79075
+ { command: "samva templates list --json" },
79076
+ { command: "samva templates list --shared true --json" }
79077
+ ]), withHandler(({ cursor, limit, namespace, shared, all, json }) => gen2(function* () {
78857
79078
  if (all && !isJsonLinesRequested())
78858
79079
  return yield* new CliUsageError({
78859
79080
  code: "all_requires_jsonl",
@@ -78865,7 +79086,8 @@ var listRemote = make29("list", {
78865
79086
  const query = {
78866
79087
  cursor: cursorValue,
78867
79088
  limit: limitValue?.toString(),
78868
- ...namespaceField(namespace)
79089
+ ...namespaceField(namespace),
79090
+ ...match(shared, { onNone: () => ({}), onSome: (value) => ({ shared: value }) })
78869
79091
  };
78870
79092
  if (all) {
78871
79093
  let nextCursor = cursorValue;
@@ -78892,7 +79114,8 @@ var listRemote = make29("list", {
78892
79114
  ["Name", item.name],
78893
79115
  ["Slug", item.slug],
78894
79116
  ["Status", item.status],
78895
- ["Namespace", item.namespaceId ?? "organization"]
79117
+ ["Namespace", item.namespaceId ?? "organization"],
79118
+ ["Shared", item.shared ? "yes" : "no"]
78896
79119
  ]);
78897
79120
  if (data.nextCursor !== null)
78898
79121
  yield* printFields([["Next cursor", data.nextCursor]]);
@@ -78907,20 +79130,35 @@ var getRemote = make29("get", { id: templateIdArgument2, json: jsonFlag }).pipe(
78907
79130
  var createRemote = make29("create", {
78908
79131
  data: String9("data").pipe(withDescription2("Complete template JSON payload")),
78909
79132
  namespace: namespaceFlag("create"),
79133
+ shared: Boolean6("shared").pipe(withDescription2("Share the new organization-level project with every namespace; namespace-scoped keys read and send it but cannot change it"), withDefault5(false)),
78910
79134
  dryRun: dryRunFlag6,
78911
79135
  json: jsonFlag
78912
79136
  }).pipe(withDescription3("Create a remote template from JSON"), withExamples([
78913
79137
  {
78914
79138
  command: `samva templates create --data '{"name":"Welcome","projectName":"welcome-email","idempotencyKey":"welcome-email-v1"}' --dry-run`
78915
79139
  }
78916
- ]), withHandler(({ data, namespace, dryRun, json }) => gen2(function* () {
79140
+ ]), withHandler(({ data, namespace, shared, dryRun, json }) => gen2(function* () {
78917
79141
  const decoded = yield* decodePayload(data, "--data", CreateTemplatePayload);
79142
+ if (shared && decoded.shared === false)
79143
+ return yield* new CliUsageError({
79144
+ code: "shared_conflict",
79145
+ message: "--shared and shared: false in --data disagree."
79146
+ });
79147
+ if (shared && (isSome2(namespace) || decoded.namespaceId != null))
79148
+ return yield* new CliUsageError({
79149
+ code: "shared_namespace_conflict",
79150
+ message: "--shared shares an organization-level project; it cannot have a namespace."
79151
+ });
78918
79152
  if (isSome2(namespace) && decoded.namespaceId != null && decoded.namespaceId !== namespace.value)
78919
79153
  return yield* new CliUsageError({
78920
79154
  code: "namespace_conflict",
78921
79155
  message: "--namespace and the namespaceId in --data name different namespaces."
78922
79156
  });
78923
- const payload = { ...decoded, ...namespaceField(namespace) };
79157
+ const payload = {
79158
+ ...decoded,
79159
+ ...namespaceField(namespace),
79160
+ ...shared ? { shared: true } : {}
79161
+ };
78924
79162
  if (dryRun)
78925
79163
  return yield* json ? printJson({ dryRun: true, action: "templates.create", request: payload }) : printFields([
78926
79164
  ["Action", "templates.create"],
@@ -78935,14 +79173,22 @@ var createRemote = make29("create", {
78935
79173
  var updateRemote = make29("update", {
78936
79174
  id: templateIdArgument2,
78937
79175
  data: String9("data").pipe(withDescription2("JSON object of template fields to update")),
79176
+ shared: Literals3("shared", ["true", "false"]).pipe(withDescription2("Share the template's organization-level project with every namespace (true) or stop sharing it (false); applies to every entry in the project"), optional4),
78938
79177
  dryRun: dryRunFlag6,
78939
79178
  json: jsonFlag
78940
79179
  }).pipe(withDescription3("Update a remote template from JSON"), withExamples([
78941
79180
  {
78942
79181
  command: `samva templates update tmpl_abc123 --data '{"name":"Welcome back"}' --dry-run`
78943
79182
  }
78944
- ]), withHandler(({ id, data, dryRun, json }) => gen2(function* () {
78945
- const changes = yield* parseObject(data, "--data");
79183
+ ]), withHandler(({ id, data, shared, dryRun, json }) => gen2(function* () {
79184
+ const parsed = yield* parseObject(data, "--data");
79185
+ const sharedValue = map(shared, (value) => value === "true");
79186
+ if (isSome2(sharedValue) && parsed.shared !== undefined && parsed.shared !== sharedValue.value)
79187
+ return yield* new CliUsageError({
79188
+ code: "shared_conflict",
79189
+ message: "--shared and shared in --data disagree."
79190
+ });
79191
+ const changes = isSome2(sharedValue) ? { ...parsed, shared: sharedValue.value } : parsed;
78946
79192
  const payload = yield* decodePayload(JSON.stringify({ data: changes }), "--data", UpdateTemplatePayload);
78947
79193
  if (dryRun)
78948
79194
  return yield* json ? printJson({ dryRun: true, action: "templates.update", id, request: payload }) : printFields([
@@ -79054,6 +79300,10 @@ var templatesCommand = make29("templates").pipe(withDescription3("Create, author
79054
79300
  command: "samva templates clone tmpl_abc123 --dir welcome-email",
79055
79301
  description: "Clone an existing project with its repository credential helper"
79056
79302
  },
79303
+ {
79304
+ command: "samva templates from-website example.com",
79305
+ description: "Create a project from a website and print the editor link that matches its brand"
79306
+ },
79057
79307
  {
79058
79308
  command: "samva templates dev",
79059
79309
  description: "Start the local visual editor for templates/ and emails/"
@@ -79094,6 +79344,7 @@ var templatesCommand = make29("templates").pipe(withDescription3("Create, author
79094
79344
  createRemote,
79095
79345
  updateRemote,
79096
79346
  removeRemote,
79347
+ fromWebsiteCommand,
79097
79348
  devCommand,
79098
79349
  check5,
79099
79350
  renderCommand,
@@ -79166,6 +79417,9 @@ var WebhookEndpoint = /* @__PURE__ */ Struct({
79166
79417
  Literal2("webhook.test"),
79167
79418
  Literal2("domain.updated"),
79168
79419
  Literal2("domain.warming.updated"),
79420
+ Literal2("template.registration.updated"),
79421
+ Literal2("flow.registration.updated"),
79422
+ Literal2("whatsapp.number.connected"),
79169
79423
  Literal2("mailbox.message.received"),
79170
79424
  Literal2("mailbox.message.sent"),
79171
79425
  Literal2("mailbox.action.pending_approval"),
@@ -79215,6 +79469,9 @@ var WebhookEndpointListItem = /* @__PURE__ */ Struct({
79215
79469
  Literal2("webhook.test"),
79216
79470
  Literal2("domain.updated"),
79217
79471
  Literal2("domain.warming.updated"),
79472
+ Literal2("template.registration.updated"),
79473
+ Literal2("flow.registration.updated"),
79474
+ Literal2("whatsapp.number.connected"),
79218
79475
  Literal2("mailbox.message.received"),
79219
79476
  Literal2("mailbox.message.sent"),
79220
79477
  Literal2("mailbox.action.pending_approval"),
@@ -79329,6 +79586,9 @@ var WebhooksCreateRequestJson = /* @__PURE__ */ Struct({
79329
79586
  Literal2("webhook.test"),
79330
79587
  Literal2("domain.updated"),
79331
79588
  Literal2("domain.warming.updated"),
79589
+ Literal2("template.registration.updated"),
79590
+ Literal2("flow.registration.updated"),
79591
+ Literal2("whatsapp.number.connected"),
79332
79592
  Literal2("mailbox.message.received"),
79333
79593
  Literal2("mailbox.message.sent"),
79334
79594
  Literal2("mailbox.action.pending_approval"),
@@ -79404,6 +79664,9 @@ var WebhooksUpdateRequestJson = /* @__PURE__ */ Struct({
79404
79664
  Literal2("webhook.test"),
79405
79665
  Literal2("domain.updated"),
79406
79666
  Literal2("domain.warming.updated"),
79667
+ Literal2("template.registration.updated"),
79668
+ Literal2("flow.registration.updated"),
79669
+ Literal2("whatsapp.number.connected"),
79407
79670
  Literal2("mailbox.message.received"),
79408
79671
  Literal2("mailbox.message.sent"),
79409
79672
  Literal2("mailbox.action.pending_approval"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samva/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Samva CLI — email and platform operations from your terminal",
5
5
  "keywords": [
6
6
  "cli",
@@ -50,22 +50,22 @@
50
50
  "@effect/vitest": "4.0.0",
51
51
  "@samva/contracts": "0.0.0",
52
52
  "@samva/core": "0.0.0",
53
- "@samva/mailbox": "0.5.1",
54
- "@samva/markup": "^0.13.1",
55
- "@samva/vite": "^0.13.1",
53
+ "@samva/mailbox": "0.5.2",
54
+ "@samva/markup": "0.15.0",
55
+ "@samva/vite": "0.15.0",
56
56
  "@types/node": "^26.6.2",
57
57
  "better-auth": "1.7.5",
58
58
  "effect": "4.0.0",
59
- "samva": "0.21.0",
59
+ "samva": "0.22.0",
60
60
  "typescript": "^7.0.2",
61
61
  "vite": "~8.3.0",
62
62
  "vitest": "^5.0.3"
63
63
  },
64
64
  "optionalDependencies": {
65
- "@samva/cli-darwin-arm64": "0.18.0",
66
- "@samva/cli-darwin-x64": "0.18.0",
67
- "@samva/cli-linux-arm64": "0.18.0",
68
- "@samva/cli-linux-x64": "0.18.0"
65
+ "@samva/cli-darwin-arm64": "0.19.0",
66
+ "@samva/cli-darwin-x64": "0.19.0",
67
+ "@samva/cli-linux-arm64": "0.19.0",
68
+ "@samva/cli-linux-x64": "0.19.0"
69
69
  },
70
70
  "engines": {
71
71
  "node": ">=18"