@samva/cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs ADDED
@@ -0,0 +1,4624 @@
1
+ #!/usr/bin/env bun
2
+ import { BunRuntime, BunServices } from "@effect/platform-bun";
3
+ import * as Console from "effect/Console";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Logger from "effect/Logger";
7
+ import * as LogLevel from "effect/LogLevel";
8
+ import * as Predicate from "effect/Predicate";
9
+ import * as CliError from "effect/unstable/cli/CliError";
10
+ import * as CliOutput from "effect/unstable/cli/CliOutput";
11
+ import * as Command from "effect/unstable/cli/Command";
12
+ import { identity } from "effect/Function";
13
+ import * as Option from "effect/Option";
14
+ import * as References from "effect/References";
15
+ import * as Argument from "effect/unstable/cli/Argument";
16
+ import * as Flag from "effect/unstable/cli/Flag";
17
+ import * as Campaigns from "samva/effect/campaigns";
18
+ import * as Schema from "effect/Schema";
19
+ import { ConflictError } from "samva/effect";
20
+ import * as Clock from "effect/Clock";
21
+ import * as Client from "samva/effect/client";
22
+ import * as FileSystem from "effect/FileSystem";
23
+ import * as GlobalFlag from "effect/unstable/cli/GlobalFlag";
24
+ import * as Path from "effect/Path";
25
+ import { isatty } from "node:tty";
26
+ import * as Prompt from "effect/unstable/cli/Prompt";
27
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
28
+ import { dirname, join, resolve } from "node:path";
29
+ import { formatDiagnostic } from "@samva/markup/diagnostics";
30
+ import { flattenFontTokens } from "@samva/markup/theme";
31
+ import { parseThemeCss, printThemeCss } from "@samva/markup/theme-css";
32
+ import { Result } from "effect";
33
+ import * as Design from "samva/effect/design";
34
+ import * as Email from "samva/effect/email";
35
+ import * as Organizations from "samva/effect/organizations";
36
+ import * as Duration from "effect/Duration";
37
+ import * as Schedule from "effect/Schedule";
38
+ import * as Messages from "samva/effect/messages";
39
+ import * as Ref from "effect/Ref";
40
+ import * as ScheduledMessages from "samva/effect/scheduled-messages";
41
+ import MarkupPackageJson from "@samva/markup/package.json" with { type: "json" };
42
+ import VitePackageJson from "@samva/vite/package.json" with { type: "json" };
43
+ import * as Templates from "samva/effect/templates";
44
+ import { createHash } from "node:crypto";
45
+ import * as Redacted from "effect/Redacted";
46
+ import * as Webhooks from "samva/effect/webhooks";
47
+ //#region package.json
48
+ var version = "0.1.1";
49
+ //#endregion
50
+ //#region src/errors.ts
51
+ /**
52
+ * A user-facing CLI error. Its `message` is printed to stderr and the process
53
+ * exits non-zero. Use this for expected failures (missing key, API errors,
54
+ * bad input) — not for programmer bugs, which should surface as defects.
55
+ */
56
+ var SamvaCliError = class extends Schema.TaggedErrorClass()("SamvaCliError", { message: Schema.String }) {};
57
+ /** Expected process failures remain typed through the CLI boundary. */
58
+ var CliUsageError = class extends Schema.TaggedErrorClass()("CliUsageError", {
59
+ message: Schema.String,
60
+ code: Schema.String
61
+ }) {};
62
+ var CliConfigError = class extends Schema.TaggedErrorClass()("CliConfigError", {
63
+ message: Schema.String,
64
+ code: Schema.String
65
+ }) {};
66
+ var CliAuthError = class extends Schema.TaggedErrorClass()("CliAuthError", {
67
+ message: Schema.String,
68
+ code: Schema.String
69
+ }) {};
70
+ var CliTimeoutError = class extends Schema.TaggedErrorClass()("CliTimeoutError", {
71
+ message: Schema.String,
72
+ code: Schema.String
73
+ }) {};
74
+ /** A completed operation failed, but its structured result remains authoritative. */
75
+ var CliResultError = class extends Schema.TaggedErrorClass()("CliResultError", {
76
+ message: Schema.String,
77
+ code: Schema.String,
78
+ result: Schema.Unknown,
79
+ human: Schema.String
80
+ }) {};
81
+ /**
82
+ * A lifecycle command rendered its last authoritative Message before failing.
83
+ * The process boundary preserves that output by branching on this class
84
+ * (`instanceof`), so no marker field is needed on the error itself.
85
+ */
86
+ var CliMessageLifecycleError = class extends Schema.TaggedErrorClass()("CliMessageLifecycleError", {
87
+ message: Schema.String,
88
+ code: Schema.String,
89
+ partialMessage: Schema.Unknown,
90
+ next: Schema.String,
91
+ human: Schema.String
92
+ }) {};
93
+ const processErrorCode = (error) => {
94
+ if (CliError.isCliError(error)) return 2;
95
+ if (error instanceof CliUsageError || error instanceof CliConfigError) return 2;
96
+ if (error instanceof CliAuthError) return 4;
97
+ if (error instanceof CliTimeoutError) return 124;
98
+ if (error instanceof CliResultError) return 1;
99
+ if (error instanceof CliMessageLifecycleError) return error.code === "message_wait_timeout" ? 124 : 1;
100
+ return 1;
101
+ };
102
+ const processErrorReason = (error) => {
103
+ if (CliError.isCliError(error)) return "usage_error";
104
+ if (error instanceof CliUsageError) return error.code;
105
+ if (error instanceof CliConfigError) return error.code;
106
+ if (error instanceof CliAuthError) return error.code;
107
+ if (error instanceof CliTimeoutError) return error.code;
108
+ if (error instanceof CliResultError) return error.code;
109
+ if (error instanceof CliMessageLifecycleError) return error.code;
110
+ return "operational_failure";
111
+ };
112
+ const processErrorNext = (error) => error instanceof CliMessageLifecycleError ? error.next : void 0;
113
+ /**
114
+ * Build a friendly one-line message from an SDK error. The Effect SDK's error
115
+ * channel carries the server's real tagged error classes (or `HttpClientError`
116
+ * / `SchemaError`) directly — the failure value itself has the message, not a
117
+ * `.cause` wrapper. Most tagged errors expose `.message`; a few
118
+ * (`FlagDisabledError`, `WebhookEnqueueError`) only expose `.errorMessage`.
119
+ */
120
+ const formatApiError = (prefix, error) => {
121
+ if (Predicate.isObject(error)) {
122
+ if (Predicate.hasProperty(error, "message") && typeof error["message"] === "string") return `${prefix}: ${error["message"]}`;
123
+ if (Predicate.hasProperty(error, "errorMessage") && typeof error["errorMessage"] === "string") return `${prefix}: ${error["errorMessage"]}`;
124
+ }
125
+ return prefix;
126
+ };
127
+ const getConflictError = (error) => error instanceof ConflictError ? error : void 0;
128
+ //#endregion
129
+ //#region src/env.ts
130
+ /**
131
+ * The CLI's environment module — the single legitimate reader of `process.env`.
132
+ *
133
+ * Cloudflare Workers (apps/api, apps/web) get their env from bindings, so the
134
+ * `node/no-process-env` lint rule steers them to typed env modules. A CLI runs
135
+ * as a normal Node process, where `process.env` *is* the source of truth; this
136
+ * file is the env module for it (the rule exempts `**​/env.*`). Read env here and
137
+ * nowhere else.
138
+ */
139
+ const env = process.env;
140
+ /** API key for authenticated calls (`samva email send`, `samva org list`, …). */
141
+ const SAMVA_API_KEY = env["SAMVA_API_KEY"];
142
+ /** API base URL override; falls back to config then the production default. */
143
+ const SAMVA_API_URL = env["SAMVA_API_URL"];
144
+ /** Named execution profile override for humans, CI, and agents. */
145
+ const SAMVA_PROFILE = env["SAMVA_PROFILE"];
146
+ /** OAuth organization override; rejected when SAMVA_API_KEY is in use. */
147
+ const SAMVA_ORG = env["SAMVA_ORG"];
148
+ /** XDG config root; the CLI stores state under `$XDG_CONFIG_HOME/samva`. */
149
+ const XDG_CONFIG_HOME = env["XDG_CONFIG_HOME"];
150
+ /** User home directory (used when `XDG_CONFIG_HOME` is unset). */
151
+ const HOME = env["HOME"] ?? env["USERPROFILE"];
152
+ //#endregion
153
+ //#region src/lib/runtime.ts
154
+ /** Process-wide output and execution settings. */
155
+ const Json = GlobalFlag.setting("json")({ flag: Flag.boolean("json").pipe(Flag.withDefault(false), Flag.withDescription("Output machine-readable JSON")) });
156
+ const JsonLines = GlobalFlag.setting("jsonl")({ flag: Flag.boolean("jsonl").pipe(Flag.withDefault(false), Flag.withDescription("Output newline-delimited JSON result envelopes")) });
157
+ const Quiet = GlobalFlag.setting("quiet")({ flag: Flag.boolean("quiet").pipe(Flag.withDefault(false), Flag.withDescription("Suppress all successful output")) });
158
+ const Profile = GlobalFlag.setting("profile")({ flag: Flag.string("profile").pipe(Flag.optional, Flag.withDescription("Execution profile (overrides SAMVA_PROFILE and the active profile)")) });
159
+ const ApiUrl = GlobalFlag.setting("api-url")({ flag: Flag.string("api-url").pipe(Flag.optional, Flag.withDescription("Explicit API URL (development/E2E escape hatch)")) });
160
+ const Org = GlobalFlag.setting("org")({ flag: Flag.string("org").pipe(Flag.optional, Flag.withDescription("Organization slug for OAuth profiles")) });
161
+ const NoInput = GlobalFlag.setting("no-input")({ flag: Flag.boolean("no-input").pipe(Flag.withDefault(false), Flag.withDescription("Never prompt or open a browser")) });
162
+ const globalFlags = [
163
+ Json,
164
+ JsonLines,
165
+ Quiet,
166
+ Profile,
167
+ ApiUrl,
168
+ Org,
169
+ NoInput
170
+ ];
171
+ const outputFlags = /* @__PURE__ */ new Set([
172
+ "--json",
173
+ "--jsonl",
174
+ "--quiet"
175
+ ]);
176
+ const outputAssignments = /^(--json|--jsonl|--quiet)=/;
177
+ /** Final M3 process-boundary metadata, solely for stable envelope command names. */
178
+ const commandTree = {
179
+ login: {},
180
+ logout: {},
181
+ org: {
182
+ list: {},
183
+ use: {},
184
+ current: {}
185
+ },
186
+ email: {
187
+ send: {},
188
+ doctor: {},
189
+ receiving: {
190
+ status: {},
191
+ enable: {}
192
+ },
193
+ domains: {
194
+ list: {},
195
+ add: {},
196
+ check: {},
197
+ remove: {}
198
+ },
199
+ senders: {
200
+ list: {},
201
+ add: {},
202
+ check: {},
203
+ remove: {}
204
+ },
205
+ theme: {
206
+ pull: {},
207
+ push: {}
208
+ }
209
+ },
210
+ messages: {
211
+ list: {},
212
+ get: {},
213
+ events: {},
214
+ wait: {}
215
+ },
216
+ campaigns: {
217
+ create: {},
218
+ list: {},
219
+ get: {},
220
+ update: {},
221
+ archive: {},
222
+ runs: {
223
+ create: {},
224
+ list: {},
225
+ get: {},
226
+ pause: {},
227
+ resume: {},
228
+ cancel: {},
229
+ recipients: {}
230
+ }
231
+ },
232
+ "scheduled-messages": {
233
+ create: {},
234
+ list: {},
235
+ get: {},
236
+ cancel: {}
237
+ },
238
+ templates: {
239
+ init: {},
240
+ push: {},
241
+ pull: {},
242
+ list: {},
243
+ get: {},
244
+ create: {},
245
+ update: {},
246
+ publish: {},
247
+ unpublish: {},
248
+ remove: {},
249
+ versions: {
250
+ list: {},
251
+ restore: {}
252
+ }
253
+ },
254
+ webhooks: {
255
+ list: {},
256
+ get: {},
257
+ create: {},
258
+ update: {},
259
+ test: {},
260
+ logs: {},
261
+ stats: {},
262
+ retry: {},
263
+ "rotate-secret": {},
264
+ remove: {}
265
+ },
266
+ profile: {
267
+ list: {},
268
+ show: {},
269
+ create: {},
270
+ use: {},
271
+ delete: {}
272
+ },
273
+ help: {}
274
+ };
275
+ /** Command handlers remain the grammar authority. */
276
+ const isKnownRootCommand = (token) => token in commandTree;
277
+ const globalOptionsWithValue = /* @__PURE__ */ new Set([
278
+ "--profile",
279
+ "--api-url",
280
+ "--org",
281
+ "--log-level"
282
+ ]);
283
+ /**
284
+ * Return the released command path in its stable dotted form. This only reads
285
+ * tokens before `--`; positional data after a recognized leaf never becomes
286
+ * command metadata.
287
+ */
288
+ const commandNameFromArgv = (argv) => {
289
+ let tree = commandTree;
290
+ const path = [];
291
+ for (let index = 0; index < argv.length; index++) {
292
+ const token = argv[index];
293
+ if (token === "--") break;
294
+ if (globalOptionsWithValue.has(token)) {
295
+ index++;
296
+ continue;
297
+ }
298
+ if (token.startsWith("--profile=") || token.startsWith("--api-url=") || token.startsWith("--org=") || token.startsWith("--log-level=")) continue;
299
+ if (token.startsWith("-")) continue;
300
+ if (path.length === 0) {
301
+ if (token in tree) {
302
+ path.push(token);
303
+ tree = tree[token];
304
+ }
305
+ continue;
306
+ }
307
+ if (token in tree) {
308
+ path.push(token);
309
+ tree = tree[token];
310
+ continue;
311
+ }
312
+ break;
313
+ }
314
+ return path.length === 0 ? "samva" : path.join(".");
315
+ };
316
+ /**
317
+ * Validate the tiny set of process-wide syntax rules before Effect CLI begins
318
+ * parsing command-local flags. Stops at `--`, so a positional value cannot
319
+ * accidentally select machine mode.
320
+ */
321
+ const validateProcessArgs = (argv) => Effect.gen(function* () {
322
+ const selected = /* @__PURE__ */ new Set();
323
+ for (const token of argv) {
324
+ if (token === "--") break;
325
+ if (outputAssignments.test(token)) return yield* new CliUsageError({
326
+ message: "Use --json, --jsonl, or --quiet without =true or =false.",
327
+ code: "invalid_output_mode_syntax"
328
+ });
329
+ if (outputFlags.has(token)) selected.add(token);
330
+ }
331
+ if (selected.size > 1) return yield* new CliUsageError({
332
+ message: "Use only one of --json, --jsonl, or --quiet.",
333
+ code: "mutually_exclusive_output_mode"
334
+ });
335
+ });
336
+ const outputModeFromArgv = (argv) => {
337
+ let selected = "human";
338
+ for (const token of argv) {
339
+ if (token === "--") break;
340
+ if (token === "--jsonl" || token.startsWith("--jsonl=")) selected = "jsonl";
341
+ else if (token === "--json" || token.startsWith("--json=")) {
342
+ if (selected !== "jsonl") selected = "json";
343
+ } else if (token === "--quiet" || token.startsWith("--quiet=")) {
344
+ if (selected === "human") selected = "quiet";
345
+ }
346
+ }
347
+ return selected;
348
+ };
349
+ const isJsonRequested = () => outputModeFromArgv(process.argv.slice(2)) === "json";
350
+ const isJsonLinesRequested = () => outputModeFromArgv(process.argv.slice(2)) === "jsonl";
351
+ //#endregion
352
+ //#region src/config/credential-store.ts
353
+ const SERVICE = "dev.samva.cli";
354
+ const readCredentialCommand = (platform, command, profile) => platform === "darwin" ? { args: [
355
+ command,
356
+ "find-generic-password",
357
+ "-s",
358
+ SERVICE,
359
+ "-a",
360
+ profile,
361
+ "-w"
362
+ ] } : { args: [
363
+ command,
364
+ "lookup",
365
+ "service",
366
+ SERVICE,
367
+ "profile",
368
+ profile
369
+ ] };
370
+ const writeCredentialCommand = (platform, command, profile, secret) => platform === "darwin" ? {
371
+ args: [command, "-i"],
372
+ input: `add-generic-password -U -s ${SERVICE} -a ${profile} -X ${Buffer.from(secret, "utf8").toString("hex")}\n`
373
+ } : {
374
+ args: [
375
+ command,
376
+ "store",
377
+ "--label",
378
+ `Samva CLI profile ${profile}`,
379
+ "service",
380
+ SERVICE,
381
+ "profile",
382
+ profile
383
+ ],
384
+ input: secret
385
+ };
386
+ const commandForPlatform = () => {
387
+ if (process.platform === "darwin") return "/usr/bin/security";
388
+ if (process.platform === "linux") return "secret-tool";
389
+ };
390
+ const runSecretCommand = (args, input) => Effect.tryPromise({
391
+ try: async () => {
392
+ const child = Bun.spawn({
393
+ cmd: [...args],
394
+ stdin: input === void 0 ? "ignore" : new Blob([input]),
395
+ stdout: "pipe",
396
+ stderr: "pipe"
397
+ });
398
+ const [stdout, stderr, exitCode] = await Promise.all([
399
+ new Response(child.stdout).text(),
400
+ new Response(child.stderr).text(),
401
+ child.exited
402
+ ]);
403
+ return {
404
+ stdout: stdout.trim(),
405
+ stderr: stderr.trim(),
406
+ exitCode
407
+ };
408
+ },
409
+ catch: () => new CliConfigError({
410
+ code: "credential_store_unavailable",
411
+ message: "Could not run the operating-system credential store."
412
+ })
413
+ });
414
+ const keyring = () => {
415
+ const command = commandForPlatform();
416
+ return command === void 0 ? Effect.fail(new CliConfigError({
417
+ code: "credential_store_unsupported",
418
+ message: "Samva CLI credentials require macOS Keychain or Linux secret-tool."
419
+ })) : Effect.succeed(command);
420
+ };
421
+ const isMissingSecretResult = (platform, result) => platform === "darwin" ? result.exitCode === 44 : platform === "linux" ? result.exitCode === 1 && result.stdout.length === 0 && result.stderr.length === 0 : false;
422
+ /** Read one profile secret. A missing item is normal; no plaintext fallback exists. */
423
+ const readCredentialSecret = (profile) => Effect.gen(function* () {
424
+ const command = yield* keyring();
425
+ const spec = readCredentialCommand(process.platform, command, profile);
426
+ const result = yield* runSecretCommand(spec.args, spec.input);
427
+ if (result.exitCode !== 0) {
428
+ if (isMissingSecretResult(process.platform, result)) return void 0;
429
+ return yield* new CliConfigError({
430
+ code: "credential_store_read_failed",
431
+ message: "Could not read credentials from the operating-system credential store."
432
+ });
433
+ }
434
+ return result.stdout;
435
+ });
436
+ /** Store a secret in the OS keyring. Failure is explicit and never falls back to a file. */
437
+ const writeCredentialSecret = (profile, secret) => Effect.gen(function* () {
438
+ const command = yield* keyring();
439
+ const spec = writeCredentialCommand(process.platform, command, profile, secret);
440
+ if ((yield* runSecretCommand(spec.args, spec.input)).exitCode !== 0) return yield* new CliConfigError({
441
+ code: "credential_store_write_failed",
442
+ message: "Could not save credentials in the operating-system credential store."
443
+ });
444
+ });
445
+ const clearCredentialSecret = (profile) => Effect.gen(function* () {
446
+ const command = yield* keyring();
447
+ const result = process.platform === "darwin" ? yield* runSecretCommand([
448
+ command,
449
+ "delete-generic-password",
450
+ "-s",
451
+ SERVICE,
452
+ "-a",
453
+ profile
454
+ ]) : yield* runSecretCommand([
455
+ command,
456
+ "clear",
457
+ "service",
458
+ SERVICE,
459
+ "profile",
460
+ profile
461
+ ]);
462
+ if (result.exitCode !== 0 && !isMissingSecretResult(process.platform, result)) return yield* new CliConfigError({
463
+ code: "credential_store_delete_failed",
464
+ message: "Could not remove credentials from the operating-system credential store."
465
+ });
466
+ });
467
+ //#endregion
468
+ //#region src/config/paths.ts
469
+ /**
470
+ * Absolute path to the Samva CLI config directory:
471
+ * `$XDG_CONFIG_HOME/samva` when set, otherwise `~/.config/samva`.
472
+ */
473
+ const configDir = Effect.gen(function* () {
474
+ const path = yield* Path.Path;
475
+ const base = XDG_CONFIG_HOME && XDG_CONFIG_HOME.length > 0 ? XDG_CONFIG_HOME : HOME && HOME.length > 0 ? path.join(HOME, ".config") : yield* new SamvaCliError({ message: "Could not resolve config directory. Set XDG_CONFIG_HOME, HOME, or USERPROFILE." });
476
+ return path.join(base, "samva");
477
+ });
478
+ /** Path to `config.json` (api url + active org). */
479
+ const configFilePath = Effect.gen(function* () {
480
+ const path = yield* Path.Path;
481
+ const dir = yield* configDir;
482
+ return path.join(dir, "config.json");
483
+ });
484
+ //#endregion
485
+ //#region src/config/store.ts
486
+ const DEFAULT_API_URL = "https://api.samva.dev";
487
+ const LOCAL_API_URL = "http://127.0.0.1:8787";
488
+ const ProfileNameSchema = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/));
489
+ const ProfileSchema = Schema.Struct({
490
+ target: Schema.Literals(["production", "local"]),
491
+ orgSlug: Schema.optional(Schema.String),
492
+ credentialIdentity: Schema.Literals(["none", "oauth"])
493
+ });
494
+ const ConfigSchema = Schema.Struct({
495
+ version: Schema.Literal(2),
496
+ activeProfile: Schema.optional(Schema.String),
497
+ profiles: Schema.Record(ProfileNameSchema, ProfileSchema)
498
+ });
499
+ const CredentialsSchema = Schema.Struct({
500
+ accessToken: Schema.String,
501
+ tokenType: Schema.String,
502
+ expiresAt: Schema.Finite,
503
+ scope: Schema.optional(Schema.String)
504
+ });
505
+ const emptyConfig = {
506
+ version: 2,
507
+ profiles: {}
508
+ };
509
+ const validateProfileName = (name) => Schema.decodeUnknownEffect(ProfileNameSchema)(name).pipe(Effect.mapError(() => new CliUsageError({
510
+ code: "invalid_profile_name",
511
+ message: "Profile names must be 1-64 characters and contain only letters, numbers, dots, underscores, or hyphens."
512
+ })));
513
+ const parseConfig = (raw, file) => Effect.gen(function* () {
514
+ const rawValue = yield* Effect.try({
515
+ try: () => JSON.parse(raw),
516
+ catch: () => new CliConfigError({
517
+ code: "invalid_config",
518
+ message: `Could not parse CLI config at ${file}. Fix or delete it.`
519
+ })
520
+ });
521
+ return yield* Schema.decodeUnknownEffect(ConfigSchema)(rawValue).pipe(Effect.mapError(() => new CliConfigError({
522
+ code: "invalid_config",
523
+ message: `Could not parse CLI config at ${file}. Fix or delete it.`
524
+ })));
525
+ });
526
+ const readConfig = Effect.gen(function* () {
527
+ const fs = yield* FileSystem.FileSystem;
528
+ const file = yield* configFilePath;
529
+ if (!(yield* fs.exists(file))) return emptyConfig;
530
+ return yield* parseConfig(yield* fs.readFileString(file), file);
531
+ });
532
+ const writeConfig = Effect.fnUntraced(function* (config) {
533
+ const fs = yield* FileSystem.FileSystem;
534
+ yield* fs.makeDirectory(yield* configDir, { recursive: true });
535
+ yield* fs.writeFileString(yield* configFilePath, `${JSON.stringify(config, null, 2)}\n`);
536
+ });
537
+ /** Resolve flags/profile/target/identity without reading or decoding a secret. */
538
+ const resolveExecutionSelection = Effect.gen(function* () {
539
+ const profileFlag = Option.getOrUndefined(yield* Profile);
540
+ const apiUrlFlag = Option.getOrUndefined(yield* ApiUrl);
541
+ const orgFlag = Option.getOrUndefined(yield* Org);
542
+ const config = yield* readConfig;
543
+ const profile = profileFlag ?? SAMVA_PROFILE ?? config.activeProfile ?? "default";
544
+ if (profile !== "default" && config.profiles[profile] === void 0) return yield* new CliConfigError({
545
+ code: "unknown_profile",
546
+ message: `Profile ${profile} does not exist. Create it with \`samva profile create ${profile}\`.`
547
+ });
548
+ const stored = config.profiles[profile] ?? {
549
+ target: "production",
550
+ credentialIdentity: "none"
551
+ };
552
+ const apiUrl = apiUrlFlag ?? SAMVA_API_URL ?? (stored.target === "local" ? LOCAL_API_URL : DEFAULT_API_URL);
553
+ const orgSlug = orgFlag ?? SAMVA_ORG ?? stored.orgSlug;
554
+ const apiKey = SAMVA_API_KEY && SAMVA_API_KEY.length > 0 ? SAMVA_API_KEY : void 0;
555
+ if (apiKey && (orgFlag !== void 0 || SAMVA_ORG !== void 0)) return yield* new CliAuthError({
556
+ code: "api_key_org_override",
557
+ message: "--org/SAMVA_ORG cannot override an organization-scoped SAMVA_API_KEY."
558
+ });
559
+ if (apiKey) return {
560
+ profile,
561
+ target: stored.target,
562
+ apiUrl,
563
+ apiKey
564
+ };
565
+ return {
566
+ profile,
567
+ target: stored.target,
568
+ apiUrl,
569
+ orgSlug,
570
+ profileOrgSlug: stored.orgSlug,
571
+ apiKey
572
+ };
573
+ });
574
+ const resolveExecutionContext = Effect.gen(function* () {
575
+ const selection = yield* resolveExecutionSelection;
576
+ if (selection.apiKey) return {
577
+ ...selection,
578
+ credentials: void 0
579
+ };
580
+ const rawCredentials = ((yield* readConfig).profiles[selection.profile] ?? {
581
+ target: "production",
582
+ credentialIdentity: "none"
583
+ }).credentialIdentity === "oauth" ? yield* readCredentialSecret(selection.profile) : void 0;
584
+ const credentials = rawCredentials ? yield* decodeStoredCredentials(selection.profile, rawCredentials) : void 0;
585
+ return {
586
+ ...selection,
587
+ credentials
588
+ };
589
+ });
590
+ /** Decode a keyring payload only after its non-secret profile selection is fixed. */
591
+ const decodeStoredCredentials = (profile, rawCredentials) => Effect.try({
592
+ try: () => JSON.parse(rawCredentials),
593
+ catch: () => new CliConfigError({
594
+ code: "invalid_credential_record",
595
+ message: `Stored credentials for profile ${profile} are invalid. Run \`samva logout\` then \`samva login\`.`
596
+ })
597
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(CredentialsSchema)), Effect.mapError(() => new CliConfigError({
598
+ code: "invalid_credential_record",
599
+ message: `Stored credentials for profile ${profile} are invalid. Run \`samva logout\` then \`samva login\`.`
600
+ })));
601
+ /** URL-only callers must not decode credentials before they begin recovery. */
602
+ const resolveApiUrl = Effect.map(resolveExecutionSelection, (selection) => selection.apiUrl);
603
+ /**
604
+ * OAuth access tokens carry a second-resolution expiry. Compared against the
605
+ * Effect `Clock` (not `Date.now`) so the check is deterministic under TestClock.
606
+ */
607
+ const isCredentialExpired = (credentials, nowMillis) => credentials.expiresAt <= Math.floor(nowMillis / 1e3);
608
+ Effect.gen(function* () {
609
+ const context = yield* resolveExecutionContext;
610
+ if (!context.credentials) return void 0;
611
+ if (isCredentialExpired(context.credentials, yield* Clock.currentTimeMillis)) return yield* new CliAuthError({
612
+ code: "expired_credentials",
613
+ message: "Your session has expired. Run `samva login` to sign in again."
614
+ });
615
+ return context.credentials.accessToken;
616
+ });
617
+ const writeCredentials = Effect.fnUntraced(function* (credentials) {
618
+ const context = yield* resolveExecutionSelection;
619
+ const config = yield* readConfig;
620
+ const current = config.profiles[context.profile] ?? {
621
+ target: context.target,
622
+ credentialIdentity: "none"
623
+ };
624
+ yield* writeConfig({
625
+ ...config,
626
+ profiles: {
627
+ ...config.profiles,
628
+ [context.profile]: {
629
+ ...current,
630
+ credentialIdentity: "oauth"
631
+ }
632
+ }
633
+ });
634
+ yield* writeCredentialSecret(context.profile, JSON.stringify(credentials));
635
+ });
636
+ const readCredentials = Effect.map(resolveExecutionContext, (context) => context.credentials);
637
+ const clearCredentials = Effect.gen(function* () {
638
+ const context = yield* resolveExecutionSelection;
639
+ const config = yield* readConfig;
640
+ const current = config.profiles[context.profile];
641
+ if (!current || current.credentialIdentity === "none") return;
642
+ yield* writeConfig({
643
+ ...config,
644
+ profiles: {
645
+ ...config.profiles,
646
+ [context.profile]: {
647
+ ...current,
648
+ credentialIdentity: "none"
649
+ }
650
+ }
651
+ });
652
+ yield* clearCredentialSecret(context.profile);
653
+ });
654
+ //#endregion
655
+ //#region src/lib/client.ts
656
+ const missingOrgMessage = "No active organization. Run `samva org use <slug>` after `samva login`, or use SAMVA_API_KEY for single-org automation.";
657
+ const provider = (config) => ({ provide: (effect) => Effect.provide(effect, Client.layerFetch(config)) });
658
+ /** Build an authenticated SDK operation provider for an explicit API base URL. */
659
+ const getClientProviderForContext = (context, baseUrl) => Effect.gen(function* () {
660
+ if (context.apiKey) return provider({
661
+ apiKey: context.apiKey,
662
+ baseUrl
663
+ });
664
+ if (context.credentials) {
665
+ if (isCredentialExpired(context.credentials, yield* Clock.currentTimeMillis)) return yield* new CliAuthError({
666
+ code: "expired_credentials",
667
+ message: "Your session has expired. Run `samva login` to sign in again."
668
+ });
669
+ const authToken = context.credentials.accessToken;
670
+ const activeOrgSlug = context.orgSlug;
671
+ if (!activeOrgSlug) return yield* new CliAuthError({
672
+ code: "missing_organization",
673
+ message: missingOrgMessage
674
+ });
675
+ return provider({
676
+ authToken,
677
+ baseUrl,
678
+ headers: { "x-org-slug": activeOrgSlug }
679
+ });
680
+ }
681
+ return yield* new CliAuthError({
682
+ code: "missing_credentials",
683
+ message: "No credentials found. Run `samva login`, or set SAMVA_API_KEY for automation."
684
+ });
685
+ });
686
+ /**
687
+ * Build an authenticated Samva SDK operation provider. This is the single seam all command
688
+ * handlers go through. `SAMVA_API_KEY` takes precedence over stored OAuth
689
+ * credentials. OAuth is the interactive fallback; its user-scoped token sends
690
+ * the active org (`samva org use`) as the `x-org-slug` header.
691
+ */
692
+ const getClientProvider = Effect.flatMap(resolveExecutionContext, (context) => getClientProviderForContext(context, context.apiUrl));
693
+ //#endregion
694
+ //#region src/lib/confirm.ts
695
+ /** Confirm a destructive action in a real TTY; automation must pass --yes. */
696
+ const confirmDestructive = (options) => Effect.gen(function* () {
697
+ if (options.yes) return;
698
+ if ((yield* NoInput) || options.json || !isatty(0) || !isatty(1)) return yield* new CliUsageError({
699
+ code: "confirmation_required",
700
+ message: options.nonInteractiveMessage
701
+ });
702
+ if (!(yield* Prompt.run(Prompt.confirm({
703
+ message: options.message,
704
+ initial: false
705
+ })))) return yield* new CliUsageError({
706
+ code: "confirmation_declined",
707
+ message: "Cancelled; no changes were made."
708
+ });
709
+ });
710
+ //#endregion
711
+ //#region src/lib/output.ts
712
+ const CLI_SCHEMA_VERSION = 1;
713
+ const resultEnvelope = (command, data, partial) => ({
714
+ schemaVersion: CLI_SCHEMA_VERSION,
715
+ type: "result",
716
+ command,
717
+ data,
718
+ partial
719
+ });
720
+ const errorEnvelope = (error, command, message) => ({
721
+ schemaVersion: CLI_SCHEMA_VERSION,
722
+ type: "error",
723
+ command,
724
+ reason: processErrorReason(error),
725
+ message,
726
+ next: processErrorNext(error)
727
+ });
728
+ /**
729
+ * Shared `--json` flag for read commands. Hidden from help because the global
730
+ * `--json` (see {@link Json} in lib/runtime) already documents it — otherwise it
731
+ * lists twice, e.g. `samva org current --help`. Parsing behavior is unchanged.
732
+ */
733
+ const jsonFlag = Flag.boolean("json").pipe(Flag.withDefault(isJsonRequested() || isJsonLinesRequested()), Flag.withDescription("Output machine-readable JSON instead of formatted text"), Flag.withHidden);
734
+ /** Print a value as pretty-printed JSON. */
735
+ const printJson = (value) => {
736
+ if (isJsonLinesRequested()) return Effect.sync(() => process.stdout.write(`${JSON.stringify(resultEnvelope(commandNameFromArgv(process.argv.slice(2)), value))}\n`));
737
+ return Console.log(JSON.stringify(value, null, 2));
738
+ };
739
+ /** A stable diagnostics schema paired with the versioned result envelope. */
740
+ const printJsonError = (error, message) => {
741
+ const payload = errorEnvelope(error, commandNameFromArgv(process.argv.slice(2)), message);
742
+ return Console.error(JSON.stringify(payload, null, isJsonLinesRequested() ? void 0 : 2));
743
+ };
744
+ /** NDJSON is intentionally unbuffered and versioned, one outer envelope per resource. */
745
+ const printJsonLine = (value) => Effect.sync(() => process.stdout.write(`${JSON.stringify(resultEnvelope(commandNameFromArgv(process.argv.slice(2)), value))}\n`));
746
+ const printResult = (value, human) => isJsonRequested() || isJsonLinesRequested() ? printJson(value) : Console.log(human);
747
+ /**
748
+ * Whether to emit ANSI styling. `Bun.enableANSIColors` already accounts for a
749
+ * TTY, `NO_COLOR`, and `FORCE_COLOR`, so styling stays off when stdout is piped
750
+ * or the machine path is in use. Read lazily (not at module load) so importing
751
+ * this module stays free of runtime-global side effects — process e2e tests run
752
+ * the CLI under Bun, but unit tests import these helpers under Node/vitest.
753
+ */
754
+ const colorsEnabled = () => Bun.enableANSIColors;
755
+ const sgr = (open, close) => (text) => colorsEnabled() ? `\x1b[${open}m${text}\x1b[${close}m` : text;
756
+ /** Dim text (gated on {@link colorsEnabled}). */
757
+ const dim = sgr(2, 22);
758
+ const MAX_DUMP_LINES = 40;
759
+ const MAX_DUMP_CHARS = 4e3;
760
+ /**
761
+ * Bound a large human-facing text dump (an email body, a template source, a
762
+ * payload preview) so a huge input can't flood the terminal or an agent's
763
+ * context. Appends an explicit truncation tail stating what was dropped. The
764
+ * `--json` path is never truncated — an agent that asked for the payload gets
765
+ * all of it.
766
+ */
767
+ const truncateForDisplay = (text, maxLines = MAX_DUMP_LINES, maxChars = MAX_DUMP_CHARS) => {
768
+ let out = text;
769
+ const lines = out.split("\n");
770
+ if (lines.length > maxLines) out = `${lines.slice(0, maxLines).join("\n")}\n${dim(`… ${lines.length - maxLines} more line(s) truncated`)}`;
771
+ if (out.length > maxChars) out = `${out.slice(0, maxChars)}\n${dim(`… truncated at ${maxChars} characters`)}`;
772
+ return out;
773
+ };
774
+ /** Print aligned `Label: value` lines, skipping nullish/empty values. */
775
+ const printFields = (fields) => {
776
+ const visible = fields.filter(([, value]) => value !== null && value !== void 0 && value !== "");
777
+ const width = Math.max(0, ...visible.map(([label]) => Bun.stringWidth(label)));
778
+ const lines = visible.map(([label, value]) => {
779
+ return `${label}${" ".repeat(Math.max(0, width - Bun.stringWidth(label)))} ${String(value)}`;
780
+ });
781
+ return Console.log(lines.join("\n"));
782
+ };
783
+ //#endregion
784
+ //#region src/commands/campaigns.ts
785
+ const CAMPAIGN_STATUSES = ["draft", "archived"];
786
+ const RUN_STATUSES = [
787
+ "scheduled",
788
+ "resolving",
789
+ "dispatching",
790
+ "paused",
791
+ "completed",
792
+ "cancelled",
793
+ "failed"
794
+ ];
795
+ const RECIPIENT_STATUSES = [
796
+ "pending",
797
+ "dispatching",
798
+ "dispatched",
799
+ "failed",
800
+ "skipped"
801
+ ];
802
+ const ABSOLUTE_INSTANT_PATTERN$1 = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
803
+ const isAbsoluteInstant$1 = (value) => {
804
+ const match = ABSOLUTE_INSTANT_PATTERN$1.exec(value);
805
+ if (!match || Number.isNaN(Date.parse(value))) return false;
806
+ const year = Number(match[1]);
807
+ const month = Number(match[2]);
808
+ const day = Number(match[3]);
809
+ const calendarDate = new Date(Date.UTC(year, month - 1, day));
810
+ return calendarDate.getUTCFullYear() === year && calendarDate.getUTCMonth() === month - 1 && calendarDate.getUTCDate() === day;
811
+ };
812
+ /**
813
+ * Flatten repeated and comma-separated flag values into a clean tag list.
814
+ * `--include-tags a,b --include-tags c` and `--include-tags a --include-tags b`
815
+ * both collapse to `["a", "b", ...]`. Pure so it can be unit-tested.
816
+ */
817
+ const parseTagList = (values) => values.flatMap((value) => value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0));
818
+ /**
819
+ * Assemble the `campaigns.create` email payload from resolved flags. Pure so the
820
+ * flag-to-request mapping can be unit-tested without the network.
821
+ */
822
+ const assembleCampaignCreatePayload = (input) => {
823
+ const hasPolicy = input.unsubscribeGroupId !== void 0 || input.pageSize !== void 0;
824
+ return {
825
+ name: input.name,
826
+ channel: "email",
827
+ content: {
828
+ channel: "email",
829
+ email: {
830
+ subject: input.subject,
831
+ html: input.html,
832
+ text: input.text,
833
+ templateId: input.templateId,
834
+ templateSlug: input.templateSlug,
835
+ templateData: input.templateData,
836
+ ...input.tracking === void 0 ? {} : { tracking: input.tracking }
837
+ }
838
+ },
839
+ audience: {
840
+ includeTags: input.includeTags.length > 0 ? [...input.includeTags] : void 0,
841
+ excludeTags: input.excludeTags.length > 0 ? [...input.excludeTags] : void 0,
842
+ contactIds: input.contactIds.length > 0 ? [...input.contactIds] : void 0
843
+ },
844
+ policy: hasPolicy ? {
845
+ unsubscribeGroupId: input.unsubscribeGroupId,
846
+ pageSize: input.pageSize
847
+ } : void 0
848
+ };
849
+ };
850
+ const parseTemplateData$2 = (raw) => Option.match(raw, {
851
+ onNone: () => Effect.succeed(void 0),
852
+ onSome: (value) => Effect.try({
853
+ try: () => JSON.parse(value),
854
+ catch: () => new SamvaCliError({ message: "--template-data must be a JSON object." })
855
+ }).pipe(Effect.flatMap((parsed) => Predicate.isObject(parsed) ? Effect.succeed(parsed) : Effect.fail(new SamvaCliError({ message: "--template-data must be a JSON object." }))))
856
+ });
857
+ const nameFlag = Flag.string("name").pipe(Flag.withDescription("Campaign name"));
858
+ const subjectFlag$2 = Flag.string("subject").pipe(Flag.withDescription("Email subject line"), Flag.optional);
859
+ const htmlFlag$2 = Flag.string("html").pipe(Flag.withDescription("HTML body"), Flag.optional);
860
+ const textFlag$2 = Flag.string("text").pipe(Flag.withDescription("Plain-text body"), Flag.optional);
861
+ const templateIdFlag$2 = Flag.string("template-id").pipe(Flag.withDescription("Template id (uuid) to render instead of inline body"), Flag.optional);
862
+ const templateSlugFlag$2 = Flag.string("template-slug").pipe(Flag.withDescription("Template handle to render instead of inline body, e.g. welcome-email"), Flag.optional);
863
+ const templateDataFlag$2 = Flag.string("template-data").pipe(Flag.withDescription("JSON object of variables for the template"), Flag.optional);
864
+ const includeTagsFlag = Flag.string("include-tags").pipe(Flag.withDescription("Contact tags to include (comma-separated or repeated)"), Flag.atLeast(0));
865
+ const excludeTagsFlag = Flag.string("exclude-tags").pipe(Flag.withDescription("Contact tags to exclude from the include set (comma-separated or repeated)"), Flag.atLeast(0));
866
+ const contactIdsFlag = Flag.string("contact-ids").pipe(Flag.withDescription("Explicit contact ids to target (comma-separated or repeated)"), Flag.atLeast(0));
867
+ const unsubscribeGroupIdFlag = Flag.string("unsubscribe-group-id").pipe(Flag.withDescription("Unsubscribe group id applied to every send"), Flag.optional);
868
+ const pageSizeFlag = Flag.integer("page-size").pipe(Flag.withDescription("Recipients dispatched per batch"), Flag.optional);
869
+ const dryRunFlag$4 = Flag.boolean("dry-run").pipe(Flag.withDescription("Validate inputs and print the request payload without calling the API"), Flag.withDefault(false));
870
+ const trackOpensFlag$2 = Flag.choice("track-opens", ["on", "off"]).pipe(Flag.withDescription("Override open tracking for campaign sends; recipient opt-out wins"), Flag.optional);
871
+ const trackClicksFlag$2 = Flag.choice("track-clicks", ["on", "off"]).pipe(Flag.withDescription("Override click tracking for campaign sends; recipient opt-out wins"), Flag.optional);
872
+ const atFlag$1 = Flag.string("at").pipe(Flag.withDescription("Absolute run start instant as ISO 8601 (defaults to now)"), Flag.optional);
873
+ const idempotencyKeyFlag$2 = Flag.string("idempotency-key").pipe(Flag.withDescription("Organization-scoped key so retries return the original run"), Flag.optional);
874
+ const campaignStatusFlag = Flag.choice("status", CAMPAIGN_STATUSES).pipe(Flag.withDescription("Filter by status"), Flag.optional);
875
+ const runStatusFlag = Flag.choice("status", RUN_STATUSES).pipe(Flag.withDescription("Filter by run status"), Flag.optional);
876
+ const recipientStatusFlag = Flag.choice("status", RECIPIENT_STATUSES).pipe(Flag.withDescription("Filter by recipient status"), Flag.optional);
877
+ const pageFlag$2 = Flag.integer("page").pipe(Flag.withDescription("Page number (1-based)"), Flag.optional);
878
+ const limitFlag$2 = Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional);
879
+ const allFlag$2 = Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false));
880
+ const create$3 = Command.make("create", {
881
+ name: nameFlag,
882
+ subject: subjectFlag$2,
883
+ html: htmlFlag$2,
884
+ text: textFlag$2,
885
+ templateId: templateIdFlag$2,
886
+ templateSlug: templateSlugFlag$2,
887
+ templateData: templateDataFlag$2,
888
+ includeTags: includeTagsFlag,
889
+ excludeTags: excludeTagsFlag,
890
+ contactIds: contactIdsFlag,
891
+ unsubscribeGroupId: unsubscribeGroupIdFlag,
892
+ pageSize: pageSizeFlag,
893
+ trackOpens: trackOpensFlag$2,
894
+ trackClicks: trackClicksFlag$2,
895
+ dryRun: dryRunFlag$4,
896
+ json: jsonFlag
897
+ }).pipe(Command.withDescription("Create an email campaign targeting tags or explicit contacts"), Command.withExamples([{
898
+ command: "samva campaigns create --name \"Spring launch\" --subject \"We are live\" --html \"<h1>Hi</h1>\" --include-tags newsletter,customers",
899
+ description: "Campaign to tagged contacts"
900
+ }, {
901
+ command: "samva campaigns create --name \"VIP note\" --subject Hi --html \"<p>Hello</p>\" --text Hello --contact-ids c_1 --contact-ids c_2 --dry-run",
902
+ description: "Target explicit contacts and preview the payload"
903
+ }]), Command.withHandler(({ name, subject, html, text, templateId, templateSlug, templateData, includeTags, excludeTags, contactIds, unsubscribeGroupId, pageSize, trackOpens, trackClicks, dryRun, json }) => Effect.gen(function* () {
904
+ const htmlValue = Option.getOrUndefined(html);
905
+ const textValue = Option.getOrUndefined(text);
906
+ const templateIdValue = Option.getOrUndefined(templateId);
907
+ const templateSlugValue = Option.getOrUndefined(templateSlug);
908
+ const templateDataValue = yield* parseTemplateData$2(templateData);
909
+ if (templateIdValue && templateSlugValue) return yield* new SamvaCliError({ message: "Use either --template-id or --template-slug, not both." });
910
+ const templateRefValue = templateSlugValue ?? templateIdValue;
911
+ if (templateRefValue && (Option.isSome(subject) || htmlValue || textValue)) return yield* new SamvaCliError({ message: "Choose one content source: a template, or inline --subject with --html (and optional --text)." });
912
+ if (!templateRefValue && Option.isNone(subject)) return yield* new SamvaCliError({ message: "Provide --subject with inline --html, or reference a published template." });
913
+ if (!templateRefValue && !htmlValue) return yield* new SamvaCliError({ message: "Provide --html for inline email; --text is an optional fallback." });
914
+ const includeTagsValue = parseTagList(includeTags);
915
+ const excludeTagsValue = parseTagList(excludeTags);
916
+ const contactIdsValue = parseTagList(contactIds);
917
+ if (includeTagsValue.length === 0 && contactIdsValue.length === 0) return yield* new SamvaCliError({ message: "Provide an audience: at least one of --include-tags or --contact-ids (--exclude-tags only narrows an include set)." });
918
+ const payload = assembleCampaignCreatePayload({
919
+ name,
920
+ subject: Option.getOrUndefined(subject),
921
+ html: htmlValue,
922
+ text: textValue,
923
+ templateId: templateIdValue,
924
+ templateSlug: templateSlugValue,
925
+ templateData: templateDataValue,
926
+ includeTags: includeTagsValue,
927
+ excludeTags: excludeTagsValue,
928
+ contactIds: contactIdsValue,
929
+ unsubscribeGroupId: Option.getOrUndefined(unsubscribeGroupId),
930
+ pageSize: Option.getOrUndefined(pageSize),
931
+ tracking: Option.isSome(trackOpens) || Option.isSome(trackClicks) ? {
932
+ ...Option.isSome(trackOpens) ? { opens: trackOpens.value === "on" } : {},
933
+ ...Option.isSome(trackClicks) ? { clicks: trackClicks.value === "on" } : {}
934
+ } : void 0
935
+ });
936
+ if (dryRun) {
937
+ if (json) return yield* printJson({
938
+ dryRun: true,
939
+ request: payload
940
+ });
941
+ yield* Effect.logInfo("Dry run — no campaign created.\n");
942
+ yield* printFields([
943
+ ["Name", name],
944
+ ["Include tags", includeTagsValue.join(", ")],
945
+ ["Exclude tags", excludeTagsValue.join(", ")],
946
+ ["Contact ids", contactIdsValue.join(", ")]
947
+ ]);
948
+ return yield* Console.log(`\nPayload:\n${truncateForDisplay(JSON.stringify(payload, null, 2))}`);
949
+ }
950
+ const client = yield* getClientProvider;
951
+ const data = yield* Campaigns.create(payload).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Failed to create campaign", error) })));
952
+ if (json) return yield* printJson(data);
953
+ yield* Effect.logInfo("Campaign created.\n");
954
+ yield* printFields([
955
+ ["ID", data.id],
956
+ ["Name", data.name],
957
+ ["Channel", data.channel],
958
+ ["Status", data.status]
959
+ ]);
960
+ yield* Effect.logInfo(`\nCreate a run: samva campaigns runs create ${data.id}`);
961
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
962
+ const list$7 = Command.make("list", {
963
+ status: campaignStatusFlag,
964
+ page: pageFlag$2,
965
+ limit: limitFlag$2,
966
+ all: allFlag$2,
967
+ json: jsonFlag
968
+ }).pipe(Command.withDescription("List campaigns"), Command.withExamples([
969
+ { command: "samva campaigns list" },
970
+ { command: "samva campaigns list --status draft --json" },
971
+ {
972
+ command: "samva campaigns list --all --jsonl",
973
+ description: "Stream every page as JSONL"
974
+ }
975
+ ]), Command.withHandler(({ status, page, limit, all, json }) => Effect.gen(function* () {
976
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
977
+ code: "all_requires_jsonl",
978
+ message: "--all is an unbounded traversal and requires --jsonl."
979
+ });
980
+ const pageValue = Option.getOrUndefined(page);
981
+ const limitValue = Option.getOrUndefined(limit);
982
+ const client = yield* getClientProvider;
983
+ const query = {
984
+ status: Option.getOrUndefined(status),
985
+ page: pageValue?.toString(),
986
+ limit: limitValue?.toString()
987
+ };
988
+ if (all) {
989
+ let nextPage = pageValue ?? 1;
990
+ for (;;) {
991
+ const data = yield* Campaigns.list({
992
+ ...query,
993
+ page: nextPage.toString()
994
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list campaigns", error) })));
995
+ for (const item of data.items) yield* printJsonLine(item);
996
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
997
+ nextPage += 1;
998
+ }
999
+ }
1000
+ const data = yield* Campaigns.list(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list campaigns", error) })));
1001
+ if (isJsonLinesRequested()) {
1002
+ for (const item of data.items) yield* printJsonLine(item);
1003
+ return;
1004
+ }
1005
+ if (json) return yield* printJson(data);
1006
+ if (data.items.length === 0) return yield* Console.log("No campaigns yet. Create one with `samva campaigns create`.");
1007
+ for (const campaign of data.items) {
1008
+ yield* printFields([
1009
+ ["ID", campaign.id],
1010
+ ["Name", campaign.name],
1011
+ ["Channel", campaign.channel],
1012
+ ["Status", campaign.status]
1013
+ ]);
1014
+ yield* Console.log("");
1015
+ }
1016
+ yield* Console.log(`Page ${String(data.pagination.page)} of ${String(data.pagination.totalPages)} (${String(data.pagination.total)} total)`);
1017
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1018
+ const printCampaign = (campaign) => printFields([
1019
+ ["ID", campaign.id],
1020
+ ["Name", campaign.name],
1021
+ ["Channel", campaign.channel],
1022
+ ["Status", campaign.status],
1023
+ ["Include tags", (campaign.audience.includeTags ?? []).join(", ")],
1024
+ ["Exclude tags", (campaign.audience.excludeTags ?? []).join(", ")],
1025
+ ["Contact ids", (campaign.audience.contactIds ?? []).join(", ")],
1026
+ ["Created", campaign.createdAt],
1027
+ ["Updated", campaign.updatedAt]
1028
+ ]);
1029
+ const get$3 = Command.make("get", {
1030
+ id: Argument.string("id"),
1031
+ json: jsonFlag
1032
+ }).pipe(Command.withDescription("Show a campaign by id"), Command.withExamples([{ command: "samva campaigns get camp_abc123" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
1033
+ const client = yield* getClientProvider;
1034
+ const data = yield* Campaigns.get({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not fetch campaign", error) })));
1035
+ if (json) return yield* printJson(data);
1036
+ yield* printCampaign(data);
1037
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1038
+ const update$1 = Command.make("update", {
1039
+ id: Argument.string("id"),
1040
+ name: Flag.string("name").pipe(Flag.withDescription("New campaign name"), Flag.optional),
1041
+ subject: subjectFlag$2,
1042
+ html: htmlFlag$2,
1043
+ text: textFlag$2,
1044
+ templateId: templateIdFlag$2,
1045
+ templateSlug: templateSlugFlag$2,
1046
+ templateData: templateDataFlag$2,
1047
+ includeTags: includeTagsFlag,
1048
+ excludeTags: excludeTagsFlag,
1049
+ contactIds: contactIdsFlag,
1050
+ unsubscribeGroupId: unsubscribeGroupIdFlag,
1051
+ pageSize: pageSizeFlag,
1052
+ trackOpens: trackOpensFlag$2,
1053
+ trackClicks: trackClicksFlag$2,
1054
+ json: jsonFlag
1055
+ }).pipe(Command.withDescription("Update a draft campaign before its first run"), Command.withExamples([{ command: "samva campaigns update camp_abc123 --name \"August launch\"" }, {
1056
+ command: "samva campaigns update camp_abc123 --template-slug august-launch --include-tags newsletter",
1057
+ description: "Replace content and audience while the definition is still editable"
1058
+ }]), Command.withHandler(({ id, name, subject, html, text, templateId, templateSlug, templateData, includeTags, excludeTags, contactIds, unsubscribeGroupId, pageSize, trackOpens, trackClicks, json }) => Effect.gen(function* () {
1059
+ const htmlValue = Option.getOrUndefined(html);
1060
+ const textValue = Option.getOrUndefined(text);
1061
+ const templateIdValue = Option.getOrUndefined(templateId);
1062
+ const templateSlugValue = Option.getOrUndefined(templateSlug);
1063
+ if (templateIdValue && templateSlugValue) return yield* new SamvaCliError({ message: "Use either --template-id or --template-slug, not both." });
1064
+ const templateDataValue = yield* parseTemplateData$2(templateData);
1065
+ const templateRefValue = templateSlugValue ?? templateIdValue;
1066
+ const hasContentFlags = Option.isSome(subject) || Boolean(htmlValue || textValue || templateRefValue) || Option.isSome(templateData) || Option.isSome(trackOpens) || Option.isSome(trackClicks);
1067
+ if (templateRefValue && (Option.isSome(subject) || htmlValue || textValue)) return yield* new SamvaCliError({ message: "Choose one content source: a template, or inline --subject with --html (and optional --text)." });
1068
+ if (hasContentFlags && !templateRefValue && Option.isNone(subject)) return yield* new SamvaCliError({ message: "Provide --subject with inline --html, or reference a published template." });
1069
+ if (hasContentFlags && !templateRefValue && !htmlValue) return yield* new SamvaCliError({ message: "Provide --html for inline email; --text is an optional fallback." });
1070
+ const includeTagsValue = parseTagList(includeTags);
1071
+ const excludeTagsValue = parseTagList(excludeTags);
1072
+ const contactIdsValue = parseTagList(contactIds);
1073
+ const hasAudience = includeTagsValue.length > 0 || excludeTagsValue.length > 0 || contactIdsValue.length > 0;
1074
+ const hasPolicy = Option.isSome(unsubscribeGroupId) || Option.isSome(pageSize);
1075
+ const hasContent = hasContentFlags;
1076
+ const tracking = Option.isSome(trackOpens) || Option.isSome(trackClicks) ? {
1077
+ ...Option.isSome(trackOpens) ? { opens: trackOpens.value === "on" } : {},
1078
+ ...Option.isSome(trackClicks) ? { clicks: trackClicks.value === "on" } : {}
1079
+ } : void 0;
1080
+ if (hasAudience && includeTagsValue.length === 0 && contactIdsValue.length === 0) return yield* new SamvaCliError({ message: "A replacement audience requires --include-tags or --contact-ids; --exclude-tags only narrows it." });
1081
+ if (!Option.isSome(name) && !hasContent && !hasAudience && !hasPolicy) return yield* new SamvaCliError({ message: "Provide at least one field to update. Run `samva campaigns update --help`." });
1082
+ const payload = {
1083
+ name: Option.getOrUndefined(name),
1084
+ content: hasContent ? {
1085
+ channel: "email",
1086
+ email: {
1087
+ subject: Option.getOrUndefined(subject),
1088
+ html: htmlValue,
1089
+ text: textValue,
1090
+ templateId: templateIdValue,
1091
+ templateSlug: templateSlugValue,
1092
+ templateData: templateDataValue,
1093
+ ...tracking === void 0 ? {} : { tracking }
1094
+ }
1095
+ } : void 0,
1096
+ audience: hasAudience ? {
1097
+ includeTags: includeTagsValue.length > 0 ? includeTagsValue : void 0,
1098
+ excludeTags: excludeTagsValue.length > 0 ? excludeTagsValue : void 0,
1099
+ contactIds: contactIdsValue.length > 0 ? contactIdsValue : void 0
1100
+ } : void 0,
1101
+ policy: hasPolicy ? {
1102
+ unsubscribeGroupId: Option.getOrUndefined(unsubscribeGroupId),
1103
+ pageSize: Option.getOrUndefined(pageSize)
1104
+ } : void 0
1105
+ };
1106
+ const client = yield* getClientProvider;
1107
+ const data = yield* Campaigns.update({
1108
+ id,
1109
+ ...payload
1110
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not update campaign", error) })));
1111
+ if (json) return yield* printJson(data);
1112
+ yield* Effect.logInfo("Campaign updated. Definitions freeze after the first run.\n");
1113
+ yield* printCampaign(data);
1114
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1115
+ const archive = Command.make("archive", {
1116
+ id: Argument.string("id"),
1117
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
1118
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
1119
+ json: jsonFlag
1120
+ }).pipe(Command.withDescription("Archive a campaign"), Command.withExamples([{ command: "samva campaigns archive camp_abc123 --dry-run" }, { command: "samva campaigns archive camp_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
1121
+ if (dryRun) return yield* json ? printJson({
1122
+ dryRun: true,
1123
+ action: "campaigns.archive",
1124
+ id
1125
+ }) : Console.log(`[dry-run] Would archive campaign ${id}.`);
1126
+ yield* confirmDestructive({
1127
+ yes,
1128
+ json,
1129
+ message: `Archive campaign ${id}? It will no longer be eligible to run.`,
1130
+ nonInteractiveMessage: "Campaign archival requires --yes in non-interactive mode. Preview with --dry-run."
1131
+ });
1132
+ const client = yield* getClientProvider;
1133
+ const data = yield* Campaigns.archive({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not archive campaign", error) })));
1134
+ if (json) return yield* printJson(data);
1135
+ yield* Effect.logInfo("Campaign archived.\n");
1136
+ yield* printFields([
1137
+ ["ID", data.id],
1138
+ ["Name", data.name],
1139
+ ["Status", data.status]
1140
+ ]);
1141
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1142
+ const printRun = (run) => printFields([
1143
+ ["Run ID", run.id],
1144
+ ["Campaign", run.campaignId],
1145
+ ["Status", run.status],
1146
+ ["Scheduled for", run.scheduledFor],
1147
+ ["Started", run.startedAt],
1148
+ ["Completed", run.completedAt],
1149
+ ["Recipients", String(run.totalRecipients)],
1150
+ ["Dispatched", String(run.dispatchedCount)],
1151
+ ["Failed", String(run.failedCount)],
1152
+ ["Skipped", String(run.skippedCount)],
1153
+ ["Failure", run.failureReason]
1154
+ ]);
1155
+ const runCreate = Command.make("create", {
1156
+ campaignId: Argument.string("campaignId"),
1157
+ at: atFlag$1,
1158
+ idempotencyKey: idempotencyKeyFlag$2,
1159
+ dryRun: dryRunFlag$4,
1160
+ json: jsonFlag
1161
+ }).pipe(Command.withDescription("Create a run for a campaign (starts immediately without --at)"), Command.withExamples([{
1162
+ command: "samva campaigns runs create camp_abc123",
1163
+ description: "Start now"
1164
+ }, {
1165
+ command: "samva campaigns runs create camp_abc123 --at 2026-08-01T09:00:00Z",
1166
+ description: "Start at a future time"
1167
+ }]), Command.withHandler(({ campaignId, at, idempotencyKey, dryRun, json }) => Effect.gen(function* () {
1168
+ const atValue = Option.getOrUndefined(at);
1169
+ if (atValue !== void 0 && !isAbsoluteInstant$1(atValue)) return yield* new SamvaCliError({ message: "--at must be an absolute ISO 8601 instant with Z or an offset, e.g. 2026-08-01T09:00:00Z." });
1170
+ const idempotencyKeyValue = Option.getOrUndefined(idempotencyKey);
1171
+ if (dryRun) return yield* json ? printJson({
1172
+ dryRun: true,
1173
+ action: "campaigns.runs.create",
1174
+ campaignId,
1175
+ scheduledFor: atValue,
1176
+ idempotencyKey: idempotencyKeyValue
1177
+ }) : Console.log(`[dry-run] Would create a run for campaign ${campaignId}${atValue ? ` at ${atValue}` : " now"}.`);
1178
+ const client = yield* getClientProvider;
1179
+ const data = yield* Campaigns.scheduleRun({
1180
+ id: campaignId,
1181
+ scheduledFor: atValue === void 0 ? void 0 : new Date(atValue),
1182
+ idempotencyKey: idempotencyKeyValue
1183
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not schedule run", error) })));
1184
+ if (json) return yield* printJson(data);
1185
+ yield* Effect.logInfo("Run scheduled.\n");
1186
+ yield* printRun(data);
1187
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1188
+ const runList = Command.make("list", {
1189
+ campaignId: Argument.string("campaignId"),
1190
+ status: runStatusFlag,
1191
+ page: pageFlag$2,
1192
+ limit: limitFlag$2,
1193
+ all: allFlag$2,
1194
+ json: jsonFlag
1195
+ }).pipe(Command.withDescription("List runs for a campaign"), Command.withExamples([{ command: "samva campaigns runs list camp_abc123" }]), Command.withHandler(({ campaignId, status, page, limit, all, json }) => Effect.gen(function* () {
1196
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
1197
+ code: "all_requires_jsonl",
1198
+ message: "--all is an unbounded traversal and requires --jsonl."
1199
+ });
1200
+ const pageValue = Option.getOrUndefined(page);
1201
+ const limitValue = Option.getOrUndefined(limit);
1202
+ const client = yield* getClientProvider;
1203
+ const query = {
1204
+ id: campaignId,
1205
+ status: Option.getOrUndefined(status),
1206
+ page: pageValue?.toString(),
1207
+ limit: limitValue?.toString()
1208
+ };
1209
+ if (all) {
1210
+ let nextPage = pageValue ?? 1;
1211
+ for (;;) {
1212
+ const data = yield* Campaigns.listRuns({
1213
+ ...query,
1214
+ page: nextPage.toString()
1215
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list runs", error) })));
1216
+ for (const item of data.items) yield* printJsonLine(item);
1217
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
1218
+ nextPage += 1;
1219
+ }
1220
+ }
1221
+ const data = yield* Campaigns.listRuns(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list runs", error) })));
1222
+ if (isJsonLinesRequested()) {
1223
+ for (const item of data.items) yield* printJsonLine(item);
1224
+ return;
1225
+ }
1226
+ if (json) return yield* printJson(data);
1227
+ if (data.items.length === 0) return yield* Console.log("No runs yet. Create one with `samva campaigns runs create`.");
1228
+ for (const run of data.items) {
1229
+ yield* printFields([
1230
+ ["Run ID", run.id],
1231
+ ["Status", run.status],
1232
+ ["Scheduled for", run.scheduledFor],
1233
+ ["Dispatched", String(run.dispatchedCount)],
1234
+ ["Failed", String(run.failedCount)],
1235
+ ["Skipped", String(run.skippedCount)]
1236
+ ]);
1237
+ yield* Console.log("");
1238
+ }
1239
+ yield* Console.log(`Page ${String(data.pagination.page)} of ${String(data.pagination.totalPages)} (${String(data.pagination.total)} total)`);
1240
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1241
+ const runGet = Command.make("get", {
1242
+ campaignId: Argument.string("campaignId"),
1243
+ runId: Argument.string("runId"),
1244
+ json: jsonFlag
1245
+ }).pipe(Command.withDescription("Show a campaign run"), Command.withExamples([{ command: "samva campaigns runs get camp_abc123 run_def456" }]), Command.withHandler(({ campaignId, runId, json }) => Effect.gen(function* () {
1246
+ const client = yield* getClientProvider;
1247
+ const data = yield* Campaigns.getRun({
1248
+ id: campaignId,
1249
+ runId
1250
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not fetch run", error) })));
1251
+ if (json) return yield* printJson(data);
1252
+ yield* printRun(data);
1253
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1254
+ const runPause = Command.make("pause", {
1255
+ campaignId: Argument.string("campaignId"),
1256
+ runId: Argument.string("runId"),
1257
+ dryRun: dryRunFlag$4,
1258
+ json: jsonFlag
1259
+ }).pipe(Command.withDescription("Pause a running campaign run"), Command.withExamples([{ command: "samva campaigns runs pause camp_abc123 run_def456" }]), Command.withHandler(({ campaignId, runId, dryRun, json }) => Effect.gen(function* () {
1260
+ if (dryRun) return yield* json ? printJson({
1261
+ dryRun: true,
1262
+ action: "campaigns.runs.pause",
1263
+ campaignId,
1264
+ runId
1265
+ }) : Console.log(`[dry-run] Would pause campaign run ${runId}.`);
1266
+ const client = yield* getClientProvider;
1267
+ const data = yield* Campaigns.pauseRun({
1268
+ id: campaignId,
1269
+ runId
1270
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not pause run", error) })));
1271
+ if (json) return yield* printJson(data);
1272
+ yield* Effect.logInfo("Run paused.\n");
1273
+ yield* printRun(data.item);
1274
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1275
+ const runResume = Command.make("resume", {
1276
+ campaignId: Argument.string("campaignId"),
1277
+ runId: Argument.string("runId"),
1278
+ dryRun: dryRunFlag$4,
1279
+ json: jsonFlag
1280
+ }).pipe(Command.withDescription("Resume a paused campaign run"), Command.withExamples([{ command: "samva campaigns runs resume camp_abc123 run_def456" }]), Command.withHandler(({ campaignId, runId, dryRun, json }) => Effect.gen(function* () {
1281
+ if (dryRun) return yield* json ? printJson({
1282
+ dryRun: true,
1283
+ action: "campaigns.runs.resume",
1284
+ campaignId,
1285
+ runId
1286
+ }) : Console.log(`[dry-run] Would resume campaign run ${runId}.`);
1287
+ const client = yield* getClientProvider;
1288
+ const data = yield* Campaigns.resumeRun({
1289
+ id: campaignId,
1290
+ runId
1291
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not resume run", error) })));
1292
+ if (json) return yield* printJson(data);
1293
+ yield* Effect.logInfo("Run resumed.\n");
1294
+ yield* printRun(data.item);
1295
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1296
+ const runCancel = Command.make("cancel", {
1297
+ campaignId: Argument.string("campaignId"),
1298
+ runId: Argument.string("runId"),
1299
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
1300
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
1301
+ json: jsonFlag
1302
+ }).pipe(Command.withDescription("Cancel a campaign run (best effort once dispatch has started; terminal when scheduled)"), Command.withExamples([{ command: "samva campaigns runs cancel camp_abc123 run_def456 --dry-run" }, { command: "samva campaigns runs cancel camp_abc123 run_def456 --yes" }]), Command.withHandler(({ campaignId, runId, dryRun, yes, json }) => Effect.gen(function* () {
1303
+ if (dryRun) return yield* json ? printJson({
1304
+ dryRun: true,
1305
+ action: "campaigns.runs.cancel",
1306
+ campaignId,
1307
+ runId
1308
+ }) : Console.log(`[dry-run] Would cancel campaign run ${runId}.`);
1309
+ yield* confirmDestructive({
1310
+ yes,
1311
+ json,
1312
+ message: `Cancel campaign run ${runId}? Dispatches already in progress may still complete.`,
1313
+ nonInteractiveMessage: "Campaign run cancellation requires --yes in non-interactive mode. Preview with --dry-run."
1314
+ });
1315
+ const client = yield* getClientProvider;
1316
+ const data = yield* Campaigns.cancelRun({
1317
+ id: campaignId,
1318
+ runId
1319
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not cancel run", error) })));
1320
+ if (json) return yield* printJson(data);
1321
+ yield* Effect.logInfo("Run cancelled.\n");
1322
+ yield* printRun(data.item);
1323
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1324
+ const runRecipients = Command.make("recipients", {
1325
+ campaignId: Argument.string("campaignId"),
1326
+ runId: Argument.string("runId"),
1327
+ status: recipientStatusFlag,
1328
+ page: pageFlag$2,
1329
+ limit: limitFlag$2,
1330
+ all: allFlag$2,
1331
+ json: jsonFlag
1332
+ }).pipe(Command.withDescription("List recipients for a campaign run"), Command.withExamples([{ command: "samva campaigns runs recipients camp_abc123 run_def456" }, {
1333
+ command: "samva campaigns runs recipients camp_abc123 run_def456 --status failed",
1334
+ description: "Only failed recipients"
1335
+ }]), Command.withHandler(({ campaignId, runId, status, page, limit, all, json }) => Effect.gen(function* () {
1336
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
1337
+ code: "all_requires_jsonl",
1338
+ message: "--all is an unbounded traversal and requires --jsonl."
1339
+ });
1340
+ const pageValue = Option.getOrUndefined(page);
1341
+ const limitValue = Option.getOrUndefined(limit);
1342
+ const client = yield* getClientProvider;
1343
+ const query = {
1344
+ id: campaignId,
1345
+ runId,
1346
+ status: Option.getOrUndefined(status),
1347
+ page: pageValue?.toString(),
1348
+ limit: limitValue?.toString()
1349
+ };
1350
+ if (all) {
1351
+ let nextPage = pageValue ?? 1;
1352
+ for (;;) {
1353
+ const data = yield* Campaigns.listRecipients({
1354
+ ...query,
1355
+ page: nextPage.toString()
1356
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list recipients", error) })));
1357
+ for (const item of data.items) yield* printJsonLine(item);
1358
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
1359
+ nextPage += 1;
1360
+ }
1361
+ }
1362
+ const data = yield* Campaigns.listRecipients(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list recipients", error) })));
1363
+ if (isJsonLinesRequested()) {
1364
+ for (const item of data.items) yield* printJsonLine(item);
1365
+ return;
1366
+ }
1367
+ if (json) return yield* printJson(data);
1368
+ if (data.items.length === 0) return yield* Console.log("No recipients for this run.");
1369
+ for (const recipient of data.items) {
1370
+ yield* printFields([
1371
+ ["Contact", recipient.contactId],
1372
+ ["Status", recipient.status],
1373
+ ["Message ID", recipient.messageId],
1374
+ ["Skipped", recipient.skippedReason],
1375
+ ["Failure", recipient.failureReason]
1376
+ ]);
1377
+ yield* Console.log("");
1378
+ }
1379
+ yield* Console.log(`Page ${String(data.pagination.page)} of ${String(data.pagination.totalPages)} (${String(data.pagination.total)} total)`);
1380
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1381
+ const runsCommand = Command.make("runs").pipe(Command.withDescription("Manage campaign runs"), Command.withExamples([{
1382
+ command: "samva campaigns runs create camp_abc123",
1383
+ description: "Start a run now"
1384
+ }, {
1385
+ command: "samva campaigns runs list camp_abc123",
1386
+ description: "List runs"
1387
+ }]), Command.withSubcommands([
1388
+ runCreate,
1389
+ runList,
1390
+ runGet,
1391
+ runPause,
1392
+ runResume,
1393
+ runCancel,
1394
+ runRecipients
1395
+ ]));
1396
+ const campaignsCommand = Command.make("campaigns").pipe(Command.withDescription("Create and operate audience-targeted campaigns"), Command.withExamples([{
1397
+ command: "samva campaigns create --name \"Spring launch\" --subject Hi --html \"<h1>Hi</h1>\" --include-tags newsletter",
1398
+ description: "Create a campaign"
1399
+ }, {
1400
+ command: "samva campaigns runs create camp_abc123",
1401
+ description: "Create a run"
1402
+ }]), Command.withSubcommands([
1403
+ create$3,
1404
+ list$7,
1405
+ get$3,
1406
+ update$1,
1407
+ archive,
1408
+ runsCommand
1409
+ ]));
1410
+ //#endregion
1411
+ //#region src/commands/design.ts
1412
+ const fileFlag = Flag.file("file").pipe(Flag.withDescription("Path to the Tailwind-v4 @theme CSS file"), Flag.withDefault("theme.css"));
1413
+ const dryRunFlag$3 = Flag.boolean("dry-run").pipe(Flag.withDescription("Preview without updating the API (theme push may read current tokens)"), Flag.withDefault(false));
1414
+ const toMarkupFontToken = (token) => ({
1415
+ family: token.family,
1416
+ fallback: token.fallback,
1417
+ faces: token.faces == null ? void 0 : token.faces.map((face) => ({
1418
+ weight: face.weight,
1419
+ style: face.style,
1420
+ src: face.src,
1421
+ unicodeRange: face.unicodeRange ?? void 0
1422
+ }))
1423
+ });
1424
+ const toMarkupFontFamilies = (fontFamilies) => {
1425
+ const result = {};
1426
+ for (const [name, token] of Object.entries(fontFamilies)) result[name] = toMarkupFontToken(token);
1427
+ return result;
1428
+ };
1429
+ /**
1430
+ * The GET response folds the persisted font tokens onto the compiler's inline
1431
+ * stack string (the same `flattenFontTokens` transform apps/api and the web
1432
+ * canvas share), and every other namespace's `null` onto `undefined` so the
1433
+ * printer sees the ParsedTheme shape it expects.
1434
+ */
1435
+ const fromServerTheme = (theme) => ({
1436
+ colors: theme.colors ?? void 0,
1437
+ colorsDark: theme.colorsDark ?? void 0,
1438
+ radiusPx: theme.radiusPx ?? void 0,
1439
+ fontFamilies: theme.fontFamilies == null ? void 0 : flattenFontTokens(toMarkupFontFamilies(theme.fontFamilies)).fontFamilies,
1440
+ fontWeights: theme.fontWeights ?? void 0,
1441
+ text: theme.text ?? void 0,
1442
+ leading: theme.leading ?? void 0,
1443
+ spacingPx: theme.spacingPx ?? void 0,
1444
+ containerPx: theme.containerPx ?? void 0,
1445
+ tracking: theme.tracking ?? void 0
1446
+ });
1447
+ const FALLBACK_STACK_DEFAULT = "sans-serif";
1448
+ const unquote = (value) => value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1) : value;
1449
+ /** Split a flattened inline stack (`'Family', fallback, generic`) into the
1450
+ * leading family (quotes stripped) and the remaining fallback, for building a
1451
+ * system font token from a `--font-*` line that doesn't match any existing
1452
+ * server-hosted token. */
1453
+ const splitFontStack = (stack) => {
1454
+ const commaIndex = stack.indexOf(",");
1455
+ if (commaIndex === -1) return {
1456
+ family: unquote(stack.trim()),
1457
+ fallback: FALLBACK_STACK_DEFAULT
1458
+ };
1459
+ const family = unquote(stack.slice(0, commaIndex).trim());
1460
+ const fallback = stack.slice(commaIndex + 1).trim();
1461
+ return {
1462
+ family,
1463
+ fallback: fallback.length > 0 ? fallback : FALLBACK_STACK_DEFAULT
1464
+ };
1465
+ };
1466
+ /**
1467
+ * Resolve the PUT payload's `fontFamilies` from the parsed CSS against the
1468
+ * org's current server tokens. A `--font-*` line whose flattened value is
1469
+ * unchanged from the server's keeps that server token verbatim (preserving a
1470
+ * hosted google/custom token's faces); a new or edited line becomes a
1471
+ * `source: "system"` token built from the stack text. Names absent from the
1472
+ * parsed CSS are dropped — full-replace, matching every other namespace.
1473
+ */
1474
+ const buildPushFontFamilies = (parsedFontFamilies, serverFontFamilies) => {
1475
+ if (parsedFontFamilies === void 0) return {
1476
+ fontFamilies: void 0,
1477
+ notices: []
1478
+ };
1479
+ const serverFlattened = serverFontFamilies === void 0 ? {} : flattenFontTokens(toMarkupFontFamilies(serverFontFamilies)).fontFamilies;
1480
+ const notices = [];
1481
+ const result = {};
1482
+ for (const [name, stack] of Object.entries(parsedFontFamilies)) {
1483
+ const serverToken = serverFontFamilies?.[name];
1484
+ if (serverToken !== void 0 && serverFlattened[name] === stack) {
1485
+ result[name] = serverToken;
1486
+ continue;
1487
+ }
1488
+ if (serverToken !== void 0 && serverToken.source !== "system") notices.push(`Editing --font-${name} in CSS converts it to a system font stack; use the dashboard font picker for hosted/Google fonts.`);
1489
+ const { family, fallback } = splitFontStack(stack);
1490
+ result[name] = {
1491
+ source: "system",
1492
+ family,
1493
+ fallback
1494
+ };
1495
+ }
1496
+ return {
1497
+ fontFamilies: result,
1498
+ notices
1499
+ };
1500
+ };
1501
+ /** Fold ParsedTheme onto the persisted shape for the PUT payload. `fontFamilies`
1502
+ * comes from `buildPushFontFamilies` (the parsed stack strings alone can't
1503
+ * recover a hosted token's source/faces). */
1504
+ const toServerTheme = (parsed, fontFamilies) => {
1505
+ const tracking = parsed.tracking ?? parsed.trackingEm;
1506
+ return {
1507
+ ...parsed.colors === void 0 ? {} : { colors: parsed.colors },
1508
+ ...parsed.colorsDark === void 0 ? {} : { colorsDark: parsed.colorsDark },
1509
+ ...parsed.radiusPx === void 0 ? {} : { radiusPx: parsed.radiusPx },
1510
+ ...fontFamilies === void 0 ? {} : { fontFamilies },
1511
+ ...parsed.fontWeights === void 0 ? {} : { fontWeights: parsed.fontWeights },
1512
+ ...parsed.text === void 0 ? {} : { text: parsed.text },
1513
+ ...parsed.leading === void 0 ? {} : { leading: parsed.leading },
1514
+ ...parsed.spacingPx === void 0 ? {} : { spacingPx: parsed.spacingPx },
1515
+ ...parsed.containerPx === void 0 ? {} : { containerPx: parsed.containerPx },
1516
+ ...tracking === void 0 ? {} : { tracking }
1517
+ };
1518
+ };
1519
+ const pull$1 = Command.make("pull", {
1520
+ file: fileFlag,
1521
+ dryRun: dryRunFlag$3,
1522
+ json: jsonFlag
1523
+ }).pipe(Command.withDescription("Write the org's email theme to a Tailwind-v4 @theme CSS file"), Command.withExamples([{
1524
+ command: "samva email design pull",
1525
+ description: "Write the org theme to theme.css"
1526
+ }, {
1527
+ command: "samva email design pull --file emails/theme.css",
1528
+ description: "Write the org theme to a specific path"
1529
+ }]), Command.withHandler(({ file, dryRun, json }) => Effect.gen(function* () {
1530
+ const target = resolve(file);
1531
+ if (dryRun) return yield* json ? printJson({
1532
+ dryRun: true,
1533
+ action: "theme.pull",
1534
+ file: target
1535
+ }) : Console.log(`[dry-run] Would write the org theme to ${target}.`);
1536
+ const client = yield* getClientProvider;
1537
+ const theme = (yield* Design.get().pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not read design", error) })))).design?.theme ?? null;
1538
+ if (theme === null) {
1539
+ if (json) return yield* printJson({
1540
+ action: "design.pull",
1541
+ file: target,
1542
+ theme: null,
1543
+ written: false
1544
+ });
1545
+ return yield* Console.log("This org has no configured theme yet. Author one and run `samva email design push`.");
1546
+ }
1547
+ const css = printThemeCss(fromServerTheme(theme));
1548
+ yield* Effect.tryPromise({
1549
+ try: async () => {
1550
+ await mkdir(dirname(target), { recursive: true });
1551
+ await writeFile(target, css, "utf8");
1552
+ },
1553
+ catch: (cause) => new SamvaCliError({ message: `Could not write ${target}: ${String(cause)}` })
1554
+ });
1555
+ if (json) return yield* printJson({
1556
+ action: "design.pull",
1557
+ file: target,
1558
+ theme,
1559
+ written: true
1560
+ });
1561
+ yield* Effect.logInfo(`Wrote ${target}`);
1562
+ yield* Console.log(`\n${css}`);
1563
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1564
+ const push$1 = Command.make("push", {
1565
+ file: fileFlag,
1566
+ dryRun: dryRunFlag$3,
1567
+ json: jsonFlag
1568
+ }).pipe(Command.withDescription("Parse a @theme CSS file and set it as the org's email theme"), Command.withExamples([{
1569
+ command: "samva email design push",
1570
+ description: "Parse theme.css and set it as the org theme"
1571
+ }, {
1572
+ command: "samva email design push --dry-run",
1573
+ description: "Preview without updating the org theme (may read current tokens)"
1574
+ }]), Command.withHandler(({ file, dryRun, json }) => Effect.gen(function* () {
1575
+ const target = resolve(file);
1576
+ const css = yield* Effect.tryPromise({
1577
+ try: () => readFile(target, "utf8"),
1578
+ catch: () => new SamvaCliError({ message: `Could not read ${target}. Run \`samva email design pull\` first, or point --file at your @theme CSS.` })
1579
+ });
1580
+ const parsed = parseThemeCss(css);
1581
+ if (Result.isFailure(parsed)) {
1582
+ const diagnostics = parsed.failure;
1583
+ if (json) yield* printJson({
1584
+ action: "theme.push",
1585
+ file: target,
1586
+ ok: false,
1587
+ diagnostics: diagnostics.map((diagnostic) => ({
1588
+ code: diagnostic.code,
1589
+ message: diagnostic.message,
1590
+ span: diagnostic.span
1591
+ }))
1592
+ });
1593
+ else {
1594
+ yield* Effect.logWarning(`${target} has ${diagnostics.length} theme error(s):\n`);
1595
+ for (const diagnostic of diagnostics) yield* Effect.logWarning(` ✗ ${formatDiagnostic(css, diagnostic)}`);
1596
+ }
1597
+ return yield* new SamvaCliError({ message: `Theme not pushed — fix ${diagnostics.length} diagnostic(s) in ${target}.` });
1598
+ }
1599
+ const client = yield* getClientProvider;
1600
+ const current = yield* Design.get().pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not read design", error) })));
1601
+ const { fontFamilies, notices } = buildPushFontFamilies(parsed.success.fontFamilies, current.design?.theme?.fontFamilies ?? void 0);
1602
+ for (const notice of notices) yield* Effect.logWarning(notice);
1603
+ const theme = toServerTheme(parsed.success, fontFamilies);
1604
+ const design = {
1605
+ ...current.design,
1606
+ theme
1607
+ };
1608
+ if (dryRun) {
1609
+ if (json) return yield* printJson({
1610
+ action: "design.push",
1611
+ file: target,
1612
+ dryRun: true,
1613
+ theme
1614
+ });
1615
+ yield* Effect.logInfo(`Parsed ${target}`);
1616
+ return yield* Console.log(`\n${truncateForDisplay(JSON.stringify(theme, null, 2))}`);
1617
+ }
1618
+ yield* Design.update({ design }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not set design", error) })));
1619
+ if (json) return yield* printJson({
1620
+ action: "design.push",
1621
+ file: target,
1622
+ dryRun: false,
1623
+ theme
1624
+ });
1625
+ yield* Effect.logInfo(`✓ Set org theme from ${target}`);
1626
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1627
+ const designCommand = Command.make("design").pipe(Command.withDescription("Pull and push the org's email theme as Tailwind-v4 @theme CSS"), Command.withExamples([{
1628
+ command: "samva email design pull",
1629
+ description: "Write the org theme to theme.css"
1630
+ }, {
1631
+ command: "samva email design push --dry-run",
1632
+ description: "Parse theme.css and preview the tokens"
1633
+ }]), Command.withSubcommands([pull$1, push$1]));
1634
+ //#endregion
1635
+ //#region src/lib/http.ts
1636
+ const postJson = (url, payload) => Effect.tryPromise({
1637
+ try: async () => {
1638
+ const res = await fetch(url, {
1639
+ method: "POST",
1640
+ headers: { "content-type": "application/json" },
1641
+ body: JSON.stringify(payload)
1642
+ });
1643
+ return {
1644
+ ok: res.ok,
1645
+ status: res.status,
1646
+ body: await res.text()
1647
+ };
1648
+ },
1649
+ catch: (cause) => new SamvaCliError({ message: `Request to ${url} failed: ${String(cause)}` })
1650
+ });
1651
+ const getText = (url) => Effect.tryPromise({
1652
+ try: async () => {
1653
+ const res = await fetch(url);
1654
+ return {
1655
+ ok: res.ok,
1656
+ status: res.status,
1657
+ body: await res.text()
1658
+ };
1659
+ },
1660
+ catch: (cause) => new SamvaCliError({ message: `Request to ${url} failed: ${String(cause)}` })
1661
+ });
1662
+ //#endregion
1663
+ //#region src/commands/email/doctor.ts
1664
+ /** Surface the real failure text so a failed check is actionable, not uniform. */
1665
+ const detailFromError = (error) => Predicate.isObject(error) && Predicate.hasProperty(error, "message") && typeof error["message"] === "string" ? error["message"] : "Read failed";
1666
+ const check$2 = (name, run) => run.pipe(Effect.match({
1667
+ onFailure: (error) => ({
1668
+ name,
1669
+ status: "failed",
1670
+ detail: detailFromError(error)
1671
+ }),
1672
+ onSuccess: (detail) => ({
1673
+ name,
1674
+ status: "passed",
1675
+ detail
1676
+ })
1677
+ }));
1678
+ const reachability = (apiUrl) => check$2("API reachability", getText(`${apiUrl.replace(/\/+$/, "")}/v1/health/ping`).pipe(Effect.flatMap((response) => response.ok ? Effect.succeed("Health endpoint responded") : Effect.fail(new SamvaCliError({ message: `Health endpoint returned ${response.status}` })))));
1679
+ const orgAccess = check$2("Organization permissions", getClientProvider.pipe(Effect.flatMap((client) => Organizations.get().pipe(client.provide, Effect.map((organization) => `${organization.name} (${organization.slug})`), Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Organization read failed", error) }))))));
1680
+ const domains = check$2("Sending domains", getClientProvider.pipe(Effect.flatMap((client) => Email.listDomains({ limit: "1" }).pipe(client.provide, Effect.map((result) => {
1681
+ const verified = result.items.filter((domain) => domain.status === "verified").length;
1682
+ return `${result.pagination.total} configured; ${verified} verified on this page`;
1683
+ }), Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Domain read failed", error) }))))));
1684
+ const senders = check$2("Senders", getClientProvider.pipe(Effect.flatMap((client) => Email.listSenders({ limit: "1" }).pipe(client.provide, Effect.map((result) => {
1685
+ const verified = result.items.filter((sender) => sender.verificationStatus === "verified").length;
1686
+ return `${result.pagination.total} configured; ${verified} verified on this page`;
1687
+ }), Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Sender read failed", error) }))))));
1688
+ const domainVerificationReadback = getClientProvider.pipe(Effect.flatMap((client) => Email.listDomains({ limit: "1" }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Domain verification read failed", error) })), Effect.flatMap((result) => {
1689
+ const domain = result.items[0];
1690
+ if (!domain) return Effect.succeed({
1691
+ name: "Domain verification readback",
1692
+ status: "unavailable",
1693
+ detail: "No sending domain configured"
1694
+ });
1695
+ return Email.getDomainStatus({ id: domain.id }).pipe(client.provide, Effect.map((status) => {
1696
+ const verification = status.status.toLowerCase();
1697
+ return {
1698
+ name: "Domain verification readback",
1699
+ status: verification === "verified" ? "passed" : verification === "failed" ? "failed" : "unavailable",
1700
+ detail: `${status.domain}: ${status.status}`
1701
+ };
1702
+ }), Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Domain verification read failed", error) })));
1703
+ }))), Effect.catch((error) => Effect.succeed({
1704
+ name: "Domain verification readback",
1705
+ status: "failed",
1706
+ detail: detailFromError(error)
1707
+ })));
1708
+ const doctorCommand = Command.make("doctor", { json: jsonFlag }).pipe(Command.withDescription("Read local configuration and remote email state"), Command.withExamples([{ command: "samva email doctor --json" }]), Command.withHandler(({ json }) => Effect.gen(function* () {
1709
+ const context = yield* resolveExecutionContext;
1710
+ const authSource = context.apiKey ? "SAMVA_API_KEY" : context.credentials ? "stored OAuth credential" : "none";
1711
+ const checks = yield* Effect.all([
1712
+ reachability(context.apiUrl),
1713
+ orgAccess,
1714
+ domains,
1715
+ senders,
1716
+ domainVerificationReadback
1717
+ ]);
1718
+ const ok = checks.every((result) => result.status !== "failed");
1719
+ const payload = {
1720
+ schemaVersion: 1,
1721
+ mode: "read-only",
1722
+ ok,
1723
+ limitations: "Checks CLI/API visibility only. It does not send, mutate, replay fixtures, verify DNS, or verify provider readiness.",
1724
+ resolved: {
1725
+ profile: context.profile,
1726
+ target: context.target,
1727
+ apiUrl: context.apiUrl,
1728
+ authSource,
1729
+ organization: context.orgSlug ?? null
1730
+ },
1731
+ checks
1732
+ };
1733
+ if (!ok) yield* Effect.sync(() => void (process.exitCode = 1));
1734
+ if (json) return yield* printJson(payload);
1735
+ yield* printFields([
1736
+ ["Profile", payload.resolved.profile],
1737
+ ["Target", payload.resolved.target],
1738
+ ["API", payload.resolved.apiUrl],
1739
+ ["Auth source", payload.resolved.authSource],
1740
+ ["Organization", payload.resolved.organization]
1741
+ ]);
1742
+ yield* Console.log("\nRead-only checks:");
1743
+ for (const result of checks) {
1744
+ const glyph = result.status === "passed" ? "✓" : result.status === "unavailable" ? "!" : "✗";
1745
+ yield* Console.log(`${glyph} ${result.name}: ${result.detail}`);
1746
+ }
1747
+ yield* Console.log(`\n${payload.limitations}`);
1748
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1749
+ //#endregion
1750
+ //#region src/commands/email/domains.ts
1751
+ const list$6 = Command.make("list", {
1752
+ page: Flag.integer("page").pipe(Flag.withDescription("Page number"), Flag.optional),
1753
+ limit: Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional),
1754
+ all: Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false)),
1755
+ json: jsonFlag
1756
+ }).pipe(Command.withDescription("List email sending domains"), Command.withExamples([{ command: "samva email domains list" }, { command: "samva email domains list --json" }]), Command.withHandler(({ page, limit, all, json }) => Effect.gen(function* () {
1757
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
1758
+ code: "all_requires_jsonl",
1759
+ message: "--all is an unbounded traversal and requires --jsonl."
1760
+ });
1761
+ const client = yield* getClientProvider;
1762
+ const pageValue = Option.getOrUndefined(page);
1763
+ const limitValue = Option.getOrUndefined(limit);
1764
+ const query = {
1765
+ page: pageValue?.toString(),
1766
+ limit: limitValue?.toString()
1767
+ };
1768
+ if (all) {
1769
+ let nextPage = pageValue ?? 1;
1770
+ for (;;) {
1771
+ const data = yield* Email.listDomains({
1772
+ ...query,
1773
+ page: nextPage.toString()
1774
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list domains", error) })));
1775
+ for (const item of data.items) yield* printJsonLine(item);
1776
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
1777
+ nextPage += 1;
1778
+ }
1779
+ }
1780
+ const data = yield* Email.listDomains(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list domains", error) })));
1781
+ if (isJsonLinesRequested()) {
1782
+ for (const item of data.items) yield* printJsonLine(item);
1783
+ return;
1784
+ }
1785
+ if (json) return yield* printJson(data.items);
1786
+ if (data.items.length === 0) return yield* Console.log("No domains yet. Add one with `samva email domains add --domain example.com`.");
1787
+ for (const domain of data.items) {
1788
+ yield* printFields([
1789
+ ["Domain", domain.domain],
1790
+ ["Status", domain.status],
1791
+ ["ID", domain.id]
1792
+ ]);
1793
+ yield* Console.log("");
1794
+ }
1795
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1796
+ const add$1 = Command.make("add", {
1797
+ domain: Flag.string("domain").pipe(Flag.withDescription("Domain to add, e.g. example.com")),
1798
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the planned change without calling the API"), Flag.withDefault(false)),
1799
+ json: jsonFlag
1800
+ }).pipe(Command.withDescription("Add an email sending domain"), Command.withExamples([{
1801
+ command: "samva email domains add --domain example.com --dry-run",
1802
+ description: "Preview add"
1803
+ }, {
1804
+ command: "samva email domains add --domain example.com",
1805
+ description: "Apply domain add"
1806
+ }]), Command.withHandler(({ domain, dryRun, json }) => Effect.gen(function* () {
1807
+ if (dryRun) {
1808
+ if (json) return yield* printJson({
1809
+ dryRun: true,
1810
+ action: "email.domains.add",
1811
+ domain
1812
+ });
1813
+ return yield* Console.log(`[dry-run] Would add domain "${domain}".`);
1814
+ }
1815
+ const client = yield* getClientProvider;
1816
+ yield* Effect.matchEffect(Email.addDomain({ domain }).pipe(client.provide), {
1817
+ onFailure: (error) => {
1818
+ const conflict = getConflictError(error);
1819
+ if (!conflict) return Effect.fail(new SamvaCliError({ message: formatApiError("Could not add domain", error) }));
1820
+ const findExisting = Effect.gen(function* () {
1821
+ if (typeof conflict.id === "string" && conflict.id.length > 0) {
1822
+ const fetched = yield* Email.getDomainById({ id: conflict.id }).pipe(client.provide, Effect.catchTag("ResourceNotFoundError", () => Effect.void), Effect.mapError((lookupError) => new SamvaCliError({ message: formatApiError("Domain already exists but could not fetch it by id", lookupError) })));
1823
+ if (fetched) return fetched;
1824
+ }
1825
+ let page = 1;
1826
+ while (true) {
1827
+ const listed = yield* Email.listDomains({
1828
+ page: page.toString(),
1829
+ limit: "100"
1830
+ }).pipe(client.provide, Effect.mapError((listError) => new SamvaCliError({ message: formatApiError("Domain may already exist but could not verify idempotently", listError) })));
1831
+ const existing = listed.items.find((item) => item.domain === domain);
1832
+ if (existing) return existing;
1833
+ const totalPages = Number(listed.pagination.totalPages);
1834
+ if (!Number.isFinite(totalPages) || page >= totalPages) break;
1835
+ page += 1;
1836
+ }
1837
+ return yield* new SamvaCliError({ message: formatApiError("Could not add domain", error) });
1838
+ });
1839
+ return Effect.gen(function* () {
1840
+ const existing = yield* findExisting;
1841
+ if (json) yield* printJson({
1842
+ ...existing,
1843
+ idempotent: true
1844
+ });
1845
+ else yield* Effect.logWarning(`Domain "${existing.domain}" already exists (status: ${existing.status}).`);
1846
+ });
1847
+ },
1848
+ onSuccess: (data) => Effect.gen(function* () {
1849
+ if (json) return yield* printJson(data);
1850
+ yield* Effect.logInfo(`Domain "${data.domain}" added (status: ${data.status}). Add these DNS records:\n`);
1851
+ for (const record of data.dnsRecords) {
1852
+ yield* printFields([
1853
+ ["Type", record.type],
1854
+ ["Name", record.name],
1855
+ ["Value", record.value],
1856
+ ["TTL", record.ttl],
1857
+ ["Priority", record.priority]
1858
+ ]);
1859
+ yield* Console.log("");
1860
+ }
1861
+ yield* Effect.logInfo(`Then verify: samva email domains check ${data.id}`);
1862
+ })
1863
+ });
1864
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1865
+ const check$1 = Command.make("check", {
1866
+ id: Argument.string("id").pipe(Argument.withDescription("Domain id to check")),
1867
+ json: jsonFlag
1868
+ }).pipe(Command.withDescription("Check an email domain's verification status"), Command.withExamples([{
1869
+ command: "samva email domains check dom_abc123",
1870
+ description: "Poll domain verification"
1871
+ }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
1872
+ const client = yield* getClientProvider;
1873
+ const data = yield* Email.checkDomainVerification({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not check domain", error) })));
1874
+ if (json) return yield* printJson(data);
1875
+ yield* printFields([
1876
+ ["Domain", data.domain],
1877
+ ["Verified", data.isVerified ? "yes" : "no"],
1878
+ ["Verification status", data.verificationStatus],
1879
+ ["Mail-from", data.mailFromStatus]
1880
+ ]);
1881
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1882
+ const remove$3 = Command.make("remove", {
1883
+ id: Argument.string("id").pipe(Argument.withDescription("Domain id to permanently remove")),
1884
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the deletion plan without calling the API"), Flag.withDefault(false)),
1885
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm permanent deletion (required for non-interactive use)"), Flag.withDefault(false)),
1886
+ json: jsonFlag
1887
+ }).pipe(Command.withDescription("Permanently remove an email sending domain"), Command.withExamples([{ command: "samva email domains remove dom_abc123 --dry-run" }, { command: "samva email domains remove dom_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
1888
+ if (dryRun) return yield* json ? printJson({
1889
+ dryRun: true,
1890
+ action: "email.domains.remove",
1891
+ id
1892
+ }) : Console.log(`[dry-run] Would permanently remove domain ${id}.`);
1893
+ yield* confirmDestructive({
1894
+ yes,
1895
+ json,
1896
+ message: `Permanently remove domain ${id}?`,
1897
+ nonInteractiveMessage: "Permanent domain deletion requires --yes in non-interactive mode. Preview with --dry-run."
1898
+ });
1899
+ const client = yield* getClientProvider;
1900
+ const data = yield* Email.removeDomain({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not remove domain", error) })));
1901
+ if (json) return yield* printJson(data);
1902
+ yield* Console.log(`Removed domain ${id}.`);
1903
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1904
+ const printBrandedTracking = (data) => Effect.gen(function* () {
1905
+ if (!data.enabled || data.brandedTracking === null) return yield* Console.log("Branded tracking is not enabled for this domain.");
1906
+ const branded = data.brandedTracking;
1907
+ yield* printFields([
1908
+ ["Status", branded.status],
1909
+ ["Host", branded.hostname],
1910
+ ["Region", branded.region],
1911
+ ["Certificate", branded.sslStatus ?? "pending"],
1912
+ ["CAA blocked", branded.caaBlocked ? "yes" : "no"]
1913
+ ]);
1914
+ yield* Console.log("\nCreate these DNS records:");
1915
+ for (const record of [branded.dnsRecords.tracking, branded.dnsRecords.dcvDelegation]) {
1916
+ if (record === null) continue;
1917
+ yield* printFields([
1918
+ ["Type", record.type],
1919
+ ["Name", record.name],
1920
+ ["Value", record.value]
1921
+ ]);
1922
+ yield* Console.log("");
1923
+ }
1924
+ });
1925
+ const brandedEnable = Command.make("enable", {
1926
+ id: Argument.string("id").pipe(Argument.withDescription("Domain id to enable branded tracking for")),
1927
+ label: Flag.string("label").pipe(Flag.withDescription("First-level label under the domain (default: track)"), Flag.optional),
1928
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the planned change without calling the API"), Flag.withDefault(false)),
1929
+ json: jsonFlag
1930
+ }).pipe(Command.withDescription("Enable a branded tracking domain"), Command.withExamples([{ command: "samva email domains branded-tracking enable dom_abc123" }, { command: "samva email domains branded-tracking enable dom_abc123 --label links" }]), Command.withHandler(({ id, label, dryRun, json }) => Effect.gen(function* () {
1931
+ const chosenLabel = Option.getOrUndefined(label);
1932
+ if (dryRun) return yield* json ? printJson({
1933
+ dryRun: true,
1934
+ action: "email.domains.branded-tracking.enable",
1935
+ id,
1936
+ label: chosenLabel
1937
+ }) : Console.log(`[dry-run] Would enable branded tracking for ${id}.`);
1938
+ const client = yield* getClientProvider;
1939
+ const data = yield* Email.enableBrandedTracking({
1940
+ id,
1941
+ ...chosenLabel === void 0 ? {} : { label: chosenLabel }
1942
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not enable branded tracking", error) })));
1943
+ if (json) return yield* printJson(data);
1944
+ yield* printBrandedTracking(data);
1945
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1946
+ const brandedStatus = Command.make("status", {
1947
+ id: Argument.string("id").pipe(Argument.withDescription("Domain id to inspect")),
1948
+ json: jsonFlag
1949
+ }).pipe(Command.withDescription("Show a domain's branded tracking status"), Command.withExamples([{ command: "samva email domains branded-tracking status dom_abc123" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
1950
+ const client = yield* getClientProvider;
1951
+ const data = yield* Email.getBrandedTracking({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get branded tracking status", error) })));
1952
+ if (json) return yield* printJson(data);
1953
+ yield* printBrandedTracking(data);
1954
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1955
+ const brandedDisable = Command.make("disable", {
1956
+ id: Argument.string("id").pipe(Argument.withDescription("Domain id to disable branded tracking for")),
1957
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the teardown plan without calling the API"), Flag.withDefault(false)),
1958
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm teardown (required for non-interactive use)"), Flag.withDefault(false)),
1959
+ json: jsonFlag
1960
+ }).pipe(Command.withDescription("Disable and tear down a branded tracking domain"), Command.withExamples([{ command: "samva email domains branded-tracking disable dom_abc123 --dry-run" }, { command: "samva email domains branded-tracking disable dom_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
1961
+ if (dryRun) return yield* json ? printJson({
1962
+ dryRun: true,
1963
+ action: "email.domains.branded-tracking.disable",
1964
+ id
1965
+ }) : Console.log(`[dry-run] Would disable branded tracking for ${id}.`);
1966
+ yield* confirmDestructive({
1967
+ yes,
1968
+ json,
1969
+ message: `Disable branded tracking for ${id}? Existing branded links in delivered mail will break.`,
1970
+ nonInteractiveMessage: "Branded tracking teardown requires --yes in non-interactive mode. Preview with --dry-run."
1971
+ });
1972
+ const client = yield* getClientProvider;
1973
+ const data = yield* Email.disableBrandedTracking({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not disable branded tracking", error) })));
1974
+ if (json) return yield* printJson(data);
1975
+ yield* Console.log(`Disabled branded tracking for ${id}.`);
1976
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
1977
+ const brandedTracking = Command.make("branded-tracking").pipe(Command.withDescription("Manage a domain's branded tracking (custom open/click subdomain)"), Command.withExamples([{ command: "samva email domains branded-tracking enable dom_abc123" }]), Command.withSubcommands([
1978
+ brandedEnable,
1979
+ brandedStatus,
1980
+ brandedDisable
1981
+ ]));
1982
+ const domainsCommand = Command.make("domains").pipe(Command.withDescription("Manage email sending domains"), Command.withExamples([{
1983
+ command: "samva email domains list",
1984
+ description: "List configured domains"
1985
+ }]), Command.withSubcommands([
1986
+ list$6,
1987
+ add$1,
1988
+ check$1,
1989
+ remove$3,
1990
+ brandedTracking
1991
+ ]));
1992
+ //#endregion
1993
+ //#region src/commands/email/receiving.ts
1994
+ const status = Command.make("status", {
1995
+ domainId: Argument.string("domain-id").pipe(Argument.withDescription("Sending domain id")),
1996
+ json: jsonFlag
1997
+ }).pipe(Command.withDescription("Get a domain's receiving prerequisite status"), Command.withExamples([{ command: "samva email receiving status dom_abc123 --json" }]), Command.withHandler(({ domainId, json }) => Effect.gen(function* () {
1998
+ const client = yield* getClientProvider;
1999
+ const data = yield* Email.getDomainStatus({ id: domainId }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get receiving status", error) })));
2000
+ if (json) return yield* printJson(data);
2001
+ yield* printFields([["Domain", data.domain], ["Status", data.status]]);
2002
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2003
+ const enable = Command.make("enable", {
2004
+ domainId: Argument.string("domain-id").pipe(Argument.withDescription("Sending domain id")),
2005
+ domain: Flag.string("domain").pipe(Flag.withDescription("Verified domain name")),
2006
+ address: Flag.string("address").pipe(Flag.withDescription("Recipient address to receive (repeatable)"), Flag.atLeast(1)),
2007
+ catchAll: Flag.boolean("catch-all").pipe(Flag.withDescription("Also receive unmatched addresses"), Flag.withDefault(false)),
2008
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the provider setup request without calling the API"), Flag.withDefault(false)),
2009
+ json: jsonFlag
2010
+ }).pipe(Command.withDescription("Enable inbound receiving for a verified domain"), Command.withExamples([{ command: "samva email receiving enable dom_abc123 --domain example.com --address replies@example.com --dry-run" }, { command: "samva email receiving enable dom_abc123 --domain example.com --address replies@example.com" }]), Command.withHandler(({ domainId, domain, address, catchAll, dryRun, json }) => Effect.gen(function* () {
2011
+ const request = {
2012
+ domain,
2013
+ catchAll,
2014
+ addresses: [...address]
2015
+ };
2016
+ if (dryRun) return yield* json ? printJson({
2017
+ dryRun: true,
2018
+ action: "email.receiving.enable",
2019
+ domainId,
2020
+ request
2021
+ }) : Console.log(`[dry-run] Would enable receiving for ${domain} (${address.join(", ")}).`);
2022
+ const client = yield* getClientProvider;
2023
+ const data = yield* Email.enableReceiving({
2024
+ id: domainId,
2025
+ ...request
2026
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not enable receiving", error) })));
2027
+ if (json) return yield* printJson(data);
2028
+ yield* printFields([["Rule", data.ruleName], ["Recipients", data.recipients.join(", ")]]);
2029
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2030
+ const receivingCommand = Command.make("receiving").pipe(Command.withDescription("Configure inbound email receiving"), Command.withExamples([{ command: "samva email receiving status dom_abc123" }]), Command.withSubcommands([status, enable]));
2031
+ //#endregion
2032
+ //#region src/lib/stdin.ts
2033
+ /** Read piped stdin as UTF-8 (fd 0). Fails fast when stdin is a TTY (no pipe). */
2034
+ const readStdinUtf8 = Effect.gen(function* () {
2035
+ if (isatty(0)) return yield* new SamvaCliError({ message: "Expected piped stdin for `-`; redirect input (e.g. `echo … | samva …`) or pass the body inline instead of `-`." });
2036
+ return yield* Effect.tryPromise({
2037
+ try: () => readFile(0, "utf8"),
2038
+ catch: () => new SamvaCliError({ message: "Could not read stdin." })
2039
+ });
2040
+ });
2041
+ //#endregion
2042
+ //#region src/commands/messages.ts
2043
+ /** The deliberately small Message shape shared by send, get, and wait. */
2044
+ const projectMessage = (message) => ({
2045
+ id: message.id,
2046
+ channel: message.channel,
2047
+ status: message.status,
2048
+ conversationId: message.conversationId,
2049
+ createdAt: message.createdAt,
2050
+ updatedAt: message.updatedAt,
2051
+ deliveries: message.deliveries.map((delivery) => ({
2052
+ status: delivery.status,
2053
+ recipient: delivery.recipientId
2054
+ }))
2055
+ });
2056
+ const printMessage = (message) => printFields([
2057
+ ["Message ID", message.id],
2058
+ ["Channel", message.channel],
2059
+ ["Status", message.status],
2060
+ ["Conversation", message.conversationId],
2061
+ ["Created", message.createdAt],
2062
+ ["Updated", message.updatedAt]
2063
+ ]);
2064
+ const classifyMessageLifecycle = (message) => {
2065
+ const failed = (status) => status === "failed" || status === "bounced" || status === "complained";
2066
+ const succeeded = (status) => status === "delivered" || status === "read";
2067
+ const messageStatus = message.status.toLowerCase();
2068
+ const deliveryStatuses = message.deliveries.map((delivery) => delivery.status.toLowerCase());
2069
+ if (failed(messageStatus) || deliveryStatuses.some(failed)) return "failure";
2070
+ if (succeeded(messageStatus) || deliveryStatuses.length > 0 && deliveryStatuses.every(succeeded)) return "success";
2071
+ return "pending";
2072
+ };
2073
+ var MessageStillPending = class extends Schema.TaggedErrorClass()("MessageStillPending", { message: Schema.String }) {};
2074
+ const messageLifecycleError = (code, message, partialMessage) => new CliMessageLifecycleError({
2075
+ code,
2076
+ message,
2077
+ partialMessage,
2078
+ next: `samva messages wait ${partialMessage.id}`,
2079
+ human: [
2080
+ `Message ID ${partialMessage.id}`,
2081
+ `Status ${partialMessage.status}`,
2082
+ `Next samva messages wait ${partialMessage.id}`
2083
+ ].join("\n")
2084
+ });
2085
+ /**
2086
+ * `--timeout` is modelled as an optional flag rather than one with a baked-in
2087
+ * default so `Option.isSome` tells callers whether the user actually passed it.
2088
+ * The parser owns detection, so a flag *value* of `--timeout` (e.g.
2089
+ * `--subject "--timeout"`) and a trailing `--` no longer false-positive.
2090
+ */
2091
+ const timeoutFlag = Flag.string("timeout").pipe(Flag.withDescription("Maximum wait duration (default: 2m; only used with --wait)"), Flag.optional);
2092
+ /** Duration.fromInput unit word for each `--timeout` compact-form suffix. */
2093
+ const DURATION_UNIT_WORD_BY_SUFFIX = {
2094
+ ms: "millis",
2095
+ s: "seconds",
2096
+ m: "minutes"
2097
+ };
2098
+ const durationFromFlag = (raw) => {
2099
+ const invalidTimeout = () => Effect.fail(new CliUsageError({
2100
+ code: "invalid_timeout",
2101
+ message: "--timeout must be a positive duration, for example `--timeout 2m` or `--timeout \"2 minutes\"`."
2102
+ }));
2103
+ const compact = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/i.exec(raw);
2104
+ const value = compact ? `${compact[1]} ${DURATION_UNIT_WORD_BY_SUFFIX[compact[2]?.toLowerCase() ?? ""] ?? "hours"}` : raw;
2105
+ const isDurationInput = (input) => /^\d+(?:\.\d+)?\s+(?:nanos?|micros?|millis?|seconds?|minutes?|hours?)$/.test(input);
2106
+ if (!isDurationInput(value)) return invalidTimeout();
2107
+ return Option.match(Duration.fromInput(value), {
2108
+ onNone: invalidTimeout,
2109
+ onSome: (duration) => Duration.isFinite(duration) && Duration.toMillis(duration) > 0 ? Effect.succeed(duration) : invalidTimeout()
2110
+ });
2111
+ };
2112
+ const getMessage = (id) => getClientProvider.pipe(Effect.flatMap((client) => Messages.get({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get message", error) })))));
2113
+ /** Poll with an owned Effect timeout; interruption cancels the active request/fiber. */
2114
+ const waitForMessage = (initial, timeout) => Effect.gen(function* () {
2115
+ let latest = initial;
2116
+ if (classifyMessageLifecycle(latest) !== "pending") return latest;
2117
+ const result = yield* Effect.gen(function* () {
2118
+ latest = yield* getMessage(initial.id);
2119
+ if (classifyMessageLifecycle(latest) === "pending") return yield* new MessageStillPending({ message: `Message ${latest.id} is ${latest.status}.` });
2120
+ return latest;
2121
+ }).pipe(Effect.retry({
2122
+ schedule: Schedule.spaced("1 second"),
2123
+ while: (error) => error instanceof MessageStillPending
2124
+ })).pipe(Effect.timeoutOption(timeout), Effect.catch((error) => messageLifecycleError("message_observation_failed", formatApiError(`Could not continue observing message ${latest.id}`, error), projectMessage(latest))));
2125
+ return Option.getOrElse(result, () => latest);
2126
+ });
2127
+ const lifecycleResult = (message, timeout) => Effect.gen(function* () {
2128
+ const result = yield* waitForMessage(message, timeout);
2129
+ const resultTerminal = classifyMessageLifecycle(result);
2130
+ return {
2131
+ message: result,
2132
+ resultTerminal,
2133
+ timedOut: resultTerminal === "pending"
2134
+ };
2135
+ });
2136
+ const pageFlag$1 = Flag.integer("page").pipe(Flag.withDescription("Page number (1-based)"), Flag.optional);
2137
+ const limitFlag$1 = Flag.integer("limit").pipe(Flag.withDescription("Maximum messages per page"), Flag.optional);
2138
+ const statusFlag$1 = Flag.string("status").pipe(Flag.withDescription("Filter by message status"), Flag.optional);
2139
+ const channelFlag = Flag.string("channel").pipe(Flag.withDescription("Filter by channel"), Flag.optional);
2140
+ const conversationIdFlag = Flag.string("conversation-id").pipe(Flag.withDescription("Filter by conversation id"), Flag.optional);
2141
+ const contactIdFlag = Flag.string("contact-id").pipe(Flag.withDescription("Filter by contact id"), Flag.optional);
2142
+ const startDateFlag = Flag.string("start-date").pipe(Flag.withDescription("Filter messages created on or after this ISO 8601 instant"), Flag.optional);
2143
+ const endDateFlag = Flag.string("end-date").pipe(Flag.withDescription("Filter messages created on or before this ISO 8601 instant"), Flag.optional);
2144
+ const allFlag$1 = Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false));
2145
+ const list$5 = Command.make("list", {
2146
+ page: pageFlag$1,
2147
+ limit: limitFlag$1,
2148
+ status: statusFlag$1,
2149
+ channel: channelFlag,
2150
+ conversationId: conversationIdFlag,
2151
+ contactId: contactIdFlag,
2152
+ startDate: startDateFlag,
2153
+ endDate: endDateFlag,
2154
+ all: allFlag$1,
2155
+ json: jsonFlag
2156
+ }).pipe(Command.withDescription("List messages"), Command.withExamples([
2157
+ { command: "samva messages list --channel email --limit 25" },
2158
+ { command: "samva messages list --status failed --json" },
2159
+ {
2160
+ command: "samva messages list --all --jsonl",
2161
+ description: "Stream every page as JSONL"
2162
+ }
2163
+ ]), Command.withHandler(({ page, limit, status, channel, conversationId, contactId, startDate, endDate, all, json }) => Effect.gen(function* () {
2164
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
2165
+ code: "all_requires_jsonl",
2166
+ message: "--all is an unbounded traversal and requires --jsonl."
2167
+ });
2168
+ const client = yield* getClientProvider;
2169
+ const pageValue = Option.getOrUndefined(page);
2170
+ const limitValue = Option.getOrUndefined(limit);
2171
+ const query = {
2172
+ channel: Option.getOrUndefined(channel),
2173
+ status: Option.getOrUndefined(status),
2174
+ conversationId: Option.getOrUndefined(conversationId),
2175
+ contactId: Option.getOrUndefined(contactId),
2176
+ startDate: Option.getOrUndefined(startDate),
2177
+ endDate: Option.getOrUndefined(endDate),
2178
+ page: pageValue?.toString(),
2179
+ limit: limitValue?.toString()
2180
+ };
2181
+ if (all) {
2182
+ let nextPage = pageValue ?? 1;
2183
+ for (;;) {
2184
+ const data = yield* Messages.list({
2185
+ ...query,
2186
+ page: nextPage.toString()
2187
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list messages", error) })));
2188
+ for (const item of data.items) yield* printJsonLine(projectMessage(item));
2189
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
2190
+ nextPage += 1;
2191
+ }
2192
+ }
2193
+ const data = yield* Messages.list(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list messages", error) })));
2194
+ const payload = {
2195
+ items: data.items.map(projectMessage),
2196
+ pagination: data.pagination
2197
+ };
2198
+ if (isJsonLinesRequested()) {
2199
+ for (const item of payload.items) yield* printJsonLine(item);
2200
+ return;
2201
+ }
2202
+ if (json) return yield* printJson(payload);
2203
+ if (payload.items.length === 0) return yield* Console.log("No messages found.");
2204
+ for (const message of payload.items) {
2205
+ yield* printMessage(message);
2206
+ yield* Console.log("");
2207
+ }
2208
+ yield* printFields([["Page", payload.pagination.page], ["Total", payload.pagination.total]]);
2209
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2210
+ const get$2 = Command.make("get", {
2211
+ id: Argument.string("id"),
2212
+ json: jsonFlag
2213
+ }).pipe(Command.withDescription("Get a message"), Command.withExamples([{ command: "samva messages get msg_abc123 --json" }]), Command.withHandler(({ id, json }) => getMessage(id).pipe(Effect.flatMap((message) => json ? printJson(projectMessage(message)) : printMessage(projectMessage(message))), (effect) => json ? effect.pipe(Effect.provideService(References.MinimumLogLevel, "None")) : effect)));
2214
+ const events = Command.make("events", {
2215
+ id: Argument.string("id"),
2216
+ json: jsonFlag
2217
+ }).pipe(Command.withDescription("List lifecycle events for a message"), Command.withExamples([{ command: "samva messages events msg_abc123 --json" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
2218
+ const client = yield* getClientProvider;
2219
+ const data = yield* Messages.getEvents({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list message events", error) })));
2220
+ if (json) return yield* printJson(data);
2221
+ for (const event of data) yield* printFields([
2222
+ ["Event", event.eventType],
2223
+ ["Status", event.status],
2224
+ ["Time", event.timestamp]
2225
+ ]);
2226
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2227
+ const wait = Command.make("wait", {
2228
+ id: Argument.string("id"),
2229
+ timeout: timeoutFlag,
2230
+ json: jsonFlag
2231
+ }).pipe(Command.withDescription("Wait for a message to be delivered or read"), Command.withExamples([{ command: "samva messages wait msg_abc123" }, { command: "samva messages wait msg_abc123 --timeout 30s --json" }]), Command.withHandler(({ id, timeout, json }) => Effect.gen(function* () {
2232
+ const duration = yield* durationFromFlag(Option.getOrElse(timeout, () => "2m"));
2233
+ const initial = yield* getMessage(id);
2234
+ const result = yield* lifecycleResult(initial, duration);
2235
+ const projection = projectMessage(result.message);
2236
+ if (result.timedOut) return yield* messageLifecycleError("message_wait_timeout", `Timed out waiting for message ${projection.id}; it remains ${projection.status}.`, projection);
2237
+ if (result.resultTerminal === "failure") return yield* messageLifecycleError("message_terminal_failure", `Message ${projection.id} reached terminal status ${projection.status}.`, projection);
2238
+ if (json) return yield* printJson(projection);
2239
+ yield* printMessage(projection);
2240
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2241
+ const messagesCommand = Command.make("messages").pipe(Command.withDescription("Inspect messages and their lifecycle"), Command.withExamples([{ command: "samva messages list --channel email --json" }]), Command.withSubcommands([
2242
+ list$5,
2243
+ get$2,
2244
+ events,
2245
+ wait
2246
+ ]));
2247
+ //#endregion
2248
+ //#region src/commands/email/send.ts
2249
+ /** Human status line for a definite (non-`undefined`) lifecycle outcome. */
2250
+ const STATUS_LINE_BY_OUTCOME = {
2251
+ success: "Email lifecycle completed.",
2252
+ failure: "Email delivery failed."
2253
+ };
2254
+ const toFlag$1 = Flag.string("to").pipe(Flag.withDescription("Recipient email address (repeatable)"), Flag.atLeast(1));
2255
+ const ccFlag$1 = Flag.string("cc").pipe(Flag.withDescription("Cc recipient email address"), Flag.optional);
2256
+ const bccFlag$1 = Flag.string("bcc").pipe(Flag.withDescription("Bcc recipient email address"), Flag.optional);
2257
+ const subjectFlag$1 = Flag.string("subject").pipe(Flag.withDescription("Email subject line (required for inline content; omit for templates)"), Flag.optional);
2258
+ const htmlFlag$1 = Flag.string("html").pipe(Flag.withDescription("HTML body (use `-` to read from stdin)"), Flag.optional);
2259
+ const textFlag$1 = Flag.string("text").pipe(Flag.withDescription("Plain-text body (use `-` to read from stdin)"), Flag.optional);
2260
+ const templateIdFlag$1 = Flag.string("template-id").pipe(Flag.withDescription("Template id (uuid) to render instead of inline body"), Flag.optional);
2261
+ const templateSlugFlag$1 = Flag.string("template-slug").pipe(Flag.withDescription("Template handle to render instead of inline body, e.g. welcome-email"), Flag.optional);
2262
+ const templateDataFlag$1 = Flag.string("template-data").pipe(Flag.withDescription("JSON object of variables for the template"), Flag.optional);
2263
+ const replyToMessageFlag = Flag.string("reply-to-message-id").pipe(Flag.withDescription("Message id this email replies to (threads the conversation)"), Flag.optional);
2264
+ const replyToAddressFlag = Flag.string("reply-to").pipe(Flag.withDescription("Reply-To header email address"), Flag.optional);
2265
+ const idempotencyKeyFlag$1 = Flag.string("idempotency-key").pipe(Flag.withDescription("Key (1-255 characters) so identical retries return the original email"), Flag.optional);
2266
+ const trackOpensFlag$1 = Flag.choice("track-opens", ["on", "off"]).pipe(Flag.withDescription("Override open tracking for this send; recipient opt-out still wins"), Flag.optional);
2267
+ const trackClicksFlag$1 = Flag.choice("track-clicks", ["on", "off"]).pipe(Flag.withDescription("Override click tracking for this send; recipient opt-out still wins"), Flag.optional);
2268
+ const dryRunFlag$2 = Flag.boolean("dry-run").pipe(Flag.withDescription("Validate inputs and print the request payload without calling the API"), Flag.withDefault(false));
2269
+ const bodySourceName = (input) => {
2270
+ if (input.templateRefValue) return "template";
2271
+ if (input.stdin) return "stdin";
2272
+ if (input.resolvedHtml) return "html";
2273
+ return "text";
2274
+ };
2275
+ const parseTemplateData$1 = (raw) => Option.match(raw, {
2276
+ onNone: () => Effect.succeed(void 0),
2277
+ onSome: (value) => Effect.try({
2278
+ try: () => JSON.parse(value),
2279
+ catch: () => new SamvaCliError({ message: "--template-data must be a JSON object." })
2280
+ }).pipe(Effect.flatMap((parsed) => Predicate.isObject(parsed) ? Effect.succeed(parsed) : Effect.fail(new SamvaCliError({ message: "--template-data must be a JSON object." }))))
2281
+ });
2282
+ const send = Command.make("send", {
2283
+ to: toFlag$1,
2284
+ cc: ccFlag$1,
2285
+ bcc: bccFlag$1,
2286
+ subject: subjectFlag$1,
2287
+ html: htmlFlag$1,
2288
+ text: textFlag$1,
2289
+ templateId: templateIdFlag$1,
2290
+ templateSlug: templateSlugFlag$1,
2291
+ templateData: templateDataFlag$1,
2292
+ replyTo: replyToMessageFlag,
2293
+ replyToAddress: replyToAddressFlag,
2294
+ idempotencyKey: idempotencyKeyFlag$1,
2295
+ trackOpens: trackOpensFlag$1,
2296
+ trackClicks: trackClicksFlag$1,
2297
+ dryRun: dryRunFlag$2,
2298
+ wait: Flag.boolean("wait").pipe(Flag.withDescription("Wait for delivered/read or a terminal delivery failure"), Flag.withDefault(false)),
2299
+ timeout: timeoutFlag,
2300
+ json: jsonFlag
2301
+ }).pipe(Command.withDescription("Send an email"), Command.withExamples([
2302
+ {
2303
+ command: "samva email send --to you@example.com --subject Hi --text Hello",
2304
+ description: "Plain-text body (SAMVA_API_KEY)"
2305
+ },
2306
+ {
2307
+ command: "samva email send --to you@example.com --template-slug welcome-email --template-data '{\"name\":\"Ada\"}'",
2308
+ description: "Render a template by handle"
2309
+ },
2310
+ {
2311
+ command: "echo \"Hello\" | samva email send --to you@example.com --subject Hi --text -",
2312
+ description: "Pipe plain-text body from stdin"
2313
+ },
2314
+ {
2315
+ command: "samva email send --to you@example.com --subject Hi --text Hello --dry-run",
2316
+ description: "Validate and print the request payload without sending"
2317
+ },
2318
+ {
2319
+ command: "samva email send --to you@example.com --subject Hi --text Hello --idempotency-key welcome-email-ada",
2320
+ description: "Retry a send safely with an idempotency key"
2321
+ }
2322
+ ]), Command.withHandler(({ to, cc, bcc, subject, html, text, templateId, templateSlug, templateData, replyTo, replyToAddress, idempotencyKey, trackOpens, trackClicks, dryRun, wait, timeout, json }) => Effect.gen(function* () {
2323
+ const subjectValue = Option.getOrUndefined(subject);
2324
+ const htmlValue = Option.getOrUndefined(html);
2325
+ const textValue = Option.getOrUndefined(text);
2326
+ const templateIdValue = Option.getOrUndefined(templateId);
2327
+ const templateSlugValue = Option.getOrUndefined(templateSlug);
2328
+ const replyToValue = Option.getOrUndefined(replyTo);
2329
+ const replyToAddressValue = Option.getOrUndefined(replyToAddress);
2330
+ const idempotencyKeyValue = Option.getOrUndefined(idempotencyKey);
2331
+ const trackOpensValue = Option.getOrUndefined(trackOpens);
2332
+ const trackClicksValue = Option.getOrUndefined(trackClicks);
2333
+ const ccValue = Option.getOrUndefined(cc);
2334
+ const bccValue = Option.getOrUndefined(bcc);
2335
+ const templateDataValue = yield* parseTemplateData$1(templateData);
2336
+ if (idempotencyKeyValue !== void 0 && (idempotencyKeyValue.length < 1 || idempotencyKeyValue.length > 255)) return yield* new SamvaCliError({ message: "--idempotency-key must contain between 1 and 255 characters." });
2337
+ if (templateIdValue && templateSlugValue) return yield* new SamvaCliError({ message: "Use either --template-id or --template-slug, not both." });
2338
+ const templateRefValue = templateSlugValue ?? templateIdValue;
2339
+ if (templateRefValue && subjectValue) return yield* new SamvaCliError({ message: "Template sends derive their subject from the template. Omit --subject." });
2340
+ if (!templateRefValue && !subjectValue) return yield* new CliUsageError({
2341
+ code: "missing_subject",
2342
+ message: "Inline email content requires --subject. Run `samva email send --help` for examples."
2343
+ });
2344
+ if (textValue === "-" && htmlValue === "-") return yield* new CliUsageError({
2345
+ code: "multiple_stdin_bodies",
2346
+ message: "Only one body can be read from stdin: use --text - or --html -, not both."
2347
+ });
2348
+ const resolvedText = textValue === "-" ? yield* readStdinUtf8 : textValue;
2349
+ const resolvedHtml = htmlValue === "-" ? yield* readStdinUtf8 : htmlValue;
2350
+ if (!resolvedHtml && !resolvedText && !templateRefValue) return yield* new SamvaCliError({ message: "Provide a body: one of --html, --text (or --text - for stdin), --template-id, or --template-slug." });
2351
+ if ([
2352
+ resolvedHtml,
2353
+ resolvedText,
2354
+ templateRefValue
2355
+ ].filter(Boolean).length > 1) return yield* new SamvaCliError({ message: "Choose exactly one body source: --html, --text, or a template (--template-id/--template-slug)." });
2356
+ const request = {
2357
+ to: [...to],
2358
+ ...ccValue ? { cc: [{ email: ccValue }] } : {},
2359
+ ...bccValue ? { bcc: [{ email: bccValue }] } : {},
2360
+ ...subjectValue === void 0 ? {} : { subject: subjectValue },
2361
+ ...resolvedHtml === void 0 ? {} : { html: resolvedHtml },
2362
+ ...resolvedText === void 0 ? {} : { text: resolvedText },
2363
+ ...templateIdValue === void 0 ? {} : { templateId: templateIdValue },
2364
+ ...templateSlugValue === void 0 ? {} : { templateSlug: templateSlugValue },
2365
+ ...templateDataValue === void 0 ? {} : { templateData: templateDataValue },
2366
+ ...replyToValue === void 0 ? {} : { inReplyToMessageId: replyToValue },
2367
+ ...replyToAddressValue === void 0 ? {} : { replyTo: replyToAddressValue },
2368
+ ...trackOpensValue !== void 0 || trackClicksValue !== void 0 ? { tracking: {
2369
+ ...trackOpensValue !== void 0 ? { opens: trackOpensValue === "on" } : {},
2370
+ ...trackClicksValue !== void 0 ? { clicks: trackClicksValue === "on" } : {}
2371
+ } } : {}
2372
+ };
2373
+ const sendOptions = idempotencyKeyValue !== void 0 ? { idempotencyKey: idempotencyKeyValue } : {};
2374
+ const source = bodySourceName({
2375
+ templateRefValue,
2376
+ resolvedHtml,
2377
+ stdin: textValue === "-" || htmlValue === "-"
2378
+ });
2379
+ if (dryRun && wait) return yield* new CliUsageError({
2380
+ code: "dry_run_wait_conflict",
2381
+ message: "--wait cannot be combined with --dry-run; a dry run sends nothing to wait on."
2382
+ });
2383
+ if (!wait && Option.isSome(timeout)) return yield* new CliUsageError({
2384
+ code: "timeout_requires_wait",
2385
+ message: "--timeout is meaningful only with --wait."
2386
+ });
2387
+ const waitDuration = wait ? yield* durationFromFlag(Option.getOrElse(timeout, () => "2m")) : void 0;
2388
+ if (dryRun) {
2389
+ if (json) return yield* printJson({
2390
+ dryRun: true,
2391
+ bodySource: source,
2392
+ request,
2393
+ idempotencyKey: idempotencyKeyValue
2394
+ });
2395
+ yield* Effect.logInfo("Dry run — no credentials, configuration, DNS, or API request used.");
2396
+ yield* printFields([["Body source", source]]);
2397
+ yield* Console.log("");
2398
+ yield* printFields([
2399
+ ["To", request.to.join(", ")],
2400
+ ...ccValue ? [["Cc", ccValue]] : [],
2401
+ ...bccValue ? [["Bcc", bccValue]] : [],
2402
+ ["Subject", request.subject],
2403
+ ...request.templateSlug ? [["Template", `${request.templateSlug} (handle)`]] : request.templateId ? [["Template", request.templateId]] : [],
2404
+ ...request.inReplyToMessageId ? [["Reply to", request.inReplyToMessageId]] : [],
2405
+ ...request.replyTo ? [["Reply-To", request.replyTo]] : [],
2406
+ ...idempotencyKeyValue ? [["Idempotency key", idempotencyKeyValue]] : [],
2407
+ ...request.tracking ? [["Track opens", request.tracking.opens], ["Track clicks", request.tracking.clicks]] : []
2408
+ ]);
2409
+ if (request.text) yield* Console.log(`\nText body (${request.text.length} chars):\n${truncateForDisplay(request.text)}`);
2410
+ if (request.html) yield* Console.log(`\nHTML body (${request.html.length} chars):\n${truncateForDisplay(request.html)}`);
2411
+ if (request.templateData) yield* Console.log(`\nTemplate data:\n${truncateForDisplay(JSON.stringify(request.templateData, null, 2))}`);
2412
+ return;
2413
+ }
2414
+ const client = yield* getClientProvider;
2415
+ const data = yield* Email.send(request, sendOptions).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Failed to send email", error) })));
2416
+ const finalMessage = waitDuration === void 0 ? data : yield* waitForMessage(data, waitDuration);
2417
+ const projection = projectMessage(finalMessage);
2418
+ const outcome = wait ? classifyMessageLifecycle(finalMessage) : void 0;
2419
+ if (outcome === "pending") return yield* messageLifecycleError("message_wait_timeout", `Timed out waiting for message ${projection.id}; it remains ${projection.status}.`, projection);
2420
+ if (outcome === "failure") return yield* messageLifecycleError("message_terminal_failure", `Message ${projection.id} reached terminal status ${projection.status}.`, projection);
2421
+ if (json) yield* printJson(projection);
2422
+ else {
2423
+ const statusLine = outcome === void 0 ? "Email queued." : STATUS_LINE_BY_OUTCOME[outcome] ?? "Email delivery timed out.";
2424
+ yield* Effect.logInfo(`${statusLine}\n`);
2425
+ yield* printFields([
2426
+ ["Message ID", projection.id],
2427
+ ["Status", projection.status],
2428
+ ["Conversation", projection.conversationId]
2429
+ ]);
2430
+ }
2431
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2432
+ //#endregion
2433
+ //#region src/commands/email/senders.ts
2434
+ const list$4 = Command.make("list", {
2435
+ page: Flag.integer("page").pipe(Flag.withDescription("Page number"), Flag.optional),
2436
+ limit: Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional),
2437
+ all: Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false)),
2438
+ json: jsonFlag
2439
+ }).pipe(Command.withDescription("List verified sender addresses"), Command.withExamples([{ command: "samva email senders list" }, { command: "samva email senders list --json" }]), Command.withHandler(({ page, limit, all, json }) => Effect.gen(function* () {
2440
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
2441
+ code: "all_requires_jsonl",
2442
+ message: "--all is an unbounded traversal and requires --jsonl."
2443
+ });
2444
+ const client = yield* getClientProvider;
2445
+ const pageValue = Option.getOrUndefined(page);
2446
+ const limitValue = Option.getOrUndefined(limit);
2447
+ const query = {
2448
+ page: pageValue?.toString(),
2449
+ limit: limitValue?.toString()
2450
+ };
2451
+ if (all) {
2452
+ let nextPage = pageValue ?? 1;
2453
+ for (;;) {
2454
+ const data = yield* Email.listSenders({
2455
+ ...query,
2456
+ page: nextPage.toString()
2457
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list senders", error) })));
2458
+ for (const item of data.items) yield* printJsonLine(item);
2459
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
2460
+ nextPage += 1;
2461
+ }
2462
+ }
2463
+ const data = yield* Email.listSenders(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list senders", error) })));
2464
+ if (isJsonLinesRequested()) {
2465
+ for (const item of data.items) yield* printJsonLine(item);
2466
+ return;
2467
+ }
2468
+ if (json) return yield* printJson(data.items);
2469
+ if (data.items.length === 0) return yield* Console.log("No senders yet. Add one with `samva email senders add --email you@example.com`.");
2470
+ for (const sender of data.items) {
2471
+ yield* printFields([
2472
+ ["Email", sender.email],
2473
+ ["Name", sender.name],
2474
+ ["Status", sender.verificationStatus],
2475
+ ["Default", sender.isDefault ? "yes" : "no"],
2476
+ ["ID", sender.id]
2477
+ ]);
2478
+ yield* Console.log("");
2479
+ }
2480
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2481
+ const add = Command.make("add", {
2482
+ email: Flag.string("email").pipe(Flag.withDescription("Sender email address")),
2483
+ name: Flag.string("name").pipe(Flag.withDescription("Display name for the sender"), Flag.optional),
2484
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the planned change without calling the API"), Flag.withDefault(false)),
2485
+ json: jsonFlag
2486
+ }).pipe(Command.withDescription("Add a sender address"), Command.withExamples([{
2487
+ command: "samva email senders add --email hello@example.com --dry-run",
2488
+ description: "Preview add"
2489
+ }, {
2490
+ command: "samva email senders add --email hello@example.com",
2491
+ description: "Apply sender add"
2492
+ }]), Command.withHandler(({ email, name, dryRun, json }) => Effect.gen(function* () {
2493
+ const nameValue = Option.getOrUndefined(name);
2494
+ if (dryRun) {
2495
+ if (json) return yield* printJson({
2496
+ dryRun: true,
2497
+ action: "email.senders.add",
2498
+ email,
2499
+ name: nameValue
2500
+ });
2501
+ return yield* Console.log(`[dry-run] Would add sender "${email}".`);
2502
+ }
2503
+ const client = yield* getClientProvider;
2504
+ yield* Effect.matchEffect(Email.addSender({
2505
+ email,
2506
+ ...nameValue === void 0 ? {} : { name: nameValue }
2507
+ }).pipe(client.provide), {
2508
+ onFailure: (error) => {
2509
+ const conflict = getConflictError(error);
2510
+ if (!conflict) return Effect.fail(new SamvaCliError({ message: formatApiError("Could not add sender", error) }));
2511
+ return Effect.gen(function* () {
2512
+ let page = 1;
2513
+ while (true) {
2514
+ const listed = yield* Email.listSenders({
2515
+ page: page.toString(),
2516
+ limit: "100"
2517
+ }).pipe(client.provide, Effect.mapError((listError) => new SamvaCliError({ message: formatApiError("Sender may already exist but could not verify idempotently", listError) })));
2518
+ const existing = (conflict.id ? listed.items.find((item) => item.id === conflict.id) : void 0) ?? listed.items.find((item) => item.email === email);
2519
+ if (existing) {
2520
+ if (json) yield* printJson({
2521
+ ...existing,
2522
+ idempotent: true
2523
+ });
2524
+ else {
2525
+ yield* printFields([
2526
+ ["Email", existing.email],
2527
+ ["Status", existing.verificationStatus],
2528
+ ["ID", existing.id]
2529
+ ]);
2530
+ yield* Effect.logWarning("\nSender already exists.");
2531
+ }
2532
+ return;
2533
+ }
2534
+ const totalPages = Number(listed.pagination.totalPages);
2535
+ if (!Number.isFinite(totalPages) || page >= totalPages) break;
2536
+ page += 1;
2537
+ }
2538
+ return yield* new SamvaCliError({ message: formatApiError("Could not add sender", error) });
2539
+ });
2540
+ },
2541
+ onSuccess: (data) => Effect.gen(function* () {
2542
+ if (json) return yield* printJson(data);
2543
+ yield* printFields([
2544
+ ["Email", data.email],
2545
+ ["Status", data.status],
2546
+ ["ID", data.id]
2547
+ ]);
2548
+ yield* Effect.logInfo(`\nVerify: samva email senders check ${data.id}`);
2549
+ })
2550
+ });
2551
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2552
+ const check = Command.make("check", {
2553
+ id: Argument.string("id").pipe(Argument.withDescription("Sender id to check")),
2554
+ json: jsonFlag
2555
+ }).pipe(Command.withDescription("Check a sender's verification status"), Command.withExamples([{
2556
+ command: "samva email senders check snd_abc123",
2557
+ description: "Poll sender verification"
2558
+ }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
2559
+ const client = yield* getClientProvider;
2560
+ const data = yield* Email.checkSenderVerification({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not check sender", error) })));
2561
+ if (json) return yield* printJson(data);
2562
+ yield* printFields([
2563
+ ["Email", data.email],
2564
+ ["Status", data.status],
2565
+ ["Verified at", data.verifiedAt ?? null]
2566
+ ]);
2567
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2568
+ const remove$2 = Command.make("remove", {
2569
+ id: Argument.string("id").pipe(Argument.withDescription("Sender id to permanently remove")),
2570
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the deletion plan without calling the API"), Flag.withDefault(false)),
2571
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm permanent deletion (required for non-interactive use)"), Flag.withDefault(false)),
2572
+ json: jsonFlag
2573
+ }).pipe(Command.withDescription("Permanently remove a sender identity"), Command.withExamples([{ command: "samva email senders remove snd_abc123 --dry-run" }, { command: "samva email senders remove snd_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
2574
+ if (dryRun) return yield* json ? printJson({
2575
+ dryRun: true,
2576
+ action: "email.senders.remove",
2577
+ id
2578
+ }) : Console.log(`[dry-run] Would permanently remove sender ${id}.`);
2579
+ yield* confirmDestructive({
2580
+ yes,
2581
+ json,
2582
+ message: `Permanently remove sender ${id}?`,
2583
+ nonInteractiveMessage: "Permanent sender deletion requires --yes in non-interactive mode. Preview with --dry-run."
2584
+ });
2585
+ const client = yield* getClientProvider;
2586
+ const data = yield* Email.removeSender({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not remove sender", error) })));
2587
+ if (json) return yield* printJson(data);
2588
+ yield* Console.log(`Removed sender ${id}.`);
2589
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2590
+ const sendersCommand = Command.make("senders").pipe(Command.withDescription("Manage sender addresses"), Command.withExamples([{
2591
+ command: "samva email senders list",
2592
+ description: "List sender addresses"
2593
+ }]), Command.withSubcommands([
2594
+ list$4,
2595
+ add,
2596
+ check,
2597
+ remove$2
2598
+ ]));
2599
+ //#endregion
2600
+ //#region src/commands/email/tracking.ts
2601
+ const toggle = (name, description) => Flag.choice(name, ["on", "off"]).pipe(Flag.withDescription(description), Flag.optional);
2602
+ const validateRecipientEmail = (email) => Schema.decodeUnknownEffect(Email.TrackingRecipientEmail)(email).pipe(Effect.mapError(() => new CliUsageError({
2603
+ code: "invalid_recipient_email",
2604
+ message: "Recipient must be a valid email address."
2605
+ })));
2606
+ const defaultsGet = Command.make("get", { json: jsonFlag }).pipe(Command.withDescription("Get organization email tracking defaults"), Command.withExamples([{ command: "samva email tracking defaults get --json" }]), Command.withHandler(({ json }) => Effect.gen(function* () {
2607
+ const client = yield* getClientProvider;
2608
+ const data = yield* Email.getTrackingDefaults().pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get email tracking defaults", error) })));
2609
+ if (json) return yield* printJson(data);
2610
+ yield* printFields([["Track opens", data.opens ? "on" : "off"], ["Track clicks", data.clicks ? "on" : "off"]]);
2611
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2612
+ const defaultsSet = Command.make("set", {
2613
+ opens: toggle("opens", "Set the organization open-tracking default (on or off)"),
2614
+ clicks: toggle("clicks", "Set the organization click-tracking default (on or off)"),
2615
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the planned change without calling the API"), Flag.withDefault(false)),
2616
+ json: jsonFlag
2617
+ }).pipe(Command.withDescription("Update one or both organization email tracking defaults"), Command.withExamples([{ command: "samva email tracking defaults set --opens off --dry-run" }, { command: "samva email tracking defaults set --opens on --clicks off" }]), Command.withHandler(({ opens, clicks, dryRun, json }) => Effect.gen(function* () {
2618
+ const opensValue = Option.getOrUndefined(opens);
2619
+ const clicksValue = Option.getOrUndefined(clicks);
2620
+ if (opensValue === void 0 && clicksValue === void 0) return yield* new CliUsageError({
2621
+ code: "missing_tracking_default",
2622
+ message: "Provide --opens, --clicks, or both."
2623
+ });
2624
+ const request = {
2625
+ ...opensValue === void 0 ? {} : { opens: opensValue === "on" },
2626
+ ...clicksValue === void 0 ? {} : { clicks: clicksValue === "on" }
2627
+ };
2628
+ if (dryRun) {
2629
+ if (json) return yield* printJson({
2630
+ dryRun: true,
2631
+ action: "email.tracking.defaults.set",
2632
+ request
2633
+ });
2634
+ return yield* Console.log(`[dry-run] Would update email tracking defaults: ${JSON.stringify(request)}.`);
2635
+ }
2636
+ const client = yield* getClientProvider;
2637
+ const data = yield* Email.updateTrackingDefaults(request).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not update email tracking defaults", error) })));
2638
+ if (json) return yield* printJson(data);
2639
+ yield* printFields([["Track opens", data.opens ? "on" : "off"], ["Track clicks", data.clicks ? "on" : "off"]]);
2640
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2641
+ const recipientGet = Command.make("get", {
2642
+ email: Argument.string("email").pipe(Argument.withDescription("Recipient email address")),
2643
+ json: jsonFlag
2644
+ }).pipe(Command.withDescription("Get a recipient's email tracking preference"), Command.withExamples([{ command: "samva email tracking recipient get person@example.com --json" }]), Command.withHandler(({ email, json }) => Effect.gen(function* () {
2645
+ const validatedEmail = yield* validateRecipientEmail(email);
2646
+ const client = yield* getClientProvider;
2647
+ const data = yield* Email.getRecipientTrackingPreference({ email: validatedEmail }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get recipient tracking preference", error) })));
2648
+ if (json) return yield* printJson(data);
2649
+ yield* printFields([
2650
+ ["Email", data.email],
2651
+ ["Tracking", data.optedOut ? "opted out" : "opted in"],
2652
+ ["Opted out at", data.trackingOptedOutAt ?? null],
2653
+ ["Updated at", data.updatedAt ?? null]
2654
+ ]);
2655
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2656
+ const recipientSet = Command.make("set", {
2657
+ email: Argument.string("email").pipe(Argument.withDescription("Recipient email address")),
2658
+ state: Flag.choice("state", ["opted-in", "opted-out"]).pipe(Flag.withDescription("Set the recipient tracking state (opted-in or opted-out)")),
2659
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Print the planned change without calling the API"), Flag.withDefault(false)),
2660
+ json: jsonFlag
2661
+ }).pipe(Command.withDescription("Opt a recipient in or out of email engagement tracking"), Command.withExamples([{ command: "samva email tracking recipient set person@example.com --state opted-out --dry-run" }, { command: "samva email tracking recipient set person@example.com --state opted-in" }]), Command.withHandler(({ email, state, dryRun, json }) => Effect.gen(function* () {
2662
+ const request = {
2663
+ email: yield* validateRecipientEmail(email),
2664
+ optedOut: state === "opted-out"
2665
+ };
2666
+ if (dryRun) {
2667
+ if (json) return yield* printJson({
2668
+ dryRun: true,
2669
+ action: "email.tracking.recipient.set",
2670
+ request
2671
+ });
2672
+ return yield* Console.log(`[dry-run] Would set ${email} to ${state} for email engagement tracking.`);
2673
+ }
2674
+ const client = yield* getClientProvider;
2675
+ const data = yield* Email.updateRecipientTrackingPreference(request).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not update recipient tracking preference", error) })));
2676
+ if (json) return yield* printJson(data);
2677
+ yield* printFields([
2678
+ ["Email", data.email],
2679
+ ["Tracking", data.optedOut ? "opted out" : "opted in"],
2680
+ ["Opted out at", data.trackingOptedOutAt ?? null],
2681
+ ["Updated at", data.updatedAt ?? null]
2682
+ ]);
2683
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2684
+ const defaults = Command.make("defaults").pipe(Command.withDescription("Read or update organization tracking defaults"), Command.withSubcommands([defaultsGet, defaultsSet]));
2685
+ const recipient = Command.make("recipient").pipe(Command.withDescription("Read or update a recipient tracking preference"), Command.withSubcommands([recipientGet, recipientSet]));
2686
+ const trackingCommand = Command.make("tracking").pipe(Command.withDescription("Configure email engagement tracking"), Command.withExamples([{ command: "samva email tracking defaults get" }]), Command.withSubcommands([defaults, recipient]));
2687
+ //#endregion
2688
+ //#region src/commands/email.ts
2689
+ const emailCommand = Command.make("email").pipe(Command.withDescription("Send, inspect, and configure email"), Command.withExamples([{
2690
+ command: "samva email send --to you@example.com --subject Hi --text Hello",
2691
+ description: "Send a plain-text email"
2692
+ }, {
2693
+ command: "samva email doctor",
2694
+ description: "Inspect read-only email readiness"
2695
+ }]), Command.withSubcommands([
2696
+ send,
2697
+ doctorCommand,
2698
+ receivingCommand,
2699
+ domainsCommand,
2700
+ sendersCommand,
2701
+ trackingCommand,
2702
+ designCommand
2703
+ ]));
2704
+ //#endregion
2705
+ //#region src/lib/examples.ts
2706
+ /** Shown in help descriptions for automation-oriented commands. */
2707
+ const AGENT_AUTH_HINT = "For CI and agents, set SAMVA_API_KEY instead of samva login.";
2708
+ //#endregion
2709
+ //#region src/lib/oauth.ts
2710
+ /**
2711
+ * OAuth device-authorization flow for `samva login`. These call Better Auth's
2712
+ * device endpoints under `/v1/auth/*` directly (not the product SDK) — the same
2713
+ * documented exception as `lib/http` for auth/ingress/health endpoints.
2714
+ */
2715
+ /** First-party public client id; allow-listed by the AS `validateClient`. */
2716
+ const SAMVA_CLI_CLIENT_ID = "samva-cli";
2717
+ const DEVICE_SCOPE = "openid profile email";
2718
+ const parseJson = (body, context) => Effect.try({
2719
+ try: () => JSON.parse(body),
2720
+ catch: () => new SamvaCliError({ message: `Unexpected response from ${context}.` })
2721
+ });
2722
+ /** RFC 8628 §3.1 — request a device code + user code. */
2723
+ const requestDeviceCode = (apiUrl) => Effect.gen(function* () {
2724
+ const res = yield* postJson(`${apiUrl}/v1/auth/device/code`, {
2725
+ client_id: SAMVA_CLI_CLIENT_ID,
2726
+ scope: DEVICE_SCOPE
2727
+ });
2728
+ if (!res.ok) return yield* new SamvaCliError({ message: `Could not start login (device/code returned ${res.status}). ${res.body}` });
2729
+ return yield* parseJson(res.body, "device/code");
2730
+ });
2731
+ /**
2732
+ * Internal retry signal for the RFC 8628 poll loop: the grant is not yet
2733
+ * decided (`authorization_pending` / `slow_down`). Never escapes
2734
+ * `pollForToken` — the retry schedule always recurs on it and only the
2735
+ * `expires_in` deadline ends the loop.
2736
+ */
2737
+ var DeviceAuthorizationPending = class extends Schema.TaggedErrorClass()("DeviceAuthorizationPending", {}) {};
2738
+ /**
2739
+ * RFC 8628 §3.4 — poll the token endpoint until the user approves, denies, or
2740
+ * the code expires. Honors `authorization_pending` / `slow_down` backoff.
2741
+ *
2742
+ * Clock/Schedule-driven: waits come from a retry schedule whose delay reads a
2743
+ * `Ref` holding the current interval (§3.5 `slow_down` grows it by 5s), and
2744
+ * the `expires_in` deadline is an `Effect.timeoutOrElse` — all TestClock-
2745
+ * advanceable.
2746
+ */
2747
+ const pollForToken = (apiUrl, deviceCode, intervalSeconds, expiresInSeconds) => Effect.gen(function* () {
2748
+ const initialInterval = Duration.seconds(Math.max(intervalSeconds, 1));
2749
+ const interval = yield* Ref.make(initialInterval);
2750
+ const pollOnce = Effect.gen(function* () {
2751
+ const res = yield* postJson(`${apiUrl}/v1/auth/device/token`, {
2752
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
2753
+ device_code: deviceCode,
2754
+ client_id: SAMVA_CLI_CLIENT_ID
2755
+ });
2756
+ if (res.ok) return yield* parseJson(res.body, "device/token");
2757
+ const { error, error_description } = yield* parseJson(res.body, "device/token");
2758
+ if (error === "authorization_pending") return yield* new DeviceAuthorizationPending();
2759
+ if (error === "slow_down") {
2760
+ yield* Ref.update(interval, Duration.sum(Duration.seconds(5)));
2761
+ return yield* new DeviceAuthorizationPending();
2762
+ }
2763
+ if (error === "access_denied") return yield* new CliAuthError({
2764
+ code: "authorization_denied",
2765
+ message: "Login was denied in the browser."
2766
+ });
2767
+ if (error === "expired_token") return yield* new CliTimeoutError({
2768
+ code: "login_timeout",
2769
+ message: "The login request expired before approval. Run `samva login` again."
2770
+ });
2771
+ return yield* new SamvaCliError({ message: `Login failed: ${error_description ?? error ?? res.body}` });
2772
+ });
2773
+ const pendingRetry = Schedule.forever.pipe(Schedule.setInputType(), Schedule.while(({ input }) => input instanceof DeviceAuthorizationPending), Schedule.modifyDelay(() => Ref.get(interval)));
2774
+ return yield* Effect.gen(function* () {
2775
+ yield* Effect.sleep(initialInterval);
2776
+ return yield* pollOnce.pipe(Effect.retry(pendingRetry));
2777
+ }).pipe(Effect.timeoutOrElse({
2778
+ duration: Duration.seconds(expiresInSeconds),
2779
+ orElse: () => Effect.fail(new CliTimeoutError({
2780
+ code: "login_timeout",
2781
+ message: "Login timed out. Run `samva login` again."
2782
+ }))
2783
+ }), Effect.catchTag("DeviceAuthorizationPending", () => Effect.fail(new CliTimeoutError({
2784
+ code: "login_timeout",
2785
+ message: "Login timed out. Run `samva login` again."
2786
+ }))));
2787
+ });
2788
+ /** Open a URL in the system browser. Best-effort — never fails the flow. */
2789
+ const openBrowser = (url) => {
2790
+ const platform = process.platform;
2791
+ const [command, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", [
2792
+ "/c",
2793
+ "start",
2794
+ "",
2795
+ url
2796
+ ]] : ["xdg-open", [url]];
2797
+ return Effect.tryPromise(async () => {
2798
+ const { spawn } = await import("node:child_process");
2799
+ const child = spawn(command, [...args], {
2800
+ stdio: "ignore",
2801
+ detached: true
2802
+ });
2803
+ child.on("error", () => void 0);
2804
+ child.unref();
2805
+ }).pipe(Effect.catchTag("UnknownError", () => Effect.void));
2806
+ };
2807
+ /** Best-effort server-side session revoke on logout (Better Auth `/sign-out`). */
2808
+ const revokeSession = (apiUrl, accessToken) => Effect.tryPromise({
2809
+ try: async () => {
2810
+ await fetch(`${apiUrl}/v1/auth/sign-out`, {
2811
+ method: "POST",
2812
+ headers: {
2813
+ "content-type": "application/json",
2814
+ authorization: `Bearer ${accessToken}`
2815
+ }
2816
+ });
2817
+ },
2818
+ catch: (cause) => new SamvaCliError({ message: `Sign-out request failed: ${String(cause)}` })
2819
+ });
2820
+ /**
2821
+ * List the organizations the signed-in user belongs to (Better Auth org plugin
2822
+ * `/organization/list`). User-scoped — unlike the product `organizations.get()`
2823
+ * endpoint it needs no `x-org-slug`, so it works immediately after `samva login`.
2824
+ */
2825
+ const listOrganizations = (apiUrl, accessToken) => Effect.gen(function* () {
2826
+ const result = yield* Effect.tryPromise({
2827
+ try: async () => {
2828
+ const res = await fetch(`${apiUrl}/v1/auth/organization/list`, { headers: { authorization: `Bearer ${accessToken}` } });
2829
+ return {
2830
+ ok: res.ok,
2831
+ status: res.status,
2832
+ body: await res.text()
2833
+ };
2834
+ },
2835
+ catch: (cause) => new SamvaCliError({ message: `Could not list organizations: ${String(cause)}` })
2836
+ });
2837
+ if (!result.ok) return yield* new SamvaCliError({ message: `Could not list organizations (returned ${result.status}). ${result.body}` });
2838
+ return yield* parseJson(result.body, "organization/list");
2839
+ });
2840
+ //#endregion
2841
+ //#region src/commands/login.ts
2842
+ const noBrowserFlag = Flag.boolean("no-browser").pipe(Flag.withDescription("Print the verification URL instead of opening a browser"));
2843
+ const loginCommand = Command.make("login", { noBrowser: noBrowserFlag }).pipe(Command.withDescription(`Sign in to Samva in your browser (OAuth device flow). ${AGENT_AUTH_HINT}`), Command.withExamples([{
2844
+ command: "samva login",
2845
+ description: "Interactive OAuth (opens browser)"
2846
+ }, {
2847
+ command: "samva login --no-browser",
2848
+ description: "Print verification URL instead of opening a browser"
2849
+ }]), Command.withHandler(Effect.fnUntraced(function* ({ noBrowser }) {
2850
+ if ((yield* Json) || (yield* JsonLines) || (yield* NoInput)) return yield* new CliUsageError({
2851
+ code: "interactive_login_disabled",
2852
+ message: "Login is interactive and unavailable with --json, --jsonl, or --no-input. Use human mode, or set SAMVA_API_KEY for automation."
2853
+ });
2854
+ if (!process.stdin.isTTY && !noBrowser) return yield* new CliUsageError({
2855
+ code: "non_interactive_login_requires_device_flow",
2856
+ message: "Login without a terminal requires SAMVA_API_KEY for automation. To use OAuth device flow explicitly, run `samva login --no-browser`."
2857
+ });
2858
+ const apiUrl = yield* resolveApiUrl;
2859
+ const device = yield* requestDeviceCode(apiUrl);
2860
+ const verifyUrl = device.verification_uri_complete ?? device.verification_uri;
2861
+ yield* Console.error(`\nTo authorize the Samva CLI, visit:\n\n ${device.verification_uri}\n`);
2862
+ yield* Console.error(`and enter the code:\n\n ${device.user_code}\n`);
2863
+ if (noBrowser) yield* Console.error(`Or open this URL directly:\n\n ${verifyUrl}\n`);
2864
+ else {
2865
+ yield* openBrowser(verifyUrl);
2866
+ yield* Effect.logInfo("Opened your browser to finish login. Waiting for approval…");
2867
+ }
2868
+ const token = yield* pollForToken(apiUrl, device.device_code, device.interval, device.expires_in);
2869
+ const nowMs = yield* Clock.currentTimeMillis;
2870
+ yield* writeCredentials({
2871
+ accessToken: token.access_token,
2872
+ tokenType: token.token_type,
2873
+ expiresAt: Math.floor(nowMs / 1e3) + token.expires_in,
2874
+ scope: token.scope
2875
+ });
2876
+ yield* printResult({ action: "login" }, "Logged in.");
2877
+ })));
2878
+ //#endregion
2879
+ //#region src/commands/logout.ts
2880
+ const logoutCommand = Command.make("logout").pipe(Command.withDescription("Sign out and remove stored credentials"), Command.withExamples([{
2881
+ command: "samva logout",
2882
+ description: "Clear stored OAuth tokens"
2883
+ }]), Command.withHandler(Effect.fnUntraced(function* () {
2884
+ const credentials = yield* readCredentials.pipe(Effect.catchIf((error) => error instanceof CliConfigError && error.code === "invalid_credential_record", () => Effect.void));
2885
+ if (credentials) {
2886
+ const apiUrl = yield* resolveApiUrl;
2887
+ yield* revokeSession(apiUrl, credentials.accessToken).pipe(Effect.catchTag("SamvaCliError", () => Effect.void));
2888
+ }
2889
+ yield* clearCredentials;
2890
+ yield* printResult({ action: "logout" }, "Logged out.");
2891
+ })));
2892
+ //#endregion
2893
+ //#region src/commands/usage.ts
2894
+ const periodFlag = Flag.choice("period", [
2895
+ "current",
2896
+ "last_30_days",
2897
+ "last_90_days",
2898
+ "last_year"
2899
+ ]).pipe(Flag.withDescription("Reporting period"), Flag.withDefault("current"));
2900
+ const usageCommand = Command.make("usage", {
2901
+ period: periodFlag,
2902
+ json: jsonFlag
2903
+ }).pipe(Command.withDescription("Get organization usage"), Command.withExamples([{ command: "samva org usage --period last_30_days --json" }]), Command.withHandler(({ period, json }) => Effect.gen(function* () {
2904
+ const client = yield* getClientProvider;
2905
+ const data = yield* Organizations.getUsage({ period }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get usage", error) })));
2906
+ if (json) return yield* printJson(data);
2907
+ const { periodStart, periodEnd, ...counts } = data.usage;
2908
+ yield* printFields([
2909
+ ["Start", periodStart],
2910
+ ["End", periodEnd],
2911
+ ["Plan emails", data.billing.includedEmails === null ? "—" : `${data.billing.usedEmails} of ${data.billing.includedEmails}`],
2912
+ ["Overage", data.billing.overageCost === null ? "—" : `${data.billing.overageCost} ${data.billing.currency}`]
2913
+ ]);
2914
+ yield* printJson(counts);
2915
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2916
+ //#endregion
2917
+ //#region src/commands/org.ts
2918
+ const list$3 = Command.make("list", { json: jsonFlag }).pipe(Command.withDescription("List the organizations you can access (multi-org under OAuth; single org under API key)"), Command.withExamples([{
2919
+ command: "samva org list",
2920
+ description: "List orgs (OAuth) or show API-key org"
2921
+ }, {
2922
+ command: "samva org list --json",
2923
+ description: "Machine-readable org list"
2924
+ }]), Command.withHandler(({ json }) => Effect.gen(function* () {
2925
+ const context = yield* resolveExecutionContext;
2926
+ if (context.credentials) {
2927
+ if (isCredentialExpired(context.credentials, yield* Clock.currentTimeMillis)) return yield* new CliAuthError({
2928
+ code: "expired_credentials",
2929
+ message: "Your session has expired. Run `samva login` to sign in again."
2930
+ });
2931
+ const orgs = yield* listOrganizations(context.apiUrl, context.credentials.accessToken);
2932
+ if (json) return yield* printJson(orgs);
2933
+ if (orgs.length === 0) return yield* Console.log("You are not a member of any organizations yet.");
2934
+ const lines = orgs.map((org) => {
2935
+ return `${org.slug === context.profileOrgSlug ? "* " : " "}${org.slug} ${org.name}`;
2936
+ });
2937
+ yield* Console.log(lines.join("\n"));
2938
+ return yield* Effect.logInfo("\nSet the active organization with `samva org use <slug>`.");
2939
+ }
2940
+ const client = yield* getClientProvider;
2941
+ const data = yield* Organizations.get().pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not load organization", error) })));
2942
+ if (json) return yield* printJson([data]);
2943
+ yield* printFields([
2944
+ ["Name", data.name],
2945
+ ["Slug", data.slug],
2946
+ ["ID", data.id],
2947
+ ["Plan", data.plan]
2948
+ ]);
2949
+ yield* Effect.logInfo("\nNote: an API key is scoped to a single organization. Run `samva login` for multi-org access.");
2950
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
2951
+ const use$1 = Command.make("use", { slug: Argument.string("slug") }).pipe(Command.withDescription("Set the active organization for OAuth sessions (ignored when using SAMVA_API_KEY)"), Command.withExamples([{
2952
+ command: "samva org use my-org-slug",
2953
+ description: "Set active org after samva login"
2954
+ }]), Command.withHandler(Effect.fnUntraced(function* ({ slug }) {
2955
+ const config = yield* readConfig;
2956
+ const context = yield* resolveExecutionContext;
2957
+ if (context.apiKey) return yield* new CliAuthError({
2958
+ code: "api_key_org_override",
2959
+ message: "samva org use is unavailable with SAMVA_API_KEY; API keys are organization-scoped."
2960
+ });
2961
+ const current = config.profiles[context.profile] ?? {
2962
+ target: "production",
2963
+ credentialIdentity: "none"
2964
+ };
2965
+ yield* writeConfig({
2966
+ ...config,
2967
+ profiles: {
2968
+ ...config.profiles,
2969
+ [context.profile]: {
2970
+ ...current,
2971
+ orgSlug: slug
2972
+ }
2973
+ }
2974
+ });
2975
+ yield* printResult({
2976
+ action: "org.use",
2977
+ slug
2978
+ }, `Active organization set to "${slug}".`);
2979
+ })));
2980
+ const current = Command.make("current", { json: jsonFlag }).pipe(Command.withDescription("Show the active organization slug"), Command.withExamples([{
2981
+ command: "samva org current",
2982
+ description: "Print active org slug"
2983
+ }, {
2984
+ command: "samva org current --json",
2985
+ description: "JSON: { \"activeOrgSlug\": \"...\" }"
2986
+ }]), Command.withHandler(({ json }) => Effect.gen(function* () {
2987
+ const slug = (yield* resolveExecutionContext).orgSlug;
2988
+ if (json) {
2989
+ if (!slug) return yield* new CliAuthError({
2990
+ code: "missing_organization",
2991
+ message: "No active organization set. Run `samva org use <slug>` or `samva org list`."
2992
+ });
2993
+ return yield* printJson({ activeOrgSlug: slug });
2994
+ }
2995
+ if (!slug) return yield* new CliAuthError({
2996
+ code: "missing_organization",
2997
+ message: "No active organization set. Run `samva org use <slug>` or `samva org list`."
2998
+ });
2999
+ yield* Console.log(slug);
3000
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3001
+ const orgCommand = Command.make("org").pipe(Command.withDescription(`Manage organizations. ${AGENT_AUTH_HINT}`), Command.withExamples([{
3002
+ command: "samva org list",
3003
+ description: "List accessible organizations"
3004
+ }, {
3005
+ command: "samva org use my-org-slug",
3006
+ description: "Set active org (OAuth)"
3007
+ }]), Command.withSubcommands([
3008
+ list$3,
3009
+ use$1,
3010
+ current,
3011
+ usageCommand
3012
+ ]));
3013
+ //#endregion
3014
+ //#region src/commands/profile.ts
3015
+ const list$2 = Command.make("list").pipe(Command.withDescription("List user-level execution profiles"), Command.withHandler(Effect.fnUntraced(function* () {
3016
+ const config = yield* readConfig;
3017
+ if ((yield* Json) || (yield* JsonLines)) return yield* printJson({
3018
+ activeProfile: config.activeProfile ?? "default",
3019
+ profiles: config.profiles
3020
+ });
3021
+ const active = config.activeProfile ?? "default";
3022
+ const lines = Object.entries(config.profiles).map(([name, profile]) => `${name === active ? "* " : " "}${name}\t${profile.target}${profile.orgSlug ? `\t${profile.orgSlug}` : ""}`);
3023
+ yield* Console.log(lines.length > 0 ? lines.join("\n") : "No profiles yet. Use `samva profile create <name>`.");
3024
+ })));
3025
+ const show = Command.make("show", { name: Argument.string("name").pipe(Argument.optional) }).pipe(Command.withDescription("Show the resolved execution context"), Command.withHandler(Effect.fnUntraced(function* ({ name }) {
3026
+ const context = yield* resolveExecutionContext;
3027
+ const selected = Option.getOrUndefined(name) ?? context.profile;
3028
+ const profile = (yield* readConfig).profiles[selected];
3029
+ if (!profile && selected !== "default") return yield* new CliUsageError({
3030
+ code: "unknown_profile",
3031
+ message: `Profile ${selected} does not exist.`
3032
+ });
3033
+ const output = {
3034
+ name: selected,
3035
+ ...profile ?? {
3036
+ target: "production",
3037
+ credentialIdentity: "none"
3038
+ },
3039
+ apiUrl: selected === context.profile ? context.apiUrl : void 0
3040
+ };
3041
+ if ((yield* Json) || (yield* JsonLines)) return yield* printJson(output);
3042
+ yield* Console.log(`${output.name}\t${output.target}${output.orgSlug ? `\t${output.orgSlug}` : ""}`);
3043
+ })));
3044
+ const create$2 = Command.make("create", {
3045
+ name: Argument.string("name"),
3046
+ target: Flag.choice("target", ["production", "local"]).pipe(Flag.withDefault("production")),
3047
+ org: Flag.string("org").pipe(Flag.optional)
3048
+ }).pipe(Command.withDescription("Create a non-secret execution profile"), Command.withHandler(Effect.fnUntraced(function* ({ name, target, org }) {
3049
+ yield* validateProfileName(name);
3050
+ const config = yield* readConfig;
3051
+ if (config.profiles[name]) return yield* new CliUsageError({
3052
+ code: "profile_exists",
3053
+ message: `Profile ${name} already exists. Delete it first or choose another name.`
3054
+ });
3055
+ const orgSlug = Option.getOrUndefined(org);
3056
+ yield* writeConfig({
3057
+ ...config,
3058
+ profiles: {
3059
+ ...config.profiles,
3060
+ [name]: {
3061
+ target,
3062
+ orgSlug,
3063
+ credentialIdentity: "none"
3064
+ }
3065
+ }
3066
+ });
3067
+ yield* printResult({
3068
+ action: "profile.create",
3069
+ name,
3070
+ target,
3071
+ orgSlug
3072
+ }, `Created profile ${name}.`);
3073
+ })));
3074
+ const use = Command.make("use", { name: Argument.string("name") }).pipe(Command.withDescription("Select the active user-level execution profile"), Command.withHandler(Effect.fnUntraced(function* ({ name }) {
3075
+ const config = yield* readConfig;
3076
+ if (!config.profiles[name]) return yield* new CliUsageError({
3077
+ code: "unknown_profile",
3078
+ message: `Profile ${name} does not exist. Create it with \`samva profile create ${name}\`.`
3079
+ });
3080
+ yield* writeConfig({
3081
+ ...config,
3082
+ activeProfile: name
3083
+ });
3084
+ yield* printResult({
3085
+ action: "profile.use",
3086
+ name
3087
+ }, `Active profile set to ${name}.`);
3088
+ })));
3089
+ const remove$1 = Command.make("delete", {
3090
+ name: Argument.string("name"),
3091
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false))
3092
+ }).pipe(Command.withDescription("Delete a profile and its OS-keyring credential"), Command.withHandler(Effect.fnUntraced(function* ({ name, yes }) {
3093
+ const config = yield* readConfig;
3094
+ const profile = config.profiles[name];
3095
+ if (!profile) return yield* new CliUsageError({
3096
+ code: "unknown_profile",
3097
+ message: `Profile ${name} does not exist.`
3098
+ });
3099
+ const machine = (yield* Json) || (yield* JsonLines);
3100
+ yield* confirmDestructive({
3101
+ yes,
3102
+ json: machine,
3103
+ message: `Delete profile ${name} and its saved credential?`,
3104
+ nonInteractiveMessage: "Profile deletion requires --yes in non-interactive mode."
3105
+ });
3106
+ const { [name]: _, ...profiles } = config.profiles;
3107
+ yield* writeConfig(config.activeProfile === name ? {
3108
+ version: 2,
3109
+ profiles
3110
+ } : {
3111
+ ...config,
3112
+ profiles
3113
+ });
3114
+ if (profile.credentialIdentity === "oauth") yield* clearCredentialSecret(name);
3115
+ yield* printResult({
3116
+ action: "profile.delete",
3117
+ name
3118
+ }, `Deleted profile ${name}.`);
3119
+ })));
3120
+ const profileCommand = Command.make("profile").pipe(Command.withDescription("Manage user-level target, credential identity, and organization contexts"), Command.withSubcommands([
3121
+ list$2,
3122
+ show,
3123
+ create$2,
3124
+ use,
3125
+ remove$1
3126
+ ]));
3127
+ //#endregion
3128
+ //#region src/commands/scheduled.ts
3129
+ const SCHEDULED_STATUSES = [
3130
+ "pending",
3131
+ "dispatching",
3132
+ "dispatched",
3133
+ "cancelled",
3134
+ "failed"
3135
+ ];
3136
+ const ABSOLUTE_INSTANT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
3137
+ const isAbsoluteInstant = (value) => {
3138
+ const match = ABSOLUTE_INSTANT_PATTERN.exec(value);
3139
+ if (!match || Number.isNaN(Date.parse(value))) return false;
3140
+ const year = Number(match[1]);
3141
+ const month = Number(match[2]);
3142
+ const day = Number(match[3]);
3143
+ const calendarDate = new Date(Date.UTC(year, month - 1, day));
3144
+ return calendarDate.getUTCFullYear() === year && calendarDate.getUTCMonth() === month - 1 && calendarDate.getUTCDate() === day;
3145
+ };
3146
+ /**
3147
+ * Assemble the `scheduledMessages.create` email payload from resolved flags.
3148
+ * Pure so the flag-to-request mapping can be unit-tested without the network.
3149
+ */
3150
+ const assembleScheduledEmailPayload = (input) => ({
3151
+ send: {
3152
+ channel: "email",
3153
+ to: input.to.map((email) => ({ email })),
3154
+ cc: input.cc ? [{ email: input.cc }] : void 0,
3155
+ bcc: input.bcc ? [{ email: input.bcc }] : void 0,
3156
+ email: {
3157
+ subject: input.subject,
3158
+ html: input.html,
3159
+ text: input.text,
3160
+ templateId: input.templateId,
3161
+ templateSlug: input.templateSlug,
3162
+ templateData: input.templateData,
3163
+ ...input.tracking === void 0 ? {} : { tracking: input.tracking }
3164
+ }
3165
+ },
3166
+ scheduledFor: input.scheduledFor,
3167
+ timezone: input.timezone,
3168
+ idempotencyKey: input.idempotencyKey,
3169
+ referenceId: input.referenceId
3170
+ });
3171
+ const parseTemplateData = (raw) => Option.match(raw, {
3172
+ onNone: () => Effect.succeed(void 0),
3173
+ onSome: (value) => Effect.try({
3174
+ try: () => JSON.parse(value),
3175
+ catch: () => new SamvaCliError({ message: "--template-data must be a JSON object." })
3176
+ }).pipe(Effect.flatMap((parsed) => Predicate.isObject(parsed) ? Effect.succeed(parsed) : Effect.fail(new SamvaCliError({ message: "--template-data must be a JSON object." }))))
3177
+ });
3178
+ const toFlag = Flag.string("to").pipe(Flag.withDescription("Recipient email address (repeatable)"), Flag.atLeast(1));
3179
+ const ccFlag = Flag.string("cc").pipe(Flag.withDescription("Cc recipient email address"), Flag.optional);
3180
+ const bccFlag = Flag.string("bcc").pipe(Flag.withDescription("Bcc recipient email address"), Flag.optional);
3181
+ const subjectFlag = Flag.string("subject").pipe(Flag.withDescription("Email subject line (required with inline --html; omit for templates)"), Flag.optional);
3182
+ const htmlFlag = Flag.string("html").pipe(Flag.withDescription("HTML body"), Flag.optional);
3183
+ const textFlag = Flag.string("text").pipe(Flag.withDescription("Plain-text body"), Flag.optional);
3184
+ const templateIdFlag = Flag.string("template-id").pipe(Flag.withDescription("Template id (uuid) to render instead of inline body"), Flag.optional);
3185
+ const templateSlugFlag = Flag.string("template-slug").pipe(Flag.withDescription("Template handle to render instead of inline body, e.g. welcome-email"), Flag.optional);
3186
+ const templateDataFlag = Flag.string("template-data").pipe(Flag.withDescription("JSON object of variables for the template"), Flag.optional);
3187
+ const atFlag = Flag.string("at").pipe(Flag.withDescription("Absolute send instant as an ISO 8601 datetime, e.g. 2026-08-01T09:00:00Z"));
3188
+ const timezoneFlag = Flag.string("timezone").pipe(Flag.withDescription("IANA timezone for the schedule, e.g. America/New_York"), Flag.optional);
3189
+ const idempotencyKeyFlag = Flag.string("idempotency-key").pipe(Flag.withDescription("Idempotency key so retries do not schedule duplicates"), Flag.optional);
3190
+ const referenceIdFlag = Flag.string("reference-id").pipe(Flag.withDescription("Your own reference id to correlate this schedule"), Flag.optional);
3191
+ const dryRunFlag$1 = Flag.boolean("dry-run").pipe(Flag.withDescription("Validate inputs and print the request payload without calling the API"), Flag.withDefault(false));
3192
+ const trackOpensFlag = Flag.choice("track-opens", ["on", "off"]).pipe(Flag.withDescription("Override open tracking for this scheduled send; recipient opt-out wins"), Flag.optional);
3193
+ const trackClicksFlag = Flag.choice("track-clicks", ["on", "off"]).pipe(Flag.withDescription("Override click tracking for this scheduled send; recipient opt-out wins"), Flag.optional);
3194
+ const statusFlag = Flag.choice("status", SCHEDULED_STATUSES).pipe(Flag.withDescription("Filter by status"), Flag.optional);
3195
+ const pageFlag = Flag.integer("page").pipe(Flag.withDescription("Page number (1-based)"), Flag.optional);
3196
+ const limitFlag = Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional);
3197
+ const allFlag = Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false));
3198
+ const create$1 = Command.make("create", {
3199
+ to: toFlag,
3200
+ cc: ccFlag,
3201
+ bcc: bccFlag,
3202
+ subject: subjectFlag,
3203
+ html: htmlFlag,
3204
+ text: textFlag,
3205
+ templateId: templateIdFlag,
3206
+ templateSlug: templateSlugFlag,
3207
+ templateData: templateDataFlag,
3208
+ at: atFlag,
3209
+ timezone: timezoneFlag,
3210
+ idempotencyKey: idempotencyKeyFlag,
3211
+ referenceId: referenceIdFlag,
3212
+ trackOpens: trackOpensFlag,
3213
+ trackClicks: trackClicksFlag,
3214
+ dryRun: dryRunFlag$1,
3215
+ json: jsonFlag
3216
+ }).pipe(Command.withDescription("Schedule an email to send at a future time"), Command.withExamples([
3217
+ {
3218
+ command: "samva scheduled-messages create --to you@example.com --subject Hi --html \"<p>Hello</p>\" --text Hello --at 2026-08-01T09:00:00Z",
3219
+ description: "Schedule an inline HTML email with a text fallback"
3220
+ },
3221
+ {
3222
+ command: "samva scheduled-messages create --to you@example.com --template-slug welcome-email --template-data '{\"name\":\"Ada\"}' --at 2026-08-01T09:00:00Z --timezone America/New_York",
3223
+ description: "Schedule a published template; timezone is descriptive metadata"
3224
+ },
3225
+ {
3226
+ command: "samva scheduled-messages create --to you@example.com --subject Hi --html \"<p>Hello</p>\" --at 2026-08-01T09:00:00Z --dry-run",
3227
+ description: "Validate and print the payload without scheduling"
3228
+ }
3229
+ ]), Command.withHandler(({ to, cc, bcc, subject, html, text, templateId, templateSlug, templateData, at, timezone, idempotencyKey, referenceId, trackOpens, trackClicks, dryRun, json }) => Effect.gen(function* () {
3230
+ const htmlValue = Option.getOrUndefined(html);
3231
+ const textValue = Option.getOrUndefined(text);
3232
+ const templateIdValue = Option.getOrUndefined(templateId);
3233
+ const templateSlugValue = Option.getOrUndefined(templateSlug);
3234
+ const templateDataValue = yield* parseTemplateData(templateData);
3235
+ if (templateIdValue && templateSlugValue) return yield* new SamvaCliError({ message: "Use either --template-id or --template-slug, not both." });
3236
+ const templateRefValue = templateSlugValue ?? templateIdValue;
3237
+ if (templateRefValue && (Option.isSome(subject) || htmlValue || textValue)) return yield* new SamvaCliError({ message: "Choose one content source: a template, or inline --subject with --html (and optional --text)." });
3238
+ if (!templateRefValue && Option.isNone(subject)) return yield* new SamvaCliError({ message: "Provide --subject with inline --html, or reference a published template." });
3239
+ if (!templateRefValue && !htmlValue) return yield* new SamvaCliError({ message: "Provide --html for inline email; --text is an optional fallback, not a body source." });
3240
+ if (!isAbsoluteInstant(at)) return yield* new SamvaCliError({ message: "--at must be an absolute ISO 8601 instant with Z or an offset, e.g. 2026-08-01T09:00:00Z." });
3241
+ const payload = assembleScheduledEmailPayload({
3242
+ to: [...to],
3243
+ subject: Option.getOrUndefined(subject),
3244
+ html: htmlValue,
3245
+ text: textValue,
3246
+ templateId: templateIdValue,
3247
+ templateSlug: templateSlugValue,
3248
+ templateData: templateDataValue,
3249
+ cc: Option.getOrUndefined(cc),
3250
+ bcc: Option.getOrUndefined(bcc),
3251
+ scheduledFor: new Date(at),
3252
+ timezone: Option.getOrUndefined(timezone),
3253
+ idempotencyKey: Option.getOrUndefined(idempotencyKey),
3254
+ referenceId: Option.getOrUndefined(referenceId),
3255
+ tracking: Option.isSome(trackOpens) || Option.isSome(trackClicks) ? {
3256
+ ...Option.isSome(trackOpens) ? { opens: trackOpens.value === "on" } : {},
3257
+ ...Option.isSome(trackClicks) ? { clicks: trackClicks.value === "on" } : {}
3258
+ } : void 0
3259
+ });
3260
+ if (dryRun) {
3261
+ if (json) return yield* printJson({
3262
+ dryRun: true,
3263
+ request: payload
3264
+ });
3265
+ yield* Effect.logInfo("Dry run — no schedule created.\n");
3266
+ yield* printFields([
3267
+ ["To", [...to].join(", ")],
3268
+ ["Subject", Option.getOrUndefined(subject)],
3269
+ ["Send at", at],
3270
+ ["Timezone", Option.getOrUndefined(timezone)]
3271
+ ]);
3272
+ return yield* Console.log(`\nPayload:\n${truncateForDisplay(JSON.stringify(payload, null, 2))}`);
3273
+ }
3274
+ const client = yield* getClientProvider;
3275
+ const data = yield* ScheduledMessages.create(payload).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Failed to schedule message", error) })));
3276
+ if (json) return yield* printJson(data);
3277
+ yield* Effect.logInfo("Message scheduled.\n");
3278
+ yield* printFields([
3279
+ ["ID", data.id],
3280
+ ["Status", data.status],
3281
+ ["Send at", data.scheduledFor],
3282
+ ["Timezone", data.timezone]
3283
+ ]);
3284
+ yield* Effect.logInfo(`\nInspect it: samva scheduled-messages get ${data.id}`);
3285
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3286
+ const list$1 = Command.make("list", {
3287
+ status: statusFlag,
3288
+ page: pageFlag,
3289
+ limit: limitFlag,
3290
+ all: allFlag,
3291
+ json: jsonFlag
3292
+ }).pipe(Command.withDescription("List scheduled messages"), Command.withExamples([
3293
+ {
3294
+ command: "samva scheduled-messages list",
3295
+ description: "List all scheduled messages"
3296
+ },
3297
+ {
3298
+ command: "samva scheduled-messages list --status pending",
3299
+ description: "Only pending schedules"
3300
+ },
3301
+ { command: "samva scheduled-messages list --json" },
3302
+ {
3303
+ command: "samva scheduled-messages list --all --jsonl",
3304
+ description: "Stream every page as JSONL"
3305
+ }
3306
+ ]), Command.withHandler(({ status, page, limit, all, json }) => Effect.gen(function* () {
3307
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
3308
+ code: "all_requires_jsonl",
3309
+ message: "--all is an unbounded traversal and requires --jsonl."
3310
+ });
3311
+ const pageValue = Option.getOrUndefined(page);
3312
+ const limitValue = Option.getOrUndefined(limit);
3313
+ const client = yield* getClientProvider;
3314
+ const query = {
3315
+ status: Option.getOrUndefined(status),
3316
+ page: pageValue?.toString(),
3317
+ limit: limitValue?.toString()
3318
+ };
3319
+ if (all) {
3320
+ let nextPage = pageValue ?? 1;
3321
+ for (;;) {
3322
+ const data = yield* ScheduledMessages.list({
3323
+ ...query,
3324
+ page: nextPage.toString()
3325
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list scheduled messages", error) })));
3326
+ for (const item of data.items) yield* printJsonLine(item);
3327
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
3328
+ nextPage += 1;
3329
+ }
3330
+ }
3331
+ const data = yield* ScheduledMessages.list(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list scheduled messages", error) })));
3332
+ if (isJsonLinesRequested()) {
3333
+ for (const item of data.items) yield* printJsonLine(item);
3334
+ return;
3335
+ }
3336
+ if (json) return yield* printJson(data);
3337
+ if (data.items.length === 0) return yield* Console.log("No scheduled messages. Create one with `samva scheduled-messages create`.");
3338
+ for (const item of data.items) {
3339
+ yield* printFields([
3340
+ ["ID", item.id],
3341
+ ["Status", item.status],
3342
+ ["Channel", item.send.channel],
3343
+ ["Send at", item.scheduledFor],
3344
+ ["Timezone", item.timezone]
3345
+ ]);
3346
+ yield* Console.log("");
3347
+ }
3348
+ yield* Console.log(`Page ${String(data.pagination.page)} of ${String(data.pagination.totalPages)} (${String(data.pagination.total)} total)`);
3349
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3350
+ const get$1 = Command.make("get", {
3351
+ id: Argument.string("id"),
3352
+ json: jsonFlag
3353
+ }).pipe(Command.withDescription("Show a scheduled message by id"), Command.withExamples([{ command: "samva scheduled-messages get sched_abc123" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
3354
+ const client = yield* getClientProvider;
3355
+ const data = yield* ScheduledMessages.get({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not fetch scheduled message", error) })));
3356
+ if (json) return yield* printJson(data);
3357
+ yield* printFields([
3358
+ ["ID", data.id],
3359
+ ["Status", data.status],
3360
+ ["Channel", data.send.channel],
3361
+ ["Send at", data.scheduledFor],
3362
+ ["Timezone", data.timezone],
3363
+ ["Message ID", data.messageId],
3364
+ ["Reference", data.referenceId],
3365
+ ["Failure", data.failureReason]
3366
+ ]);
3367
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3368
+ const cancel = Command.make("cancel", {
3369
+ id: Argument.string("id"),
3370
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
3371
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
3372
+ json: jsonFlag
3373
+ }).pipe(Command.withDescription("Cancel a pending scheduled message"), Command.withExamples([{ command: "samva scheduled-messages cancel sched_abc123 --dry-run" }, { command: "samva scheduled-messages cancel sched_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
3374
+ if (dryRun) return yield* json ? printJson({
3375
+ dryRun: true,
3376
+ action: "scheduled.cancel",
3377
+ id
3378
+ }) : Console.log(`[dry-run] Would cancel scheduled message ${id}.`);
3379
+ yield* confirmDestructive({
3380
+ yes,
3381
+ json,
3382
+ message: `Cancel scheduled message ${id}?`,
3383
+ nonInteractiveMessage: "Scheduled message cancellation requires --yes in non-interactive mode. Preview with --dry-run."
3384
+ });
3385
+ const client = yield* getClientProvider;
3386
+ const data = yield* ScheduledMessages.cancel({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not cancel scheduled message", error) })));
3387
+ if (json) return yield* printJson(data);
3388
+ yield* Effect.logInfo("Scheduled message cancelled.\n");
3389
+ yield* printFields([["ID", data.item.id], ["Status", data.item.status]]);
3390
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3391
+ const scheduledMessagesCommand = Command.make("scheduled-messages").pipe(Command.withDescription("Schedule messages to send at a future time"), Command.withExamples([{
3392
+ command: "samva scheduled-messages create --to you@example.com --subject Hi --html \"<p>Hello</p>\" --at 2026-08-01T09:00:00Z",
3393
+ description: "Schedule an email"
3394
+ }, {
3395
+ command: "samva scheduled-messages list",
3396
+ description: "List scheduled messages"
3397
+ }]), Command.withSubcommands([
3398
+ create$1,
3399
+ list$1,
3400
+ get$1,
3401
+ cancel
3402
+ ]));
3403
+ //#endregion
3404
+ //#region src/commands/templates/plan.ts
3405
+ const emptyLock = {
3406
+ version: 1,
3407
+ entries: {}
3408
+ };
3409
+ const sha256 = (value) => createHash("sha256").update(value, "utf8").digest("hex");
3410
+ /**
3411
+ * Derives a platform slug from a file-backed template name.
3412
+ * `welcome-email` → `welcome-email`, `accountUpdate2` → `account-update-2`.
3413
+ */
3414
+ const slugFromName = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase().replace(/^-+|-+$/g, "");
3415
+ /**
3416
+ * Builds the push plan. `platformSlugs` are the slugs currently on the platform
3417
+ * (from listTemplates) so we distinguish create vs update. `platformBaseHash`
3418
+ * maps a slug to the platform's current source hash (the CLI derives it from the
3419
+ * `source` the list now returns); when absent, divergence falls back to the lock.
3420
+ */
3421
+ const planPush = (input) => {
3422
+ const items = input.templates.map((template) => {
3423
+ if (template.source === void 0 || template.diagnostics.length > 0) return {
3424
+ file: template.file,
3425
+ name: template.name,
3426
+ slug: template.slug,
3427
+ action: "blocked",
3428
+ sourceHash: void 0,
3429
+ reason: template.diagnostics[0]?.message ?? "template did not emit valid SML"
3430
+ };
3431
+ const hash = sha256(template.source);
3432
+ const lockEntry = input.lock.entries[template.slug];
3433
+ const onPlatform = input.platformSlugs.has(template.slug);
3434
+ const platformHash = input.platformBaseHash?.[template.slug];
3435
+ if (!input.force && onPlatform && lockEntry !== void 0 && platformHash !== void 0 && platformHash !== lockEntry.sourceHash) return {
3436
+ file: template.file,
3437
+ name: template.name,
3438
+ slug: template.slug,
3439
+ action: "diverged",
3440
+ sourceHash: hash,
3441
+ reason: "the platform's template changed since your last push — re-run with --force to overwrite"
3442
+ };
3443
+ const platformMatchesLocal = platformHash === void 0 || platformHash === hash;
3444
+ if (!input.force && lockEntry !== void 0 && lockEntry.sourceHash === hash && onPlatform && platformMatchesLocal) return {
3445
+ file: template.file,
3446
+ name: template.name,
3447
+ slug: template.slug,
3448
+ action: "unchanged",
3449
+ sourceHash: hash,
3450
+ reason: void 0
3451
+ };
3452
+ return {
3453
+ file: template.file,
3454
+ name: template.name,
3455
+ slug: template.slug,
3456
+ action: onPlatform ? "update" : "create",
3457
+ sourceHash: hash,
3458
+ reason: void 0
3459
+ };
3460
+ });
3461
+ return {
3462
+ items,
3463
+ hasBlocking: items.some((item) => item.action === "blocked" || item.action === "diverged")
3464
+ };
3465
+ };
3466
+ /** Applies a successful push to the lock, replacing entries for pushed slugs. */
3467
+ const applyPushToLock = (lock, pushed, compilerVersion) => {
3468
+ const entries = { ...lock.entries };
3469
+ for (const item of pushed) entries[item.slug] = {
3470
+ slug: item.slug,
3471
+ sourceHash: item.sourceHash,
3472
+ compilerVersion
3473
+ };
3474
+ return {
3475
+ version: 1,
3476
+ entries
3477
+ };
3478
+ };
3479
+ /**
3480
+ * Compares the CLI's compiler version (from @samva/markup) against the platform's
3481
+ * reported compilerVersion. A mismatch is preview-staleness only — the platform
3482
+ * recompiles on its side — so this returns a warning string, never an error.
3483
+ */
3484
+ const skewWarning = (cliVersion, platformVersion) => {
3485
+ if (platformVersion === void 0 || platformVersion === cliVersion) return void 0;
3486
+ return `compiler version skew: your @samva/markup compiles v${cliVersion}, the platform runs v${platformVersion}. Local previews may differ from sent mail; the platform recompiles from the SML you push, so nothing wrong is sent. Update @samva/markup to match.`;
3487
+ };
3488
+ //#endregion
3489
+ //#region src/commands/templates.ts
3490
+ /**
3491
+ * `samva templates push/pull/init` — the local-repo authoring workflow for the
3492
+ * TSX email framework (design brief §7b). `push` emits SML from your `.tsx`
3493
+ * templates and (git-style) create-or-updates them on the platform; `pull`
3494
+ * writes the platform's SML back to `.samva` files; `init` scaffolds a project.
3495
+ *
3496
+ * Push sends SML ONLY — the platform recompiles server-side with the org's theme
3497
+ * layers. The templates API accepts a code-origin SML `source` (+ `origin:
3498
+ * "tsx"`) on create/update and returns it on list/get. Everything local (emit,
3499
+ * plan, divergence, skew) runs first; use --dry-run to review the SML that would
3500
+ * be sent without touching the API.
3501
+ */
3502
+ const LOCK_FILE = ".samva/push-lock.json";
3503
+ const dirFlag = Flag.directory("dir").pipe(Flag.withDescription("Directory containing your .tsx / .samva templates"), Flag.withDefault("emails"));
3504
+ const forceFlag = Flag.boolean("force").pipe(Flag.withDescription("Overwrite templates that changed on the platform since your last push"), Flag.withDefault(false));
3505
+ const dryRunFlag = Flag.boolean("dry-run").pipe(Flag.withDescription("Emit SML and print the push plan without calling the API"), Flag.withDefault(false));
3506
+ const templateIdArgument = Argument.string("id").pipe(Argument.withDescription("Remote template id"));
3507
+ const parseObject$1 = (raw, flag) => Effect.try({
3508
+ try: () => JSON.parse(raw),
3509
+ catch: () => new SamvaCliError({ message: `${flag} must be a valid JSON object.` })
3510
+ }).pipe(Effect.flatMap((value) => Predicate.isObject(value) && !Array.isArray(value) ? Effect.succeed(value) : Effect.fail(new SamvaCliError({ message: `${flag} must be a non-null JSON object.` }))));
3511
+ const decodePayload$1 = (raw, flag, schema) => parseObject$1(raw, flag).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)), Effect.mapError((error) => error instanceof SamvaCliError ? error : new SamvaCliError({ message: `${flag} does not match the API schema: ${String(error)}` })));
3512
+ const buildDir = (dir) => Effect.tryPromise({
3513
+ try: async () => {
3514
+ const child = Bun.spawn([
3515
+ "node",
3516
+ "--input-type=module",
3517
+ "--eval",
3518
+ "import { buildTemplates } from \"@samva/vite/build\"; const result = await buildTemplates(process.argv[1]); process.stdout.write(JSON.stringify(result));",
3519
+ resolve(dir)
3520
+ ], {
3521
+ cwd: process.cwd(),
3522
+ stdout: "pipe",
3523
+ stderr: "pipe"
3524
+ });
3525
+ const [stdout, stderr, exitCode] = await Promise.all([
3526
+ new Response(child.stdout).text(),
3527
+ new Response(child.stderr).text(),
3528
+ child.exited
3529
+ ]);
3530
+ if (exitCode !== 0) return Promise.reject(new SamvaCliError({ message: stderr.trim() || "Template build process failed." }));
3531
+ return JSON.parse(stdout);
3532
+ },
3533
+ catch: (cause) => new SamvaCliError({ message: `Could not build templates in ${dir}. Install Node.js 22+, @samva/markup, @samva/vite, and vite in this project: ${String(cause)}` })
3534
+ });
3535
+ /** Compile-stage diagnostics across the channel-polymorphic preview payload. */
3536
+ const compileDiagnostics = (compiled) => {
3537
+ if (compiled.channel === "email") return compiled.bases.flatMap((base) => base.ok ? [] : base.diagnostics ?? []);
3538
+ return compiled.ok ? [] : compiled.diagnostics ?? [];
3539
+ };
3540
+ /** Flattens a build result into per-template BuiltTemplate rows for planning. */
3541
+ const toBuiltTemplates = (result) => result.files.flatMap((file) => {
3542
+ const templates = file.preview.templates.map((template) => ({
3543
+ file: file.id,
3544
+ name: template.name,
3545
+ slug: slugFromName(template.name),
3546
+ source: template.source,
3547
+ variables: template.variables.map((variable) => ({
3548
+ name: variable.name,
3549
+ required: variable.required
3550
+ })),
3551
+ diagnostics: [...template.diagnostics.map((diagnostic) => ({
3552
+ code: diagnostic.code,
3553
+ message: diagnostic.message
3554
+ })), ...compileDiagnostics(template.compiled).map((diagnostic) => ({
3555
+ code: diagnostic.code,
3556
+ message: diagnostic.message
3557
+ }))]
3558
+ }));
3559
+ if (templates.length > 0) return templates;
3560
+ const errors = file.preview.diagnostics.filter((diagnostic) => diagnostic.code !== "missing-default-export");
3561
+ if (errors.length === 0) return [];
3562
+ const name = file.id.slice(file.id.lastIndexOf("/") + 1).replace(/\.(?:tsx|samva)$/, "");
3563
+ return [{
3564
+ file: file.id,
3565
+ name,
3566
+ slug: slugFromName(name),
3567
+ source: void 0,
3568
+ variables: [],
3569
+ diagnostics: errors.map((diagnostic) => ({
3570
+ code: diagnostic.code,
3571
+ message: diagnostic.message
3572
+ }))
3573
+ }];
3574
+ });
3575
+ const buildWarnings = (result) => result.files.flatMap((file) => file.preview.diagnostics.filter((diagnostic) => diagnostic.code === "missing-default-export").map((diagnostic) => ({
3576
+ file: file.id,
3577
+ code: diagnostic.code,
3578
+ message: diagnostic.message
3579
+ })));
3580
+ const compilerVersionOf = (result) => result.files[0]?.preview.compilerVersion ?? "unknown";
3581
+ /**
3582
+ * The lock lives at the PROJECT root (cwd), not inside the templates
3583
+ * directory — it's per-project push state, like a lockfile.
3584
+ */
3585
+ const readLock = () => Effect.tryPromise({
3586
+ try: async () => {
3587
+ const text = await readFile(join(process.cwd(), LOCK_FILE), "utf8").catch(() => void 0);
3588
+ if (text === void 0) return emptyLock;
3589
+ const parsed = JSON.parse(text);
3590
+ return parsed.version === 1 && parsed.entries ? parsed : emptyLock;
3591
+ },
3592
+ catch: () => new SamvaCliError({ message: `Could not read ${join(process.cwd(), LOCK_FILE)}` })
3593
+ });
3594
+ /**
3595
+ * Persist the push lock (git-index-style base) after a successful push. The lock
3596
+ * lives at the project root and is only advanced for slugs that actually landed.
3597
+ */
3598
+ const writeLock = (lock) => Effect.tryPromise({
3599
+ try: async () => {
3600
+ const target = join(process.cwd(), LOCK_FILE);
3601
+ await mkdir(dirname(target), { recursive: true });
3602
+ await writeFile(target, `${JSON.stringify(lock, null, 2)}\n`, "utf8");
3603
+ },
3604
+ catch: () => new SamvaCliError({ message: `Could not write ${join(process.cwd(), LOCK_FILE)}` })
3605
+ });
3606
+ /**
3607
+ * Page through the full template list — the slug→id map and create-vs-update
3608
+ * classification must see EVERY platform template, or an org past one page
3609
+ * would misclassify existing templates as creates (409s on push).
3610
+ */
3611
+ const listAllTemplates = (client) => Effect.gen(function* () {
3612
+ const items = [];
3613
+ let page = 1;
3614
+ for (;;) {
3615
+ const response = yield* Templates.list({
3616
+ page: page.toString(),
3617
+ limit: "200"
3618
+ }).pipe(client.provide);
3619
+ items.push(...response.items);
3620
+ const totalPages = Number(response.pagination.totalPages);
3621
+ if (!Number.isFinite(totalPages) || page >= totalPages || response.items.length === 0) return items;
3622
+ page += 1;
3623
+ }
3624
+ });
3625
+ const printPushPlan = (plan, compilerVersion, sources) => Effect.gen(function* () {
3626
+ for (const item of plan.items) {
3627
+ yield* printFields([[item.action.toUpperCase(), `${item.name} → ${item.slug}`], ...item.reason ? [["reason", item.reason]] : []]);
3628
+ if (item.action !== "blocked") {
3629
+ const source = sources.get(item.slug);
3630
+ if (source) yield* Console.log(`\n${truncateForDisplay(source)}\n`);
3631
+ }
3632
+ }
3633
+ yield* Console.log(`compiler v${compilerVersion} · ${plan.items.length} template(s)`);
3634
+ });
3635
+ const push = Command.make("push", {
3636
+ dir: dirFlag,
3637
+ force: forceFlag,
3638
+ dryRun: dryRunFlag,
3639
+ json: jsonFlag
3640
+ }).pipe(Command.withDescription("Emit SML from your TSX templates and push them to the platform"), Command.withExamples([{
3641
+ command: "samva templates push --dir emails --dry-run",
3642
+ description: "Emit SML and print the push plan without calling the API"
3643
+ }, {
3644
+ command: "samva templates push --dir emails --force",
3645
+ description: "Push, overwriting templates changed on the platform since your last push"
3646
+ }]), Command.withHandler(({ dir, force, dryRun, json }) => Effect.gen(function* () {
3647
+ const built = yield* buildDir(dir);
3648
+ const templates = toBuiltTemplates(built);
3649
+ const warnings = buildWarnings(built);
3650
+ if (templates.length === 0) return yield* new SamvaCliError({ message: `No templates found in ${dir}. Each TSX template file must default-export one <Email>, <Sms>, or <WhatsApp> template.` });
3651
+ const compilerVersion = compilerVersionOf(built);
3652
+ const lock = yield* readLock();
3653
+ const sources = /* @__PURE__ */ new Map();
3654
+ for (const template of templates) if (template.source) sources.set(template.slug, template.source);
3655
+ const client = dryRun ? void 0 : yield* getClientProvider;
3656
+ const platform = dryRun ? [] : yield* listAllTemplates(client).pipe(Effect.catch(() => Effect.succeed([])));
3657
+ const slugToId = /* @__PURE__ */ new Map();
3658
+ const platformBaseHash = {};
3659
+ for (const item of platform) {
3660
+ slugToId.set(item.slug, item.id);
3661
+ if (item.source) platformBaseHash[item.slug] = sha256(item.source);
3662
+ }
3663
+ const platformSlugs = dryRun ? new Set(Object.keys(lock.entries)) : new Set(slugToId.keys());
3664
+ const plan = planPush({
3665
+ templates,
3666
+ lock,
3667
+ platformSlugs,
3668
+ platformBaseHash,
3669
+ force
3670
+ });
3671
+ const skew = skewWarning(compilerVersion, void 0);
3672
+ if (json && dryRun) return yield* printJson({
3673
+ action: "templates.push",
3674
+ dryRun,
3675
+ compilerVersion,
3676
+ skewWarning: skew,
3677
+ plan: plan.items,
3678
+ sources: Object.fromEntries(sources),
3679
+ warnings
3680
+ });
3681
+ if (!json) {
3682
+ for (const warning of warnings) yield* Console.error(`⚠ ${warning.file}: ${warning.message}`);
3683
+ if (skew) yield* Console.error(`⚠ ${skew}\n`);
3684
+ yield* printPushPlan(plan, compilerVersion, sources);
3685
+ }
3686
+ if (dryRun) {
3687
+ yield* Effect.logInfo("\nDry run — no API request sent.");
3688
+ return;
3689
+ }
3690
+ if (plan.hasBlocking) {
3691
+ const blocked = plan.items.filter((item) => item.action === "blocked" || item.action === "diverged");
3692
+ if (json) yield* printJson({
3693
+ action: "templates.push",
3694
+ dryRun: false,
3695
+ compilerVersion,
3696
+ skewWarning: skew,
3697
+ warnings,
3698
+ blocked: blocked.map((item) => ({
3699
+ slug: item.slug,
3700
+ action: item.action,
3701
+ reason: item.reason
3702
+ })),
3703
+ results: []
3704
+ });
3705
+ return yield* new SamvaCliError({ message: `Cannot push: ${blocked.length} template(s) blocked or diverged (${blocked.map((item) => `${item.slug} [${item.action}]`).join(", ")}). Fix the diagnostics or re-run with --force for diverged templates.` });
3706
+ }
3707
+ const results = [];
3708
+ const landed = [];
3709
+ const runItem = (send, label) => send.pipe(Effect.as({
3710
+ ok: true,
3711
+ error: void 0
3712
+ }), Effect.catch((error) => Effect.succeed({
3713
+ ok: false,
3714
+ error: formatApiError(`${label} failed`, error)
3715
+ })));
3716
+ for (const item of plan.items) {
3717
+ if (item.action !== "create" && item.action !== "update") continue;
3718
+ const source = sources.get(item.slug);
3719
+ if (source === void 0 || item.sourceHash === void 0) continue;
3720
+ const outcome = yield* runItem(client.provide(item.action === "create" ? Templates.create({
3721
+ name: item.name,
3722
+ slug: item.slug,
3723
+ origin: "tsx",
3724
+ source
3725
+ }) : Templates.update({
3726
+ id: slugToId.get(item.slug),
3727
+ data: {
3728
+ origin: "tsx",
3729
+ source
3730
+ }
3731
+ })), item.action);
3732
+ results.push({
3733
+ ...item,
3734
+ ...outcome
3735
+ });
3736
+ if (outcome.ok) landed.push({
3737
+ slug: item.slug,
3738
+ sourceHash: item.sourceHash
3739
+ });
3740
+ }
3741
+ if (landed.length > 0) yield* writeLock(applyPushToLock(lock, landed, compilerVersion));
3742
+ const failed = results.filter((result) => !result.ok);
3743
+ if (json) return yield* printJson({
3744
+ action: "templates.push",
3745
+ dryRun: false,
3746
+ compilerVersion,
3747
+ skewWarning: skew,
3748
+ warnings,
3749
+ results
3750
+ });
3751
+ for (const result of results) yield* result.ok ? Effect.logInfo(`✓ ${result.action} ${result.slug}`) : Effect.logWarning(`✗ ${result.action} ${result.slug} — ${result.error}`);
3752
+ const created = results.filter((result) => result.ok && result.action === "create").length;
3753
+ const updated = results.filter((result) => result.ok && result.action === "update").length;
3754
+ yield* Effect.logInfo(`\n${created} created · ${updated} updated · ${failed.length} failed`);
3755
+ if (failed.length > 0) return yield* new SamvaCliError({ message: `${failed.length} template(s) failed to push (${failed.map((result) => result.slug).join(", ")}).` });
3756
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3757
+ const pull = Command.make("pull", {
3758
+ dir: dirFlag,
3759
+ dryRun: dryRunFlag,
3760
+ json: jsonFlag
3761
+ }).pipe(Command.withDescription("Write the platform's template SML to local .samva files (one-way)"), Command.withExamples([{
3762
+ command: "samva templates pull emails",
3763
+ description: "Write each platform template's SML to emails/<slug>.samva"
3764
+ }]), Command.withHandler(({ dir, dryRun, json }) => Effect.gen(function* () {
3765
+ const root = resolve(dir);
3766
+ if (dryRun) return yield* json ? printJson({
3767
+ dryRun: true,
3768
+ action: "templates.pull",
3769
+ dir: root
3770
+ }) : Console.log(`[dry-run] Would write remote template SML to ${root}.`);
3771
+ const client = yield* getClientProvider;
3772
+ const listed = yield* listAllTemplates(client).pipe(Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list templates", error) })));
3773
+ const withSource = listed.filter((item) => typeof item.source === "string" && item.source.length > 0);
3774
+ const skipped = listed.filter((item) => typeof item.source !== "string" || item.source.length === 0);
3775
+ yield* Effect.tryPromise({
3776
+ try: () => Promise.all(withSource.map(async (item) => {
3777
+ const target = join(root, `${item.slug}.samva`);
3778
+ await mkdir(dirname(target), { recursive: true });
3779
+ await writeFile(target, item.source, "utf8");
3780
+ })),
3781
+ catch: (cause) => new SamvaCliError({ message: `Could not write .samva files: ${String(cause)}` })
3782
+ });
3783
+ if (json) return yield* printJson({
3784
+ action: "templates.pull",
3785
+ dir: root,
3786
+ written: withSource.map((item) => ({
3787
+ slug: item.slug,
3788
+ name: item.name,
3789
+ file: `${item.slug}.samva`
3790
+ })),
3791
+ skipped: skipped.map((item) => ({
3792
+ slug: item.slug,
3793
+ name: item.name
3794
+ }))
3795
+ });
3796
+ yield* printFields([
3797
+ ["Target dir", root],
3798
+ ["Written", withSource.length],
3799
+ ["Skipped (no SML source)", skipped.length]
3800
+ ]);
3801
+ for (const item of withSource) yield* Effect.logInfo(`✓ ${item.slug}.samva`);
3802
+ for (const item of skipped) yield* Effect.logWarning(`⤫ ${item.slug} — no SML source, skipped`);
3803
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3804
+ const STARTER_WELCOME = `import { A, Button, Container, Email, Heading, Preview, Strong, Text } from "@samva/markup/components";
3805
+ import type { Variables } from "@samva/markup/variables";
3806
+
3807
+ export default function WelcomeEmail(v: Variables<{ firstName: string; workspace: string; ctaUrl: string }>) {
3808
+ return (
3809
+ <Email class="bg-gray-100">
3810
+ <Preview>Welcome aboard, {v.firstName}</Preview>
3811
+ <Container class="max-w-[600px] rounded-lg bg-white px-8 py-6">
3812
+ <Heading level={1} class="text-2xl font-bold text-gray-900">
3813
+ Welcome, {v.firstName}
3814
+ </Heading>
3815
+ <Text class="text-base text-gray-700 dark:text-gray-200">
3816
+ Thanks for joining <Strong>Samva</Strong>. Your workspace {v.workspace} is ready — open the{" "}
3817
+ <A href="https://samva.dev/docs">docs</A> to begin.
3818
+ </Text>
3819
+ <Button href="{{ctaUrl}}" class="rounded-md bg-blue-600 px-6 py-3 font-semibold text-white dark:bg-blue-400">
3820
+ Open dashboard
3821
+ </Button>
3822
+ </Container>
3823
+ </Email>
3824
+ );
3825
+ }
3826
+ `;
3827
+ const STARTER_VITE_CONFIG = `import { samvaEditor } from "@samva/vite";
3828
+ import { defineConfig } from "vite";
3829
+
3830
+ // Run \`npm run samva:dev\` and open http://localhost:5173/ for the Samva editor.
3831
+ export default defineConfig({
3832
+ plugins: [samvaEditor({ templatesDir: "emails" })],
3833
+ });
3834
+ `;
3835
+ const STARTER_TSCONFIG = `{
3836
+ "compilerOptions": {
3837
+ "target": "ES2022",
3838
+ "module": "Preserve",
3839
+ "moduleResolution": "Bundler",
3840
+ "strict": true,
3841
+ "skipLibCheck": true,
3842
+ "noEmit": true,
3843
+ "jsx": "react-jsx",
3844
+ "jsxImportSource": "@samva/markup"
3845
+ },
3846
+ "include": ["emails", "samva.vite.config.ts"]
3847
+ }
3848
+ `;
3849
+ const AUTHORING_DEV_DEPENDENCIES = {
3850
+ "@samva/markup": `^${MarkupPackageJson.version}`,
3851
+ "@samva/vite": `^${VitePackageJson.version}`,
3852
+ vite: "^8.0.0"
3853
+ };
3854
+ const AUTHORING_SCRIPTS = {
3855
+ "samva:dev": "vite --config samva.vite.config.ts",
3856
+ "samva:push": "samva templates push --dir emails"
3857
+ };
3858
+ const STARTER_PACKAGE_JSON = `${JSON.stringify({
3859
+ private: true,
3860
+ type: "module",
3861
+ scripts: {
3862
+ dev: "npm run samva:dev",
3863
+ push: "npm run samva:push",
3864
+ ...AUTHORING_SCRIPTS
3865
+ },
3866
+ devDependencies: AUTHORING_DEV_DEPENDENCIES
3867
+ }, null, 2)}\n`;
3868
+ const reconcilePackageJson = async (target) => {
3869
+ const raw = await readFile(target, "utf8").catch((error) => error.code === "ENOENT" ? void 0 : Promise.reject(error));
3870
+ if (raw === void 0) {
3871
+ await writeFile(target, STARTER_PACKAGE_JSON, { flag: "wx" });
3872
+ return {
3873
+ path: "package.json",
3874
+ status: "created"
3875
+ };
3876
+ }
3877
+ const manifest = JSON.parse(raw);
3878
+ if (!Predicate.isObject(manifest) || Array.isArray(manifest)) return Promise.reject(new SamvaCliError({ message: "package.json must contain an object." }));
3879
+ const dependencies = manifest.dependencies;
3880
+ if (dependencies !== void 0 && (!Predicate.isObject(dependencies) || Array.isArray(dependencies))) return Promise.reject(new SamvaCliError({ message: "package.json dependencies must contain an object." }));
3881
+ const devDependencies = manifest.devDependencies;
3882
+ if (devDependencies !== void 0 && (!Predicate.isObject(devDependencies) || Array.isArray(devDependencies))) return Promise.reject(new SamvaCliError({ message: "package.json devDependencies must contain an object." }));
3883
+ const scripts = manifest.scripts;
3884
+ if (scripts !== void 0 && (!Predicate.isObject(scripts) || Array.isArray(scripts))) return Promise.reject(new SamvaCliError({ message: "package.json scripts must contain an object." }));
3885
+ const nextDevDependencies = { ...devDependencies };
3886
+ for (const [name, version] of Object.entries(AUTHORING_DEV_DEPENDENCIES)) {
3887
+ if (Object.hasOwn(dependencies ?? {}, name) || Object.hasOwn(nextDevDependencies, name)) continue;
3888
+ nextDevDependencies[name] = version;
3889
+ }
3890
+ const nextScripts = { ...scripts };
3891
+ for (const [name, command] of Object.entries(AUTHORING_SCRIPTS)) if (!Object.hasOwn(nextScripts, name)) nextScripts[name] = command;
3892
+ if (Object.keys(nextDevDependencies).length === Object.keys(devDependencies ?? {}).length && Object.keys(nextScripts).length === Object.keys(scripts ?? {}).length) return {
3893
+ path: "package.json",
3894
+ status: "unchanged"
3895
+ };
3896
+ await writeFile(target, `${JSON.stringify({
3897
+ ...manifest,
3898
+ scripts: nextScripts,
3899
+ devDependencies: nextDevDependencies
3900
+ }, null, 2)}\n`);
3901
+ return {
3902
+ path: "package.json",
3903
+ status: "updated"
3904
+ };
3905
+ };
3906
+ const init = Command.make("init", {
3907
+ dir: Flag.directory("dir").pipe(Flag.withDescription("Project directory to scaffold into"), Flag.withDefault(".")),
3908
+ dryRun: dryRunFlag,
3909
+ json: jsonFlag
3910
+ }).pipe(Command.withDescription("Scaffold a Samva email templates project (vite.config, tsconfig, example)"), Command.withExamples([{
3911
+ command: "samva templates init",
3912
+ description: "Scaffold into the current directory"
3913
+ }, {
3914
+ command: "samva templates init --dir my-emails",
3915
+ description: "Scaffold into ./my-emails"
3916
+ }]), Command.withHandler(({ dir, dryRun, json }) => Effect.gen(function* () {
3917
+ const root = resolve(dir);
3918
+ const files = [
3919
+ ["samva.vite.config.ts", STARTER_VITE_CONFIG],
3920
+ ["tsconfig.json", STARTER_TSCONFIG],
3921
+ ["emails/welcome.tsx", STARTER_WELCOME]
3922
+ ];
3923
+ const paths = ["package.json", ...files.map(([path]) => path)];
3924
+ if (dryRun) return yield* json ? printJson({
3925
+ dryRun: true,
3926
+ action: "templates.init",
3927
+ dir: root,
3928
+ files: paths
3929
+ }) : Console.log(`[dry-run] Would scaffold Samva email templates in ${root}.`);
3930
+ const results = yield* Effect.tryPromise({
3931
+ try: async () => {
3932
+ await mkdir(root, { recursive: true });
3933
+ return Promise.all([reconcilePackageJson(join(root, "package.json")), ...files.map(async ([relPath, contents]) => {
3934
+ const target = join(root, relPath);
3935
+ await mkdir(dirname(target), { recursive: true });
3936
+ return writeFile(target, contents, { flag: "wx" }).then(() => ({
3937
+ path: relPath,
3938
+ status: "created"
3939
+ })).catch((error) => error.code === "EEXIST" ? {
3940
+ path: relPath,
3941
+ status: "unchanged"
3942
+ } : Promise.reject(error));
3943
+ })]);
3944
+ },
3945
+ catch: (cause) => new SamvaCliError({ message: `Could not scaffold project: ${String(cause)}` })
3946
+ });
3947
+ if (json) return yield* printJson({
3948
+ action: "templates.init",
3949
+ dir: root,
3950
+ files: paths,
3951
+ created: results.filter(({ status }) => status === "created").map(({ path }) => path),
3952
+ updated: results.filter(({ status }) => status === "updated").map(({ path }) => path),
3953
+ unchanged: results.filter(({ status }) => status === "unchanged").map(({ path }) => path)
3954
+ });
3955
+ yield* Effect.logInfo(`Initialized Samva email templates in ${root}:\n`);
3956
+ yield* printFields(results.map(({ path, status }) => [path, status]));
3957
+ yield* Effect.logInfo("\nNext:\n npm install\n npm run samva:dev # preview at http://localhost:5173");
3958
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
3959
+ const listRemote = Command.make("list", {
3960
+ page: Flag.integer("page").pipe(Flag.withDescription("Page number"), Flag.optional),
3961
+ limit: Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional),
3962
+ all: Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false)),
3963
+ json: jsonFlag
3964
+ }).pipe(Command.withDescription("List remote templates"), Command.withExamples([{ command: "samva templates list --json" }]), Command.withHandler(({ page, limit, all, json }) => Effect.gen(function* () {
3965
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
3966
+ code: "all_requires_jsonl",
3967
+ message: "--all is an unbounded traversal and requires --jsonl."
3968
+ });
3969
+ const client = yield* getClientProvider;
3970
+ const pageValue = Option.getOrUndefined(page);
3971
+ const limitValue = Option.getOrUndefined(limit);
3972
+ const query = {
3973
+ page: pageValue?.toString(),
3974
+ limit: limitValue?.toString()
3975
+ };
3976
+ if (all) {
3977
+ let nextPage = pageValue ?? 1;
3978
+ for (;;) {
3979
+ const data = yield* Templates.list({
3980
+ ...query,
3981
+ page: nextPage.toString()
3982
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list templates", error) })));
3983
+ for (const item of data.items) yield* printJsonLine(item);
3984
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
3985
+ nextPage += 1;
3986
+ }
3987
+ }
3988
+ const data = yield* Templates.list(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list templates", error) })));
3989
+ if (isJsonLinesRequested()) {
3990
+ for (const item of data.items) yield* printJsonLine(item);
3991
+ return;
3992
+ }
3993
+ if (json) return yield* printJson(data);
3994
+ for (const item of data.items) yield* printFields([
3995
+ ["ID", item.id],
3996
+ ["Name", item.name],
3997
+ ["Slug", item.slug],
3998
+ ["Status", item.status]
3999
+ ]);
4000
+ yield* printFields([["Page", data.pagination.page], ["Total", data.pagination.total]]);
4001
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4002
+ const getRemote = Command.make("get", {
4003
+ id: templateIdArgument,
4004
+ json: jsonFlag
4005
+ }).pipe(Command.withDescription("Get a remote template"), Command.withExamples([{ command: "samva templates get tmpl_abc123 --json" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
4006
+ const client = yield* getClientProvider;
4007
+ const data = yield* Templates.getById({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get template", error) })));
4008
+ if (json) return yield* printJson(data);
4009
+ yield* printJson(data);
4010
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4011
+ const createRemote = Command.make("create", {
4012
+ data: Flag.string("data").pipe(Flag.withDescription("Complete template JSON payload")),
4013
+ dryRun: dryRunFlag,
4014
+ json: jsonFlag
4015
+ }).pipe(Command.withDescription("Create a remote template from JSON"), Command.withExamples([{ command: `samva templates create --data '{"name":"Welcome","slug":"welcome","subject":"Welcome","source":"<Email />"}' --dry-run` }]), Command.withHandler(({ data, dryRun, json }) => Effect.gen(function* () {
4016
+ const payload = yield* decodePayload$1(data, "--data", Templates.CreatePayload);
4017
+ if (dryRun) return yield* json ? printJson({
4018
+ dryRun: true,
4019
+ action: "templates.create",
4020
+ request: payload
4021
+ }) : printJson(payload);
4022
+ const client = yield* getClientProvider;
4023
+ const result = yield* Templates.create(payload).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not create template", error) })));
4024
+ if (json) return yield* printJson(result);
4025
+ yield* printJson(result);
4026
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4027
+ const updateRemote = Command.make("update", {
4028
+ id: templateIdArgument,
4029
+ data: Flag.string("data").pipe(Flag.withDescription("JSON object of template fields to update")),
4030
+ dryRun: dryRunFlag,
4031
+ json: jsonFlag
4032
+ }).pipe(Command.withDescription("Update a remote template from JSON"), Command.withExamples([{ command: `samva templates update tmpl_abc123 --data '{"subject":"Welcome back"}' --dry-run` }]), Command.withHandler(({ id, data, dryRun, json }) => Effect.gen(function* () {
4033
+ const changes = yield* parseObject$1(data, "--data");
4034
+ const payload = yield* decodePayload$1(JSON.stringify({ data: changes }), "--data", Templates.UpdatePayload);
4035
+ if (dryRun) return yield* json ? printJson({
4036
+ dryRun: true,
4037
+ action: "templates.update",
4038
+ id,
4039
+ request: payload
4040
+ }) : printJson(payload);
4041
+ const client = yield* getClientProvider;
4042
+ const result = yield* Templates.update({
4043
+ id,
4044
+ ...payload
4045
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not update template", error) })));
4046
+ if (json) return yield* printJson(result);
4047
+ yield* printJson(result);
4048
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4049
+ const lifecycleMutation = (name) => Command.make(name, {
4050
+ id: templateIdArgument,
4051
+ dryRun: dryRunFlag,
4052
+ yes: Flag.boolean("yes").pipe(Flag.withDescription("Confirm destructive state change"), Flag.withDefault(false)),
4053
+ json: jsonFlag
4054
+ }).pipe(Command.withDescription(name === "publish" ? "Publish a remote template" : name === "unpublish" ? "Unpublish a remote template" : "Permanently delete a remote template"), Command.withExamples([{ command: `samva templates ${name} tmpl_abc123 --dry-run` }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
4055
+ if (dryRun) return yield* json ? printJson({
4056
+ dryRun: true,
4057
+ action: `templates.${name}`,
4058
+ id
4059
+ }) : Console.log(`[dry-run] Would ${name} template ${id}.`);
4060
+ if (name !== "publish") yield* confirmDestructive({
4061
+ yes,
4062
+ json,
4063
+ message: `${name === "remove" ? "Permanently remove" : "Unpublish"} template ${id}?`,
4064
+ nonInteractiveMessage: `${name} requires --yes in non-interactive mode. Preview with --dry-run.`
4065
+ });
4066
+ const client = yield* getClientProvider;
4067
+ let result;
4068
+ if (name === "publish") result = yield* Templates.publish({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not publish template", error) })));
4069
+ else if (name === "unpublish") result = yield* Templates.unpublish({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not unpublish template", error) })));
4070
+ else result = yield* Templates.remove({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not remove template", error) })));
4071
+ if (json) return yield* printJson(result);
4072
+ yield* printJson(result);
4073
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4074
+ const versionsList = Command.make("list", {
4075
+ id: templateIdArgument,
4076
+ json: jsonFlag
4077
+ }).pipe(Command.withDescription("List remote template version history"), Command.withExamples([{ command: "samva templates versions list tmpl_abc123 --json" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
4078
+ const client = yield* getClientProvider;
4079
+ const result = yield* Templates.versionHistory({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list template versions", error) })));
4080
+ if (json) return yield* printJson(result);
4081
+ yield* printJson(result);
4082
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4083
+ const restore = Command.make("restore", {
4084
+ id: templateIdArgument,
4085
+ versionId: Argument.string("version-id").pipe(Argument.withDescription("Version id to restore")),
4086
+ dryRun: dryRunFlag,
4087
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
4088
+ json: jsonFlag
4089
+ }).pipe(Command.withDescription("Restore a previous remote template version"), Command.withExamples([{ command: "samva templates versions restore tmpl_abc123 tv_abc123 --dry-run" }]), Command.withHandler(({ id, versionId, dryRun, yes, json }) => Effect.gen(function* () {
4090
+ if (dryRun) return yield* json ? printJson({
4091
+ dryRun: true,
4092
+ action: "templates.restore",
4093
+ id,
4094
+ versionId
4095
+ }) : Console.log(`[dry-run] Would restore ${id} to ${versionId}.`);
4096
+ yield* confirmDestructive({
4097
+ yes,
4098
+ json,
4099
+ message: `Restore template ${id} to version ${versionId}?`,
4100
+ nonInteractiveMessage: "Template restore requires --yes in non-interactive mode. Preview with --dry-run."
4101
+ });
4102
+ const client = yield* getClientProvider;
4103
+ const result = yield* Templates.restoreVersion({
4104
+ id,
4105
+ versionId
4106
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not restore template", error) })));
4107
+ if (json) return yield* printJson(result);
4108
+ yield* printJson(result);
4109
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4110
+ const versions = Command.make("versions").pipe(Command.withDescription("List and restore remote template versions"), Command.withExamples([{ command: "samva templates versions list tmpl_abc123 --json" }]), Command.withSubcommands([versionsList, restore]));
4111
+ const templatesCommand = Command.make("templates").pipe(Command.withDescription("Author, preview, and push email templates from local TSX"), Command.withExamples([{
4112
+ command: "samva templates init",
4113
+ description: "Scaffold a new templates project"
4114
+ }, {
4115
+ command: "samva templates push --dir emails --dry-run",
4116
+ description: "Emit SML and preview the push plan"
4117
+ }]), Command.withSubcommands([
4118
+ listRemote,
4119
+ getRemote,
4120
+ createRemote,
4121
+ updateRemote,
4122
+ lifecycleMutation("publish"),
4123
+ lifecycleMutation("unpublish"),
4124
+ lifecycleMutation("remove"),
4125
+ versions,
4126
+ push,
4127
+ pull,
4128
+ init
4129
+ ]));
4130
+ //#endregion
4131
+ //#region src/commands/webhooks.ts
4132
+ const idArgument = Argument.string("id").pipe(Argument.withDescription("Webhook id"));
4133
+ const parseObject = (raw, flag) => Effect.try({
4134
+ try: () => JSON.parse(raw),
4135
+ catch: () => new SamvaCliError({ message: `${flag} must be a valid JSON object.` })
4136
+ }).pipe(Effect.flatMap((value) => Predicate.isObject(value) && !Array.isArray(value) ? Effect.succeed(value) : Effect.fail(new SamvaCliError({ message: `${flag} must be a non-null JSON object.` }))));
4137
+ const decodePayload = (raw, flag, schema) => parseObject(raw, flag).pipe(Effect.flatMap(Schema.decodeUnknownEffect(schema)), Effect.mapError((error) => error instanceof SamvaCliError ? error : new SamvaCliError({ message: `${flag} does not match the API schema: ${String(error)}` })));
4138
+ const readData = (raw) => {
4139
+ if (!raw.startsWith("@")) return Effect.succeed(raw);
4140
+ const path = raw.slice(1);
4141
+ if (path.length === 0) return Effect.fail(new CliUsageError({
4142
+ code: "missing_data_file",
4143
+ message: "--data @ requires a file path."
4144
+ }));
4145
+ return Effect.tryPromise({
4146
+ try: () => readFile(path, "utf8"),
4147
+ catch: () => new CliUsageError({
4148
+ code: "data_file_unreadable",
4149
+ message: `Could not read webhook test data from ${path}.`
4150
+ })
4151
+ });
4152
+ };
4153
+ const list = Command.make("list", {
4154
+ page: Flag.integer("page").pipe(Flag.withDescription("Page number"), Flag.optional),
4155
+ limit: Flag.integer("limit").pipe(Flag.withDescription("Items per page"), Flag.optional),
4156
+ all: Flag.boolean("all").pipe(Flag.withDescription("Traverse every page (requires --jsonl)"), Flag.withDefault(false)),
4157
+ json: jsonFlag
4158
+ }).pipe(Command.withDescription("List customer webhooks"), Command.withExamples([{ command: "samva webhooks list --json" }]), Command.withHandler(({ page, limit, all, json }) => Effect.gen(function* () {
4159
+ if (all && !isJsonLinesRequested()) return yield* new CliUsageError({
4160
+ code: "all_requires_jsonl",
4161
+ message: "--all is an unbounded traversal and requires --jsonl."
4162
+ });
4163
+ const client = yield* getClientProvider;
4164
+ const pageValue = Option.getOrUndefined(page);
4165
+ const limitValue = Option.getOrUndefined(limit);
4166
+ const query = {
4167
+ page: pageValue?.toString(),
4168
+ limit: limitValue?.toString()
4169
+ };
4170
+ if (all) {
4171
+ let nextPage = pageValue ?? 1;
4172
+ for (;;) {
4173
+ const data = yield* Webhooks.list({
4174
+ ...query,
4175
+ page: nextPage.toString()
4176
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list webhooks", error) })));
4177
+ for (const item of data.items) yield* printJsonLine(item);
4178
+ if (nextPage >= data.pagination.totalPages || data.items.length === 0) return;
4179
+ nextPage += 1;
4180
+ }
4181
+ }
4182
+ const data = yield* Webhooks.list(query).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list webhooks", error) })));
4183
+ if (isJsonLinesRequested()) {
4184
+ for (const item of data.items) yield* printJsonLine(item);
4185
+ return;
4186
+ }
4187
+ if (json) return yield* printJson(data);
4188
+ if (data.items.length === 0) return yield* Console.log("No webhooks found.");
4189
+ for (const hook of data.items) yield* printFields([
4190
+ ["ID", hook.id],
4191
+ ["Name", hook.name],
4192
+ ["Status", hook.status],
4193
+ ["URL", hook.url]
4194
+ ]);
4195
+ yield* printFields([["Page", data.pagination.page], ["Total", data.pagination.total]]);
4196
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4197
+ const get = Command.make("get", {
4198
+ id: idArgument,
4199
+ json: jsonFlag
4200
+ }).pipe(Command.withDescription("Get a customer webhook"), Command.withExamples([{ command: "samva webhooks get wh_abc123 --json" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
4201
+ const client = yield* getClientProvider;
4202
+ const data = yield* Webhooks.get({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get webhook", error) })));
4203
+ if (json) return yield* printJson(data);
4204
+ yield* printFields([
4205
+ ["ID", data.id],
4206
+ ["Name", data.name],
4207
+ ["Status", data.status],
4208
+ ["URL", data.url]
4209
+ ]);
4210
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4211
+ const logs = Command.make("logs", {
4212
+ id: idArgument,
4213
+ limit: Flag.integer("limit").pipe(Flag.optional),
4214
+ json: jsonFlag
4215
+ }).pipe(Command.withDescription("List webhook delivery logs"), Command.withExamples([{ command: "samva webhooks logs wh_abc123 --limit 25 --json" }]), Command.withHandler(({ id, limit, json }) => Effect.gen(function* () {
4216
+ const client = yield* getClientProvider;
4217
+ const limitValue = Option.getOrUndefined(limit);
4218
+ const data = yield* Webhooks.getLogs({
4219
+ webhookId: id,
4220
+ ...limitValue === void 0 ? {} : { limit: limitValue.toString() }
4221
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not list webhook logs", error) })));
4222
+ if (json) return yield* printJson(data);
4223
+ yield* printJson(data.data);
4224
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4225
+ const stats = Command.make("stats", {
4226
+ id: idArgument,
4227
+ json: jsonFlag
4228
+ }).pipe(Command.withDescription("Get webhook delivery statistics"), Command.withExamples([{ command: "samva webhooks stats wh_abc123 --json" }]), Command.withHandler(({ id, json }) => Effect.gen(function* () {
4229
+ const client = yield* getClientProvider;
4230
+ const data = yield* Webhooks.getStats({ webhookId: id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not get webhook stats", error) })));
4231
+ if (json) return yield* printJson(data);
4232
+ yield* printJson(data);
4233
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4234
+ const test = Command.make("test", {
4235
+ id: idArgument,
4236
+ data: Flag.string("data").pipe(Flag.withDescription("JSON object or @file to send as the signed test payload"), Flag.optional),
4237
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDescription("Validate and preview without sending a request"), Flag.withDefault(false)),
4238
+ json: jsonFlag
4239
+ }).pipe(Command.withDescription("Send a signed test request to a webhook endpoint"), Command.withExamples([
4240
+ { command: "samva webhooks test wep_abc123 --dry-run" },
4241
+ { command: `samva webhooks test wep_abc123 --data '{"event":"webhook.test"}' --json` },
4242
+ { command: "samva webhooks test wep_abc123 --data @payload.json --json" }
4243
+ ]), Command.withHandler(({ id, data, dryRun, json }) => Effect.gen(function* () {
4244
+ const raw = Option.getOrUndefined(data);
4245
+ const payload = raw === void 0 ? yield* Schema.decodeUnknownEffect(Webhooks.TestPayload)({}) : yield* readData(raw).pipe(Effect.flatMap((content) => parseObject(content, "--data")), Effect.flatMap((value) => Schema.decodeUnknownEffect(Webhooks.TestPayload)({ data: value })), Effect.mapError((error) => error instanceof CliUsageError || error instanceof SamvaCliError ? error : new SamvaCliError({ message: `--data does not match the API schema: ${String(error)}` })));
4246
+ if (dryRun) return yield* json ? printJson({
4247
+ dryRun: true,
4248
+ action: "webhooks.test",
4249
+ id,
4250
+ request: payload
4251
+ }) : printFields([
4252
+ ["Action", "webhooks.test"],
4253
+ ["Webhook", id],
4254
+ ["Payload", payload.data ? JSON.stringify(payload.data) : "default"]
4255
+ ]);
4256
+ const client = yield* getClientProvider;
4257
+ const result = yield* Webhooks.test({
4258
+ id,
4259
+ ...payload
4260
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not test webhook", error) })));
4261
+ if (json) return yield* printJson(result);
4262
+ yield* printFields([
4263
+ ["Status", "Queued"],
4264
+ ["Event", result.eventId],
4265
+ ["Delivery", result.deliveryId]
4266
+ ]);
4267
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4268
+ const retry = Command.make("retry", {
4269
+ id: idArgument,
4270
+ deliveryId: Argument.string("delivery-id").pipe(Argument.withDescription("Webhook delivery id")),
4271
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
4272
+ json: jsonFlag
4273
+ }).pipe(Command.withDescription("Retry a failed webhook delivery"), Command.withExamples([{ command: "samva webhooks retry wh_abc123 del_abc123 --dry-run" }]), Command.withHandler(({ id, deliveryId, dryRun, json }) => Effect.gen(function* () {
4274
+ if (dryRun) return yield* json ? printJson({
4275
+ dryRun: true,
4276
+ action: "webhooks.retry",
4277
+ id,
4278
+ deliveryId
4279
+ }) : Console.log(`[dry-run] Would retry delivery ${deliveryId} for webhook ${id}.`);
4280
+ const client = yield* getClientProvider;
4281
+ const data = yield* Webhooks.retryDelivery({
4282
+ webhookId: id,
4283
+ deliveryId
4284
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not retry webhook delivery", error) })));
4285
+ if (json) return yield* printJson(data);
4286
+ yield* printJson(data);
4287
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4288
+ const rotate = Command.make("rotate-secret", {
4289
+ id: idArgument,
4290
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
4291
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
4292
+ json: jsonFlag
4293
+ }).pipe(Command.withDescription("Rotate a webhook signing secret"), Command.withExamples([{ command: "samva webhooks rotate-secret wh_abc123 --dry-run" }, { command: "samva webhooks rotate-secret wh_abc123 --yes --json" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
4294
+ if (dryRun) return yield* json ? printJson({
4295
+ dryRun: true,
4296
+ action: "webhooks.rotate-secret",
4297
+ id
4298
+ }) : Console.log(`[dry-run] Would rotate the signing secret for webhook ${id}.`);
4299
+ yield* confirmDestructive({
4300
+ yes,
4301
+ json,
4302
+ message: `Rotate the signing secret for webhook ${id}? The current secret will stop working.`,
4303
+ nonInteractiveMessage: "Secret rotation requires --yes in non-interactive mode. Preview with --dry-run."
4304
+ });
4305
+ const client = yield* getClientProvider;
4306
+ const data = yield* Webhooks.regenerateSecret({ webhookId: id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not rotate webhook secret", error) })));
4307
+ const warning = "This signing secret is shown once. Store it now; the previous secret remains valid for 24 hours.";
4308
+ const secret = Redacted.value(data.secret);
4309
+ if (json) return yield* printJson({
4310
+ secret,
4311
+ displayOnce: true,
4312
+ warning
4313
+ });
4314
+ yield* Console.error(`WARNING: ${warning}`);
4315
+ yield* Console.log(secret);
4316
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4317
+ const create = Command.make("create", {
4318
+ data: Flag.string("data").pipe(Flag.withDescription("Webhook JSON: name, url, events, and optional retry settings")),
4319
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
4320
+ json: jsonFlag
4321
+ }).pipe(Command.withDescription("Create a customer webhook from JSON"), Command.withExamples([{ command: `samva webhooks create --data '{"name":"delivery","url":"https://example.com/hook","events":["message.delivered"]}' --dry-run` }]), Command.withHandler(({ data, dryRun, json }) => Effect.gen(function* () {
4322
+ const payload = yield* decodePayload(data, "--data", Webhooks.CreatePayload);
4323
+ if (dryRun) return yield* json ? printJson({
4324
+ dryRun: true,
4325
+ action: "webhooks.create",
4326
+ request: payload
4327
+ }) : printJson(payload);
4328
+ const client = yield* getClientProvider;
4329
+ const result = yield* Webhooks.create(payload).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not create webhook", error) })));
4330
+ if (json) return yield* printJson(result);
4331
+ yield* printJson(result);
4332
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4333
+ const update = Command.make("update", {
4334
+ id: idArgument,
4335
+ data: Flag.string("data").pipe(Flag.withDescription("JSON object of webhook fields to update")),
4336
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
4337
+ json: jsonFlag
4338
+ }).pipe(Command.withDescription("Update a customer webhook from JSON"), Command.withExamples([{ command: `samva webhooks update wh_abc123 --data '{"url":"https://example.com/new"}' --dry-run` }]), Command.withHandler(({ id, data, dryRun, json }) => Effect.gen(function* () {
4339
+ const payload = yield* decodePayload(data, "--data", Webhooks.UpdatePayload);
4340
+ if (dryRun) return yield* json ? printJson({
4341
+ dryRun: true,
4342
+ action: "webhooks.update",
4343
+ id,
4344
+ request: payload
4345
+ }) : printJson(payload);
4346
+ const client = yield* getClientProvider;
4347
+ const result = yield* Webhooks.update({
4348
+ id,
4349
+ ...payload
4350
+ }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not update webhook", error) })));
4351
+ if (json) return yield* printJson(result);
4352
+ yield* printJson(result);
4353
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4354
+ const remove = Command.make("remove", {
4355
+ id: idArgument,
4356
+ dryRun: Flag.boolean("dry-run").pipe(Flag.withDefault(false)),
4357
+ yes: Flag.boolean("yes").pipe(Flag.withDefault(false)),
4358
+ json: jsonFlag
4359
+ }).pipe(Command.withDescription("Permanently delete a customer webhook"), Command.withExamples([{ command: "samva webhooks remove wh_abc123 --dry-run" }, { command: "samva webhooks remove wh_abc123 --yes" }]), Command.withHandler(({ id, dryRun, yes, json }) => Effect.gen(function* () {
4360
+ if (dryRun) return yield* json ? printJson({
4361
+ dryRun: true,
4362
+ action: "webhooks.remove",
4363
+ id
4364
+ }) : Console.log(`[dry-run] Would permanently remove webhook ${id}.`);
4365
+ yield* confirmDestructive({
4366
+ yes,
4367
+ json,
4368
+ message: `Permanently remove webhook ${id}?`,
4369
+ nonInteractiveMessage: "Permanent webhook deletion requires --yes in non-interactive mode. Preview with --dry-run."
4370
+ });
4371
+ const client = yield* getClientProvider;
4372
+ const result = yield* Webhooks.remove({ id }).pipe(client.provide, Effect.mapError((error) => new SamvaCliError({ message: formatApiError("Could not remove webhook", error) })));
4373
+ if (json) return yield* printJson(result);
4374
+ yield* Console.log(`Removed webhook ${id}.`);
4375
+ }).pipe(json ? Effect.provideService(References.MinimumLogLevel, "None") : identity)));
4376
+ const webhooksCommand = Command.make("webhooks").pipe(Command.withDescription("Inspect, test, and recover customer webhooks"), Command.withExamples([{ command: "samva webhooks list --json" }]), Command.withSubcommands([
4377
+ list,
4378
+ get,
4379
+ create,
4380
+ update,
4381
+ test,
4382
+ logs,
4383
+ stats,
4384
+ retry,
4385
+ rotate,
4386
+ remove
4387
+ ]));
4388
+ //#endregion
4389
+ //#region src/lib/help-manifest.ts
4390
+ const helpManifest = {
4391
+ schemaVersion: 1,
4392
+ name: "samva",
4393
+ commands: [
4394
+ "login",
4395
+ "logout",
4396
+ "org",
4397
+ "email",
4398
+ "messages",
4399
+ "campaigns",
4400
+ "scheduled-messages",
4401
+ "templates",
4402
+ "webhooks",
4403
+ "profile",
4404
+ "help"
4405
+ ],
4406
+ globalOptions: [
4407
+ "--json",
4408
+ "--jsonl",
4409
+ "--quiet",
4410
+ "--profile",
4411
+ "--api-url",
4412
+ "--org",
4413
+ "--no-input",
4414
+ "--completions"
4415
+ ]
4416
+ };
4417
+ //#endregion
4418
+ //#region src/cli.ts
4419
+ const rootCommands = [
4420
+ loginCommand,
4421
+ logoutCommand,
4422
+ orgCommand,
4423
+ emailCommand,
4424
+ messagesCommand,
4425
+ campaignsCommand,
4426
+ scheduledMessagesCommand,
4427
+ templatesCommand,
4428
+ webhooksCommand,
4429
+ profileCommand
4430
+ ];
4431
+ const helpCommand = Command.make("help").pipe(Command.withDescription("Describe released commands and global options"), Command.withHandler(Effect.fnUntraced(function* () {
4432
+ if ((yield* Json) || (yield* JsonLines)) return yield* printJson(helpManifest);
4433
+ if (yield* Quiet) return;
4434
+ yield* Console.log("Run `samva <command> --help` for command-specific usage, or `samva --completions <bash|zsh|fish|sh>` for shell completions.");
4435
+ })));
4436
+ /** Root `samva` command. Subcommands are thin wrappers over the `samva` SDK. */
4437
+ const cli = Command.make("samva").pipe(Command.withDescription(`Samva — email and platform operations from your terminal. ${AGENT_AUTH_HINT}`), Command.withExamples([
4438
+ {
4439
+ command: "samva email send --to you@example.com --subject Hi --text Hello",
4440
+ description: "Send email with SAMVA_API_KEY set"
4441
+ },
4442
+ {
4443
+ command: "samva org current",
4444
+ description: "Show active org (OAuth sessions; API keys are single-org)"
4445
+ },
4446
+ {
4447
+ command: "samva --completions fish > ~/.config/fish/completions/samva.fish",
4448
+ description: "Generate shell completions"
4449
+ }
4450
+ ]), Command.withSubcommands([...rootCommands, helpCommand]), Command.withGlobalFlags(globalFlags));
4451
+ //#endregion
4452
+ //#region src/bin.ts
4453
+ const argv = process.argv.slice(2);
4454
+ const outputMode = outputModeFromArgv(argv);
4455
+ const machineMode = outputMode === "json" || outputMode === "jsonl";
4456
+ const optionTerminator = argv.indexOf("--");
4457
+ const processArgs = optionTerminator === -1 ? argv : argv.slice(0, optionTerminator);
4458
+ const rootOptionsWithValue = /* @__PURE__ */ new Set([
4459
+ "--profile",
4460
+ "--api-url",
4461
+ "--org",
4462
+ "--log-level"
4463
+ ]);
4464
+ const rootMachineHelp = (() => {
4465
+ let requested = false;
4466
+ for (let index = 0; index < processArgs.length; index++) {
4467
+ const token = processArgs[index];
4468
+ if (token === "--help" || token === "-h") {
4469
+ requested = true;
4470
+ continue;
4471
+ }
4472
+ if (rootOptionsWithValue.has(token)) {
4473
+ index++;
4474
+ continue;
4475
+ }
4476
+ if (token.startsWith("-")) continue;
4477
+ return {
4478
+ requested,
4479
+ command: token
4480
+ };
4481
+ }
4482
+ return {
4483
+ requested,
4484
+ command: void 0
4485
+ };
4486
+ })();
4487
+ const isRootMachineHelp = machineMode && rootMachineHelp.requested && rootMachineHelp.command === void 0;
4488
+ const isUnknownRootMachineHelp = machineMode && rootMachineHelp.requested && rootMachineHelp.command !== void 0 && !isKnownRootCommand(rootMachineHelp.command);
4489
+ /** Status belongs on a TTY stderr; stdout is result data only. */
4490
+ const cliLogger = Logger.make(({ logLevel, message }) => {
4491
+ const text = (Array.isArray(message) ? message : [message]).map(String).join(" ");
4492
+ if (LogLevel.isGreaterThanOrEqualTo(logLevel, "Warn")) process.stderr.write(`${text}\n`);
4493
+ else if (outputMode === "human" && process.stderr.isTTY) process.stderr.write(`${text}\n`);
4494
+ });
4495
+ const messageFor = (error) => {
4496
+ if (error instanceof Error) return error.message;
4497
+ if (Predicate.isObject(error) && Predicate.hasProperty(error, "message")) {
4498
+ const message = error.message;
4499
+ if (typeof message === "string") return message;
4500
+ }
4501
+ return "Samva CLI failed unexpectedly.";
4502
+ };
4503
+ /**
4504
+ * A bare root or parent command (no subcommand) fails as `ShowHelp` with no
4505
+ * parse errors. The framework already rendered its help into the stdout buffer,
4506
+ * so this failure is really a help request — not a diagnostic.
4507
+ */
4508
+ const isBareHelp = (error) => error instanceof CliError.ShowHelp && error.errors.length === 0;
4509
+ /**
4510
+ * On a parse failure the framework renders a full-width `ERROR` block to stderr
4511
+ * via `Console.error`. We render our own single diagnostic (a `✗` line or the
4512
+ * JSON envelope), so blank the framework's error rendering here and drop the
4513
+ * resulting empty write inside the buffer. Help-doc rendering is untouched, so
4514
+ * `--help` and bare-command help are unaffected.
4515
+ */
4516
+ const formatter = {
4517
+ ...CliOutput.defaultFormatter(),
4518
+ formatError: () => "",
4519
+ formatErrors: () => ""
4520
+ };
4521
+ const flushSuccessfulResults = (state, partial = false) => Effect.gen(function* () {
4522
+ if (outputMode === "quiet" || outputMode === "jsonl") return;
4523
+ if (outputMode === "human") {
4524
+ yield* Effect.sync(() => state.lines.forEach((line) => state.originalLog(line)));
4525
+ return;
4526
+ }
4527
+ const data = yield* Effect.try({
4528
+ try: () => {
4529
+ if (state.lines.length !== 1) return {
4530
+ invalid: true,
4531
+ count: state.lines.length
4532
+ };
4533
+ return {
4534
+ invalid: false,
4535
+ data: JSON.parse(state.lines[0])
4536
+ };
4537
+ },
4538
+ catch: () => ({
4539
+ invalid: true,
4540
+ count: state.lines.length
4541
+ })
4542
+ });
4543
+ if (data.invalid) {
4544
+ process.exitCode = 1;
4545
+ yield* printJsonError(void 0, "The command did not produce exactly one machine-readable result.");
4546
+ return;
4547
+ }
4548
+ yield* Effect.sync(() => state.originalLog(JSON.stringify(resultEnvelope(commandNameFromArgv(argv), data.data, partial ? true : void 0), null, 2)));
4549
+ });
4550
+ const flushLifecycleResult = (state, error) => Effect.sync(() => {
4551
+ state.lines.length = 0;
4552
+ if (outputMode === "quiet") return;
4553
+ if (outputMode === "human") {
4554
+ state.originalLog(error.human);
4555
+ return;
4556
+ }
4557
+ const result = resultEnvelope(commandNameFromArgv(argv), error.partialMessage, true);
4558
+ state.originalLog(JSON.stringify(result, null, outputMode === "jsonl" ? void 0 : 2));
4559
+ });
4560
+ const flushFailedResult = (state, error) => Effect.sync(() => {
4561
+ state.lines.length = 0;
4562
+ if (outputMode === "quiet") return;
4563
+ if (outputMode === "human") {
4564
+ state.originalLog(error.human);
4565
+ return;
4566
+ }
4567
+ const result = resultEnvelope(commandNameFromArgv(argv), error.result);
4568
+ state.originalLog(JSON.stringify(result, null, outputMode === "jsonl" ? void 0 : 2));
4569
+ });
4570
+ const withStdoutBuffer = (program) => Effect.acquireUseRelease(Effect.sync(() => {
4571
+ const originalLog = console.log;
4572
+ const originalError = console.error;
4573
+ const lines = [];
4574
+ console.log = ((...values) => {
4575
+ lines.push(values.map(String).join(" "));
4576
+ });
4577
+ console.error = ((...values) => {
4578
+ if (values.map(String).join(" ").length === 0) return;
4579
+ originalError(...values);
4580
+ });
4581
+ return {
4582
+ originalLog,
4583
+ originalError,
4584
+ lines
4585
+ };
4586
+ }), (state) => program.pipe(Effect.tap(() => flushSuccessfulResults(state)), Effect.tapError((error) => {
4587
+ if (isBareHelp(error) && outputMode === "human") return Effect.sync(() => state.lines.forEach((line) => state.originalLog(line)));
4588
+ if (error instanceof CliMessageLifecycleError) return flushLifecycleResult(state, error);
4589
+ if (error instanceof CliResultError) return flushFailedResult(state, error);
4590
+ return Effect.sync(() => {
4591
+ state.lines.length = 0;
4592
+ });
4593
+ })), (state) => Effect.sync(() => {
4594
+ console.log = state.originalLog;
4595
+ console.error = state.originalError;
4596
+ state.lines.length = 0;
4597
+ }));
4598
+ const renderFailure = (error) => Effect.gen(function* () {
4599
+ process.exitCode = processErrorCode(error);
4600
+ if (isBareHelp(error)) {
4601
+ if (machineMode) yield* printJsonError(error, messageFor(error));
4602
+ return;
4603
+ }
4604
+ const message = error instanceof CliError.ShowHelp && error.errors.length > 0 ? `${error.errors[0]?.message ?? "Invalid command input."}\nTry: ${error.commandPath.join(" ")} --help` : messageFor(error);
4605
+ if (!machineMode) {
4606
+ yield* Console.error(`✗ ${message}`);
4607
+ return;
4608
+ }
4609
+ yield* printJsonError(error, message);
4610
+ });
4611
+ const runMachineHelp = Effect.sync(() => {
4612
+ const payload = resultEnvelope(commandNameFromArgv(argv), helpManifest);
4613
+ process.stdout.write(`${JSON.stringify(payload, null, outputMode === "jsonl" ? void 0 : 2)}\n`);
4614
+ });
4615
+ const program = (isRootMachineHelp ? validateProcessArgs(argv).pipe(Effect.flatMap(() => runMachineHelp)) : isUnknownRootMachineHelp ? validateProcessArgs(argv).pipe(Effect.flatMap(() => new CliUsageError({
4616
+ code: "unknown_subcommand",
4617
+ message: `Unknown subcommand ${JSON.stringify(rootMachineHelp.command)} for samva.`
4618
+ }))) : withStdoutBuffer(validateProcessArgs(argv).pipe(Effect.flatMap(() => Command.run(cli, { version }))))).pipe(Effect.provide(Layer.mergeAll(Logger.layer([cliLogger]), BunServices.layer, CliOutput.layer(formatter))), Effect.matchEffect({
4619
+ onFailure: renderFailure,
4620
+ onSuccess: () => Effect.void
4621
+ }));
4622
+ BunRuntime.runMain(program, { disableErrorReporting: true });
4623
+ //#endregion
4624
+ export {};