@cowliss/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/dist/guest/{driver-DaNdNhuF.js → driver-14FnzM-h.js} +1 -2
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/emails.d.ts +18 -1
- package/dist/guest/{index-DAGLEHDn.d.ts → index-BDj02EsQ.d.ts} +1 -5
- package/dist/guest/{journeys-B_xg_GnL.js → journeys-C8j8Sqvl.js} +40 -73
- package/dist/guest/journeys.d.ts +9 -7
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +1137 -1172
- package/package.json +1 -1
- package/dist/guest/constants-B0wk-87t.d.ts +0 -30
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { builtinModules, createRequire } from "node:module";
|
|
|
3
3
|
import { z, z as z$2 } from "zod";
|
|
4
4
|
import { z as z$1 } from "zod/v4";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
|
-
import { copyFile, mkdir, readFile, readdir, rm, stat,
|
|
6
|
+
import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
9
9
|
import { Command, InvalidArgumentError } from "commander";
|
|
@@ -16,7 +16,6 @@ import { create, extract } from "tar";
|
|
|
16
16
|
import { parse } from "@babel/parser";
|
|
17
17
|
import { existsSync, readFileSync } from "node:fs";
|
|
18
18
|
import { createServer } from "node:http";
|
|
19
|
-
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
20
19
|
import { createInterface } from "node:readline/promises";
|
|
21
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -64,8 +63,8 @@ const ENVIRONMENT_HEADER = "X-Cow-Environment";
|
|
|
64
63
|
*
|
|
65
64
|
* It exists so the published packages are legible in the logs. Customers
|
|
66
65
|
* pin versions and old ones live for years, and knowing which are still
|
|
67
|
-
* calling is only possible if it was collected from the first
|
|
68
|
-
* from the
|
|
66
|
+
* calling is only possible if it was collected from the first version, not
|
|
67
|
+
* from the version where it first mattered.
|
|
69
68
|
*
|
|
70
69
|
* A custom header rather than `User-Agent`: browsers forbid scripts from
|
|
71
70
|
* setting that one and drop it in silence, and `@cowliss/sdk` runs in a
|
|
@@ -217,23 +216,14 @@ const EXECUTION_LIMITS = {
|
|
|
217
216
|
logLines: 100,
|
|
218
217
|
logLineBytes: 1024
|
|
219
218
|
};
|
|
220
|
-
/** Per
|
|
221
|
-
const
|
|
219
|
+
/** Per push: manifest counts, bundle size, and the source tarball. */
|
|
220
|
+
const PUSH_LIMITS = {
|
|
222
221
|
journeys: 100,
|
|
223
222
|
templates: 200,
|
|
224
223
|
bundleBytes: 2097152,
|
|
225
224
|
sourceBytes: 5242880
|
|
226
225
|
};
|
|
227
226
|
/**
|
|
228
|
-
* The whole-release compile budget: how long `compileRelease` may spend on
|
|
229
|
-
* every bundle in a manifest before it gives up and fails the release. A
|
|
230
|
-
* project at the 300-artifact ceiling compiling at the per-bundle timeout
|
|
231
|
-
* would otherwise hold the sandbox queue for ten hours; 30 minutes is
|
|
232
|
-
* generous for a realistic project (a bundle compiles in seconds) and short
|
|
233
|
-
* enough that a stuck release frees the queue on its own.
|
|
234
|
-
*/
|
|
235
|
-
const RELEASE_COMPILE_TIMEOUT_MS = 18e5;
|
|
236
|
-
/**
|
|
237
227
|
* Where the docs site serves the `cow.json` JSON Schema (the `$schema` a
|
|
238
228
|
* project file points at). Generated from `cowConfigSchema` by the docs
|
|
239
229
|
* generator; the path is stable because project files link to it.
|
|
@@ -280,10 +270,10 @@ const searchQuerySchema = z.string().trim().max(200, "q must be at most 200 char
|
|
|
280
270
|
* always scoped to the caller's org, so echoing it would invite a client to
|
|
281
271
|
* believe it can be chosen.
|
|
282
272
|
*
|
|
283
|
-
*
|
|
273
|
+
* Pipeline keys (journeys v2 ticket 05) are the same shape on the wire: the
|
|
284
274
|
* only difference is the Clerk `deploy` scope, which decides which family a
|
|
285
275
|
* key belongs to and which routes it may reach, never anything a client
|
|
286
|
-
* sends or reads. So /v1/settings/
|
|
276
|
+
* sends or reads. So /v1/settings/pipeline-keys reuses these schemas.
|
|
287
277
|
*/
|
|
288
278
|
const nameSchema$3 = z.string().trim().min(1, "name is required").max(100, "name must be at most 100 characters");
|
|
289
279
|
/** Body for POST /v1/settings/api-keys. */
|
|
@@ -3370,6 +3360,16 @@ const consentPurposes = pgTable("consent_purposes", {
|
|
|
3370
3360
|
*/
|
|
3371
3361
|
const deliveryChannelEnum = pgEnum("delivery_channel", ["email", "webhook"]);
|
|
3372
3362
|
/**
|
|
3363
|
+
* Why this send happened. `journey` is every send a running journey made,
|
|
3364
|
+
* dry runs included. `test` is the dashboard's Send test: a manager checking
|
|
3365
|
+
* a template's copy against their own address, with no gate, no recipient
|
|
3366
|
+
* profile and no journey behind it, which is why both of those columns are
|
|
3367
|
+
* nullable. The meter and the quota gate exclude it (packages/delivery's
|
|
3368
|
+
* repository), and every per-profile read excludes it by construction,
|
|
3369
|
+
* because it has no profile to be found under.
|
|
3370
|
+
*/
|
|
3371
|
+
const deliveryKindEnum = pgEnum("delivery_kind", ["journey", "test"]);
|
|
3372
|
+
/**
|
|
3373
3373
|
* The delivery lifecycle, frozen with the API version.
|
|
3374
3374
|
*
|
|
3375
3375
|
* - `sent` is the only non-terminal status: it means SES accepted the
|
|
@@ -3419,10 +3419,15 @@ const deliveries$1 = pgTable("deliveries", {
|
|
|
3419
3419
|
orgId: text("org_id").notNull(),
|
|
3420
3420
|
/** The instance's environment, stamped from the workflow run input. */
|
|
3421
3421
|
environment: environmentEnum("environment").notNull(),
|
|
3422
|
-
/**
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3422
|
+
/**
|
|
3423
|
+
* The recipient profile (`usr_`); a text column, since a delivery is a
|
|
3424
|
+
* log row and outlives nothing but erasure. Null on a test send, which
|
|
3425
|
+
* goes to the member who asked for it rather than to anyone's profile.
|
|
3426
|
+
*/
|
|
3427
|
+
profileId: text("profile_id"),
|
|
3428
|
+
/** The journey that sent this; null on a test send, which has none. */
|
|
3429
|
+
journey: text("journey"),
|
|
3430
|
+
kind: deliveryKindEnum("kind").notNull().default("journey"),
|
|
3426
3431
|
/** Email template name or webhook destination name. */
|
|
3427
3432
|
step: text("step").notNull(),
|
|
3428
3433
|
channel: deliveryChannelEnum("channel").notNull(),
|
|
@@ -3611,13 +3616,13 @@ const executions$1 = pgTable("executions", {
|
|
|
3611
3616
|
id: text("id").primaryKey(),
|
|
3612
3617
|
orgId: text("org_id").notNull(),
|
|
3613
3618
|
environment: environmentEnum("environment").notNull(),
|
|
3614
|
-
/** The journey's key, not a foreign key: derived journey rows come and go with
|
|
3619
|
+
/** The journey's key, not a foreign key: derived journey rows come and go with pushes. */
|
|
3615
3620
|
journeyKey: text("journey_key").notNull(),
|
|
3616
3621
|
profileId: text("profile_id").notNull(),
|
|
3617
3622
|
workflowId: text("workflow_id").notNull(),
|
|
3618
3623
|
runId: text("run_id").notNull(),
|
|
3619
|
-
/** The
|
|
3620
|
-
|
|
3624
|
+
/** The journey version the execution is pinned to for its whole life. */
|
|
3625
|
+
versionId: text("version_id").notNull(),
|
|
3621
3626
|
status: executionStatusEnum("status").notNull(),
|
|
3622
3627
|
/** Text summary of the command the execution is on, for the list view. */
|
|
3623
3628
|
step: text("step"),
|
|
@@ -3659,7 +3664,7 @@ const executions$1 = pgTable("executions", {
|
|
|
3659
3664
|
precision: 3
|
|
3660
3665
|
})
|
|
3661
3666
|
}, (table) => [
|
|
3662
|
-
index("
|
|
3667
|
+
index("executions_org_id_environment_version_id_status_idx").on(table.orgId, table.environment, table.versionId, table.status),
|
|
3663
3668
|
uniqueIndex("executions_org_id_workflow_id_unique").on(table.orgId, table.workflowId),
|
|
3664
3669
|
index("executions_org_id_environment_started_at_idx").on(table.orgId, table.environment, table.startedAt)
|
|
3665
3670
|
]);
|
|
@@ -3862,35 +3867,35 @@ const insertJourneyRunSchema = createInsertSchema(journeyRuns);
|
|
|
3862
3867
|
//#endregion
|
|
3863
3868
|
//#region ../../packages/db/src/schema/journeys.ts
|
|
3864
3869
|
/**
|
|
3865
|
-
* Derived journey rows: one per (org,
|
|
3866
|
-
*
|
|
3867
|
-
*
|
|
3868
|
-
*
|
|
3869
|
-
* identity a journey has.
|
|
3870
|
+
* Derived journey rows: one per (org, key), upserted from the manifest a
|
|
3871
|
+
* push carried. Nothing here is authored through the API, which is why
|
|
3872
|
+
* there is no id of its own: the key is the name the author gave the file,
|
|
3873
|
+
* and the (org, key) pair is the only identity a journey has.
|
|
3870
3874
|
*
|
|
3871
|
-
*
|
|
3875
|
+
* One row for both environments (ADR 0011): the journey's code is its
|
|
3876
|
+
* latest ready version, and what differs per environment is the enabled
|
|
3877
|
+
* flag alone. A trigger that should fire in both names both of an app's ids.
|
|
3878
|
+
*
|
|
3879
|
+
* A push only ever adds and updates: it owns the keys its manifest carries
|
|
3872
3880
|
* and leaves every other row alone, because an org's journeys can come from
|
|
3873
|
-
* several projects and a developer may
|
|
3881
|
+
* several projects and a developer may push a project holding only some of
|
|
3874
3882
|
* the files. `projectId` records which project put the row here, which is
|
|
3875
|
-
* what makes a key another project owns a refused
|
|
3883
|
+
* what makes a key another project owns a refused push rather than a silent
|
|
3876
3884
|
* overwrite. Removing a journey is an explicit delete (ADR 0009).
|
|
3877
3885
|
*
|
|
3878
|
-
*
|
|
3879
|
-
*
|
|
3880
|
-
*
|
|
3881
|
-
* keyed the same way but outlives every release, so a redeploy never
|
|
3882
|
-
* silently re-enables something an operator turned off.
|
|
3886
|
+
* Whether a journey fires is one flag and one only: journey_states, per
|
|
3887
|
+
* environment, which outlives every push, so a push never turns anything on
|
|
3888
|
+
* or off. The author has no second gate of their own (ADR 0011).
|
|
3883
3889
|
*
|
|
3890
|
+
* The descriptive columns are what its latest ready version reported, so a
|
|
3891
|
+
* failed compile leaves both the code and its description as they were.
|
|
3884
3892
|
* `spine` is display only and never trusted: a journey's real control flow
|
|
3885
3893
|
* is whatever its code does at run time.
|
|
3886
3894
|
*/
|
|
3887
3895
|
const journeys$1 = pgTable("journeys", {
|
|
3888
3896
|
orgId: text("org_id").notNull(),
|
|
3889
|
-
environment: environmentEnum("environment").notNull(),
|
|
3890
3897
|
key: text("key").notNull(),
|
|
3891
|
-
/** The
|
|
3892
|
-
releaseId: text("release_id").notNull(),
|
|
3893
|
-
/** The project whose deploy owns this key in this environment. */
|
|
3898
|
+
/** The project whose push owns this key. */
|
|
3894
3899
|
projectId: text("project_id").notNull(),
|
|
3895
3900
|
/** The author's labels, from the manifest; the dashboard's only grouping. */
|
|
3896
3901
|
tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
|
|
@@ -3903,28 +3908,22 @@ const journeys$1 = pgTable("journeys", {
|
|
|
3903
3908
|
* the fixed pair.
|
|
3904
3909
|
*/
|
|
3905
3910
|
purpose: text("purpose").notNull(),
|
|
3906
|
-
/** The manifest's `environments` contains this environment. */
|
|
3907
|
-
active: boolean("active").notNull(),
|
|
3908
3911
|
spine: jsonb("spine").$type().notNull(),
|
|
3909
3912
|
createdAt: createdAt(),
|
|
3910
3913
|
updatedAt: updatedAt()
|
|
3911
|
-
}, (table) => [primaryKey({ columns: [
|
|
3912
|
-
table.orgId,
|
|
3913
|
-
table.environment,
|
|
3914
|
-
table.key
|
|
3915
|
-
] }), index("journeys_org_id_environment_release_id_idx").on(table.orgId, table.environment, table.releaseId)]);
|
|
3914
|
+
}, (table) => [primaryKey({ columns: [table.orgId, table.key] })]);
|
|
3916
3915
|
const selectJourneySchema = createSelectSchema(journeys$1);
|
|
3917
3916
|
const insertJourneySchema = createInsertSchema(journeys$1);
|
|
3918
3917
|
/**
|
|
3919
3918
|
* Per-org, per-environment enable state for a journey key; the only
|
|
3920
|
-
* operator write on a journey. A row exists only
|
|
3921
|
-
*
|
|
3922
|
-
*
|
|
3923
|
-
*
|
|
3919
|
+
* operator write on a journey, and the one gate on it. A row exists only
|
|
3920
|
+
* once someone turned the journey on or off in that environment: NO ROW
|
|
3921
|
+
* MEANS DISABLED (ADR 0011). A push writes none, so a key is off in both
|
|
3922
|
+
* environments until `cow enable` or a manager's toggle.
|
|
3924
3923
|
*
|
|
3925
3924
|
* Keyed by key rather than by a foreign key into `journeys` on purpose: the
|
|
3926
|
-
*
|
|
3927
|
-
*
|
|
3925
|
+
* flag outlives every push, so it survives a key that temporarily leaves a
|
|
3926
|
+
* project and comes back.
|
|
3928
3927
|
*/
|
|
3929
3928
|
const journeyStates = pgTable("journey_states", {
|
|
3930
3929
|
orgId: text("org_id").notNull(),
|
|
@@ -4019,7 +4018,7 @@ const insertOrgSettingsSchema = createInsertSchema(orgSettings);
|
|
|
4019
4018
|
//#region ../../packages/db/src/schema/projects.ts
|
|
4020
4019
|
/**
|
|
4021
4020
|
* An org's cow project: the folder a developer runs `cow init` in, and the
|
|
4022
|
-
* thing
|
|
4021
|
+
* thing pushes belong to. An org may hold several, one per repo, each with
|
|
4023
4022
|
* its own release sequence, its own current release per environment, and its
|
|
4024
4023
|
* own slice of the deployed journey rows. The name is how `cow.json` picks
|
|
4025
4024
|
* one, so it is unique within the org.
|
|
@@ -4034,6 +4033,56 @@ const projects = pgTable("projects", {
|
|
|
4034
4033
|
const selectProjectSchema = createSelectSchema(projects);
|
|
4035
4034
|
const insertProjectSchema = createInsertSchema(projects);
|
|
4036
4035
|
|
|
4036
|
+
//#endregion
|
|
4037
|
+
//#region ../../packages/db/src/schema/pushes.ts
|
|
4038
|
+
/**
|
|
4039
|
+
* One `cow push`: the whole project at one point in time. A push stores the
|
|
4040
|
+
* source archive and creates a version of every key whose bundle changed
|
|
4041
|
+
* (ADR 0011); it settles nothing and changes no flag, so it has no status
|
|
4042
|
+
* of its own. What is compiling is read off its versions.
|
|
4043
|
+
*
|
|
4044
|
+
* `seq` is the human handle and is per project, allocated inside the create
|
|
4045
|
+
* transaction; the unique index leads with the org id so it doubles as the
|
|
4046
|
+
* tenancy key.
|
|
4047
|
+
*
|
|
4048
|
+
* The manifest is kept whole rather than only sliced across the versions:
|
|
4049
|
+
* it names the source archive `cow pull` unpacks, and its entry count is
|
|
4050
|
+
* the denominator the compile progress climbs towards.
|
|
4051
|
+
*/
|
|
4052
|
+
const pushes$1 = pgTable("pushes", {
|
|
4053
|
+
id: text("id").primaryKey(),
|
|
4054
|
+
orgId: text("org_id").notNull(),
|
|
4055
|
+
projectId: text("project_id").notNull().references(() => projects.id),
|
|
4056
|
+
seq: integer("seq").notNull(),
|
|
4057
|
+
manifest: jsonb("manifest").$type().notNull(),
|
|
4058
|
+
/** sha256 of the pushed manifest; the CLI compares it to skip a no-op push. */
|
|
4059
|
+
manifestDigest: text("manifest_digest").notNull(),
|
|
4060
|
+
/** Digest of the gzipped source tarball this push stored. */
|
|
4061
|
+
sourceDigest: text("source_digest").notNull(),
|
|
4062
|
+
createdBy: text("created_by").notNull(),
|
|
4063
|
+
createdAt: createdAt()
|
|
4064
|
+
}, (table) => [uniqueIndex("pushes_org_id_project_id_seq_unique").on(table.orgId, table.projectId, table.seq), index("pushes_org_id_created_at_idx").on(table.orgId, table.createdAt)]);
|
|
4065
|
+
const selectPushSchema = createSelectSchema(pushes$1);
|
|
4066
|
+
const insertPushSchema = createInsertSchema(pushes$1);
|
|
4067
|
+
/**
|
|
4068
|
+
* Which artifacts a push uploaded or compiled, so the prune sweep can delete
|
|
4069
|
+
* the bytes no surviving push still needs. Denormalised out of the manifest
|
|
4070
|
+
* deliberately: pruning is a join, not a jsonb scan.
|
|
4071
|
+
*
|
|
4072
|
+
* A push survives while any of its versions does, so a version's module
|
|
4073
|
+
* bytes are kept by the push that compiled them.
|
|
4074
|
+
*/
|
|
4075
|
+
const pushArtifacts = pgTable("push_artifacts", {
|
|
4076
|
+
orgId: text("org_id").notNull(),
|
|
4077
|
+
pushId: text("push_id").notNull().references(() => pushes$1.id),
|
|
4078
|
+
digest: text("digest").notNull(),
|
|
4079
|
+
createdAt: createdAt()
|
|
4080
|
+
}, (table) => [primaryKey({ columns: [
|
|
4081
|
+
table.orgId,
|
|
4082
|
+
table.pushId,
|
|
4083
|
+
table.digest
|
|
4084
|
+
] })]);
|
|
4085
|
+
|
|
4037
4086
|
//#endregion
|
|
4038
4087
|
//#region ../../packages/db/src/schema/violations.ts
|
|
4039
4088
|
/**
|
|
@@ -4114,96 +4163,6 @@ const quarantineEntries = pgTable("quarantine_entries", {
|
|
|
4114
4163
|
const selectQuarantineEntrySchema = createSelectSchema(quarantineEntries);
|
|
4115
4164
|
const insertQuarantineEntrySchema = createInsertSchema(quarantineEntries);
|
|
4116
4165
|
|
|
4117
|
-
//#endregion
|
|
4118
|
-
//#region ../../packages/db/src/schema/releases.ts
|
|
4119
|
-
/**
|
|
4120
|
-
* `pending` until the compile workflow finishes, then `ready` with module
|
|
4121
|
-
* digests recorded in the manifest, or `failed` with the error. Only a
|
|
4122
|
-
* `ready` release can be deployed.
|
|
4123
|
-
*/
|
|
4124
|
-
const releaseStatusEnum = pgEnum("release_status", [
|
|
4125
|
-
"pending",
|
|
4126
|
-
"ready",
|
|
4127
|
-
"failed"
|
|
4128
|
-
]);
|
|
4129
|
-
/**
|
|
4130
|
-
* One `cow push`: the whole project at one point in time. The release, not
|
|
4131
|
-
* the journey, is the versioning unit, because it is atomic, matches git,
|
|
4132
|
-
* and pins a journey together with the templates it sends.
|
|
4133
|
-
*
|
|
4134
|
-
* `seq` is the human handle (`rel_… #12`) and is per project, allocated
|
|
4135
|
-
* inside the create transaction; the unique index leads with the org id so
|
|
4136
|
-
* it doubles as the tenancy key.
|
|
4137
|
-
*
|
|
4138
|
-
* Immutable except for the compile result, which is why there is no
|
|
4139
|
-
* `updatedAt`: `compiledAt`, `error` and `compiledEntries` are the only
|
|
4140
|
-
* fields that ever move. The first two move exactly once; `compiledEntries`
|
|
4141
|
-
* climbs while the compile runs, because a push is minutes of silence
|
|
4142
|
-
* otherwise and the manifest's own length is the denominator.
|
|
4143
|
-
*
|
|
4144
|
-
* A counter rather than a row per entry: the question both the CLI and the
|
|
4145
|
-
* dashboard ask is "how far along", which one integer answers at a fraction
|
|
4146
|
-
* of the write volume. Naming which entry is compiling needs per-entry rows
|
|
4147
|
-
* and is a different question.
|
|
4148
|
-
*/
|
|
4149
|
-
const releases$1 = pgTable("releases", {
|
|
4150
|
-
id: text("id").primaryKey(),
|
|
4151
|
-
orgId: text("org_id").notNull(),
|
|
4152
|
-
projectId: text("project_id").notNull().references(() => projects.id),
|
|
4153
|
-
seq: integer("seq").notNull(),
|
|
4154
|
-
status: releaseStatusEnum("status").notNull(),
|
|
4155
|
-
manifest: jsonb("manifest").$type().notNull(),
|
|
4156
|
-
/** sha256 of the pushed manifest; `cow deploy` compares it to decide whether to push. */
|
|
4157
|
-
manifestDigest: text("manifest_digest").notNull(),
|
|
4158
|
-
createdBy: text("created_by").notNull(),
|
|
4159
|
-
createdAt: createdAt(),
|
|
4160
|
-
compiledAt: timestamp("compiled_at", {
|
|
4161
|
-
withTimezone: true,
|
|
4162
|
-
mode: "date",
|
|
4163
|
-
precision: 3
|
|
4164
|
-
}),
|
|
4165
|
-
error: text("error"),
|
|
4166
|
-
/** How many manifest entries have compiled and passed their checks. */
|
|
4167
|
-
compiledEntries: integer("compiled_entries").notNull().default(0)
|
|
4168
|
-
}, (table) => [uniqueIndex("releases_org_id_project_id_seq_unique").on(table.orgId, table.projectId, table.seq), index("releases_org_id_created_at_idx").on(table.orgId, table.createdAt)]);
|
|
4169
|
-
const selectReleaseSchema = createSelectSchema(releases$1);
|
|
4170
|
-
const insertReleaseSchema = createInsertSchema(releases$1);
|
|
4171
|
-
/**
|
|
4172
|
-
* Which artifacts a release references, so the prune sweep can delete the
|
|
4173
|
-
* bytes no surviving release still needs. Denormalised out of the manifest
|
|
4174
|
-
* deliberately: pruning is a join, not a jsonb scan.
|
|
4175
|
-
*/
|
|
4176
|
-
const releaseArtifacts = pgTable("release_artifacts", {
|
|
4177
|
-
orgId: text("org_id").notNull(),
|
|
4178
|
-
releaseId: text("release_id").notNull().references(() => releases$1.id),
|
|
4179
|
-
digest: text("digest").notNull(),
|
|
4180
|
-
createdAt: createdAt()
|
|
4181
|
-
}, (table) => [primaryKey({ columns: [
|
|
4182
|
-
table.orgId,
|
|
4183
|
-
table.releaseId,
|
|
4184
|
-
table.digest
|
|
4185
|
-
] })]);
|
|
4186
|
-
/**
|
|
4187
|
-
* Append-only: an environment's current release, for one project, is that
|
|
4188
|
-
* project's latest row, and rollback is another deployment of an earlier
|
|
4189
|
-
* release rather than a mutation. That is what makes the deployment history
|
|
4190
|
-
* readable and makes "the last two deployments" the whole of `cow rollback`.
|
|
4191
|
-
*
|
|
4192
|
-
* `projectId` is the release's own project, denormalised so "this project's
|
|
4193
|
-
* latest deployment in this environment" is one indexed read.
|
|
4194
|
-
*/
|
|
4195
|
-
const deployments$1 = pgTable("deployments", {
|
|
4196
|
-
id: text("id").primaryKey(),
|
|
4197
|
-
orgId: text("org_id").notNull(),
|
|
4198
|
-
environment: environmentEnum("environment").notNull(),
|
|
4199
|
-
releaseId: text("release_id").notNull().references(() => releases$1.id),
|
|
4200
|
-
projectId: text("project_id").notNull().references(() => projects.id),
|
|
4201
|
-
deployedBy: text("deployed_by").notNull(),
|
|
4202
|
-
createdAt: createdAt()
|
|
4203
|
-
}, (table) => [index("deployments_org_id_environment_created_at_idx").on(table.orgId, table.environment, table.createdAt), index("deployments_org_id_project_id_environment_created_at_idx").on(table.orgId, table.projectId, table.environment, table.createdAt)]);
|
|
4204
|
-
const selectDeploymentSchema = createSelectSchema(deployments$1);
|
|
4205
|
-
const insertDeploymentSchema = createInsertSchema(deployments$1);
|
|
4206
|
-
|
|
4207
4166
|
//#endregion
|
|
4208
4167
|
//#region ../../packages/db/src/schema/segments.ts
|
|
4209
4168
|
/**
|
|
@@ -4433,6 +4392,68 @@ const suppressedAddresses = pgTable("suppressed_addresses", {
|
|
|
4433
4392
|
const selectSuppressedAddressSchema = createSelectSchema(suppressedAddresses);
|
|
4434
4393
|
const insertSuppressedAddressSchema = createInsertSchema(suppressedAddresses);
|
|
4435
4394
|
|
|
4395
|
+
//#endregion
|
|
4396
|
+
//#region ../../packages/db/src/schema/versions.ts
|
|
4397
|
+
const versionKindEnum = pgEnum("version_kind", ["journey", "template"]);
|
|
4398
|
+
/**
|
|
4399
|
+
* `compiling` until the push's compile workflow reaches this entry, then
|
|
4400
|
+
* `ready` with the module digest recorded, or `failed` with the error. Only
|
|
4401
|
+
* a `ready` version runs, so a failed compile leaves the previous one
|
|
4402
|
+
* running (ADR 0011).
|
|
4403
|
+
*/
|
|
4404
|
+
const versionStatusEnum = pgEnum("version_status", [
|
|
4405
|
+
"compiling",
|
|
4406
|
+
"ready",
|
|
4407
|
+
"failed"
|
|
4408
|
+
]);
|
|
4409
|
+
/**
|
|
4410
|
+
* An immutable, content-addressed snapshot of one journey or one template.
|
|
4411
|
+
* The latest ready version of a key is what new executions and new sends
|
|
4412
|
+
* use; an execution pins the journey version it started on for its whole
|
|
4413
|
+
* life (ADR 0004).
|
|
4414
|
+
*
|
|
4415
|
+
* `moduleDigest` is the content address and is null until the compile
|
|
4416
|
+
* settles: a version that never compiled has no module to be addressed by,
|
|
4417
|
+
* and Postgres leaves nulls out of the unique index, so a key that fails to
|
|
4418
|
+
* compile twice records both failures.
|
|
4419
|
+
*
|
|
4420
|
+
* `pushedAt` is the ordering key rather than a creation stamp, because an
|
|
4421
|
+
* unchanged key gets no new version: a push that re-sends bytes a version
|
|
4422
|
+
* already holds bumps this instead, which is what makes `git revert` plus
|
|
4423
|
+
* `cow push` put the reverted code back in front.
|
|
4424
|
+
*/
|
|
4425
|
+
const versions$1 = pgTable("versions", {
|
|
4426
|
+
id: text("id").primaryKey(),
|
|
4427
|
+
orgId: text("org_id").notNull(),
|
|
4428
|
+
/** The file basename its author wrote; unique per org within a kind. */
|
|
4429
|
+
key: text("key").notNull(),
|
|
4430
|
+
kind: versionKindEnum("kind").notNull(),
|
|
4431
|
+
/** The project that pushed this key; ADR 0009's ownership. */
|
|
4432
|
+
projectId: text("project_id").notNull().references(() => projects.id),
|
|
4433
|
+
/** The push that compiled this version's module. */
|
|
4434
|
+
pushId: text("push_id").notNull().references(() => pushes$1.id),
|
|
4435
|
+
moduleDigest: text("module_digest"),
|
|
4436
|
+
manifest: jsonb("manifest").$type().notNull(),
|
|
4437
|
+
status: versionStatusEnum("status").notNull(),
|
|
4438
|
+
pushedAt: timestamp("pushed_at", {
|
|
4439
|
+
withTimezone: true,
|
|
4440
|
+
mode: "date",
|
|
4441
|
+
precision: 3
|
|
4442
|
+
}).notNull().defaultNow(),
|
|
4443
|
+
compiledAt: timestamp("compiled_at", {
|
|
4444
|
+
withTimezone: true,
|
|
4445
|
+
mode: "date",
|
|
4446
|
+
precision: 3
|
|
4447
|
+
}),
|
|
4448
|
+
error: text("error")
|
|
4449
|
+
}, (table) => [
|
|
4450
|
+
uniqueIndex("versions_org_id_kind_key_module_digest_unique").on(table.orgId, table.kind, table.key, table.moduleDigest),
|
|
4451
|
+
index("versions_org_id_kind_key_pushed_at_idx").on(table.orgId, table.kind, table.key, table.pushedAt),
|
|
4452
|
+
index("versions_org_id_push_id_idx").on(table.orgId, table.pushId)
|
|
4453
|
+
]);
|
|
4454
|
+
const selectVersionSchema = createSelectSchema(versions$1);
|
|
4455
|
+
const insertVersionSchema = createInsertSchema(versions$1);
|
|
4456
|
+
|
|
4436
4457
|
//#endregion
|
|
4437
4458
|
//#region ../../packages/db/src/schema/wallets.ts
|
|
4438
4459
|
/**
|
|
@@ -4589,12 +4610,11 @@ const ERROR_CODE_STATUS = {
|
|
|
4589
4610
|
journey_error: 500,
|
|
4590
4611
|
journey_output_invalid: 500,
|
|
4591
4612
|
journey_nondeterministic: 500,
|
|
4592
|
-
|
|
4593
|
-
|
|
4613
|
+
push_invalid: 422,
|
|
4614
|
+
version_not_ready: 409,
|
|
4594
4615
|
project_exists: 409,
|
|
4595
4616
|
project_missing: 404,
|
|
4596
|
-
artifact_too_large: 413
|
|
4597
|
-
deploy_key_scope: 403
|
|
4617
|
+
artifact_too_large: 413
|
|
4598
4618
|
};
|
|
4599
4619
|
/** First zod issue's message: enough signal for a 422 without a novel format. */
|
|
4600
4620
|
function firstIssueMessage(error) {
|
|
@@ -4657,10 +4677,10 @@ const identifierDtoSchema = z.object({
|
|
|
4657
4677
|
//#endregion
|
|
4658
4678
|
//#region ../../packages/shared/src/journeys-v2/manifest.ts
|
|
4659
4679
|
/**
|
|
4660
|
-
* The
|
|
4680
|
+
* The pushed manifest (spec: Build; Push and compile): what `cow build`
|
|
4661
4681
|
* extracts from a project and `cow push` uploads with the bundles. The
|
|
4662
|
-
* server validates it with these schemas, compiles every bundle
|
|
4663
|
-
*
|
|
4682
|
+
* server validates it with these schemas, compiles every bundle whose key
|
|
4683
|
+
* changed, and stores that entry on the version it creates.
|
|
4664
4684
|
*/
|
|
4665
4685
|
/**
|
|
4666
4686
|
* A journey or template key: the file basename under `journeys/` or
|
|
@@ -4740,7 +4760,7 @@ const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1
|
|
|
4740
4760
|
* reuses it.
|
|
4741
4761
|
*
|
|
4742
4762
|
* Both members are strict, so a journey holding the retired `source` key
|
|
4743
|
-
* fails to compile
|
|
4763
|
+
* fails to compile instead of silently triggering on every app.
|
|
4744
4764
|
* There is deliberately no pipe filter: a trigger narrows by the app the
|
|
4745
4765
|
* write is attributed to, the same token a segment definition names.
|
|
4746
4766
|
*/
|
|
@@ -4809,6 +4829,13 @@ const spineEntrySchema = z.object({
|
|
|
4809
4829
|
return z.array(spineEntrySchema).optional();
|
|
4810
4830
|
}
|
|
4811
4831
|
}).meta({ id: "JourneySpineEntry" });
|
|
4832
|
+
/**
|
|
4833
|
+
* One journey in a stored manifest. A plain (non-strict) object on purpose:
|
|
4834
|
+
* a manifest pushed before the author's rollout gate went away still carries
|
|
4835
|
+
* `environments`, and every stored manifest has to keep parsing for good
|
|
4836
|
+
* (ADR 0011). Zod strips the key, and the journey runs wherever the
|
|
4837
|
+
* per-environment enabled flag says it runs, which is the one gate there is.
|
|
4838
|
+
*/
|
|
4812
4839
|
const manifestJourneySchema = z.object({
|
|
4813
4840
|
key: journeyKeySchema,
|
|
4814
4841
|
/** The author's labels; the dashboard's only grouping. */
|
|
@@ -4816,7 +4843,7 @@ const manifestJourneySchema = z.object({
|
|
|
4816
4843
|
trigger: triggerSchema,
|
|
4817
4844
|
/**
|
|
4818
4845
|
* A fixed purpose or one the project declares. Checked against the org's
|
|
4819
|
-
* declared set at
|
|
4846
|
+
* declared set at push time, not here: a manifest is built and pushed
|
|
4820
4847
|
* without ever reaching the org whose rows say what exists.
|
|
4821
4848
|
*/
|
|
4822
4849
|
purpose: consentPurposeKeySchema,
|
|
@@ -4827,12 +4854,10 @@ const manifestJourneySchema = z.object({
|
|
|
4827
4854
|
* development, which is the one thing a journey must not do.
|
|
4828
4855
|
*
|
|
4829
4856
|
* Optional here and required at `defineJourney`, exactly like `purposes`: a
|
|
4830
|
-
*
|
|
4857
|
+
* version pushed before the field existed carries none and its stored
|
|
4831
4858
|
* manifest still parses. The author's build is where the error is useful.
|
|
4832
4859
|
*/
|
|
4833
4860
|
senderIdentity: destinationNameSchema.optional(),
|
|
4834
|
-
/** The author's rollout gate: the journey is active only in these. */
|
|
4835
|
-
environments: environmentsSchema,
|
|
4836
4861
|
spine: z.array(spineEntrySchema),
|
|
4837
4862
|
bundle: digestSchema
|
|
4838
4863
|
});
|
|
@@ -4864,27 +4889,27 @@ function uniqueKeys(items, ctx, path) {
|
|
|
4864
4889
|
}
|
|
4865
4890
|
/**
|
|
4866
4891
|
* What `cow build` writes to `.cow/build/manifest.json`, and the shape a
|
|
4867
|
-
*
|
|
4892
|
+
* push row stores for good, entry by entry, on the versions it creates.
|
|
4868
4893
|
*
|
|
4869
4894
|
* `protocol` is any positive integer rather than the current constant on
|
|
4870
|
-
* purpose: a
|
|
4871
|
-
* started on, so the day the protocol is bumped every stored
|
|
4872
|
-
* still parse, or the runner
|
|
4873
|
-
*
|
|
4874
|
-
*
|
|
4875
|
-
*
|
|
4876
|
-
*
|
|
4895
|
+
* purpose: a version is immutable and an execution stays pinned to the one
|
|
4896
|
+
* it started on, so the day the protocol is bumped every stored push must
|
|
4897
|
+
* still parse, or the runner and every API response the client validates
|
|
4898
|
+
* break at once for any org with history. The literal lives at push time
|
|
4899
|
+
* only (`createPushBodySchema`), which is where a stale CLI is the
|
|
4900
|
+
* developer's own fixable problem, and the runner refuses a version built
|
|
4901
|
+
* for another protocol with a message naming both numbers.
|
|
4877
4902
|
*/
|
|
4878
4903
|
const manifestSchema = z.object({
|
|
4879
4904
|
protocol: z.number().int().positive(),
|
|
4880
4905
|
/** The `@cowliss/cli` version the project was built with. */
|
|
4881
4906
|
sdk: z.string().min(1),
|
|
4882
|
-
journeys: z.array(manifestJourneySchema).max(
|
|
4883
|
-
templates: z.array(manifestTemplateSchema).max(
|
|
4907
|
+
journeys: z.array(manifestJourneySchema).max(PUSH_LIMITS.journeys),
|
|
4908
|
+
templates: z.array(manifestTemplateSchema).max(PUSH_LIMITS.templates),
|
|
4884
4909
|
/**
|
|
4885
4910
|
* The consent purposes this project declares, copied from `cow.json`.
|
|
4886
|
-
* Optional rather than defaulted: a
|
|
4887
|
-
*
|
|
4911
|
+
* Optional rather than defaulted: a push made before purposes existed
|
|
4912
|
+
* carries none, and its stored manifest still parses.
|
|
4888
4913
|
*/
|
|
4889
4914
|
purposes: purposesSchema.optional(),
|
|
4890
4915
|
/** Digest of the gzipped source tarball. */
|
|
@@ -4895,27 +4920,12 @@ const manifestSchema = z.object({
|
|
|
4895
4920
|
uniqueKeys(manifest.purposes ?? [], ctx, "purposes");
|
|
4896
4921
|
});
|
|
4897
4922
|
/**
|
|
4898
|
-
*
|
|
4899
|
-
*
|
|
4900
|
-
*
|
|
4901
|
-
*
|
|
4902
|
-
*
|
|
4903
|
-
* Journeys and templates are keyed separately because they share a key
|
|
4904
|
-
* space: `welcome.ts` and `welcome.tsx` are one journey and the email it
|
|
4905
|
-
* sends in every example, and a flat map would let one overwrite the other.
|
|
4906
|
-
*
|
|
4907
|
-
* `plugin` is a toolchain record, not a linked artifact: modules are
|
|
4908
|
-
* statically linked, so the plugin bytes are inside each module. It says
|
|
4909
|
-
* which engine compiled the release, which is what a later bug report or a
|
|
4910
|
-
* reproducible rebuild needs.
|
|
4923
|
+
* One entry of a stored manifest: what a version is a snapshot of. Journeys
|
|
4924
|
+
* and templates are kept apart because they share a key space (`welcome.ts`
|
|
4925
|
+
* and `welcome.tsx` are one journey and the email it sends in every
|
|
4926
|
+
* example), and the version's own `kind` says which of the two this is.
|
|
4911
4927
|
*/
|
|
4912
|
-
const
|
|
4913
|
-
modules: z.object({
|
|
4914
|
-
journeys: z.record(journeyKeySchema, digestSchema),
|
|
4915
|
-
templates: z.record(journeyKeySchema, digestSchema)
|
|
4916
|
-
}),
|
|
4917
|
-
plugin: digestSchema
|
|
4918
|
-
});
|
|
4928
|
+
const versionManifestSchema = z.union([manifestJourneySchema, manifestTemplateSchema]);
|
|
4919
4929
|
|
|
4920
4930
|
//#endregion
|
|
4921
4931
|
//#region ../../packages/shared/src/timestamp.ts
|
|
@@ -5543,6 +5553,8 @@ const listDeliveriesQuerySchema = paginationQuerySchema.extend({
|
|
|
5543
5553
|
direction: sortDirectionSchema.default("desc"),
|
|
5544
5554
|
status: deliveryStatusSchema.optional(),
|
|
5545
5555
|
journey: z.string().trim().min(1).max(200).optional(),
|
|
5556
|
+
/** The template or destination this attempt was for; a template page's own log. */
|
|
5557
|
+
step: z.string().trim().min(1).max(200).optional(),
|
|
5546
5558
|
channel: deliveryChannelSchema.optional(),
|
|
5547
5559
|
appId: z.string().trim().min(1).max(200).optional(),
|
|
5548
5560
|
/** One person's deliveries, merged ids included, same as the event feed. */
|
|
@@ -5703,6 +5715,20 @@ const domainDnsSetupSchema = z.object({
|
|
|
5703
5715
|
provider: z.string().nullable()
|
|
5704
5716
|
});
|
|
5705
5717
|
|
|
5718
|
+
//#endregion
|
|
5719
|
+
//#region ../../packages/shared/src/enabled.ts
|
|
5720
|
+
/**
|
|
5721
|
+
* The enable flag per environment; absent rows read as off (ADR 0011).
|
|
5722
|
+
*
|
|
5723
|
+
* Its own module because both `journeys.ts` and `pushes.ts` need it and
|
|
5724
|
+
* neither may import the other: a journey carries the flag, and so does the
|
|
5725
|
+
* project status a push is read back through.
|
|
5726
|
+
*/
|
|
5727
|
+
const journeyEnabledSchema = z.object({
|
|
5728
|
+
development: z.boolean(),
|
|
5729
|
+
production: z.boolean()
|
|
5730
|
+
});
|
|
5731
|
+
|
|
5706
5732
|
//#endregion
|
|
5707
5733
|
//#region ../../packages/shared/src/patterns.ts
|
|
5708
5734
|
/**
|
|
@@ -5999,7 +6025,6 @@ const manifestOutputSchema = z.discriminatedUnion("kind", [manifestJourneySchema
|
|
|
5999
6025
|
trigger: true,
|
|
6000
6026
|
purpose: true,
|
|
6001
6027
|
senderIdentity: true,
|
|
6002
|
-
environments: true,
|
|
6003
6028
|
tags: true
|
|
6004
6029
|
}).extend({ kind: z.literal("journey") }), manifestTemplateSchema.pick({
|
|
6005
6030
|
sendClass: true,
|
|
@@ -6030,8 +6055,8 @@ const executionStatusSchema = z.enum(EXECUTION_STATUSES);
|
|
|
6030
6055
|
//#endregion
|
|
6031
6056
|
//#region ../../packages/shared/src/executions.ts
|
|
6032
6057
|
/**
|
|
6033
|
-
* Executions: one run of one journey for one profile, pinned to the
|
|
6034
|
-
* it started
|
|
6058
|
+
* Executions: one run of one journey for one profile, pinned to the journey
|
|
6059
|
+
* version it started on. This replaces v1's journey instances, which were read
|
|
6035
6060
|
* straight from Temporal: an execution row outlives its workflow's retention
|
|
6036
6061
|
* and can be listed and filtered without a Temporal round trip, while
|
|
6037
6062
|
* Temporal stays the source of truth for the run itself.
|
|
@@ -6065,7 +6090,8 @@ const executionDetailSchema = executionSchema.extend({
|
|
|
6065
6090
|
const listExecutionsQuerySchema = paginationQuerySchema.extend({
|
|
6066
6091
|
/** A journey key; the environment comes from the selection header. */
|
|
6067
6092
|
journey: z.string().trim().min(1).max(200).optional(),
|
|
6068
|
-
|
|
6093
|
+
/** One version's own runs, for the history rows on a journey. */
|
|
6094
|
+
version: z.string().trim().min(1).max(200).optional(),
|
|
6069
6095
|
status: executionStatusSchema.optional(),
|
|
6070
6096
|
profileId: z.string().trim().min(1).max(200).optional(),
|
|
6071
6097
|
/**
|
|
@@ -6077,20 +6103,215 @@ const listExecutionsQuerySchema = paginationQuerySchema.extend({
|
|
|
6077
6103
|
*/
|
|
6078
6104
|
since: z.iso.datetime().optional()
|
|
6079
6105
|
});
|
|
6106
|
+
/**
|
|
6107
|
+
* What to stop: every live execution of one journey in the selected
|
|
6108
|
+
* environment, or the ones still pinned to one version. Cancelling is
|
|
6109
|
+
* explicit and per environment (ADR 0011): disabling a journey stops new
|
|
6110
|
+
* entries and lets what is running finish, and this is the other verb.
|
|
6111
|
+
*/
|
|
6112
|
+
const cancelExecutionsBodySchema = z.object({ data: z.union([z.object({ journey: z.string().trim().min(1).max(200) }), z.object({ version: z.string().trim().min(1).max(200) })]) });
|
|
6113
|
+
/**
|
|
6114
|
+
* Cancelling is a request, not an edit: each execution stops when it reaches
|
|
6115
|
+
* its next step, so the answer is that the sweep is under way rather than a
|
|
6116
|
+
* count of what has already stopped.
|
|
6117
|
+
*/
|
|
6118
|
+
const cancellingSchema = z.object({ cancelling: z.literal(true) });
|
|
6119
|
+
|
|
6120
|
+
//#endregion
|
|
6121
|
+
//#region ../../packages/shared/src/journeys-v2/config.ts
|
|
6122
|
+
/**
|
|
6123
|
+
* A project's name: what `cow init` asked for, and what the `projects` row
|
|
6124
|
+
* stores. Lives here rather than beside the release DTOs because `cow.json`
|
|
6125
|
+
* is the file a developer types it into; `createProjectBodySchema` reuses it.
|
|
6126
|
+
*/
|
|
6127
|
+
const projectNameSchema = z.string().trim().min(1).max(200);
|
|
6128
|
+
/**
|
|
6129
|
+
* `cow.json` (spec: Project layout): the org, and which of its projects this
|
|
6130
|
+
* directory is. The environment is always a flag, and auth never lives in
|
|
6131
|
+
* the project. Strict, so a typo'd key is a build error rather than a
|
|
6132
|
+
* silently ignored setting.
|
|
6133
|
+
*/
|
|
6134
|
+
const cowConfigSchema = z.strictObject({
|
|
6135
|
+
$schema: z.url().optional(),
|
|
6136
|
+
orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
|
|
6137
|
+
/**
|
|
6138
|
+
* Which project of the org this directory pushes to. One org can hold
|
|
6139
|
+
* several, one per repo, each with its own release sequence and its own
|
|
6140
|
+
* slice of the deployed journeys, so every project says which it is.
|
|
6141
|
+
*/
|
|
6142
|
+
project: projectNameSchema,
|
|
6143
|
+
/**
|
|
6144
|
+
* The consent purposes this project declares. They are org-wide, so two
|
|
6145
|
+
* projects declaring one key must agree on its label and default or the
|
|
6146
|
+
* deploy is refused; a deploy adds and updates them and never deletes
|
|
6147
|
+
* one, because profiles hold answers against them.
|
|
6148
|
+
*/
|
|
6149
|
+
purposes: purposesSchema.optional(),
|
|
6150
|
+
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
6151
|
+
apiUrl: z.url().optional(),
|
|
6152
|
+
/**
|
|
6153
|
+
* Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
|
|
6154
|
+
* login against one Cowliss's dashboard yields a token the other's API
|
|
6155
|
+
* rejects, so a config that names an API names its dashboard too.
|
|
6156
|
+
*/
|
|
6157
|
+
webUrl: z.url().optional()
|
|
6158
|
+
}).meta({
|
|
6159
|
+
title: "cow.json",
|
|
6160
|
+
description: "A cow project: the organization and the project it pushes to."
|
|
6161
|
+
});
|
|
6162
|
+
|
|
6163
|
+
//#endregion
|
|
6164
|
+
//#region ../../packages/shared/src/pushes.ts
|
|
6165
|
+
/**
|
|
6166
|
+
* The push lifecycle's wire contracts (ADR 0011): the project a developer
|
|
6167
|
+
* runs `cow init` in, the artifacts `cow push` uploads, the push itself, and
|
|
6168
|
+
* the versions its compile produces.
|
|
6169
|
+
*
|
|
6170
|
+
* Row-derived through drizzle-zod where a row is what the wire carries, so
|
|
6171
|
+
* the DTO cannot drift from the column set; the jsonb manifest gets its
|
|
6172
|
+
* explicit shared schema, because the column type is opaque to drizzle-zod.
|
|
6173
|
+
*/
|
|
6174
|
+
const projectSchema = selectProjectSchema.extend({
|
|
6175
|
+
createdAt: z.iso.datetime(),
|
|
6176
|
+
updatedAt: z.iso.datetime()
|
|
6177
|
+
});
|
|
6178
|
+
/** `cow init` names the project after the folder it created. */
|
|
6179
|
+
const createProjectBodySchema = z.object({ data: z.object({ name: projectNameSchema }) });
|
|
6180
|
+
/** Which project to read: the one the caller's `cow.json` names. */
|
|
6181
|
+
const getProjectQuerySchema = z.object({ name: projectNameSchema });
|
|
6182
|
+
const artifactDigestParams = z.object({ digest: digestSchema });
|
|
6183
|
+
/**
|
|
6184
|
+
* Which kind of artifact the uploaded bytes are. Named by the client rather
|
|
6185
|
+
* than sniffed, because only the build knows whether a blob is a journey
|
|
6186
|
+
* bundle or the source tarball, and the value is metadata for the prune
|
|
6187
|
+
* sweep and the sandbox's cache rather than a trust boundary: what a module
|
|
6188
|
+
* is, is decided by the server-side compile step. `module` is absent on
|
|
6189
|
+
* purpose, since only that compile step ever writes one.
|
|
6190
|
+
*/
|
|
6191
|
+
const putArtifactQuerySchema = z.object({ kind: z.enum(["bundle", "source"]) });
|
|
6192
|
+
/**
|
|
6193
|
+
* One version as the API reports it: the immutable snapshot of one journey
|
|
6194
|
+
* or one template, with the manifest entry it was built from. `manifest` is
|
|
6195
|
+
* the pushed entry either way; which of the two shapes it is follows `kind`.
|
|
6196
|
+
*/
|
|
6197
|
+
const versionSchema = selectVersionSchema.extend({
|
|
6198
|
+
manifest: z.union([manifestJourneySchema, manifestTemplateSchema]),
|
|
6199
|
+
pushedAt: z.iso.datetime(),
|
|
6200
|
+
compiledAt: z.iso.datetime().nullable()
|
|
6201
|
+
});
|
|
6202
|
+
/**
|
|
6203
|
+
* A version as another resource points at it: what the dashboard's "latest
|
|
6204
|
+
* version" column shows (the short digest, when it was pushed, whether it
|
|
6205
|
+
* compiled) and the push its source archive lives on. The manifest entry is
|
|
6206
|
+
* left out, because a journey row already carries what its own entry says.
|
|
6207
|
+
*/
|
|
6208
|
+
const versionSummarySchema = selectVersionSchema.pick({
|
|
6209
|
+
id: true,
|
|
6210
|
+
status: true,
|
|
6211
|
+
moduleDigest: true,
|
|
6212
|
+
pushId: true,
|
|
6213
|
+
projectId: true,
|
|
6214
|
+
error: true
|
|
6215
|
+
}).extend({ pushedAt: z.iso.datetime() });
|
|
6216
|
+
/**
|
|
6217
|
+
* A push as the API reports it, with the versions it created: `cow push`
|
|
6218
|
+
* returns as soon as the upload is stored, so what is compiling, what is
|
|
6219
|
+
* ready and what failed is read back from here.
|
|
6220
|
+
*
|
|
6221
|
+
* `compiledEntries` and `totalEntries` are not columns: they are the settled
|
|
6222
|
+
* and the total count of those versions, so a counter can never disagree
|
|
6223
|
+
* with the rows it counts. A key whose module did not change gets no new
|
|
6224
|
+
* version, which is why the denominator is the changed keys rather than the
|
|
6225
|
+
* manifest's length.
|
|
6226
|
+
*/
|
|
6227
|
+
const pushSchema = selectPushSchema.extend({
|
|
6228
|
+
manifest: manifestSchema,
|
|
6229
|
+
versions: z.array(versionSchema),
|
|
6230
|
+
compiledEntries: z.number().int(),
|
|
6231
|
+
totalEntries: z.number().int(),
|
|
6232
|
+
createdAt: z.iso.datetime()
|
|
6233
|
+
});
|
|
6234
|
+
const createPushBodySchema = z.object({ data: z.object({
|
|
6235
|
+
/**
|
|
6236
|
+
* The one place the protocol is pinned to the constant. The stored shape
|
|
6237
|
+
* takes any positive integer, because a version outlives a bump; a push
|
|
6238
|
+
* is a live CLI talking to a live platform, so a mismatch here is a 422
|
|
6239
|
+
* the developer fixes by updating `@cowliss/cli`, and nothing is stored.
|
|
6240
|
+
*/
|
|
6241
|
+
manifest: manifestSchema.safeExtend({ protocol: z.literal(2) }),
|
|
6242
|
+
/** The `cow.json` project this push belongs to. */
|
|
6243
|
+
project: projectNameSchema
|
|
6244
|
+
}) });
|
|
6245
|
+
const listPushesQuerySchema = paginationQuerySchema.extend({
|
|
6246
|
+
/** One project's own pushes; absent lists the org's. */
|
|
6247
|
+
project: projectNameSchema.optional() });
|
|
6248
|
+
/**
|
|
6249
|
+
* One key's version history, newest first. Both the key and the kind are
|
|
6250
|
+
* required: journeys and templates share a key space, so `welcome` alone
|
|
6251
|
+
* names two different histories.
|
|
6252
|
+
*/
|
|
6253
|
+
const listVersionsQuerySchema = paginationQuerySchema.extend({
|
|
6254
|
+
key: z.string().trim().min(1).max(200),
|
|
6255
|
+
kind: z.enum(["journey", "template"])
|
|
6256
|
+
});
|
|
6257
|
+
/**
|
|
6258
|
+
* One key of a project as `cow status` and the MCP `status` tool report it:
|
|
6259
|
+
* the code the server holds for it, whether it is on, and what is still
|
|
6260
|
+
* running. Journeys and templates share the shape, because a developer asks
|
|
6261
|
+
* the same question of both; the two fields only a journey has are null on a
|
|
6262
|
+
* template.
|
|
6263
|
+
*/
|
|
6264
|
+
const projectStatusKeySchema = z.object({
|
|
6265
|
+
kind: z.enum(["journey", "template"]),
|
|
6266
|
+
key: z.string(),
|
|
6267
|
+
/**
|
|
6268
|
+
* The newest version of this key, whatever its compile state, which is
|
|
6269
|
+
* what says whether the tree in front of the developer is the code the
|
|
6270
|
+
* server holds. Null only for a journey whose versions are all pruned.
|
|
6271
|
+
*/
|
|
6272
|
+
latestVersion: versionSchema.nullable(),
|
|
6273
|
+
/** The flag per environment; null on a template, which has none. */
|
|
6274
|
+
enabled: journeyEnabledSchema.nullable(),
|
|
6275
|
+
/** Executions still running or waiting, per environment. */
|
|
6276
|
+
liveExecutions: z.object({
|
|
6277
|
+
development: z.number().int(),
|
|
6278
|
+
production: z.number().int()
|
|
6279
|
+
})
|
|
6280
|
+
});
|
|
6281
|
+
/**
|
|
6282
|
+
* Everything the server knows about one project's keys, in one read: the
|
|
6283
|
+
* whole project rather than a page of it, because a manifest holds at most
|
|
6284
|
+
* `PUSH_LIMITS.journeys + PUSH_LIMITS.templates` keys and the answer to "what
|
|
6285
|
+
* is my project doing" is useless split across cursors.
|
|
6286
|
+
*
|
|
6287
|
+
* `warnings` is what a deploy used to answer with (ADR 0011 retired the
|
|
6288
|
+
* deploy): a name a journey references that the environment does not define
|
|
6289
|
+
* yet. They never block, because a segment or a destination may be created
|
|
6290
|
+
* right after a push.
|
|
6291
|
+
*/
|
|
6292
|
+
const projectStatusSchema = z.object({
|
|
6293
|
+
project: projectNameSchema,
|
|
6294
|
+
keys: z.array(projectStatusKeySchema),
|
|
6295
|
+
warnings: z.object({
|
|
6296
|
+
development: z.array(z.string()),
|
|
6297
|
+
production: z.array(z.string())
|
|
6298
|
+
})
|
|
6299
|
+
});
|
|
6080
6300
|
|
|
6081
6301
|
//#endregion
|
|
6082
6302
|
//#region ../../packages/shared/src/journeys.ts
|
|
6083
6303
|
/**
|
|
6084
|
-
* Journeys as the API reports them: the rows a
|
|
6085
|
-
*
|
|
6086
|
-
*
|
|
6304
|
+
* Journeys as the API reports them: the rows a push derives from its
|
|
6305
|
+
* manifest, one per (org, key). Nothing here is authored through the API, so
|
|
6306
|
+
* the only write is the enable flag.
|
|
6087
6307
|
*
|
|
6088
6308
|
* Derived from the Drizzle table via drizzle-zod so the wire DTO and the row
|
|
6089
6309
|
* share one source of truth; the jsonb columns get explicit wire schemas
|
|
6090
6310
|
* because the row types are opaque to drizzle-zod. `enabled` is not a
|
|
6091
|
-
* column: it is the
|
|
6092
|
-
*
|
|
6093
|
-
*
|
|
6311
|
+
* column: it is the journey_states value per environment, absent meaning
|
|
6312
|
+
* off, and both environments are reported whichever one the caller selected,
|
|
6313
|
+
* because "is this on in production?" is the question the list exists to
|
|
6314
|
+
* answer.
|
|
6094
6315
|
*/
|
|
6095
6316
|
const journeyTriggerSchema = triggerSchema;
|
|
6096
6317
|
/**
|
|
@@ -6103,23 +6324,26 @@ const journeySchema = selectJourneySchema.extend({
|
|
|
6103
6324
|
trigger: journeyTriggerSchema,
|
|
6104
6325
|
purpose: consentPurposeKeySchema,
|
|
6105
6326
|
spine: z.array(journeySpineEntrySchema),
|
|
6106
|
-
enabled:
|
|
6327
|
+
enabled: journeyEnabledSchema,
|
|
6328
|
+
/**
|
|
6329
|
+
* The newest version of this key, whatever it compiled to, so a list can
|
|
6330
|
+
* say "pushed 2 hours ago" and "compiling" without a second read. What the
|
|
6331
|
+
* journey RUNS is the newest one that finished compiling, which is this
|
|
6332
|
+
* one unless it is still compiling or failed. Null only for a key whose
|
|
6333
|
+
* versions have all been pruned.
|
|
6334
|
+
*/
|
|
6335
|
+
latestVersion: versionSummarySchema.nullable(),
|
|
6107
6336
|
createdAt: z.iso.datetime(),
|
|
6108
6337
|
updatedAt: z.iso.datetime()
|
|
6109
6338
|
});
|
|
6110
6339
|
/**
|
|
6111
|
-
* Enable/disable is the only operator write on a journey, and it is per org
|
|
6112
|
-
* and per environment: the environment comes from the selection header, not
|
|
6113
|
-
* from the body, so the toggle acts on whichever one the caller is in.
|
|
6114
|
-
*/
|
|
6115
|
-
const updateJourneyBodySchema = z.object({ data: z.object({ enabled: z.boolean() }) });
|
|
6116
|
-
/**
|
|
6117
6340
|
* Journey list query. `q` is a substring search over the key and the tags,
|
|
6118
6341
|
* the two things an author names a journey by; `tag` is "has this tag",
|
|
6119
6342
|
* exact and case-sensitive, so a badge in the table is the way into it.
|
|
6120
6343
|
* Both are server-side, like every other list filter.
|
|
6121
6344
|
*/
|
|
6122
6345
|
const listJourneysQuerySchema = paginationQuerySchema.extend({
|
|
6346
|
+
/** On or off in the environment the call selected. */
|
|
6123
6347
|
enabled: z.enum(["true", "false"]).optional(),
|
|
6124
6348
|
q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0),
|
|
6125
6349
|
tag: z.string().trim().max(50, "tag must be at most 50 characters").optional()
|
|
@@ -6171,7 +6395,21 @@ const userJourneySchema = z.object({
|
|
|
6171
6395
|
});
|
|
6172
6396
|
const listUserJourneysQuerySchema = paginationQuerySchema;
|
|
6173
6397
|
/**
|
|
6174
|
-
*
|
|
6398
|
+
* Which journeys the flag acts on: the keys the caller named, or every
|
|
6399
|
+
* journey of one project. Exactly one of the two, because a call carrying
|
|
6400
|
+
* both would have to decide which it meant.
|
|
6401
|
+
*
|
|
6402
|
+
* A list rather than one key per call so every surface can do what `cow
|
|
6403
|
+
* enable` does: name several keys, or `--all` (which is `project`, resolved
|
|
6404
|
+
* server-side, so an agent turning a project on is one call too).
|
|
6405
|
+
*/
|
|
6406
|
+
const setJourneysEnabledBodySchema = z.object({ data: z.object({
|
|
6407
|
+
keys: z.array(journeyKeySchema).min(1).max(PUSH_LIMITS.journeys).optional(),
|
|
6408
|
+
/** Every journey of this project, by the name `cow.json` carries. */
|
|
6409
|
+
project: projectNameSchema.optional()
|
|
6410
|
+
}).refine((data) => data.keys === void 0 !== (data.project === void 0), { message: "name keys or a project, not both" }) });
|
|
6411
|
+
/**
|
|
6412
|
+
* Dry run: one real profile, the journey's latest ready version, and every
|
|
6175
6413
|
* send gated into a `would_*` row instead of a message. The body is the v1
|
|
6176
6414
|
* one; the response is the started execution, because executions are rows
|
|
6177
6415
|
* now and a v1 instance was a Temporal read.
|
|
@@ -6179,56 +6417,13 @@ const listUserJourneysQuerySchema = paginationQuerySchema;
|
|
|
6179
6417
|
const dryRunJourneyBodySchema = z.object({ data: z.object({ profileId: z.string().min(1) }) });
|
|
6180
6418
|
|
|
6181
6419
|
//#endregion
|
|
6182
|
-
//#region ../../packages/shared/src/journeys-v2/
|
|
6420
|
+
//#region ../../packages/shared/src/journeys-v2/sandbox.ts
|
|
6183
6421
|
/**
|
|
6184
|
-
*
|
|
6185
|
-
*
|
|
6186
|
-
*
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
/**
|
|
6190
|
-
* `cow.json` (spec: Project layout): the org, and which of its projects this
|
|
6191
|
-
* directory is. The environment is always a flag, and auth never lives in
|
|
6192
|
-
* the project. Strict, so a typo'd key is a build error rather than a
|
|
6193
|
-
* silently ignored setting.
|
|
6194
|
-
*/
|
|
6195
|
-
const cowConfigSchema = z.strictObject({
|
|
6196
|
-
$schema: z.url().optional(),
|
|
6197
|
-
orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
|
|
6198
|
-
/**
|
|
6199
|
-
* Which project of the org this directory pushes to. One org can hold
|
|
6200
|
-
* several, one per repo, each with its own release sequence and its own
|
|
6201
|
-
* slice of the deployed journeys, so every project says which it is.
|
|
6202
|
-
*/
|
|
6203
|
-
project: projectNameSchema,
|
|
6204
|
-
/**
|
|
6205
|
-
* The consent purposes this project declares. They are org-wide, so two
|
|
6206
|
-
* projects declaring one key must agree on its label and default or the
|
|
6207
|
-
* deploy is refused; a deploy adds and updates them and never deletes
|
|
6208
|
-
* one, because profiles hold answers against them.
|
|
6209
|
-
*/
|
|
6210
|
-
purposes: purposesSchema.optional(),
|
|
6211
|
-
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
6212
|
-
apiUrl: z.url().optional(),
|
|
6213
|
-
/**
|
|
6214
|
-
* Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
|
|
6215
|
-
* login against one Cowliss's dashboard yields a token the other's API
|
|
6216
|
-
* rejects, so a config that names an API names its dashboard too.
|
|
6217
|
-
*/
|
|
6218
|
-
webUrl: z.url().optional()
|
|
6219
|
-
}).meta({
|
|
6220
|
-
title: "cow.json",
|
|
6221
|
-
description: "A cow project: the organization and the project it deploys to."
|
|
6222
|
-
});
|
|
6223
|
-
|
|
6224
|
-
//#endregion
|
|
6225
|
-
//#region ../../packages/shared/src/journeys-v2/sandbox.ts
|
|
6226
|
-
/**
|
|
6227
|
-
* What the sandbox worker's activities return (spec: Sandbox worker). A
|
|
6228
|
-
* guest failure is a value, not a throw: the runner has to journal it and
|
|
6229
|
-
* record it on the execution, and a Temporal retry would only reproduce it.
|
|
6230
|
-
* Infrastructure failures (a missing binary, a spawn error, Postgres down)
|
|
6231
|
-
* throw and retry on the activity's bounded policy.
|
|
6422
|
+
* What the sandbox worker's activities return (spec: Sandbox worker). A
|
|
6423
|
+
* guest failure is a value, not a throw: the runner has to journal it and
|
|
6424
|
+
* record it on the execution, and a Temporal retry would only reproduce it.
|
|
6425
|
+
* Infrastructure failures (a missing binary, a spawn error, Postgres down)
|
|
6426
|
+
* throw and retry on the activity's bounded policy.
|
|
6232
6427
|
*/
|
|
6233
6428
|
/** Every way a guest invocation can fail, as the runner records it. */
|
|
6234
6429
|
const SANDBOX_FAILURE_CODES = [
|
|
@@ -6320,115 +6515,6 @@ const notificationPreferencesSchema = z.object({ purposes: z.array(notificationP
|
|
|
6320
6515
|
*/
|
|
6321
6516
|
const updateNotificationPreferencesBodySchema = z.object({ data: z.object({ purposes: consentPatchSchema }) });
|
|
6322
6517
|
|
|
6323
|
-
//#endregion
|
|
6324
|
-
//#region ../../packages/shared/src/releases.ts
|
|
6325
|
-
/**
|
|
6326
|
-
* The release lifecycle's wire contracts (spec: Releases, deployments,
|
|
6327
|
-
* artifacts): the project a developer runs `cow init` in, the artifacts
|
|
6328
|
-
* `cow push` uploads, the releases it creates, and the deployments that make
|
|
6329
|
-
* one of them the environment's current code.
|
|
6330
|
-
*
|
|
6331
|
-
* Row-derived through drizzle-zod where a row is what the wire carries, so
|
|
6332
|
-
* the DTO cannot drift from the column set; the jsonb manifest gets its
|
|
6333
|
-
* explicit shared schema, because the column type is opaque to drizzle-zod.
|
|
6334
|
-
*/
|
|
6335
|
-
const projectSchema = selectProjectSchema.extend({
|
|
6336
|
-
createdAt: z.iso.datetime(),
|
|
6337
|
-
updatedAt: z.iso.datetime()
|
|
6338
|
-
});
|
|
6339
|
-
/** `cow init` names the project after the folder it created. */
|
|
6340
|
-
const createProjectBodySchema = z.object({ data: z.object({ name: projectNameSchema }) });
|
|
6341
|
-
/** Which project to read: the one the caller's `cow.json` names. */
|
|
6342
|
-
const getProjectQuerySchema = z.object({ name: projectNameSchema });
|
|
6343
|
-
const artifactDigestParams = z.object({ digest: digestSchema });
|
|
6344
|
-
/**
|
|
6345
|
-
* Which kind of artifact the uploaded bytes are. Named by the client rather
|
|
6346
|
-
* than sniffed, because only the build knows whether a blob is a journey
|
|
6347
|
-
* bundle or the source tarball, and the value is metadata for the prune
|
|
6348
|
-
* sweep and the sandbox's cache rather than a trust boundary: what a module
|
|
6349
|
-
* is, is decided by the server-side compile step. `module` is absent on
|
|
6350
|
-
* purpose, since only that compile step ever writes one.
|
|
6351
|
-
*/
|
|
6352
|
-
const putArtifactQuerySchema = z.object({ kind: z.enum(["bundle", "source"]) });
|
|
6353
|
-
/**
|
|
6354
|
-
* A release as the API reports it. The manifest is the pushed one while the
|
|
6355
|
-
* release is `pending` or `failed`, and the compiled one (module digests per
|
|
6356
|
-
* key, plus the engine plugin digest) once it is `ready`; the union says so
|
|
6357
|
-
* rather than making the compiled fields optional on one shape.
|
|
6358
|
-
*
|
|
6359
|
-
* `deployedIn` is not a column: it is the environments whose latest
|
|
6360
|
-
* deployment names this release, computed on read, so it can never fall out
|
|
6361
|
-
* of sync with the append-only deployment log. Neither is `totalEntries`:
|
|
6362
|
-
* the manifest is already here and its journeys plus its templates are the
|
|
6363
|
-
* denominator, so storing it beside `compiledEntries` would only invite the
|
|
6364
|
-
* two to disagree.
|
|
6365
|
-
*/
|
|
6366
|
-
const releaseSchema = selectReleaseSchema.extend({
|
|
6367
|
-
manifest: z.union([compiledManifestSchema, manifestSchema]),
|
|
6368
|
-
deployedIn: z.array(environmentSchema),
|
|
6369
|
-
/**
|
|
6370
|
-
* How many journeys and templates the manifest holds: the denominator
|
|
6371
|
-
* `compiledEntries` climbs towards, counted on read rather than stored.
|
|
6372
|
-
*/
|
|
6373
|
-
totalEntries: z.number().int(),
|
|
6374
|
-
createdAt: z.iso.datetime(),
|
|
6375
|
-
compiledAt: z.iso.datetime().nullable()
|
|
6376
|
-
});
|
|
6377
|
-
const createReleaseBodySchema = z.object({ data: z.object({
|
|
6378
|
-
/**
|
|
6379
|
-
* The one place the protocol is pinned to the constant. The stored shape
|
|
6380
|
-
* takes any positive integer, because a release outlives a bump; a push
|
|
6381
|
-
* is a live CLI talking to a live platform, so a mismatch here is a 422
|
|
6382
|
-
* the developer fixes by updating `@cowliss/cli`, and nothing is stored.
|
|
6383
|
-
*/
|
|
6384
|
-
manifest: manifestSchema.safeExtend({ protocol: z.literal(2) }),
|
|
6385
|
-
/** The `cow.json` project this push belongs to. */
|
|
6386
|
-
project: projectNameSchema
|
|
6387
|
-
}) });
|
|
6388
|
-
const listReleasesQuerySchema = paginationQuerySchema.extend({
|
|
6389
|
-
/** One project's own releases; absent lists the org's. */
|
|
6390
|
-
project: projectNameSchema.optional() });
|
|
6391
|
-
/**
|
|
6392
|
-
* How many live executions the cancel sweep was asked to stop, and the
|
|
6393
|
-
* workflow doing the stopping. The count is taken when the request lands, so
|
|
6394
|
-
* it is the size of the job rather than a result: the sweep is asynchronous
|
|
6395
|
-
* and the runner records `cancelled` as each execution reaches its next
|
|
6396
|
-
* step. A second call while a sweep runs answers with that sweep's id.
|
|
6397
|
-
*/
|
|
6398
|
-
const cancelExecutionsSchema = z.object({
|
|
6399
|
-
releaseId: z.string(),
|
|
6400
|
-
environment: environmentSchema,
|
|
6401
|
-
requested: z.number().int(),
|
|
6402
|
-
workflowId: z.string()
|
|
6403
|
-
});
|
|
6404
|
-
/**
|
|
6405
|
-
* A deployment, plus the warnings the deploy raised: segment names in
|
|
6406
|
-
* triggers and destination names in spines that do not exist in the target
|
|
6407
|
-
* environment. They never block, because either may be created afterwards.
|
|
6408
|
-
* `left` is the other half of the same conversation: what the deploy did not
|
|
6409
|
-
* touch, because a deploy only ever adds and updates (ADR 0009).
|
|
6410
|
-
*/
|
|
6411
|
-
const deploymentSchema = selectDeploymentSchema.extend({
|
|
6412
|
-
createdAt: z.iso.datetime(),
|
|
6413
|
-
warnings: z.array(z.string()),
|
|
6414
|
-
/**
|
|
6415
|
-
* Journeys this project still runs in the environment that the deployed
|
|
6416
|
-
* release does not carry. A deploy never removes one, so these keep running
|
|
6417
|
-
* until someone deletes them; the CLI prints them and says how.
|
|
6418
|
-
*/
|
|
6419
|
-
left: z.array(z.string()),
|
|
6420
|
-
/** The deployed release's per-project sequence number, its human handle. */
|
|
6421
|
-
releaseSeq: z.number().int()
|
|
6422
|
-
});
|
|
6423
|
-
const createDeploymentBodySchema = z.object({ data: z.object({ releaseId: z.string().min(1) }) });
|
|
6424
|
-
const listDeploymentsQuerySchema = paginationQuerySchema.extend({
|
|
6425
|
-
environment: environmentSchema.optional(),
|
|
6426
|
-
/** One release's own deployment history, across both environments. */
|
|
6427
|
-
release: z.string().trim().min(1).max(200).optional(),
|
|
6428
|
-
/** One project's own deployments; absent lists the org's. */
|
|
6429
|
-
project: projectNameSchema.optional()
|
|
6430
|
-
});
|
|
6431
|
-
|
|
6432
6518
|
//#endregion
|
|
6433
6519
|
//#region ../../packages/shared/src/violations.ts
|
|
6434
6520
|
/**
|
|
@@ -6807,6 +6893,54 @@ const suppressedAddressSchema = selectSuppressedAddressSchema.extend({
|
|
|
6807
6893
|
*/
|
|
6808
6894
|
const listSuppressionsQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
|
|
6809
6895
|
|
|
6896
|
+
//#endregion
|
|
6897
|
+
//#region ../../packages/shared/src/templates.ts
|
|
6898
|
+
/**
|
|
6899
|
+
* Templates as the API reports them. A template has versions and nothing
|
|
6900
|
+
* else (ADR 0011): no row of its own, no environment, no flag. What is
|
|
6901
|
+
* reported here is one key, described by its newest version, plus the
|
|
6902
|
+
* journeys whose steps send it.
|
|
6903
|
+
*
|
|
6904
|
+
* The descriptive fields come from that version's manifest entry rather than
|
|
6905
|
+
* from a column, which is what keeps them true for a key whose latest push
|
|
6906
|
+
* is still compiling: the entry describes the code that was pushed, and the
|
|
6907
|
+
* send resolves the latest ready version on its own at send time.
|
|
6908
|
+
*/
|
|
6909
|
+
const templateSchema = z.object({
|
|
6910
|
+
key: journeyKeySchema,
|
|
6911
|
+
/** The project whose push owns this key (ADR 0009). */
|
|
6912
|
+
projectId: z.string(),
|
|
6913
|
+
/** The author's labels; the dashboard's only grouping. */
|
|
6914
|
+
tags: tagsSchema,
|
|
6915
|
+
sendClass: z.enum(SEND_CLASSES),
|
|
6916
|
+
/** True when the template asks for a signed `verifyUrl` prop at send time. */
|
|
6917
|
+
verifyLink: z.boolean(),
|
|
6918
|
+
/** JSON Schema of the template's props, as `cow build` converted them. */
|
|
6919
|
+
propsSchema: z.record(z.string(), z.unknown()),
|
|
6920
|
+
/** The newest version of this key, whatever it compiled to. */
|
|
6921
|
+
latestVersion: versionSummarySchema,
|
|
6922
|
+
/** Keys of the journeys whose steps send this template. */
|
|
6923
|
+
journeys: z.array(z.string())
|
|
6924
|
+
});
|
|
6925
|
+
/**
|
|
6926
|
+
* Template list query. `q` is a substring search over the key, which is the
|
|
6927
|
+
* name the author gave the file. Tags are not a filter here the way they are
|
|
6928
|
+
* on journeys: a template has no row, so its tags live inside the version's
|
|
6929
|
+
* manifest entry, and searching inside that is a query the database layer
|
|
6930
|
+
* would have to grow a helper for. The badges in each row still show them.
|
|
6931
|
+
*/
|
|
6932
|
+
const listTemplatesQuerySchema = paginationQuerySchema.extend({ q: z.string().trim().max(200, "q must be at most 200 characters").optional().transform((value) => value ? value : void 0) });
|
|
6933
|
+
/** The props a preview or a test send renders the template with. */
|
|
6934
|
+
const renderTemplateBodySchema = z.object({ data: z.object({ props: z.record(z.string(), z.unknown()).default({}) }) });
|
|
6935
|
+
/** What the sandbox rendered: the message as it would go out. */
|
|
6936
|
+
const templatePreviewSchema = z.object({
|
|
6937
|
+
subject: z.string(),
|
|
6938
|
+
html: z.string(),
|
|
6939
|
+
text: z.string(),
|
|
6940
|
+
/** The version that rendered it: the latest one that finished compiling. */
|
|
6941
|
+
versionId: z.string()
|
|
6942
|
+
});
|
|
6943
|
+
|
|
6810
6944
|
//#endregion
|
|
6811
6945
|
//#region ../../packages/shared/src/users.ts
|
|
6812
6946
|
/**
|
|
@@ -7055,28 +7189,11 @@ async function clearCredentials$1(path = defaultCredentialsPath()) {
|
|
|
7055
7189
|
}
|
|
7056
7190
|
}
|
|
7057
7191
|
|
|
7058
|
-
//#endregion
|
|
7059
|
-
//#region ../../packages/shared/src/stable-json.ts
|
|
7060
|
-
/**
|
|
7061
|
-
* A key-sorted JSON rendering, for comparing or hashing two values a JSON
|
|
7062
|
-
* round trip may have reordered. `undefined` members are dropped the way
|
|
7063
|
-
* `JSON.stringify` drops them.
|
|
7064
|
-
*
|
|
7065
|
-
* Its own module with no imports at all, because both users need it and
|
|
7066
|
-
* they sit on opposite sides of a boundary: the release digest (which pulls
|
|
7067
|
-
* in node:crypto) and the compile workflow (which may not pull in anything).
|
|
7068
|
-
*/
|
|
7069
|
-
function stableJson(value) {
|
|
7070
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
7071
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`).join(",")}}`;
|
|
7072
|
-
return JSON.stringify(value) ?? "null";
|
|
7073
|
-
}
|
|
7074
|
-
|
|
7075
7192
|
//#endregion
|
|
7076
7193
|
//#region ../../packages/shared/src/digest.ts
|
|
7077
7194
|
/**
|
|
7078
7195
|
* `sha256:` plus the lowercase hex digest of these exact bytes: how every
|
|
7079
|
-
* artifact
|
|
7196
|
+
* artifact a push uploads is named. `cow build` computes it over
|
|
7080
7197
|
* each bundle and the source tarball, and the API recomputes it over an
|
|
7081
7198
|
* upload's body before storing it, so the two must agree byte for byte.
|
|
7082
7199
|
*
|
|
@@ -7087,16 +7204,6 @@ function stableJson(value) {
|
|
|
7087
7204
|
function digestOf(bytes) {
|
|
7088
7205
|
return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
7089
7206
|
}
|
|
7090
|
-
/**
|
|
7091
|
-
* The digest of a release manifest, computed over a key-sorted rendering so
|
|
7092
|
-
* `cow push` and the API agree on the value whatever order the JSON arrived
|
|
7093
|
-
* in. It is what decides "this tree is already released": a disagreement
|
|
7094
|
-
* would silently turn the unchanged-tree skip off, which is why neither side
|
|
7095
|
-
* hashes `JSON.stringify(manifest)` directly.
|
|
7096
|
-
*/
|
|
7097
|
-
function manifestDigestOf(manifest) {
|
|
7098
|
-
return digestOf(stableJson(manifest));
|
|
7099
|
-
}
|
|
7100
7207
|
|
|
7101
7208
|
//#endregion
|
|
7102
7209
|
//#region src/lib/package.ts
|
|
@@ -7492,7 +7599,7 @@ const TYPES_FILE = join(".cow", "types.d.ts");
|
|
|
7492
7599
|
* release records the project as the repo declares it, not the local file
|
|
7493
7600
|
* whoever pushed happened to point at.
|
|
7494
7601
|
*/
|
|
7495
|
-
const CONFIG_FILES
|
|
7602
|
+
const CONFIG_FILES = [
|
|
7496
7603
|
"cow.json",
|
|
7497
7604
|
"package.json",
|
|
7498
7605
|
"tsconfig.json"
|
|
@@ -7553,7 +7660,7 @@ async function listFiles(dir) {
|
|
|
7553
7660
|
*/
|
|
7554
7661
|
async function projectSources(projectDir) {
|
|
7555
7662
|
return [
|
|
7556
|
-
...(await Promise.all(CONFIG_FILES
|
|
7663
|
+
...(await Promise.all(CONFIG_FILES.map(async (file) => await stat(join(projectDir, file)).then(() => file, () => null)))).filter((file) => file !== null),
|
|
7557
7664
|
...(await listFiles(join(projectDir, "journeys"))).map((file) => `journeys/${file}`),
|
|
7558
7665
|
...(await listFiles(join(projectDir, "emails"))).map((file) => `emails/${file}`)
|
|
7559
7666
|
].sort();
|
|
@@ -7779,12 +7886,12 @@ function emptyProfile(id = "", traits = {}, purposes = []) {
|
|
|
7779
7886
|
segments: []
|
|
7780
7887
|
};
|
|
7781
7888
|
}
|
|
7782
|
-
/** The
|
|
7889
|
+
/** The push limits (spec: Limits), naming the offending count or size. */
|
|
7783
7890
|
function limitFailure(sizes) {
|
|
7784
|
-
if (sizes.journeys >
|
|
7785
|
-
if (sizes.templates >
|
|
7786
|
-
for (const bundle of sizes.bundles) if (bundle.bytes >
|
|
7787
|
-
if (sizes.sourceBytes >
|
|
7891
|
+
if (sizes.journeys > PUSH_LIMITS.journeys) return `This project has ${sizes.journeys} journeys; a push carries at most ${PUSH_LIMITS.journeys}.`;
|
|
7892
|
+
if (sizes.templates > PUSH_LIMITS.templates) return `This project has ${sizes.templates} templates; a push carries at most ${PUSH_LIMITS.templates}.`;
|
|
7893
|
+
for (const bundle of sizes.bundles) if (bundle.bytes > PUSH_LIMITS.bundleBytes) return `Bundle "${bundle.key}" is ${bundle.bytes} bytes; a bundle is at most ${PUSH_LIMITS.bundleBytes}.`;
|
|
7894
|
+
if (sizes.sourceBytes > PUSH_LIMITS.sourceBytes) return `The source tarball is ${sizes.sourceBytes} bytes; a push carries at most ${PUSH_LIMITS.sourceBytes}.`;
|
|
7788
7895
|
return null;
|
|
7789
7896
|
}
|
|
7790
7897
|
async function writeSourceTarball(projectDir, outFile) {
|
|
@@ -7893,7 +8000,6 @@ async function buildProject(projectDir) {
|
|
|
7893
8000
|
trigger: report.trigger,
|
|
7894
8001
|
purpose: report.purpose,
|
|
7895
8002
|
senderIdentity: report.senderIdentity,
|
|
7896
|
-
environments: report.environments,
|
|
7897
8003
|
spine: readSpine(await readFile(built.source.file, "utf8")),
|
|
7898
8004
|
bundle: built.digest
|
|
7899
8005
|
});
|
|
@@ -7974,22 +8080,22 @@ function clearCredentials(env) {
|
|
|
7974
8080
|
}
|
|
7975
8081
|
/**
|
|
7976
8082
|
* The token every command sends, and where it came from. Precedence:
|
|
7977
|
-
* `
|
|
7978
|
-
* because a machine that has one is CI
|
|
7979
|
-
*
|
|
8083
|
+
* `COW_PIPELINE_KEY` > `COW_TOKEN` > the cached login. The pipeline key wins
|
|
8084
|
+
* because a machine that has one is CI, and a stale session left in `~/.cow`
|
|
8085
|
+
* on that machine must not quietly become the
|
|
7980
8086
|
* credential a pipeline runs as. `COW_TOKEN` is a session token handed over
|
|
7981
8087
|
* explicitly, so it reports as one and outranks the cached file.
|
|
7982
8088
|
*
|
|
7983
|
-
* A
|
|
7984
|
-
* a box that exports one answers 401 rather than falling back to the
|
|
8089
|
+
* A pipeline key only pushes, flips journeys and reads executions, so a
|
|
8090
|
+
* `cow users list` on a box that exports one answers 401 rather than falling back to the
|
|
7985
8091
|
* session. That is the honest failure: the two credentials are different
|
|
7986
8092
|
* identities, and silently switching between them per command is how a
|
|
7987
8093
|
* pipeline ends up passing locally and failing in CI.
|
|
7988
8094
|
*/
|
|
7989
8095
|
function resolveCredential(env, credentials) {
|
|
7990
|
-
if (env.
|
|
7991
|
-
token: env.
|
|
7992
|
-
kind: "
|
|
8096
|
+
if (env.COW_PIPELINE_KEY) return {
|
|
8097
|
+
token: env.COW_PIPELINE_KEY,
|
|
8098
|
+
kind: "pipelineKey"
|
|
7993
8099
|
};
|
|
7994
8100
|
if (env.COW_TOKEN) return {
|
|
7995
8101
|
token: env.COW_TOKEN,
|
|
@@ -8009,7 +8115,7 @@ function resolveCredential(env, credentials) {
|
|
|
8009
8115
|
* login) > default.
|
|
8010
8116
|
*
|
|
8011
8117
|
* The project's own `apiUrl` outranks the credentials file because it is the
|
|
8012
|
-
* more specific answer: `cow.json` says
|
|
8118
|
+
* more specific answer: `cow.json` says which API this project talks to, while the
|
|
8013
8119
|
* credentials only remember where somebody last logged in. Without it a
|
|
8014
8120
|
* checkout whose config names production silently fell through to
|
|
8015
8121
|
* `DEFAULT_API_URL`, which is how a production deploy reaches localhost.
|
|
@@ -8018,6 +8124,15 @@ function resolveApiUrl(env, credentials, flag, projectApiUrl) {
|
|
|
8018
8124
|
return flag ?? env.COW_API_URL ?? projectApiUrl ?? credentials?.apiUrl ?? "http://localhost:3400";
|
|
8019
8125
|
}
|
|
8020
8126
|
/**
|
|
8127
|
+
* The dashboard this project belongs to: the flag, the env var, the
|
|
8128
|
+
* project's own `cow.json`, then the default. Same precedence as the API URL
|
|
8129
|
+
* and for the same reason: a checkout pointed at production must not send
|
|
8130
|
+
* the developer (or a push's links) to localhost.
|
|
8131
|
+
*/
|
|
8132
|
+
function resolveWebUrl(env, projectWebUrl, flag) {
|
|
8133
|
+
return flag ?? env.COW_WEB_URL ?? projectWebUrl ?? "http://localhost:5273";
|
|
8134
|
+
}
|
|
8135
|
+
/**
|
|
8021
8136
|
* Decode a JWT payload without verification. Display only: the API is the
|
|
8022
8137
|
* verifier; the CLI just shows what it is about to send.
|
|
8023
8138
|
*/
|
|
@@ -8121,7 +8236,7 @@ async function login(env, options) {
|
|
|
8121
8236
|
if (options.token !== void 0) {
|
|
8122
8237
|
token = options.token.trim();
|
|
8123
8238
|
if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
|
|
8124
|
-
} else token = await collectTokenViaBrowser(
|
|
8239
|
+
} else token = await collectTokenViaBrowser(resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl, options.webUrl), 3e5, options.noOpen === true);
|
|
8125
8240
|
const claims = decodeSessionToken(token);
|
|
8126
8241
|
if (claims === null) throw new Error("Token is not a decodable JWT");
|
|
8127
8242
|
if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
|
|
@@ -8234,12 +8349,12 @@ function registerAuth(program, env, io) {
|
|
|
8234
8349
|
program.command("whoami").description("show which credential commands will use, and for a session its subject, org, role, and expiry").action(async (opts) => {
|
|
8235
8350
|
const credentials = await readCredentials(env);
|
|
8236
8351
|
const { kind } = resolveCredential(env, credentials);
|
|
8237
|
-
if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set
|
|
8238
|
-
if (kind === "
|
|
8352
|
+
if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set COW_PIPELINE_KEY.");
|
|
8353
|
+
if (kind === "pipelineKey") {
|
|
8239
8354
|
emit({ data: {
|
|
8240
8355
|
credential: kind,
|
|
8241
|
-
source: "
|
|
8242
|
-
note: "A
|
|
8356
|
+
source: "COW_PIPELINE_KEY",
|
|
8357
|
+
note: "A pipeline key may push, enable, disable, and read executions."
|
|
8243
8358
|
} }, io, opts.json);
|
|
8244
8359
|
return;
|
|
8245
8360
|
}
|
|
@@ -8652,10 +8767,16 @@ const ingestionErrors = errors("invalid_key", "validation_failed", "over_quota",
|
|
|
8652
8767
|
const SESSION_AUTH = [{ clerkSession: [] }];
|
|
8653
8768
|
const API_KEY_AUTH = [{ orgApiKey: [] }];
|
|
8654
8769
|
/**
|
|
8655
|
-
*
|
|
8656
|
-
*
|
|
8770
|
+
* What a pipeline does: push, enable, disable, and read executions. A
|
|
8771
|
+
* dashboard session, or an org pipeline key: two alternatives, so a caller
|
|
8772
|
+
* needs either one, never both.
|
|
8773
|
+
*
|
|
8774
|
+
* The gate itself is path-scoped in `apps/api/src/app.ts`, so it reaches
|
|
8775
|
+
* `/pushes/*` whole (a pipeline reads back what it pushed) and the two
|
|
8776
|
+
* journey flags by their exact paths; this block is what says so in the
|
|
8777
|
+
* OpenAPI document.
|
|
8657
8778
|
*/
|
|
8658
|
-
const
|
|
8779
|
+
const PIPELINE_AUTH = [{ clerkSession: [] }, { orgPipelineKey: [] }];
|
|
8659
8780
|
/** Public routes: their own signature or token check is the authorization. */
|
|
8660
8781
|
const NO_AUTH = [];
|
|
8661
8782
|
/** Operations kept off the CLI and MCP surfaces. */
|
|
@@ -8683,7 +8804,7 @@ function htmlResponse(description) {
|
|
|
8683
8804
|
|
|
8684
8805
|
//#endregion
|
|
8685
8806
|
//#region ../../packages/shared/src/contract/apps.ts
|
|
8686
|
-
const params$
|
|
8807
|
+
const params$11 = z.object({ id: z.string() });
|
|
8687
8808
|
/**
|
|
8688
8809
|
* An app is the attribution unit: what a developer names, what ingestion
|
|
8689
8810
|
* stamps, and what segments and journey triggers filter on. Its inbound
|
|
@@ -8722,7 +8843,7 @@ const apps = defineModule(defineRoute({
|
|
|
8722
8843
|
tags: ["apps"],
|
|
8723
8844
|
summary: "Fetch one app",
|
|
8724
8845
|
security: SESSION_AUTH,
|
|
8725
|
-
request: { params: params$
|
|
8846
|
+
request: { params: params$11 },
|
|
8726
8847
|
responses: {
|
|
8727
8848
|
200: envelope(appSchema),
|
|
8728
8849
|
...sessionErrors,
|
|
@@ -8736,7 +8857,7 @@ const apps = defineModule(defineRoute({
|
|
|
8736
8857
|
summary: "Rename or archive an app",
|
|
8737
8858
|
security: SESSION_AUTH,
|
|
8738
8859
|
request: {
|
|
8739
|
-
params: params$
|
|
8860
|
+
params: params$11,
|
|
8740
8861
|
body: jsonBody(updateAppBodySchema)
|
|
8741
8862
|
},
|
|
8742
8863
|
responses: {
|
|
@@ -8765,7 +8886,7 @@ const artifacts = defineModule(defineRoute({
|
|
|
8765
8886
|
operationId: "artifacts.head",
|
|
8766
8887
|
tags: ["artifacts"],
|
|
8767
8888
|
summary: "Does this organization already have this artifact?",
|
|
8768
|
-
security:
|
|
8889
|
+
security: PIPELINE_AUTH,
|
|
8769
8890
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
8770
8891
|
request: { params: artifactDigestParams },
|
|
8771
8892
|
responses: {
|
|
@@ -8779,7 +8900,7 @@ const artifacts = defineModule(defineRoute({
|
|
|
8779
8900
|
operationId: "artifacts.put",
|
|
8780
8901
|
tags: ["artifacts"],
|
|
8781
8902
|
summary: "Upload an artifact by its digest",
|
|
8782
|
-
security:
|
|
8903
|
+
security: PIPELINE_AUTH,
|
|
8783
8904
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
8784
8905
|
request: {
|
|
8785
8906
|
params: artifactDigestParams,
|
|
@@ -8852,7 +8973,7 @@ const billing = defineModule(defineRoute({
|
|
|
8852
8973
|
|
|
8853
8974
|
//#endregion
|
|
8854
8975
|
//#region ../../packages/shared/src/contract/catalog.ts
|
|
8855
|
-
const params$
|
|
8976
|
+
const params$10 = z.object({ name: z.string() });
|
|
8856
8977
|
const writeErrors$1 = errors("validation_failed", "malformed_request");
|
|
8857
8978
|
/** The tracking plan: entries are addressed by name, unique per org. */
|
|
8858
8979
|
const catalog = defineModule(defineRoute({
|
|
@@ -8889,7 +9010,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8889
9010
|
tags: ["catalog"],
|
|
8890
9011
|
summary: "Fetch one catalog event",
|
|
8891
9012
|
security: SESSION_AUTH,
|
|
8892
|
-
request: { params: params$
|
|
9013
|
+
request: { params: params$10 },
|
|
8893
9014
|
responses: {
|
|
8894
9015
|
200: envelope(catalogEventSchema),
|
|
8895
9016
|
...sessionErrors,
|
|
@@ -8903,7 +9024,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8903
9024
|
summary: "Update a catalog event",
|
|
8904
9025
|
security: SESSION_AUTH,
|
|
8905
9026
|
request: {
|
|
8906
|
-
params: params$
|
|
9027
|
+
params: params$10,
|
|
8907
9028
|
body: jsonBody(updateCatalogEventBodySchema)
|
|
8908
9029
|
},
|
|
8909
9030
|
responses: {
|
|
@@ -8920,7 +9041,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8920
9041
|
summary: "Allow or deny an event name",
|
|
8921
9042
|
security: SESSION_AUTH,
|
|
8922
9043
|
request: {
|
|
8923
|
-
params: params$
|
|
9044
|
+
params: params$10,
|
|
8924
9045
|
body: jsonBody(setGovernanceStateBodySchema)
|
|
8925
9046
|
},
|
|
8926
9047
|
responses: {
|
|
@@ -8962,7 +9083,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8962
9083
|
tags: ["catalog"],
|
|
8963
9084
|
summary: "Fetch one catalog trait",
|
|
8964
9085
|
security: SESSION_AUTH,
|
|
8965
|
-
request: { params: params$
|
|
9086
|
+
request: { params: params$10 },
|
|
8966
9087
|
responses: {
|
|
8967
9088
|
200: envelope(catalogTraitSchema),
|
|
8968
9089
|
...sessionErrors,
|
|
@@ -8976,7 +9097,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8976
9097
|
summary: "Update a catalog trait",
|
|
8977
9098
|
security: SESSION_AUTH,
|
|
8978
9099
|
request: {
|
|
8979
|
-
params: params$
|
|
9100
|
+
params: params$10,
|
|
8980
9101
|
body: jsonBody(updateCatalogTraitBodySchema)
|
|
8981
9102
|
},
|
|
8982
9103
|
responses: {
|
|
@@ -8993,7 +9114,7 @@ const catalog = defineModule(defineRoute({
|
|
|
8993
9114
|
summary: "Allow or deny a trait name",
|
|
8994
9115
|
security: SESSION_AUTH,
|
|
8995
9116
|
request: {
|
|
8996
|
-
params: params$
|
|
9117
|
+
params: params$10,
|
|
8997
9118
|
body: jsonBody(setGovernanceStateBodySchema)
|
|
8998
9119
|
},
|
|
8999
9120
|
responses: {
|
|
@@ -9064,9 +9185,8 @@ const consent = defineModule(defineRoute({
|
|
|
9064
9185
|
* one-click unsubscribe. Those answer the caller's convention, not the
|
|
9065
9186
|
* envelope.
|
|
9066
9187
|
*
|
|
9067
|
-
* The two reads
|
|
9068
|
-
*
|
|
9069
|
-
* second to see what the deploy sent is not one loop.
|
|
9188
|
+
* The two reads are dashboard reads: `cow dev` and its pipeline-key tail are
|
|
9189
|
+
* gone (ADR 0011), so a pipeline key has no business in the delivery log.
|
|
9070
9190
|
*/
|
|
9071
9191
|
const deliveries = defineModule(defineRoute({
|
|
9072
9192
|
method: "get",
|
|
@@ -9074,7 +9194,7 @@ const deliveries = defineModule(defineRoute({
|
|
|
9074
9194
|
operationId: "deliveries.list",
|
|
9075
9195
|
tags: ["deliveries"],
|
|
9076
9196
|
summary: "List deliveries, newest first",
|
|
9077
|
-
security:
|
|
9197
|
+
security: SESSION_AUTH,
|
|
9078
9198
|
request: { query: listDeliveriesQuerySchema },
|
|
9079
9199
|
responses: {
|
|
9080
9200
|
200: list(deliverySchema),
|
|
@@ -9087,7 +9207,7 @@ const deliveries = defineModule(defineRoute({
|
|
|
9087
9207
|
operationId: "deliveries.get",
|
|
9088
9208
|
tags: ["deliveries"],
|
|
9089
9209
|
summary: "Fetch one delivery with its payload",
|
|
9090
|
-
security:
|
|
9210
|
+
security: SESSION_AUTH,
|
|
9091
9211
|
request: { params: z.object({ id: z.string() }) },
|
|
9092
9212
|
responses: {
|
|
9093
9213
|
200: envelope(deliveryDetailSchema),
|
|
@@ -9144,48 +9264,9 @@ all: z.literal("1").optional() }) },
|
|
|
9144
9264
|
}
|
|
9145
9265
|
}));
|
|
9146
9266
|
|
|
9147
|
-
//#endregion
|
|
9148
|
-
//#region ../../packages/shared/src/contract/deployments.ts
|
|
9149
|
-
/**
|
|
9150
|
-
* Deployments: which release an environment currently runs. Append-only, so
|
|
9151
|
-
* the list is the whole history and there is no rollback endpoint: rolling
|
|
9152
|
-
* back is deploying the release before the current one, which `cow rollback`
|
|
9153
|
-
* works out from the last two rows.
|
|
9154
|
-
*
|
|
9155
|
-
* The target environment comes from the selection header, like every other
|
|
9156
|
-
* environment-scoped operation, rather than from the body.
|
|
9157
|
-
*/
|
|
9158
|
-
const deployments = defineModule(defineRoute({
|
|
9159
|
-
method: "get",
|
|
9160
|
-
path: "/v1/deployments",
|
|
9161
|
-
operationId: "deployments.list",
|
|
9162
|
-
tags: ["deployments"],
|
|
9163
|
-
summary: "List deployments, newest first",
|
|
9164
|
-
security: RELEASE_AUTH,
|
|
9165
|
-
request: { query: listDeploymentsQuerySchema },
|
|
9166
|
-
responses: {
|
|
9167
|
-
200: list(deploymentSchema),
|
|
9168
|
-
...sessionErrors,
|
|
9169
|
-
...errors("validation_failed")
|
|
9170
|
-
}
|
|
9171
|
-
}), defineRoute({
|
|
9172
|
-
method: "post",
|
|
9173
|
-
path: "/v1/deployments",
|
|
9174
|
-
operationId: "deployments.create",
|
|
9175
|
-
tags: ["deployments"],
|
|
9176
|
-
summary: "Deploy a release to the selected environment",
|
|
9177
|
-
security: RELEASE_AUTH,
|
|
9178
|
-
request: { body: jsonBody(createDeploymentBodySchema) },
|
|
9179
|
-
responses: {
|
|
9180
|
-
201: envelope(deploymentSchema, "The deployment, with any warnings"),
|
|
9181
|
-
...sessionErrors,
|
|
9182
|
-
...errors("not_found", "release_not_ready", "validation_failed", "malformed_request")
|
|
9183
|
-
}
|
|
9184
|
-
}));
|
|
9185
|
-
|
|
9186
9267
|
//#endregion
|
|
9187
9268
|
//#region ../../packages/shared/src/contract/destinations.ts
|
|
9188
|
-
const params$
|
|
9269
|
+
const params$9 = z.object({ id: z.string() });
|
|
9189
9270
|
const destinations = defineModule(defineRoute({
|
|
9190
9271
|
method: "post",
|
|
9191
9272
|
path: "/v1/destinations",
|
|
@@ -9219,7 +9300,7 @@ const destinations = defineModule(defineRoute({
|
|
|
9219
9300
|
tags: ["destinations"],
|
|
9220
9301
|
summary: "Fetch one destination",
|
|
9221
9302
|
security: SESSION_AUTH,
|
|
9222
|
-
request: { params: params$
|
|
9303
|
+
request: { params: params$9 },
|
|
9223
9304
|
responses: {
|
|
9224
9305
|
200: envelope(destinationSchema),
|
|
9225
9306
|
...sessionErrors,
|
|
@@ -9233,7 +9314,7 @@ const destinations = defineModule(defineRoute({
|
|
|
9233
9314
|
summary: "Update a destination",
|
|
9234
9315
|
security: SESSION_AUTH,
|
|
9235
9316
|
request: {
|
|
9236
|
-
params: params$
|
|
9317
|
+
params: params$9,
|
|
9237
9318
|
body: jsonBody(updateDestinationBodySchema)
|
|
9238
9319
|
},
|
|
9239
9320
|
responses: {
|
|
@@ -9248,7 +9329,7 @@ const destinations = defineModule(defineRoute({
|
|
|
9248
9329
|
tags: ["destinations"],
|
|
9249
9330
|
summary: "Delete a destination",
|
|
9250
9331
|
security: SESSION_AUTH,
|
|
9251
|
-
request: { params: params$
|
|
9332
|
+
request: { params: params$9 },
|
|
9252
9333
|
responses: {
|
|
9253
9334
|
200: envelope(deletedSchema),
|
|
9254
9335
|
...sessionErrors,
|
|
@@ -9258,7 +9339,7 @@ const destinations = defineModule(defineRoute({
|
|
|
9258
9339
|
|
|
9259
9340
|
//#endregion
|
|
9260
9341
|
//#region ../../packages/shared/src/contract/domains.ts
|
|
9261
|
-
const params$
|
|
9342
|
+
const params$8 = z.object({ id: z.string() });
|
|
9262
9343
|
const domains = defineModule(defineRoute({
|
|
9263
9344
|
method: "get",
|
|
9264
9345
|
path: "/v1/domains",
|
|
@@ -9279,7 +9360,7 @@ const domains = defineModule(defineRoute({
|
|
|
9279
9360
|
tags: ["domains"],
|
|
9280
9361
|
summary: "Fetch one sending domain",
|
|
9281
9362
|
security: SESSION_AUTH,
|
|
9282
|
-
request: { params: params$
|
|
9363
|
+
request: { params: params$8 },
|
|
9283
9364
|
responses: {
|
|
9284
9365
|
200: envelope(senderDomainSchema),
|
|
9285
9366
|
...sessionErrors,
|
|
@@ -9307,7 +9388,7 @@ const domains = defineModule(defineRoute({
|
|
|
9307
9388
|
description: "The Domain Connect link for this domain's DNS provider, which shows the admin the records and asks them to confirm. `url` is null when the provider does not support it.",
|
|
9308
9389
|
security: SESSION_AUTH,
|
|
9309
9390
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
9310
|
-
request: { params: params$
|
|
9391
|
+
request: { params: params$8 },
|
|
9311
9392
|
responses: {
|
|
9312
9393
|
200: envelope(domainDnsSetupSchema),
|
|
9313
9394
|
...sessionErrors,
|
|
@@ -9320,7 +9401,7 @@ const domains = defineModule(defineRoute({
|
|
|
9320
9401
|
tags: ["domains"],
|
|
9321
9402
|
summary: "Re-check DNS now",
|
|
9322
9403
|
security: SESSION_AUTH,
|
|
9323
|
-
request: { params: params$
|
|
9404
|
+
request: { params: params$8 },
|
|
9324
9405
|
responses: {
|
|
9325
9406
|
200: envelope(senderDomainSchema),
|
|
9326
9407
|
...sessionErrors,
|
|
@@ -9334,7 +9415,7 @@ const domains = defineModule(defineRoute({
|
|
|
9334
9415
|
summary: "Toggle click tracking",
|
|
9335
9416
|
security: SESSION_AUTH,
|
|
9336
9417
|
request: {
|
|
9337
|
-
params: params$
|
|
9418
|
+
params: params$8,
|
|
9338
9419
|
body: jsonBody(updateSenderDomainBodySchema)
|
|
9339
9420
|
},
|
|
9340
9421
|
responses: {
|
|
@@ -9347,9 +9428,9 @@ const domains = defineModule(defineRoute({
|
|
|
9347
9428
|
path: "/v1/domains/{id}",
|
|
9348
9429
|
operationId: "domains.delete",
|
|
9349
9430
|
tags: ["domains"],
|
|
9350
|
-
summary: "
|
|
9431
|
+
summary: "Give up a domain claim",
|
|
9351
9432
|
security: SESSION_AUTH,
|
|
9352
|
-
request: { params: params$
|
|
9433
|
+
request: { params: params$8 },
|
|
9353
9434
|
responses: {
|
|
9354
9435
|
200: envelope(deletedSchema),
|
|
9355
9436
|
...sessionErrors,
|
|
@@ -9447,12 +9528,19 @@ const events = defineModule(defineRoute({
|
|
|
9447
9528
|
|
|
9448
9529
|
//#endregion
|
|
9449
9530
|
//#region ../../packages/shared/src/contract/executions.ts
|
|
9450
|
-
const params$
|
|
9531
|
+
const params$7 = z.object({ id: z.string() });
|
|
9451
9532
|
/**
|
|
9452
9533
|
* Executions: one run of one journey for one profile. Replaces v1's journey
|
|
9453
9534
|
* instances, which were read live from Temporal and so could only be listed
|
|
9454
|
-
* per journey; these are rows, so they filter by journey,
|
|
9535
|
+
* per journey; these are rows, so they filter by journey, version, status,
|
|
9455
9536
|
* and profile in one place. The environment comes from the selection header.
|
|
9537
|
+
*
|
|
9538
|
+
* `cancel` is the only write: stopping is explicit and per environment
|
|
9539
|
+
* (ADR 0011), and each execution records `cancelled` when it reaches its
|
|
9540
|
+
* next step, so the answer is that the sweep started rather than a count.
|
|
9541
|
+
* A pipeline may push, enable, disable and READ executions; stopping runs
|
|
9542
|
+
* that are already carrying real people is a person's call, so cancel takes
|
|
9543
|
+
* a dashboard session (and org:admin in production).
|
|
9456
9544
|
*/
|
|
9457
9545
|
const executions = defineModule(defineRoute({
|
|
9458
9546
|
method: "get",
|
|
@@ -9460,21 +9548,34 @@ const executions = defineModule(defineRoute({
|
|
|
9460
9548
|
operationId: "executions.list",
|
|
9461
9549
|
tags: ["executions"],
|
|
9462
9550
|
summary: "List executions, newest first",
|
|
9463
|
-
security:
|
|
9551
|
+
security: PIPELINE_AUTH,
|
|
9464
9552
|
request: { query: listExecutionsQuerySchema },
|
|
9465
9553
|
responses: {
|
|
9466
9554
|
200: list(executionSchema),
|
|
9467
9555
|
...sessionErrors,
|
|
9468
9556
|
...errors("validation_failed")
|
|
9469
9557
|
}
|
|
9558
|
+
}), defineRoute({
|
|
9559
|
+
method: "post",
|
|
9560
|
+
path: "/v1/executions/cancel",
|
|
9561
|
+
operationId: "executions.cancel",
|
|
9562
|
+
tags: ["executions"],
|
|
9563
|
+
summary: "Cancel live executions of a journey, or of one version",
|
|
9564
|
+
security: SESSION_AUTH,
|
|
9565
|
+
request: { body: jsonBody(cancelExecutionsBodySchema) },
|
|
9566
|
+
responses: {
|
|
9567
|
+
200: envelope(cancellingSchema, "The sweep is under way; each execution stops at its next step"),
|
|
9568
|
+
...sessionErrors,
|
|
9569
|
+
...errors("not_found", "validation_failed", "malformed_request")
|
|
9570
|
+
}
|
|
9470
9571
|
}), defineRoute({
|
|
9471
9572
|
method: "get",
|
|
9472
9573
|
path: "/v1/executions/{id}",
|
|
9473
9574
|
operationId: "executions.get",
|
|
9474
9575
|
tags: ["executions"],
|
|
9475
9576
|
summary: "Inspect one execution, with its logs",
|
|
9476
|
-
security:
|
|
9477
|
-
request: { params: params$
|
|
9577
|
+
security: PIPELINE_AUTH,
|
|
9578
|
+
request: { params: params$7 },
|
|
9478
9579
|
responses: {
|
|
9479
9580
|
200: envelope(executionDetailSchema),
|
|
9480
9581
|
...sessionErrors,
|
|
@@ -9583,22 +9684,26 @@ const ingestion = defineModule(defineRoute({
|
|
|
9583
9684
|
//#region ../../packages/shared/src/contract/journeys.ts
|
|
9584
9685
|
/**
|
|
9585
9686
|
* A journey is addressed by its key: the basename of the file its author
|
|
9586
|
-
* wrote, unique across the
|
|
9587
|
-
*
|
|
9588
|
-
*
|
|
9589
|
-
*
|
|
9687
|
+
* wrote, unique across the org. The `jrn_` id it used to carry is retired,
|
|
9688
|
+
* because the rows are derived from a push rather than created through the
|
|
9689
|
+
* API, and (org, key) is the only identity they have.
|
|
9690
|
+
*
|
|
9691
|
+
* `list` and `get` report the enabled flag of BOTH environments whichever
|
|
9692
|
+
* one the caller selected, because "is this on in production?" is the
|
|
9693
|
+
* question they exist to answer; `enable` and `disable` act on the selected
|
|
9694
|
+
* one, like every other environment-scoped write (docs/standards/api.md).
|
|
9590
9695
|
*
|
|
9591
9696
|
* `journeys.listInstances` and `journeys.getInstance` are gone: executions
|
|
9592
9697
|
* are their own resource now (`executions.list` / `executions.get`), and
|
|
9593
9698
|
* `journeys.dryRun` answers with the execution it started.
|
|
9594
9699
|
*/
|
|
9595
|
-
const params$
|
|
9700
|
+
const params$6 = z.object({ key: z.string() });
|
|
9596
9701
|
const journeys = defineModule(defineRoute({
|
|
9597
9702
|
method: "get",
|
|
9598
9703
|
path: "/v1/journeys",
|
|
9599
9704
|
operationId: "journeys.list",
|
|
9600
9705
|
tags: ["journeys"],
|
|
9601
|
-
summary: "List the journeys
|
|
9706
|
+
summary: "List the organization's journeys",
|
|
9602
9707
|
security: SESSION_AUTH,
|
|
9603
9708
|
request: { query: listJourneysQuerySchema },
|
|
9604
9709
|
responses: {
|
|
@@ -9613,7 +9718,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9613
9718
|
tags: ["journeys"],
|
|
9614
9719
|
summary: "Inspect one journey",
|
|
9615
9720
|
security: SESSION_AUTH,
|
|
9616
|
-
request: { params: params$
|
|
9721
|
+
request: { params: params$6 },
|
|
9617
9722
|
responses: {
|
|
9618
9723
|
200: envelope(journeySchema),
|
|
9619
9724
|
...sessionErrors,
|
|
@@ -9626,36 +9731,48 @@ const journeys = defineModule(defineRoute({
|
|
|
9626
9731
|
tags: ["journeys"],
|
|
9627
9732
|
summary: "Journey operational stats",
|
|
9628
9733
|
security: SESSION_AUTH,
|
|
9629
|
-
request: { params: params$
|
|
9734
|
+
request: { params: params$6 },
|
|
9630
9735
|
responses: {
|
|
9631
9736
|
200: envelope(journeyStatsSchema),
|
|
9632
9737
|
...sessionErrors,
|
|
9633
9738
|
...errors("not_found")
|
|
9634
9739
|
}
|
|
9635
9740
|
}), defineRoute({
|
|
9636
|
-
method: "
|
|
9637
|
-
path: "/v1/journeys/
|
|
9638
|
-
operationId: "journeys.
|
|
9741
|
+
method: "post",
|
|
9742
|
+
path: "/v1/journeys/enable",
|
|
9743
|
+
operationId: "journeys.enable",
|
|
9639
9744
|
tags: ["journeys"],
|
|
9640
|
-
summary: "
|
|
9641
|
-
security:
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
body: jsonBody(updateJourneyBodySchema)
|
|
9645
|
-
},
|
|
9745
|
+
summary: "Turn journeys on in the selected environment",
|
|
9746
|
+
security: PIPELINE_AUTH,
|
|
9747
|
+
surfaces: { cli: false },
|
|
9748
|
+
request: { body: jsonBody(setJourneysEnabledBodySchema) },
|
|
9646
9749
|
responses: {
|
|
9647
|
-
200: envelope(journeySchema),
|
|
9750
|
+
200: envelope(z.array(journeySchema), "The journeys as they now stand"),
|
|
9648
9751
|
...sessionErrors,
|
|
9649
|
-
...errors("not_found", "validation_failed", "malformed_request")
|
|
9752
|
+
...errors("not_found", "project_missing", "validation_failed", "malformed_request")
|
|
9753
|
+
}
|
|
9754
|
+
}), defineRoute({
|
|
9755
|
+
method: "post",
|
|
9756
|
+
path: "/v1/journeys/disable",
|
|
9757
|
+
operationId: "journeys.disable",
|
|
9758
|
+
tags: ["journeys"],
|
|
9759
|
+
summary: "Turn journeys off in the selected environment",
|
|
9760
|
+
security: PIPELINE_AUTH,
|
|
9761
|
+
surfaces: { cli: false },
|
|
9762
|
+
request: { body: jsonBody(setJourneysEnabledBodySchema) },
|
|
9763
|
+
responses: {
|
|
9764
|
+
200: envelope(z.array(journeySchema), "The journeys as they now stand"),
|
|
9765
|
+
...sessionErrors,
|
|
9766
|
+
...errors("not_found", "project_missing", "validation_failed", "malformed_request")
|
|
9650
9767
|
}
|
|
9651
9768
|
}), defineRoute({
|
|
9652
9769
|
method: "delete",
|
|
9653
9770
|
path: "/v1/journeys/{key}",
|
|
9654
9771
|
operationId: "journeys.delete",
|
|
9655
9772
|
tags: ["journeys"],
|
|
9656
|
-
summary: "
|
|
9773
|
+
summary: "Delete a journey",
|
|
9657
9774
|
security: SESSION_AUTH,
|
|
9658
|
-
request: { params: params$
|
|
9775
|
+
request: { params: params$6 },
|
|
9659
9776
|
responses: {
|
|
9660
9777
|
200: envelope(deletedSchema),
|
|
9661
9778
|
...sessionErrors,
|
|
@@ -9669,7 +9786,7 @@ const journeys = defineModule(defineRoute({
|
|
|
9669
9786
|
summary: "Dry-run a journey against a real user",
|
|
9670
9787
|
security: SESSION_AUTH,
|
|
9671
9788
|
request: {
|
|
9672
|
-
params: params$
|
|
9789
|
+
params: params$6,
|
|
9673
9790
|
body: jsonBody(dryRunJourneyBodySchema)
|
|
9674
9791
|
},
|
|
9675
9792
|
responses: {
|
|
@@ -9741,20 +9858,37 @@ const project = defineModule(defineRoute({
|
|
|
9741
9858
|
operationId: "project.get",
|
|
9742
9859
|
tags: ["project"],
|
|
9743
9860
|
summary: "Get one of the organization's projects",
|
|
9744
|
-
security:
|
|
9861
|
+
security: PIPELINE_AUTH,
|
|
9745
9862
|
request: { query: getProjectQuerySchema },
|
|
9746
9863
|
responses: {
|
|
9747
9864
|
200: envelope(projectSchema),
|
|
9748
9865
|
...sessionErrors,
|
|
9749
9866
|
...errors("project_missing", "validation_failed", "malformed_request")
|
|
9750
9867
|
}
|
|
9868
|
+
}), defineRoute({
|
|
9869
|
+
method: "get",
|
|
9870
|
+
path: "/v1/project/status",
|
|
9871
|
+
operationId: "project.status",
|
|
9872
|
+
tags: ["project"],
|
|
9873
|
+
summary: "What a project's journeys and templates are doing",
|
|
9874
|
+
security: PIPELINE_AUTH,
|
|
9875
|
+
surfaces: {
|
|
9876
|
+
cli: false,
|
|
9877
|
+
mcpName: "status"
|
|
9878
|
+
},
|
|
9879
|
+
request: { query: getProjectQuerySchema },
|
|
9880
|
+
responses: {
|
|
9881
|
+
200: envelope(projectStatusSchema),
|
|
9882
|
+
...sessionErrors,
|
|
9883
|
+
...errors("project_missing", "validation_failed", "malformed_request")
|
|
9884
|
+
}
|
|
9751
9885
|
}), defineRoute({
|
|
9752
9886
|
method: "post",
|
|
9753
9887
|
path: "/v1/project",
|
|
9754
9888
|
operationId: "project.create",
|
|
9755
9889
|
tags: ["project"],
|
|
9756
9890
|
summary: "Create a project in the organization",
|
|
9757
|
-
security:
|
|
9891
|
+
security: PIPELINE_AUTH,
|
|
9758
9892
|
request: { body: jsonBody(createProjectBodySchema) },
|
|
9759
9893
|
responses: {
|
|
9760
9894
|
201: envelope(projectSchema, "The created project"),
|
|
@@ -9764,87 +9898,75 @@ const project = defineModule(defineRoute({
|
|
|
9764
9898
|
}));
|
|
9765
9899
|
|
|
9766
9900
|
//#endregion
|
|
9767
|
-
//#region ../../packages/shared/src/contract/
|
|
9768
|
-
const params$
|
|
9901
|
+
//#region ../../packages/shared/src/contract/pushes.ts
|
|
9902
|
+
const params$5 = z.object({ id: z.string() });
|
|
9769
9903
|
/**
|
|
9770
|
-
*
|
|
9904
|
+
* Pushes: one `cow push` each, the whole project at one point in time. The
|
|
9905
|
+
* push stores the source archive and its compile creates a version of every
|
|
9906
|
+
* key whose module changed; it turns nothing on (ADR 0011).
|
|
9771
9907
|
*
|
|
9772
9908
|
* `create` and `source` are hidden from the CLI and MCP surfaces because
|
|
9773
9909
|
* both only make sense next to local files: the manifest `create` takes is
|
|
9774
9910
|
* whatever `cow build` just wrote, and the tarball `source` returns is what
|
|
9775
|
-
* `cow pull` unpacks over a working tree.
|
|
9776
|
-
* `cancelExecutions` are ordinary operations an agent should have.
|
|
9911
|
+
* `cow pull` unpacks over a working tree.
|
|
9777
9912
|
*/
|
|
9778
|
-
const
|
|
9913
|
+
const pushes = defineModule(defineRoute({
|
|
9779
9914
|
method: "post",
|
|
9780
|
-
path: "/v1/
|
|
9781
|
-
operationId: "
|
|
9782
|
-
tags: ["
|
|
9783
|
-
summary: "
|
|
9784
|
-
security:
|
|
9915
|
+
path: "/v1/pushes",
|
|
9916
|
+
operationId: "pushes.create",
|
|
9917
|
+
tags: ["pushes"],
|
|
9918
|
+
summary: "Push a built project",
|
|
9919
|
+
security: PIPELINE_AUTH,
|
|
9785
9920
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
9786
|
-
request: { body: jsonBody(
|
|
9921
|
+
request: { body: jsonBody(createPushBodySchema) },
|
|
9787
9922
|
responses: {
|
|
9788
|
-
201: envelope(
|
|
9923
|
+
201: envelope(pushSchema, "The stored push and the versions it is compiling"),
|
|
9789
9924
|
...sessionErrors,
|
|
9790
|
-
...errors("project_missing", "
|
|
9925
|
+
...errors("project_missing", "push_invalid", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
9791
9926
|
}
|
|
9792
9927
|
}), defineRoute({
|
|
9793
9928
|
method: "get",
|
|
9794
|
-
path: "/v1/
|
|
9795
|
-
operationId: "
|
|
9796
|
-
tags: ["
|
|
9797
|
-
summary: "List
|
|
9798
|
-
security:
|
|
9799
|
-
request: { query:
|
|
9929
|
+
path: "/v1/pushes",
|
|
9930
|
+
operationId: "pushes.list",
|
|
9931
|
+
tags: ["pushes"],
|
|
9932
|
+
summary: "List pushes, newest first",
|
|
9933
|
+
security: SESSION_AUTH,
|
|
9934
|
+
request: { query: listPushesQuerySchema },
|
|
9800
9935
|
responses: {
|
|
9801
|
-
200: list(
|
|
9936
|
+
200: list(pushSchema),
|
|
9802
9937
|
...sessionErrors,
|
|
9803
9938
|
...errors("validation_failed")
|
|
9804
9939
|
}
|
|
9805
9940
|
}), defineRoute({
|
|
9806
9941
|
method: "get",
|
|
9807
|
-
path: "/v1/
|
|
9808
|
-
operationId: "
|
|
9809
|
-
tags: ["
|
|
9810
|
-
summary: "Inspect one
|
|
9811
|
-
security:
|
|
9812
|
-
request: { params: params$
|
|
9942
|
+
path: "/v1/pushes/{id}",
|
|
9943
|
+
operationId: "pushes.get",
|
|
9944
|
+
tags: ["pushes"],
|
|
9945
|
+
summary: "Inspect one push and what it compiled",
|
|
9946
|
+
security: SESSION_AUTH,
|
|
9947
|
+
request: { params: params$5 },
|
|
9813
9948
|
responses: {
|
|
9814
|
-
200: envelope(
|
|
9949
|
+
200: envelope(pushSchema),
|
|
9815
9950
|
...sessionErrors,
|
|
9816
9951
|
...errors("not_found")
|
|
9817
9952
|
}
|
|
9818
9953
|
}), defineRoute({
|
|
9819
9954
|
method: "get",
|
|
9820
|
-
path: "/v1/
|
|
9821
|
-
operationId: "
|
|
9822
|
-
tags: ["
|
|
9823
|
-
summary: "Download the
|
|
9824
|
-
security:
|
|
9955
|
+
path: "/v1/pushes/{id}/source",
|
|
9956
|
+
operationId: "pushes.source",
|
|
9957
|
+
tags: ["pushes"],
|
|
9958
|
+
summary: "Download the pushed source tarball",
|
|
9959
|
+
security: SESSION_AUTH,
|
|
9825
9960
|
surfaces: HIDDEN_FROM_TOOLS,
|
|
9826
|
-
request: { params: params$
|
|
9961
|
+
request: { params: params$5 },
|
|
9827
9962
|
responses: {
|
|
9828
9963
|
200: {
|
|
9829
|
-
description: "The gzipped source tarball the
|
|
9964
|
+
description: "The gzipped source tarball the push stored",
|
|
9830
9965
|
content: { "application/gzip": { schema: z.string() } }
|
|
9831
9966
|
},
|
|
9832
9967
|
...sessionErrors,
|
|
9833
9968
|
...errors("not_found")
|
|
9834
9969
|
}
|
|
9835
|
-
}), defineRoute({
|
|
9836
|
-
method: "post",
|
|
9837
|
-
path: "/v1/releases/{id}/cancel-executions",
|
|
9838
|
-
operationId: "releases.cancelExecutions",
|
|
9839
|
-
tags: ["releases"],
|
|
9840
|
-
summary: "Cancel every live execution pinned to this release",
|
|
9841
|
-
security: RELEASE_AUTH,
|
|
9842
|
-
request: { params: params$4 },
|
|
9843
|
-
responses: {
|
|
9844
|
-
202: envelope(cancelExecutionsSchema, "The cancel sweep was started"),
|
|
9845
|
-
...sessionErrors,
|
|
9846
|
-
...errors("not_found", "dependency_unavailable")
|
|
9847
|
-
}
|
|
9848
9970
|
}));
|
|
9849
9971
|
|
|
9850
9972
|
//#endregion
|
|
@@ -9872,7 +9994,7 @@ const review = defineModule(defineRoute({
|
|
|
9872
9994
|
|
|
9873
9995
|
//#endregion
|
|
9874
9996
|
//#region ../../packages/shared/src/contract/segments.ts
|
|
9875
|
-
const params$
|
|
9997
|
+
const params$4 = z.object({ id: z.string() });
|
|
9876
9998
|
const writeErrors = errors("validation_failed", "malformed_request");
|
|
9877
9999
|
/** `preview` precedes `get` so "preview" is never read as an id. */
|
|
9878
10000
|
const segments = defineModule(defineRoute({
|
|
@@ -9922,7 +10044,7 @@ const segments = defineModule(defineRoute({
|
|
|
9922
10044
|
tags: ["segments"],
|
|
9923
10045
|
summary: "Fetch one segment with its member count",
|
|
9924
10046
|
security: SESSION_AUTH,
|
|
9925
|
-
request: { params: params$
|
|
10047
|
+
request: { params: params$4 },
|
|
9926
10048
|
responses: {
|
|
9927
10049
|
200: envelope(segmentDetailSchema),
|
|
9928
10050
|
...sessionErrors,
|
|
@@ -9936,7 +10058,7 @@ const segments = defineModule(defineRoute({
|
|
|
9936
10058
|
summary: "Update a segment",
|
|
9937
10059
|
security: SESSION_AUTH,
|
|
9938
10060
|
request: {
|
|
9939
|
-
params: params$
|
|
10061
|
+
params: params$4,
|
|
9940
10062
|
body: jsonBody(updateSegmentBodySchema)
|
|
9941
10063
|
},
|
|
9942
10064
|
responses: {
|
|
@@ -9952,7 +10074,7 @@ const segments = defineModule(defineRoute({
|
|
|
9952
10074
|
tags: ["segments"],
|
|
9953
10075
|
summary: "Delete a segment",
|
|
9954
10076
|
security: SESSION_AUTH,
|
|
9955
|
-
request: { params: params$
|
|
10077
|
+
request: { params: params$4 },
|
|
9956
10078
|
responses: {
|
|
9957
10079
|
200: envelope(deletedSchema),
|
|
9958
10080
|
...sessionErrors,
|
|
@@ -9966,7 +10088,7 @@ const segments = defineModule(defineRoute({
|
|
|
9966
10088
|
summary: "List a segment's members",
|
|
9967
10089
|
security: SESSION_AUTH,
|
|
9968
10090
|
request: {
|
|
9969
|
-
params: params$
|
|
10091
|
+
params: params$4,
|
|
9970
10092
|
query: listSegmentMembersQuerySchema
|
|
9971
10093
|
},
|
|
9972
10094
|
responses: {
|
|
@@ -10030,10 +10152,10 @@ const settings = defineModule(defineRoute({
|
|
|
10030
10152
|
}
|
|
10031
10153
|
}), defineRoute({
|
|
10032
10154
|
method: "get",
|
|
10033
|
-
path: "/v1/settings/
|
|
10034
|
-
operationId: "settings.
|
|
10155
|
+
path: "/v1/settings/pipeline-keys",
|
|
10156
|
+
operationId: "settings.pipelineKeys.list",
|
|
10035
10157
|
tags: ["settings"],
|
|
10036
|
-
summary: "List org
|
|
10158
|
+
summary: "List org pipeline keys",
|
|
10037
10159
|
security: SESSION_AUTH,
|
|
10038
10160
|
request: { query: listApiKeysQuerySchema },
|
|
10039
10161
|
responses: {
|
|
@@ -10043,23 +10165,23 @@ const settings = defineModule(defineRoute({
|
|
|
10043
10165
|
}
|
|
10044
10166
|
}), defineRoute({
|
|
10045
10167
|
method: "post",
|
|
10046
|
-
path: "/v1/settings/
|
|
10047
|
-
operationId: "settings.
|
|
10168
|
+
path: "/v1/settings/pipeline-keys",
|
|
10169
|
+
operationId: "settings.pipelineKeys.create",
|
|
10048
10170
|
tags: ["settings"],
|
|
10049
|
-
summary: "Create an org
|
|
10171
|
+
summary: "Create an org pipeline key",
|
|
10050
10172
|
security: SESSION_AUTH,
|
|
10051
10173
|
request: { body: jsonBody(createApiKeyBodySchema) },
|
|
10052
10174
|
responses: {
|
|
10053
|
-
201: envelope(createdApiKeyDtoSchema, "The created
|
|
10175
|
+
201: envelope(createdApiKeyDtoSchema, "The created pipeline key; the only response carrying the raw secret"),
|
|
10054
10176
|
...sessionErrors,
|
|
10055
10177
|
...errors("validation_failed", "malformed_request")
|
|
10056
10178
|
}
|
|
10057
10179
|
}), defineRoute({
|
|
10058
10180
|
method: "post",
|
|
10059
|
-
path: "/v1/settings/
|
|
10060
|
-
operationId: "settings.
|
|
10181
|
+
path: "/v1/settings/pipeline-keys/{id}/revoke",
|
|
10182
|
+
operationId: "settings.pipelineKeys.revoke",
|
|
10061
10183
|
tags: ["settings"],
|
|
10062
|
-
summary: "Revoke an org
|
|
10184
|
+
summary: "Revoke an org pipeline key",
|
|
10063
10185
|
security: SESSION_AUTH,
|
|
10064
10186
|
request: {
|
|
10065
10187
|
params: z.object({ id: z.string() }),
|
|
@@ -10091,7 +10213,7 @@ const settings = defineModule(defineRoute({
|
|
|
10091
10213
|
//#endregion
|
|
10092
10214
|
//#region ../../packages/shared/src/contract/sources.ts
|
|
10093
10215
|
const appParams = z.object({ appId: z.string() });
|
|
10094
|
-
const params$
|
|
10216
|
+
const params$3 = z.object({ id: z.string() });
|
|
10095
10217
|
/**
|
|
10096
10218
|
* A source is one inbound pipe into an app. It is created and listed under
|
|
10097
10219
|
* its parent app (the app is the attribution unit) and addressed by its own
|
|
@@ -10137,7 +10259,7 @@ const sources = defineModule(defineRoute({
|
|
|
10137
10259
|
tags: ["sources"],
|
|
10138
10260
|
summary: "Fetch one source",
|
|
10139
10261
|
security: SESSION_AUTH,
|
|
10140
|
-
request: { params: params$
|
|
10262
|
+
request: { params: params$3 },
|
|
10141
10263
|
responses: {
|
|
10142
10264
|
200: envelope(sourceSchema),
|
|
10143
10265
|
...sessionErrors,
|
|
@@ -10151,7 +10273,7 @@ const sources = defineModule(defineRoute({
|
|
|
10151
10273
|
summary: "Set or rotate a source's config",
|
|
10152
10274
|
security: SESSION_AUTH,
|
|
10153
10275
|
request: {
|
|
10154
|
-
params: params$
|
|
10276
|
+
params: params$3,
|
|
10155
10277
|
body: jsonBody(updateSourceBodySchema)
|
|
10156
10278
|
},
|
|
10157
10279
|
responses: {
|
|
@@ -10166,7 +10288,7 @@ const sources = defineModule(defineRoute({
|
|
|
10166
10288
|
tags: ["sources"],
|
|
10167
10289
|
summary: "Archive a source",
|
|
10168
10290
|
security: SESSION_AUTH,
|
|
10169
|
-
request: { params: params$
|
|
10291
|
+
request: { params: params$3 },
|
|
10170
10292
|
responses: {
|
|
10171
10293
|
200: envelope(sourceSchema, "The archived source"),
|
|
10172
10294
|
...sessionErrors,
|
|
@@ -10223,15 +10345,104 @@ const suppressions = defineModule(defineRoute({
|
|
|
10223
10345
|
}));
|
|
10224
10346
|
|
|
10225
10347
|
//#endregion
|
|
10226
|
-
//#region ../../packages/shared/src/contract/
|
|
10227
|
-
const params$1 = z.object({ profileId: z.string() });
|
|
10348
|
+
//#region ../../packages/shared/src/contract/templates.ts
|
|
10228
10349
|
/**
|
|
10229
|
-
*
|
|
10230
|
-
*
|
|
10231
|
-
*
|
|
10350
|
+
* Templates: the emails a journey sends, addressed by key like a journey.
|
|
10351
|
+
* A template has versions and nothing else (ADR 0011), so there is no
|
|
10352
|
+
* create and no update here: templates arrive with a push, and the only
|
|
10353
|
+
* writes are rendering one to look at it, sending one to yourself, and
|
|
10354
|
+
* deleting the key.
|
|
10355
|
+
*
|
|
10356
|
+
* `preview` and `testSend` both render the latest version that finished
|
|
10357
|
+
* compiling, in the sandbox, with the props the caller passes. `testSend`
|
|
10358
|
+
* then sends that message to the caller's own address through the
|
|
10359
|
+
* organization's default sender: it reaches no recipient, so it passes no
|
|
10360
|
+
* consent gate, counts against nothing, and is logged as a test.
|
|
10232
10361
|
*/
|
|
10233
|
-
const
|
|
10234
|
-
|
|
10362
|
+
const params$2 = z.object({ key: z.string() });
|
|
10363
|
+
const templates = defineModule(defineRoute({
|
|
10364
|
+
method: "get",
|
|
10365
|
+
path: "/v1/templates",
|
|
10366
|
+
operationId: "templates.list",
|
|
10367
|
+
tags: ["templates"],
|
|
10368
|
+
summary: "List the organization's email templates",
|
|
10369
|
+
security: SESSION_AUTH,
|
|
10370
|
+
request: { query: listTemplatesQuerySchema },
|
|
10371
|
+
responses: {
|
|
10372
|
+
200: list(templateSchema),
|
|
10373
|
+
...sessionErrors,
|
|
10374
|
+
...errors("validation_failed")
|
|
10375
|
+
}
|
|
10376
|
+
}), defineRoute({
|
|
10377
|
+
method: "get",
|
|
10378
|
+
path: "/v1/templates/{key}",
|
|
10379
|
+
operationId: "templates.get",
|
|
10380
|
+
tags: ["templates"],
|
|
10381
|
+
summary: "Inspect one email template",
|
|
10382
|
+
security: SESSION_AUTH,
|
|
10383
|
+
request: { params: params$2 },
|
|
10384
|
+
responses: {
|
|
10385
|
+
200: envelope(templateSchema),
|
|
10386
|
+
...sessionErrors,
|
|
10387
|
+
...errors("not_found")
|
|
10388
|
+
}
|
|
10389
|
+
}), defineRoute({
|
|
10390
|
+
method: "post",
|
|
10391
|
+
path: "/v1/templates/{key}/preview",
|
|
10392
|
+
operationId: "templates.preview",
|
|
10393
|
+
tags: ["templates"],
|
|
10394
|
+
summary: "Render a template with the props you pass",
|
|
10395
|
+
security: SESSION_AUTH,
|
|
10396
|
+
request: {
|
|
10397
|
+
params: params$2,
|
|
10398
|
+
body: jsonBody(renderTemplateBodySchema)
|
|
10399
|
+
},
|
|
10400
|
+
responses: {
|
|
10401
|
+
200: envelope(templatePreviewSchema, "The rendered message"),
|
|
10402
|
+
...sessionErrors,
|
|
10403
|
+
...errors("not_found", "version_not_ready", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
10404
|
+
}
|
|
10405
|
+
}), defineRoute({
|
|
10406
|
+
method: "post",
|
|
10407
|
+
path: "/v1/templates/{key}/test-send",
|
|
10408
|
+
operationId: "templates.testSend",
|
|
10409
|
+
tags: ["templates"],
|
|
10410
|
+
summary: "Send a rendered template to your own address",
|
|
10411
|
+
security: SESSION_AUTH,
|
|
10412
|
+
request: {
|
|
10413
|
+
params: params$2,
|
|
10414
|
+
body: jsonBody(renderTemplateBodySchema)
|
|
10415
|
+
},
|
|
10416
|
+
responses: {
|
|
10417
|
+
201: envelope(deliverySchema, "The test send, as it was logged"),
|
|
10418
|
+
...sessionErrors,
|
|
10419
|
+
...errors("not_found", "version_not_ready", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
10420
|
+
}
|
|
10421
|
+
}), defineRoute({
|
|
10422
|
+
method: "delete",
|
|
10423
|
+
path: "/v1/templates/{key}",
|
|
10424
|
+
operationId: "templates.delete",
|
|
10425
|
+
tags: ["templates"],
|
|
10426
|
+
summary: "Delete an email template",
|
|
10427
|
+
security: SESSION_AUTH,
|
|
10428
|
+
request: { params: params$2 },
|
|
10429
|
+
responses: {
|
|
10430
|
+
200: envelope(deletedSchema),
|
|
10431
|
+
...sessionErrors,
|
|
10432
|
+
...errors("not_found", "conflict")
|
|
10433
|
+
}
|
|
10434
|
+
}));
|
|
10435
|
+
|
|
10436
|
+
//#endregion
|
|
10437
|
+
//#region ../../packages/shared/src/contract/users.ts
|
|
10438
|
+
const params$1 = z.object({ profileId: z.string() });
|
|
10439
|
+
/**
|
|
10440
|
+
* The answer a merged-away profile id gets (spec: Identity): 307 to the
|
|
10441
|
+
* same path with the survivor's id, so the method and body of a redirected
|
|
10442
|
+
* call survive the hop. Empty body; the `Location` header is the answer.
|
|
10443
|
+
*/
|
|
10444
|
+
const mergedRedirect = { 307: {
|
|
10445
|
+
description: "The profile was merged into another; Location carries the survivor's URL",
|
|
10235
10446
|
headers: z.object({ Location: z.string().meta({ description: "The survivor's URL" }) })
|
|
10236
10447
|
} };
|
|
10237
10448
|
/**
|
|
@@ -10360,6 +10571,29 @@ const users = defineModule(defineRoute({
|
|
|
10360
10571
|
}
|
|
10361
10572
|
}));
|
|
10362
10573
|
|
|
10574
|
+
//#endregion
|
|
10575
|
+
//#region ../../packages/shared/src/contract/versions.ts
|
|
10576
|
+
/**
|
|
10577
|
+
* Versions: one key's history, newest first. A version is read-only and
|
|
10578
|
+
* addressed through its key rather than on its own path, because "which
|
|
10579
|
+
* code does this journey run" is the only question asked of it, and the
|
|
10580
|
+
* answer is the first row.
|
|
10581
|
+
*/
|
|
10582
|
+
const versions = defineModule(defineRoute({
|
|
10583
|
+
method: "get",
|
|
10584
|
+
path: "/v1/versions",
|
|
10585
|
+
operationId: "versions.list",
|
|
10586
|
+
tags: ["versions"],
|
|
10587
|
+
summary: "List a journey's or a template's versions, newest first",
|
|
10588
|
+
security: SESSION_AUTH,
|
|
10589
|
+
request: { query: listVersionsQuerySchema },
|
|
10590
|
+
responses: {
|
|
10591
|
+
200: list(versionSchema),
|
|
10592
|
+
...sessionErrors,
|
|
10593
|
+
...errors("validation_failed")
|
|
10594
|
+
}
|
|
10595
|
+
}));
|
|
10596
|
+
|
|
10363
10597
|
//#endregion
|
|
10364
10598
|
//#region ../../packages/shared/src/contract/violations.ts
|
|
10365
10599
|
const params = z.object({ id: z.string() });
|
|
@@ -10434,10 +10668,11 @@ const contract = {
|
|
|
10434
10668
|
segments,
|
|
10435
10669
|
project,
|
|
10436
10670
|
artifacts,
|
|
10437
|
-
|
|
10438
|
-
|
|
10671
|
+
pushes,
|
|
10672
|
+
versions,
|
|
10439
10673
|
executions,
|
|
10440
10674
|
journeys,
|
|
10675
|
+
templates,
|
|
10441
10676
|
destinations,
|
|
10442
10677
|
domains,
|
|
10443
10678
|
deliveries,
|
|
@@ -10811,520 +11046,55 @@ function registerContractCommands(program, run) {
|
|
|
10811
11046
|
}
|
|
10812
11047
|
|
|
10813
11048
|
//#endregion
|
|
10814
|
-
//#region src/commands/
|
|
10815
|
-
/**
|
|
10816
|
-
* How long to wait for a compile before giving up on the poll. A minute past
|
|
10817
|
-
* the server's own whole-release cap, so the normal end of a stuck compile is
|
|
10818
|
-
* the release turning `failed` with a reason rather than the CLI shrugging.
|
|
10819
|
-
*/
|
|
10820
|
-
const POLL_BUDGET_MS = RELEASE_COMPILE_TIMEOUT_MS + 6e4;
|
|
10821
|
-
const POLL_START_MS = 250;
|
|
10822
|
-
const POLL_MAX_MS = 2e3;
|
|
10823
|
-
/**
|
|
10824
|
-
* This directory's project, created when the org has none by that name.
|
|
10825
|
-
* `cow init` usually got there first; a project pushed from a machine that
|
|
10826
|
-
* only ever cloned the repo has not, and creating it here is what makes that
|
|
10827
|
-
* clone work. The name is `cow.json`'s `project`.
|
|
10828
|
-
*/
|
|
10829
|
-
async function ensureProject(client, name) {
|
|
10830
|
-
try {
|
|
10831
|
-
return (await client.request(contract.project["project.get"], { query: { name } })).data;
|
|
10832
|
-
} catch (error) {
|
|
10833
|
-
if (!(error instanceof ApiError) || error.code !== "project_missing") throw error;
|
|
10834
|
-
}
|
|
10835
|
-
try {
|
|
10836
|
-
return (await client.request(contract.project["project.create"], { body: { name } })).data;
|
|
10837
|
-
} catch (error) {
|
|
10838
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
10839
|
-
throw new Error(`This organization has no project and one could not be created (${reason}). Run \`cow init\` first.`);
|
|
10840
|
-
}
|
|
10841
|
-
}
|
|
10842
|
-
/** Does the org already hold these bytes? A 404 is the answer, not a failure. */
|
|
10843
|
-
async function held(client, digest) {
|
|
10844
|
-
try {
|
|
10845
|
-
await client.request(contract.artifacts["artifacts.head"], { params: { digest } });
|
|
10846
|
-
return true;
|
|
10847
|
-
} catch (error) {
|
|
10848
|
-
if (error instanceof ApiError && error.status === 404) return false;
|
|
10849
|
-
throw error;
|
|
10850
|
-
}
|
|
10851
|
-
}
|
|
10852
|
-
/** Every artifact the manifest names, and the file `cow build` wrote it to. */
|
|
10853
|
-
function artifactFiles(projectDir, manifest) {
|
|
10854
|
-
const out = join(projectDir, BUILD_DIR);
|
|
10855
|
-
return [
|
|
10856
|
-
...manifest.journeys.map((journey) => ({
|
|
10857
|
-
digest: journey.bundle,
|
|
10858
|
-
kind: "bundle",
|
|
10859
|
-
file: join(out, "bundles", "journeys", `${journey.key}.js`)
|
|
10860
|
-
})),
|
|
10861
|
-
...manifest.templates.map((template) => ({
|
|
10862
|
-
digest: template.bundle,
|
|
10863
|
-
kind: "bundle",
|
|
10864
|
-
file: join(out, "bundles", "emails", `${template.key}.js`)
|
|
10865
|
-
})),
|
|
10866
|
-
{
|
|
10867
|
-
digest: manifest.source,
|
|
10868
|
-
kind: "source",
|
|
10869
|
-
file: join(out, "source.tgz")
|
|
10870
|
-
}
|
|
10871
|
-
];
|
|
10872
|
-
}
|
|
10873
|
-
/**
|
|
10874
|
-
* This project's most recent release, or null when it has none yet. Scoped to
|
|
10875
|
-
* the project because a sibling repo's push must not make this one look
|
|
10876
|
-
* unchanged, or changed.
|
|
10877
|
-
*/
|
|
10878
|
-
async function latestRelease(client, project) {
|
|
10879
|
-
return (await client.request(contract.releases["releases.list"], { query: {
|
|
10880
|
-
limit: 1,
|
|
10881
|
-
project
|
|
10882
|
-
} })).data[0] ?? null;
|
|
10883
|
-
}
|
|
11049
|
+
//#region src/commands/enable.ts
|
|
10884
11050
|
/**
|
|
10885
|
-
*
|
|
10886
|
-
*
|
|
10887
|
-
*
|
|
10888
|
-
*
|
|
10889
|
-
|
|
10890
|
-
function compiledFraction(release) {
|
|
10891
|
-
return `${release.compiledEntries}/${release.totalEntries}`;
|
|
10892
|
-
}
|
|
10893
|
-
/**
|
|
10894
|
-
* The progress writer for a terminal, or nothing at all when there is no one
|
|
10895
|
-
* watching. It rewrites one line on stderr, so what a command prints to
|
|
10896
|
-
* stdout is the same bytes whether or not a terminal is attached.
|
|
11051
|
+
* `cow enable` and `cow disable`: the one gate on a journey, per
|
|
11052
|
+
* environment (ADR 0011). Hand-written rather than derived from
|
|
11053
|
+
* `journeys.enable`, because the commands are top-level names a developer
|
|
11054
|
+
* types, and because of the `--env` rule below; the route itself takes the
|
|
11055
|
+
* same set of keys they do.
|
|
10897
11056
|
*
|
|
10898
|
-
*
|
|
10899
|
-
*
|
|
10900
|
-
*
|
|
11057
|
+
* `--env` is never defaulted here (`requireEnvironment`): the default
|
|
11058
|
+
* environment is production, and a flag nobody typed must not put code in
|
|
11059
|
+
* front of real recipients.
|
|
10901
11060
|
*/
|
|
10902
|
-
function progressLineFor(io, json) {
|
|
10903
|
-
if (!io.isTTY || json === true) return;
|
|
10904
|
-
let drawn = false;
|
|
10905
|
-
return (text) => {
|
|
10906
|
-
if (text === "" && !drawn) return;
|
|
10907
|
-
drawn = text !== "";
|
|
10908
|
-
io.stderr(`\r\x1b[2K${text}`);
|
|
10909
|
-
};
|
|
10910
|
-
}
|
|
10911
11061
|
/**
|
|
10912
|
-
*
|
|
10913
|
-
*
|
|
10914
|
-
*
|
|
11062
|
+
* One call, whichever way the keys were named: the route takes the set, and
|
|
11063
|
+
* `--all` is the project name, resolved on the server against the rows it
|
|
11064
|
+
* owns (so it reaches a key whose file the tree lost). A key that is not
|
|
11065
|
+
* there refuses the whole call, and nothing flips.
|
|
10915
11066
|
*/
|
|
10916
|
-
async function
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
try {
|
|
10920
|
-
for (;;) {
|
|
10921
|
-
const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
|
|
10922
|
-
if (data.status !== "pending") return data;
|
|
10923
|
-
if (waited >= POLL_BUDGET_MS) throw new Error(`Release ${releaseId} is still compiling after ${Math.round(waited / 1e3)}s (${compiledFraction(data)} compiled). Check \`cow releases get ${releaseId}\`.`);
|
|
10924
|
-
progress?.(`Compiling ${compiledFraction(data)}`);
|
|
10925
|
-
await sleep(interval);
|
|
10926
|
-
waited += interval;
|
|
10927
|
-
interval = Math.min(interval * 2, POLL_MAX_MS);
|
|
10928
|
-
}
|
|
10929
|
-
} finally {
|
|
10930
|
-
progress?.("");
|
|
10931
|
-
}
|
|
11067
|
+
async function setEnabled(client, selector, enabled) {
|
|
11068
|
+
const route = enabled ? contract.journeys["journeys.enable"] : contract.journeys["journeys.disable"];
|
|
11069
|
+
return (await client.request(route, { body: selector })).data;
|
|
10932
11070
|
}
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
const digest = manifestDigestOf(manifest);
|
|
10938
|
-
const latest = await latestRelease(client, project.name);
|
|
10939
|
-
if (latest && latest.manifestDigest === digest && !force) return {
|
|
10940
|
-
release: latest,
|
|
10941
|
-
uploaded: [],
|
|
10942
|
-
skipped: latest.status === "failed" ? `Nothing changed since ${latest.id} #${latest.seq}, which failed to compile. Push again with --force to retry it.` : `Nothing changed since ${latest.id} #${latest.seq} (${latest.status}). Nothing to push.`
|
|
10943
|
-
};
|
|
10944
|
-
const uploaded = [];
|
|
10945
|
-
for (const artifact of artifactFiles(projectDir, manifest)) {
|
|
10946
|
-
if (await held(client, artifact.digest)) continue;
|
|
10947
|
-
await client.request(contract.artifacts["artifacts.put"], {
|
|
10948
|
-
params: { digest: artifact.digest },
|
|
10949
|
-
query: { kind: artifact.kind },
|
|
10950
|
-
body: await readFile(artifact.file)
|
|
10951
|
-
});
|
|
10952
|
-
uploaded.push(artifact.digest);
|
|
10953
|
-
}
|
|
10954
|
-
return {
|
|
10955
|
-
release: await awaitCompile(client, (await client.request(contract.releases["releases.create"], { body: {
|
|
10956
|
-
manifest: {
|
|
10957
|
-
...manifest,
|
|
10958
|
-
protocol: 2
|
|
10959
|
-
},
|
|
10960
|
-
project: project.name
|
|
10961
|
-
} })).data.id, sleep, progress),
|
|
10962
|
-
uploaded
|
|
10963
|
-
};
|
|
11071
|
+
/** What the terminal says: one line per key, naming where it now stands. */
|
|
11072
|
+
function flipSummary(journeys, enabled, environment) {
|
|
11073
|
+
if (journeys.length === 0) return `This project has no journeys to turn ${enabled ? "on" : "off"}.`;
|
|
11074
|
+
return journeys.map((journey) => `${journey.key} is ${enabled ? "on" : "off"} in ${environment}.`).join("\n");
|
|
10964
11075
|
}
|
|
10965
|
-
|
|
10966
|
-
|
|
10967
|
-
|
|
10968
|
-
const { release } = outcome;
|
|
10969
|
-
const uploaded = outcome.uploaded.length === 0 ? "nothing new to upload" : `${outcome.uploaded.length} artifact${outcome.uploaded.length === 1 ? "" : "s"} uploaded`;
|
|
10970
|
-
if (release.status === "ready") return `${release.id} #${release.seq} ready (${uploaded}).`;
|
|
10971
|
-
return `${release.id} #${release.seq} failed: ${release.error ?? "no reason recorded"}`;
|
|
10972
|
-
}
|
|
10973
|
-
function registerPush(program, clientFor, io) {
|
|
10974
|
-
program.command("push").description("build the project, upload it, and create a release the server compiles").option("--force", "push even when nothing changed since the last release").action(async (opts) => {
|
|
11076
|
+
function register(program, clientFor, env, io, enabled) {
|
|
11077
|
+
const verb = enabled ? "enable" : "disable";
|
|
11078
|
+
program.command(verb).description(`turn journeys ${enabled ? "on" : "off"} in one environment (many keys, or --all for this project's)`).argument("[keys...]", "journey keys").option("--all", "every journey of this project").action(async (keys, opts) => {
|
|
10975
11079
|
const merged = {
|
|
10976
11080
|
...program.opts(),
|
|
10977
11081
|
...opts
|
|
10978
11082
|
};
|
|
10979
|
-
const
|
|
10980
|
-
|
|
10981
|
-
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
});
|
|
10985
|
-
if (merged.json === true) emit({ data: outcome.release }, io, true);
|
|
10986
|
-
else io.stdout(`${pushSummary(outcome)}\n`);
|
|
10987
|
-
if (outcome.release.status === "failed") process.exitCode = 1;
|
|
10988
|
-
});
|
|
10989
|
-
}
|
|
10990
|
-
|
|
10991
|
-
//#endregion
|
|
10992
|
-
//#region src/commands/deploy.ts
|
|
10993
|
-
/** The release to deploy, and the push message when a push produced it. */
|
|
10994
|
-
async function releaseToDeploy({ client, projectDir, releaseId, sleep, progress }) {
|
|
10995
|
-
if (releaseId) {
|
|
10996
|
-
const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
|
|
10997
|
-
return { release: data };
|
|
10998
|
-
}
|
|
10999
|
-
const pushed = await pushProject({
|
|
11000
|
-
client,
|
|
11001
|
-
projectDir,
|
|
11002
|
-
sleep,
|
|
11003
|
-
progress
|
|
11004
|
-
});
|
|
11005
|
-
return {
|
|
11006
|
-
release: pushed.release,
|
|
11007
|
-
skipped: pushed.skipped
|
|
11008
|
-
};
|
|
11009
|
-
}
|
|
11010
|
-
async function deployProject(options) {
|
|
11011
|
-
const { release, skipped } = await releaseToDeploy(options);
|
|
11012
|
-
if (release.status !== "ready") throw new Error(release.status === "failed" ? `${release.id} #${release.seq} failed to compile, so there is nothing to deploy: ${release.error ?? "no reason recorded"}` : `${release.id} #${release.seq} is still compiling (${compiledFraction(release)} compiled); wait for it to be ready.`);
|
|
11013
|
-
const { data } = await options.client.request(contract.deployments["deployments.create"], { body: { releaseId: release.id } });
|
|
11014
|
-
return {
|
|
11015
|
-
deployment: data,
|
|
11016
|
-
release,
|
|
11017
|
-
skipped
|
|
11018
|
-
};
|
|
11019
|
-
}
|
|
11020
|
-
/** The human report: what is now running, and what it references but cannot find. */
|
|
11021
|
-
function deploySummary(outcome) {
|
|
11022
|
-
const { deployment, release } = outcome;
|
|
11023
|
-
const lines = [`${release.id} #${release.seq} deployed to ${deployment.environment}.`];
|
|
11024
|
-
if (outcome.skipped) lines.unshift(outcome.skipped);
|
|
11025
|
-
for (const warning of deployment.warnings) lines.push(`Warning: ${warning}`);
|
|
11026
|
-
if (deployment.left.length > 0) lines.push(`Still running, not in this release: ${deployment.left.join(", ")}. Remove one with \`cow journeys delete <key> --env ${deployment.environment}\`.`);
|
|
11027
|
-
return lines.join("\n");
|
|
11028
|
-
}
|
|
11029
|
-
function registerDeploy(program, clientFor, env, io) {
|
|
11030
|
-
program.command("deploy").description("push the project when it changed, then make that release the environment's current code").option("--release <id>", "deploy this existing release instead of pushing the working tree").action(async (opts) => {
|
|
11031
|
-
const merged = {
|
|
11032
|
-
...program.opts(),
|
|
11033
|
-
...opts
|
|
11034
|
-
};
|
|
11035
|
-
requireEnvironment(env, merged);
|
|
11036
|
-
const outcome = await deployProject({
|
|
11037
|
-
client: await clientFor(merged),
|
|
11038
|
-
projectDir: process.cwd(),
|
|
11039
|
-
releaseId: opts.release,
|
|
11040
|
-
progress: progressLineFor(io, merged.json === true)
|
|
11041
|
-
});
|
|
11083
|
+
const environment = requireEnvironment(env, merged);
|
|
11084
|
+
if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome --env ${environment}.`);
|
|
11085
|
+
if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome --env ${environment}.`);
|
|
11086
|
+
const selector = merged.all === true ? { project: (await assertCowConfig(process.cwd())).project } : { keys };
|
|
11087
|
+
const updated = await setEnabled(await clientFor(merged), selector, enabled);
|
|
11042
11088
|
if (merged.json === true) {
|
|
11043
|
-
emit({ data:
|
|
11089
|
+
emit({ data: updated }, io, true);
|
|
11044
11090
|
return;
|
|
11045
11091
|
}
|
|
11046
|
-
io.stdout(`${
|
|
11092
|
+
io.stdout(`${flipSummary(updated, enabled, environment)}\n`);
|
|
11047
11093
|
});
|
|
11048
11094
|
}
|
|
11049
|
-
|
|
11050
|
-
|
|
11051
|
-
|
|
11052
|
-
/**
|
|
11053
|
-
* `cow dev` (spec: CLI, user story 12): the inner loop. Watch the project,
|
|
11054
|
-
* build-push-deploy to development on every save, and tail what the journeys
|
|
11055
|
-
* did, so the distance between a saved file and a rendered email is seconds.
|
|
11056
|
-
*
|
|
11057
|
-
* Two halves that never block each other: a watcher whose cycle is exactly
|
|
11058
|
-
* `cow deploy --env development` (push skips an unchanged tree, so a save
|
|
11059
|
-
* that changes nothing costs one build), and a poller over the two logs. A
|
|
11060
|
-
* failure in either prints and the loop keeps running: the whole point of a
|
|
11061
|
-
* watcher is that a typo does not end the session.
|
|
11062
|
-
*
|
|
11063
|
-
* ponytail: polling over server-sent events. Two seconds is under the
|
|
11064
|
-
* latency a person notices, and SSE costs a streaming endpoint, a reconnect
|
|
11065
|
-
* story, and a proxy that buffers it. Ceiling: neither `updatedAt` column
|
|
11066
|
-
* is indexed, so each poll walks the org's development rows; add
|
|
11067
|
-
* `(org_id, environment, updated_at)` to executions and deliveries when a
|
|
11068
|
-
* development environment grows past a few thousand of them.
|
|
11069
|
-
*/
|
|
11070
|
-
/** `cow dev` is the development loop; the root `--env` does not move it. */
|
|
11071
|
-
const DEV_ENVIRONMENT = "development";
|
|
11072
|
-
const DEBOUNCE_MS = 300;
|
|
11073
|
-
const POLL_MS = 2e3;
|
|
11074
|
-
/**
|
|
11075
|
-
* What a save can change: the two source directories and the three config
|
|
11076
|
-
* files. Watched as three watchers rather than one recursive watch of the
|
|
11077
|
-
* project root, because that root holds `node_modules` (a recursive watch of
|
|
11078
|
-
* it exhausts inotify on Linux) and `.cow/build`, which this command's own
|
|
11079
|
-
* builds write.
|
|
11080
|
-
*/
|
|
11081
|
-
const CONFIG_FILES = [
|
|
11082
|
-
"cow.json",
|
|
11083
|
-
"package.json",
|
|
11084
|
-
"tsconfig.json"
|
|
11085
|
-
];
|
|
11086
|
-
const SOURCE_DIRS = ["journeys", "emails"];
|
|
11087
|
-
/**
|
|
11088
|
-
* ponytail: the cursor starts from the CLIENT's clock, compared server-side
|
|
11089
|
-
* against database timestamps. A laptop running ahead of the database blanks
|
|
11090
|
-
* the tail until the skew burns off. A server-time field on the first
|
|
11091
|
-
* response would fix it; nothing else in the contract needs one yet.
|
|
11092
|
-
*
|
|
11093
|
-
* ponytail: the three collections grow for the life of the process, roughly
|
|
11094
|
-
* a hundred bytes per execution seen. A day's dev session is thousands, not
|
|
11095
|
-
* millions; prune below the cursor if a session ever runs long enough to
|
|
11096
|
-
* notice.
|
|
11097
|
-
*/
|
|
11098
|
-
function initialTailState(from = /* @__PURE__ */ new Date()) {
|
|
11099
|
-
const since = from.toISOString();
|
|
11100
|
-
return {
|
|
11101
|
-
executionsSince: since,
|
|
11102
|
-
deliveriesSince: since,
|
|
11103
|
-
seen: /* @__PURE__ */ new Set(),
|
|
11104
|
-
status: /* @__PURE__ */ new Map(),
|
|
11105
|
-
logged: /* @__PURE__ */ new Map()
|
|
11106
|
-
};
|
|
11107
|
-
}
|
|
11108
|
-
/**
|
|
11109
|
-
* Every page of a list whose rows changed since the cursor. Paged to
|
|
11110
|
-
* exhaustion on purpose: the page is ordered by when rows STARTED and
|
|
11111
|
-
* filtered by when they last moved, so an old execution that just completed
|
|
11112
|
-
* can sit on page two, and advancing the cursor off page one would lose it.
|
|
11113
|
-
*/
|
|
11114
|
-
async function drain(fetchPage) {
|
|
11115
|
-
const rows = [];
|
|
11116
|
-
let cursor;
|
|
11117
|
-
do {
|
|
11118
|
-
const page = await fetchPage(cursor);
|
|
11119
|
-
rows.push(...page.data);
|
|
11120
|
-
cursor = page.meta?.hasMore ? page.meta.nextCursor ?? void 0 : void 0;
|
|
11121
|
-
} while (cursor);
|
|
11122
|
-
return rows;
|
|
11123
|
-
}
|
|
11124
|
-
/** The newest `updatedAt` in a batch, or the cursor when the batch is empty. */
|
|
11125
|
-
function advance(rows, current) {
|
|
11126
|
-
return rows.reduce((latest, row) => row.updatedAt > latest ? row.updatedAt : latest, current);
|
|
11127
|
-
}
|
|
11128
|
-
function executionLine(execution) {
|
|
11129
|
-
const step = execution.step ? ` step=${execution.step}` : "";
|
|
11130
|
-
const error = execution.error ? ` ${execution.error}` : "";
|
|
11131
|
-
return ` ${execution.journeyKey} ${execution.profileId} ${execution.status}${step}${error}`;
|
|
11132
|
-
}
|
|
11133
|
-
function deliveryLine(delivery, subject, webUrl) {
|
|
11134
|
-
const title = subject ? ` "${subject}"` : "";
|
|
11135
|
-
const to = delivery.recipient ? ` to ${delivery.recipient}` : "";
|
|
11136
|
-
return ` ${delivery.journey}/${delivery.step} ${delivery.status}${to}${title} ${webUrl}/automation/deliveries/${delivery.id}`;
|
|
11137
|
-
}
|
|
11138
|
-
/**
|
|
11139
|
-
* One pass over both logs. Separated from the timer so a test can call it
|
|
11140
|
-
* twice and prove each row prints once.
|
|
11141
|
-
*/
|
|
11142
|
-
async function pollTail({ client, io, webUrl, state }) {
|
|
11143
|
-
const executions = await drain((cursor) => client.request(contract.executions["executions.list"], { query: {
|
|
11144
|
-
since: state.executionsSince,
|
|
11145
|
-
limit: 100,
|
|
11146
|
-
cursor
|
|
11147
|
-
} }));
|
|
11148
|
-
for (const execution of [...executions].reverse()) {
|
|
11149
|
-
const key = `${execution.id}:${execution.updatedAt}`;
|
|
11150
|
-
if (state.seen.has(key)) continue;
|
|
11151
|
-
const marker = `${execution.status}:${execution.step ?? ""}`;
|
|
11152
|
-
if (state.status.get(execution.id) !== marker) {
|
|
11153
|
-
state.status.set(execution.id, marker);
|
|
11154
|
-
io.stdout(`${executionLine(execution)}\n`);
|
|
11155
|
-
}
|
|
11156
|
-
const { data } = await client.request(contract.executions["executions.get"], { params: { id: execution.id } });
|
|
11157
|
-
const printed = state.logged.get(execution.id) ?? 0;
|
|
11158
|
-
for (const line of data.logs.slice(printed)) io.stdout(` log ${line.level}: ${line.message}\n`);
|
|
11159
|
-
state.logged.set(execution.id, Math.max(printed, data.logs.length));
|
|
11160
|
-
state.seen.add(key);
|
|
11161
|
-
}
|
|
11162
|
-
state.executionsSince = advance(executions, state.executionsSince);
|
|
11163
|
-
const captured = await drain((cursor) => client.request(contract.deliveries["deliveries.list"], { query: {
|
|
11164
|
-
since: state.deliveriesSince,
|
|
11165
|
-
limit: 100,
|
|
11166
|
-
cursor,
|
|
11167
|
-
direction: "asc"
|
|
11168
|
-
} }));
|
|
11169
|
-
for (const delivery of captured) {
|
|
11170
|
-
const key = `${delivery.id}:${delivery.updatedAt}`;
|
|
11171
|
-
if (state.seen.has(key)) continue;
|
|
11172
|
-
let subject = null;
|
|
11173
|
-
if (delivery.channel === "email") {
|
|
11174
|
-
const { data } = await client.request(contract.deliveries["deliveries.get"], { params: { id: delivery.id } });
|
|
11175
|
-
subject = data.payload.rendered?.subject ?? null;
|
|
11176
|
-
}
|
|
11177
|
-
io.stdout(`${deliveryLine(delivery, subject, webUrl)}\n`);
|
|
11178
|
-
state.seen.add(key);
|
|
11179
|
-
}
|
|
11180
|
-
state.deliveriesSince = advance(captured, state.deliveriesSince);
|
|
11181
|
-
}
|
|
11182
|
-
/**
|
|
11183
|
-
* The watcher, the cycle, and the tail, until the signal aborts. Resolves
|
|
11184
|
-
* when it does, which is what makes `Ctrl-C` a clean exit rather than a
|
|
11185
|
-
* killed process mid-upload.
|
|
11186
|
-
*/
|
|
11187
|
-
async function runDev(options) {
|
|
11188
|
-
const { client, projectDir, io, signal, debounceMs = DEBOUNCE_MS, pollMs = POLL_MS, deploy = (dir, api) => deployProject({
|
|
11189
|
-
client: api,
|
|
11190
|
-
projectDir: dir
|
|
11191
|
-
}) } = options;
|
|
11192
|
-
let running = false;
|
|
11193
|
-
let queued = false;
|
|
11194
|
-
async function cycle() {
|
|
11195
|
-
if (signal.aborted) return;
|
|
11196
|
-
if (running) {
|
|
11197
|
-
queued = true;
|
|
11198
|
-
return;
|
|
11199
|
-
}
|
|
11200
|
-
running = true;
|
|
11201
|
-
try {
|
|
11202
|
-
io.stdout(`${deploySummary(await deploy(projectDir, client))}\n`);
|
|
11203
|
-
} catch (error) {
|
|
11204
|
-
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
11205
|
-
} finally {
|
|
11206
|
-
running = false;
|
|
11207
|
-
}
|
|
11208
|
-
if (queued && !signal.aborted) {
|
|
11209
|
-
queued = false;
|
|
11210
|
-
await cycle();
|
|
11211
|
-
}
|
|
11212
|
-
}
|
|
11213
|
-
const tail = options.tail ? (async () => {
|
|
11214
|
-
const state = initialTailState();
|
|
11215
|
-
while (!signal.aborted) {
|
|
11216
|
-
await setTimeout$1(pollMs, void 0, { signal }).catch(() => {});
|
|
11217
|
-
if (signal.aborted) return;
|
|
11218
|
-
try {
|
|
11219
|
-
await pollTail({
|
|
11220
|
-
client,
|
|
11221
|
-
io,
|
|
11222
|
-
webUrl: options.webUrl,
|
|
11223
|
-
state
|
|
11224
|
-
});
|
|
11225
|
-
} catch (error) {
|
|
11226
|
-
io.stderr(`Tail: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
11227
|
-
}
|
|
11228
|
-
}
|
|
11229
|
-
})() : Promise.resolve();
|
|
11230
|
-
let timer;
|
|
11231
|
-
let pending = Promise.resolve();
|
|
11232
|
-
const schedule = () => {
|
|
11233
|
-
clearTimeout(timer);
|
|
11234
|
-
timer = setTimeout(() => {
|
|
11235
|
-
pending = cycle();
|
|
11236
|
-
}, debounceMs);
|
|
11237
|
-
};
|
|
11238
|
-
async function watchOne(target) {
|
|
11239
|
-
try {
|
|
11240
|
-
for await (const event of watch(target.path, {
|
|
11241
|
-
recursive: target.recursive,
|
|
11242
|
-
signal
|
|
11243
|
-
})) {
|
|
11244
|
-
if (target.names && !target.names.includes(event.filename ?? "")) continue;
|
|
11245
|
-
schedule();
|
|
11246
|
-
}
|
|
11247
|
-
} catch (error) {
|
|
11248
|
-
if (!signal.aborted) throw error;
|
|
11249
|
-
}
|
|
11250
|
-
}
|
|
11251
|
-
const watchers = (await watchTargets(projectDir)).map(watchOne);
|
|
11252
|
-
await cycle();
|
|
11253
|
-
try {
|
|
11254
|
-
await Promise.all([...watchers, tail]);
|
|
11255
|
-
} finally {
|
|
11256
|
-
clearTimeout(timer);
|
|
11257
|
-
await pending;
|
|
11258
|
-
}
|
|
11259
|
-
}
|
|
11260
|
-
/**
|
|
11261
|
-
* The project root (non-recursive, filtered to the three config files) plus
|
|
11262
|
-
* each source directory that exists. Resolved once, at start: `cow init`
|
|
11263
|
-
* creates both directories, so the only project missing one is a hand-made
|
|
11264
|
-
* one, and there `cow dev` has to be restarted after the first template.
|
|
11265
|
-
* The root is watched instead of the
|
|
11266
|
-
* files themselves because an editor that saves by writing a temp file and
|
|
11267
|
-
* renaming it over the original leaves a per-file watch pointing at an
|
|
11268
|
-
* inode nobody will write to again.
|
|
11269
|
-
*/
|
|
11270
|
-
async function watchTargets(projectDir) {
|
|
11271
|
-
const targets = [{
|
|
11272
|
-
path: projectDir,
|
|
11273
|
-
recursive: false,
|
|
11274
|
-
names: CONFIG_FILES
|
|
11275
|
-
}];
|
|
11276
|
-
for (const dir of SOURCE_DIRS) {
|
|
11277
|
-
const path = join(projectDir, dir);
|
|
11278
|
-
if (await stat(path).then((s) => s.isDirectory(), () => false)) targets.push({
|
|
11279
|
-
path,
|
|
11280
|
-
recursive: true
|
|
11281
|
-
});
|
|
11282
|
-
}
|
|
11283
|
-
return targets;
|
|
11284
|
-
}
|
|
11285
|
-
/**
|
|
11286
|
-
* What `cow dev` says about the credential it is about to use, before it
|
|
11287
|
-
* uses it (spec: deploy keys and roles, "cow dev says which it is using").
|
|
11288
|
-
* Either credential runs the whole loop, deploys and captured emails alike.
|
|
11289
|
-
* The session is the one that stops working partway through an afternoon,
|
|
11290
|
-
* so it is the one that gets a warning.
|
|
11291
|
-
*/
|
|
11292
|
-
function credentialNotices(credential) {
|
|
11293
|
-
if (credential.kind === "deployKey") return ["Using COW_DEPLOY_KEY."];
|
|
11294
|
-
if (credential.kind !== "session") return [];
|
|
11295
|
-
const expiry = decodeSessionToken(credential.token ?? "")?.expiresAt;
|
|
11296
|
-
return [`Using your cached login${expiry ? `, which expires ${expiry}` : ""}. Set COW_DEPLOY_KEY to avoid logging in again.`];
|
|
11297
|
-
}
|
|
11298
|
-
function registerDev(program, clientFor, env, io) {
|
|
11299
|
-
program.command("dev").description("watch the project, build, push, and deploy to development on every save, and tail what the journeys did").option("--no-tail", "skip the execution and delivery tail").action(async (opts) => {
|
|
11300
|
-
const merged = {
|
|
11301
|
-
...program.opts(),
|
|
11302
|
-
...opts,
|
|
11303
|
-
env: DEV_ENVIRONMENT
|
|
11304
|
-
};
|
|
11305
|
-
const credential = resolveCredential(env, await readCredentials(env));
|
|
11306
|
-
if (credential.kind === "none") throw new Error(`No credential: run \`${"cow"} login\`, or set COW_DEPLOY_KEY.`);
|
|
11307
|
-
for (const notice of credentialNotices(credential)) io.stderr(`${notice}\n`);
|
|
11308
|
-
const controller = new AbortController();
|
|
11309
|
-
const stop = () => {
|
|
11310
|
-
io.stderr("\nStopped watching.\n");
|
|
11311
|
-
controller.abort();
|
|
11312
|
-
};
|
|
11313
|
-
process.once("SIGINT", stop);
|
|
11314
|
-
try {
|
|
11315
|
-
await runDev({
|
|
11316
|
-
client: await clientFor(merged),
|
|
11317
|
-
projectDir: process.cwd(),
|
|
11318
|
-
io,
|
|
11319
|
-
webUrl: env.COW_WEB_URL ?? "http://localhost:5273",
|
|
11320
|
-
tail: opts.tail !== false,
|
|
11321
|
-
signal: controller.signal
|
|
11322
|
-
});
|
|
11323
|
-
} finally {
|
|
11324
|
-
controller.abort();
|
|
11325
|
-
process.off("SIGINT", stop);
|
|
11326
|
-
}
|
|
11327
|
-
});
|
|
11095
|
+
function registerEnable(program, clientFor, env, io) {
|
|
11096
|
+
register(program, clientFor, env, io, true);
|
|
11097
|
+
register(program, clientFor, env, io, false);
|
|
11328
11098
|
}
|
|
11329
11099
|
|
|
11330
11100
|
//#endregion
|
|
@@ -11588,7 +11358,7 @@ function registerMcp(program, clientFor, env, io) {
|
|
|
11588
11358
|
...program.opts(),
|
|
11589
11359
|
...opts
|
|
11590
11360
|
});
|
|
11591
|
-
if (resolveCredential(env, await readCredentials(env)).kind === "none") io.stderr("cow mcp: not logged in. Run `cow login`, or set COW_TOKEN or
|
|
11361
|
+
if (resolveCredential(env, await readCredentials(env)).kind === "none") io.stderr("cow mcp: not logged in. Run `cow login`, or set COW_TOKEN or COW_PIPELINE_KEY.\n");
|
|
11592
11362
|
await (await createMcpServer({ client })).connect(new StdioServerTransport());
|
|
11593
11363
|
});
|
|
11594
11364
|
}
|
|
@@ -11596,7 +11366,7 @@ function registerMcp(program, clientFor, env, io) {
|
|
|
11596
11366
|
//#endregion
|
|
11597
11367
|
//#region src/commands/pull.ts
|
|
11598
11368
|
/**
|
|
11599
|
-
* `cow pull` (spec: CLI): put a
|
|
11369
|
+
* `cow pull` (spec: CLI): put a push's sources back on disk, so a fresh
|
|
11600
11370
|
* machine or an agent starts from what was actually pushed.
|
|
11601
11371
|
*
|
|
11602
11372
|
* It only ever writes the files the tarball carries: nothing is deleted, and
|
|
@@ -11605,8 +11375,6 @@ function registerMcp(program, clientFor, env, io) {
|
|
|
11605
11375
|
* bury.
|
|
11606
11376
|
*/
|
|
11607
11377
|
const execFileAsync = promisify(execFile);
|
|
11608
|
-
/** Releases read per page while looking for the newest that compiled. */
|
|
11609
|
-
const PAGE_SIZE = 50;
|
|
11610
11378
|
/**
|
|
11611
11379
|
* Uncommitted changes under `dir`, when it sits in a git working tree. A
|
|
11612
11380
|
* directory that is not one (or a machine with no git) has nothing to lose
|
|
@@ -11638,41 +11406,30 @@ function noGitHere(error) {
|
|
|
11638
11406
|
return failure.code === "ENOENT" || String(failure.stderr ?? "").includes("not a git repository");
|
|
11639
11407
|
}
|
|
11640
11408
|
/**
|
|
11641
|
-
* The
|
|
11642
|
-
*
|
|
11643
|
-
*
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
|
|
11648
|
-
|
|
11649
|
-
|
|
11650
|
-
|
|
11651
|
-
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
11656
|
-
|
|
11657
|
-
|
|
11658
|
-
|
|
11659
|
-
|
|
11660
|
-
cursor,
|
|
11661
|
-
project
|
|
11662
|
-
} });
|
|
11663
|
-
const ready = page.data.find((release) => release.status === "ready");
|
|
11664
|
-
if (ready) return ready;
|
|
11665
|
-
cursor = page.meta?.nextCursor ?? void 0;
|
|
11666
|
-
} while (cursor);
|
|
11667
|
-
throw new Error("This organization has no release that compiled successfully. Run `cow push` first, or name one with --release.");
|
|
11668
|
-
}
|
|
11669
|
-
async function pullRelease({ client, projectDir, releaseId, environment, force }) {
|
|
11670
|
-
const release = await resolveRelease(client, releaseId, environment, await configuredProject(projectDir));
|
|
11409
|
+
* The push to restore: the one named, else this project's newest. Scoped to
|
|
11410
|
+
* this directory's project, so a sibling repo's tree is never unpacked over
|
|
11411
|
+
* this one's files.
|
|
11412
|
+
*
|
|
11413
|
+
* The newest rather than the newest that compiled: a push stores the whole
|
|
11414
|
+
* tree and its source archive whatever its keys then compiled to, so it is
|
|
11415
|
+
* the last thing anyone pushed either way.
|
|
11416
|
+
*/
|
|
11417
|
+
async function resolvePush(client, pushId, project) {
|
|
11418
|
+
if (pushId) return (await client.request(contract.pushes["pushes.get"], { params: { id: pushId } })).data;
|
|
11419
|
+
const newest = (await client.request(contract.pushes["pushes.list"], { query: {
|
|
11420
|
+
limit: 1,
|
|
11421
|
+
project
|
|
11422
|
+
} })).data[0];
|
|
11423
|
+
if (!newest) throw new Error("This project has never been pushed, so there is nothing to restore. Run `cow push` first, or name a push with --push.");
|
|
11424
|
+
return newest;
|
|
11425
|
+
}
|
|
11426
|
+
async function pullPush({ client, projectDir, pushId, force }) {
|
|
11427
|
+
const push = await resolvePush(client, pushId, await configuredProject(projectDir));
|
|
11671
11428
|
if (!force) {
|
|
11672
11429
|
const dirty = await dirtyPaths(projectDir);
|
|
11673
11430
|
if (dirty.length > 0) throw new Error(`This working tree has uncommitted changes and \`cow pull\` would write over them:\n ${dirty.join("\n ")}\nCommit or stash them, or pass --force.`);
|
|
11674
11431
|
}
|
|
11675
|
-
const tarball = await client.request(contract.
|
|
11432
|
+
const tarball = await client.request(contract.pushes["pushes.source"], { params: { id: push.id } });
|
|
11676
11433
|
const files = [];
|
|
11677
11434
|
const unpack = extract({
|
|
11678
11435
|
cwd: projectDir,
|
|
@@ -11684,71 +11441,281 @@ async function pullRelease({ client, projectDir, releaseId, environment, force }
|
|
|
11684
11441
|
unpack.end(Buffer.from(tarball));
|
|
11685
11442
|
});
|
|
11686
11443
|
return {
|
|
11687
|
-
|
|
11444
|
+
push,
|
|
11688
11445
|
files: files.sort()
|
|
11689
11446
|
};
|
|
11690
11447
|
}
|
|
11691
|
-
function registerPull(program, clientFor,
|
|
11692
|
-
program.command("pull").description("restore the project's source files from a
|
|
11448
|
+
function registerPull(program, clientFor, io) {
|
|
11449
|
+
program.command("pull").description("restore the project's source files from a push into the current directory").option("--push <id>", "the push to restore (default: the newest one)").option("--force", "extract even when the git working tree is dirty").action(async (opts) => {
|
|
11693
11450
|
const merged = {
|
|
11694
11451
|
...program.opts(),
|
|
11695
11452
|
...opts
|
|
11696
11453
|
};
|
|
11697
|
-
const outcome = await
|
|
11454
|
+
const outcome = await pullPush({
|
|
11698
11455
|
client: await clientFor(merged),
|
|
11699
11456
|
projectDir: process.cwd(),
|
|
11700
|
-
|
|
11701
|
-
environment: merged.env === void 0 ? void 0 : environmentFor(env, merged),
|
|
11457
|
+
pushId: opts.push,
|
|
11702
11458
|
force: opts.force === true
|
|
11703
11459
|
});
|
|
11704
11460
|
if (merged.json === true) {
|
|
11705
11461
|
emit({ data: outcome }, io, true);
|
|
11706
11462
|
return;
|
|
11707
11463
|
}
|
|
11708
|
-
io.stdout(
|
|
11464
|
+
io.stdout(`Push #${outcome.push.seq} restored, ${outcome.files.length} files:\n${outcome.files.map((file) => ` ${file}`).join("\n")}\n`);
|
|
11709
11465
|
});
|
|
11710
11466
|
}
|
|
11711
11467
|
|
|
11712
11468
|
//#endregion
|
|
11713
|
-
//#region src/commands/
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
11723
|
-
|
|
11469
|
+
//#region src/commands/push.ts
|
|
11470
|
+
/**
|
|
11471
|
+
* This directory's project, created when the org has none by that name.
|
|
11472
|
+
* `cow init` usually got there first; a project pushed from a machine that
|
|
11473
|
+
* only ever cloned the repo has not, and creating it here is what makes that
|
|
11474
|
+
* clone work. The name is `cow.json`'s `project`.
|
|
11475
|
+
*/
|
|
11476
|
+
async function ensureProject(client, name) {
|
|
11477
|
+
try {
|
|
11478
|
+
return (await client.request(contract.project["project.get"], { query: { name } })).data;
|
|
11479
|
+
} catch (error) {
|
|
11480
|
+
if (!(error instanceof ApiError) || error.code !== "project_missing") throw error;
|
|
11481
|
+
}
|
|
11482
|
+
try {
|
|
11483
|
+
return (await client.request(contract.project["project.create"], { body: { name } })).data;
|
|
11484
|
+
} catch (error) {
|
|
11485
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
11486
|
+
throw new Error(`This organization has no project and one could not be created (${reason}). Run \`cow init\` first.`);
|
|
11487
|
+
}
|
|
11488
|
+
}
|
|
11489
|
+
/** Does the org already hold these bytes? A 404 is the answer, not a failure. */
|
|
11490
|
+
async function held(client, digest) {
|
|
11491
|
+
try {
|
|
11492
|
+
await client.request(contract.artifacts["artifacts.head"], { params: { digest } });
|
|
11493
|
+
return true;
|
|
11494
|
+
} catch (error) {
|
|
11495
|
+
if (error instanceof ApiError && error.status === 404) return false;
|
|
11496
|
+
throw error;
|
|
11497
|
+
}
|
|
11498
|
+
}
|
|
11499
|
+
/** Every artifact the manifest names, and the file `cow build` wrote it to. */
|
|
11500
|
+
function artifactFiles(projectDir, manifest) {
|
|
11501
|
+
const out = join(projectDir, BUILD_DIR);
|
|
11502
|
+
return [
|
|
11503
|
+
...manifest.journeys.map((journey) => ({
|
|
11504
|
+
digest: journey.bundle,
|
|
11505
|
+
kind: "bundle",
|
|
11506
|
+
file: join(out, "bundles", "journeys", `${journey.key}.js`)
|
|
11507
|
+
})),
|
|
11508
|
+
...manifest.templates.map((template) => ({
|
|
11509
|
+
digest: template.bundle,
|
|
11510
|
+
kind: "bundle",
|
|
11511
|
+
file: join(out, "bundles", "emails", `${template.key}.js`)
|
|
11512
|
+
})),
|
|
11513
|
+
{
|
|
11514
|
+
digest: manifest.source,
|
|
11515
|
+
kind: "source",
|
|
11516
|
+
file: join(out, "source.tgz")
|
|
11517
|
+
}
|
|
11518
|
+
];
|
|
11519
|
+
}
|
|
11520
|
+
async function pushProject({ client, projectDir }) {
|
|
11521
|
+
const { manifest, config } = await buildProject(projectDir);
|
|
11522
|
+
const project = await ensureProject(client, config.project);
|
|
11523
|
+
if (project.orgId !== config.orgId) throw new Error(`cow.json is for ${config.orgId} but the credential belongs to ${project.orgId}. Log in to that organization, use its pipeline key, or select a config for it with --config or COW_CONFIG (for example cow.local.json).`);
|
|
11524
|
+
const uploaded = [];
|
|
11525
|
+
for (const artifact of artifactFiles(projectDir, manifest)) {
|
|
11526
|
+
if (await held(client, artifact.digest)) continue;
|
|
11527
|
+
await client.request(contract.artifacts["artifacts.put"], {
|
|
11528
|
+
params: { digest: artifact.digest },
|
|
11529
|
+
query: { kind: artifact.kind },
|
|
11530
|
+
body: await readFile(artifact.file)
|
|
11531
|
+
});
|
|
11532
|
+
uploaded.push(artifact.digest);
|
|
11533
|
+
}
|
|
11724
11534
|
return {
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
|
|
11535
|
+
push: (await client.request(contract.pushes["pushes.create"], { body: {
|
|
11536
|
+
manifest: {
|
|
11537
|
+
...manifest,
|
|
11538
|
+
protocol: 2
|
|
11539
|
+
},
|
|
11540
|
+
project: project.name
|
|
11541
|
+
} })).data,
|
|
11542
|
+
uploaded
|
|
11728
11543
|
};
|
|
11729
11544
|
}
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11545
|
+
/**
|
|
11546
|
+
* The human report: what was stored, and one line per changed key with the
|
|
11547
|
+
* page that key now has on the dashboard. A key whose code did not change
|
|
11548
|
+
* gets no new version, so a push of an unchanged tree says exactly that.
|
|
11549
|
+
*/
|
|
11550
|
+
function pushSummary(outcome, webUrl) {
|
|
11551
|
+
const { push } = outcome;
|
|
11552
|
+
const uploaded = outcome.uploaded.length === 0 ? "nothing new to upload" : `${outcome.uploaded.length} artifact${outcome.uploaded.length === 1 ? "" : "s"} uploaded`;
|
|
11553
|
+
const stored = `Push #${push.seq} stored (${uploaded}).`;
|
|
11554
|
+
if (push.versions.length === 0) return `${stored} Nothing changed.`;
|
|
11555
|
+
const width = Math.max(...push.versions.map((version) => version.key.length));
|
|
11556
|
+
return [`${stored} Compiling ${push.versions.length} ${push.versions.length === 1 ? "key" : "keys"}:`, ...push.versions.map((version) => ` ${version.key.padEnd(width)} ${webUrl}/${version.kind === "journey" ? "journeys" : "templates"}/${version.key}`)].join("\n");
|
|
11735
11557
|
}
|
|
11736
|
-
function
|
|
11737
|
-
program.command("
|
|
11558
|
+
function registerPush(program, clientFor, env, io) {
|
|
11559
|
+
program.command("push").description("build the project and upload it; the server compiles what changed").action(async (opts) => {
|
|
11738
11560
|
const merged = {
|
|
11739
11561
|
...program.opts(),
|
|
11740
11562
|
...opts
|
|
11741
11563
|
};
|
|
11742
|
-
const outcome = await
|
|
11564
|
+
const outcome = await pushProject({
|
|
11743
11565
|
client: await clientFor(merged),
|
|
11744
|
-
|
|
11745
|
-
project: await configuredProject(process.cwd())
|
|
11566
|
+
projectDir: process.cwd()
|
|
11746
11567
|
});
|
|
11747
11568
|
if (merged.json === true) {
|
|
11748
|
-
emit({ data: outcome.
|
|
11569
|
+
emit({ data: outcome.push }, io, true);
|
|
11570
|
+
return;
|
|
11571
|
+
}
|
|
11572
|
+
const webUrl = resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl);
|
|
11573
|
+
io.stdout(`${pushSummary(outcome, webUrl)}\n`);
|
|
11574
|
+
});
|
|
11575
|
+
}
|
|
11576
|
+
|
|
11577
|
+
//#endregion
|
|
11578
|
+
//#region src/commands/status.ts
|
|
11579
|
+
/**
|
|
11580
|
+
* `cow status`: what the server holds for every key of this project, and how
|
|
11581
|
+
* far the tree in front of the developer has moved from it. Hand-written
|
|
11582
|
+
* rather than derived from `project.status`, because the drift is the half
|
|
11583
|
+
* of the answer no server read can know: it comes from building the project
|
|
11584
|
+
* here and comparing the bundle digests with the versions the server has.
|
|
11585
|
+
*/
|
|
11586
|
+
/** Largest first: the first unit the gap fills is the one that reads best. */
|
|
11587
|
+
const UNITS = [
|
|
11588
|
+
["year", 31536e6],
|
|
11589
|
+
["month", 2592e6],
|
|
11590
|
+
["week", 6048e5],
|
|
11591
|
+
["day", 864e5],
|
|
11592
|
+
["hour", 36e5],
|
|
11593
|
+
["minute", 6e4],
|
|
11594
|
+
["second", 1e3]
|
|
11595
|
+
];
|
|
11596
|
+
const relative$1 = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
|
11597
|
+
/** The gap between an instant and now, in words. */
|
|
11598
|
+
function ago(value, now) {
|
|
11599
|
+
const gap = new Date(value).getTime() - now.getTime();
|
|
11600
|
+
for (const [unit, ms] of UNITS) if (Math.abs(gap) >= ms) return relative$1.format(Math.round(gap / ms), unit);
|
|
11601
|
+
return relative$1.format(0, "second");
|
|
11602
|
+
}
|
|
11603
|
+
function localKeys(manifest) {
|
|
11604
|
+
return [...manifest.journeys.map((journey) => ({
|
|
11605
|
+
kind: "journey",
|
|
11606
|
+
key: journey.key,
|
|
11607
|
+
bundle: journey.bundle
|
|
11608
|
+
})), ...manifest.templates.map((template) => ({
|
|
11609
|
+
kind: "template",
|
|
11610
|
+
key: template.key,
|
|
11611
|
+
bundle: template.bundle
|
|
11612
|
+
}))];
|
|
11613
|
+
}
|
|
11614
|
+
/**
|
|
11615
|
+
* The version's short name. The bundle digest rather than the compiled
|
|
11616
|
+
* module's, because a version is one bundle (a push that carries a bundle
|
|
11617
|
+
* the org already holds creates no second version) and the bundle is there
|
|
11618
|
+
* from the moment the push lands, while the module digest arrives only when
|
|
11619
|
+
* the compile finishes.
|
|
11620
|
+
*/
|
|
11621
|
+
function shortDigest(bundle) {
|
|
11622
|
+
return bundle.replace("sha256:", "").slice(0, 8);
|
|
11623
|
+
}
|
|
11624
|
+
function bundleOf(entry) {
|
|
11625
|
+
const manifest = entry.latestVersion?.manifest;
|
|
11626
|
+
return manifest ? manifest.bundle : null;
|
|
11627
|
+
}
|
|
11628
|
+
/** "version 3f2a1b9c, pushed 2 hours ago, still compiling" */
|
|
11629
|
+
function versionPhrase(entry, now) {
|
|
11630
|
+
const version = entry.latestVersion;
|
|
11631
|
+
if (!version) return "no version yet";
|
|
11632
|
+
const state = version.status === "compiling" ? ", still compiling" : version.status === "failed" ? ", did not compile" : "";
|
|
11633
|
+
const bundle = bundleOf(entry);
|
|
11634
|
+
return `${bundle ? `version ${shortDigest(bundle)}` : "version"}, pushed ${ago(version.pushedAt, now)}${state}`;
|
|
11635
|
+
}
|
|
11636
|
+
function flagsPhrase(entry) {
|
|
11637
|
+
if (!entry.enabled) return "";
|
|
11638
|
+
const word = (on) => on ? "on" : "off";
|
|
11639
|
+
return `${word(entry.enabled.development)} in development, ${word(entry.enabled.production)} in production`;
|
|
11640
|
+
}
|
|
11641
|
+
function livePhrase(entry) {
|
|
11642
|
+
const running = (count, environment) => count === 0 ? [] : [`${count} running in ${environment}`];
|
|
11643
|
+
return [...running(entry.liveExecutions.development, "development"), ...running(entry.liveExecutions.production, "production")].join(", ");
|
|
11644
|
+
}
|
|
11645
|
+
function pad(text, width) {
|
|
11646
|
+
return text.padEnd(width);
|
|
11647
|
+
}
|
|
11648
|
+
/**
|
|
11649
|
+
* The whole report, as lines. Pure so a test can read it: everything it
|
|
11650
|
+
* needs is the server's answer, the local build, and the clock.
|
|
11651
|
+
*
|
|
11652
|
+
* A null manifest is a tree that did not build. What the server runs is
|
|
11653
|
+
* still the answer to most of the question, so the report keeps it and drops
|
|
11654
|
+
* only the comparison.
|
|
11655
|
+
*/
|
|
11656
|
+
function statusLines(status, manifest, now) {
|
|
11657
|
+
const local = manifest ? localKeys(manifest) : [];
|
|
11658
|
+
const journeys = status.keys.filter((entry) => entry.kind === "journey");
|
|
11659
|
+
const templates = status.keys.filter((entry) => entry.kind === "template");
|
|
11660
|
+
const lines = [`${status.project}: ${journeys.length} ${journeys.length === 1 ? "journey" : "journeys"}, ${templates.length} ${templates.length === 1 ? "template" : "templates"} on the server.`];
|
|
11661
|
+
const width = Math.max(0, ...status.keys.map((entry) => entry.key.length));
|
|
11662
|
+
const section = (title, entries) => {
|
|
11663
|
+
if (entries.length === 0) return;
|
|
11664
|
+
const flagWidth = Math.max(0, ...entries.map((entry) => flagsPhrase(entry).length));
|
|
11665
|
+
lines.push("", title);
|
|
11666
|
+
for (const entry of entries) {
|
|
11667
|
+
const flags = flagsPhrase(entry);
|
|
11668
|
+
const live = livePhrase(entry);
|
|
11669
|
+
const rest = [
|
|
11670
|
+
...flags === "" ? [] : [pad(flags, flagWidth)],
|
|
11671
|
+
versionPhrase(entry, now),
|
|
11672
|
+
...live === "" ? [] : [live]
|
|
11673
|
+
];
|
|
11674
|
+
lines.push(` ${pad(entry.key, width)} ${rest.join(" ")}`.trimEnd());
|
|
11675
|
+
}
|
|
11676
|
+
};
|
|
11677
|
+
section("Journeys", journeys);
|
|
11678
|
+
section("Templates", templates);
|
|
11679
|
+
if (!manifest) {
|
|
11680
|
+
lines.push("", "This tree did not build, so nothing here is compared with it. Run cow build to see why.");
|
|
11681
|
+
return lines;
|
|
11682
|
+
}
|
|
11683
|
+
const changed = local.filter((entry) => {
|
|
11684
|
+
const served = status.keys.find((one) => one.kind === entry.kind && one.key === entry.key);
|
|
11685
|
+
return !served || bundleOf(served) !== entry.bundle;
|
|
11686
|
+
});
|
|
11687
|
+
const missing = status.keys.filter((entry) => !local.some((one) => one.kind === entry.kind && one.key === entry.key));
|
|
11688
|
+
if (changed.length > 0) lines.push("", `Changed here since the last push: ${changed.map((entry) => entry.key).join(", ")}. Run cow push.`);
|
|
11689
|
+
if (missing.length > 0) {
|
|
11690
|
+
lines.push("", "On the server and not in this tree:");
|
|
11691
|
+
for (const entry of missing) lines.push(` ${entry.key}, a ${entry.kind}. Delete it with cow ${entry.kind}s delete ${entry.key}.`);
|
|
11692
|
+
}
|
|
11693
|
+
for (const environment of ["development", "production"]) {
|
|
11694
|
+
const warnings = status.warnings[environment];
|
|
11695
|
+
if (warnings.length === 0) continue;
|
|
11696
|
+
lines.push("", `Names ${environment} does not define yet:`);
|
|
11697
|
+
for (const warning of warnings) lines.push(` ${warning}`);
|
|
11698
|
+
}
|
|
11699
|
+
return lines;
|
|
11700
|
+
}
|
|
11701
|
+
/** The project's status from the server, for this directory's `cow.json`. */
|
|
11702
|
+
async function readStatus(client, project) {
|
|
11703
|
+
return (await client.request(contract.project["project.status"], { query: { name: project } })).data;
|
|
11704
|
+
}
|
|
11705
|
+
function registerStatus(program, clientFor, io) {
|
|
11706
|
+
program.command("status").description("what this project's journeys and templates are doing, and what changed here since the last push").action(async (opts) => {
|
|
11707
|
+
const merged = {
|
|
11708
|
+
...program.opts(),
|
|
11709
|
+
...opts
|
|
11710
|
+
};
|
|
11711
|
+
const config = await assertCowConfig(process.cwd());
|
|
11712
|
+
const built = await buildProject(process.cwd()).catch(() => null);
|
|
11713
|
+
const status = await readStatus(await clientFor(merged), config.project);
|
|
11714
|
+
if (merged.json === true) {
|
|
11715
|
+
emit({ data: status }, io, true);
|
|
11749
11716
|
return;
|
|
11750
11717
|
}
|
|
11751
|
-
io.stdout(`${
|
|
11718
|
+
io.stdout(`${statusLines(status, built?.manifest ?? null, /* @__PURE__ */ new Date()).join("\n")}\n`);
|
|
11752
11719
|
});
|
|
11753
11720
|
}
|
|
11754
11721
|
|
|
@@ -12007,9 +11974,8 @@ function registerTest(program, io) {
|
|
|
12007
11974
|
//#region src/commands/index.ts
|
|
12008
11975
|
/**
|
|
12009
11976
|
* The environment a call acts on: the root `--env`, else `COW_ENVIRONMENT`,
|
|
12010
|
-
* else production. The client sets it as the selection header;
|
|
12011
|
-
* that also
|
|
12012
|
-
* parameter, not by the header) read it from here, so the two can never
|
|
11977
|
+
* else production. The client sets it as the selection header; a command
|
|
11978
|
+
* that also needs the value itself reads it from here, so the two can never
|
|
12013
11979
|
* disagree.
|
|
12014
11980
|
*/
|
|
12015
11981
|
function environmentFor(env, opts) {
|
|
@@ -12017,11 +11983,11 @@ function environmentFor(env, opts) {
|
|
|
12017
11983
|
return flag.success ? flag.data : env.COW_ENVIRONMENT ?? "production";
|
|
12018
11984
|
}
|
|
12019
11985
|
/**
|
|
12020
|
-
* The same value, but only when the caller actually chose one. `cow
|
|
12021
|
-
* and `cow
|
|
12022
|
-
* production: a bare `cow
|
|
12023
|
-
* would
|
|
12024
|
-
* `COW_ENVIRONMENT` counts as having chosen.
|
|
11986
|
+
* The same value, but only when the caller actually chose one. `cow enable`
|
|
11987
|
+
* and `cow disable` change what real recipients get, and the default is
|
|
11988
|
+
* production: a bare `cow enable welcome` typed while thinking about
|
|
11989
|
+
* development would turn it on in production. A pipeline is unaffected,
|
|
11990
|
+
* because `COW_ENVIRONMENT` counts as having chosen.
|
|
12025
11991
|
*/
|
|
12026
11992
|
function requireEnvironment(env, opts) {
|
|
12027
11993
|
if (opts.env === void 0 && env.COW_ENVIRONMENT === void 0) throw new Error(`Name the environment to act on: --env ${ENVIRONMENTS.join(" or --env ")}, or set COW_ENVIRONMENT.`);
|
|
@@ -12067,11 +12033,10 @@ function buildProgram(env, io) {
|
|
|
12067
12033
|
registerInit(program, env, io);
|
|
12068
12034
|
registerAdd(program, io);
|
|
12069
12035
|
registerBuild(program, io);
|
|
12070
|
-
registerPush(program, clientFor, io);
|
|
12071
|
-
|
|
12072
|
-
|
|
12073
|
-
|
|
12074
|
-
registerRollback(program, clientFor, env, io);
|
|
12036
|
+
registerPush(program, clientFor, env, io);
|
|
12037
|
+
registerStatus(program, clientFor, io);
|
|
12038
|
+
registerEnable(program, clientFor, env, io);
|
|
12039
|
+
registerPull(program, clientFor, io);
|
|
12075
12040
|
registerTest(program, io);
|
|
12076
12041
|
registerMcp(program, clientFor, env, io);
|
|
12077
12042
|
return program;
|
|
@@ -12089,8 +12054,8 @@ const envSchema = z.object({
|
|
|
12089
12054
|
COW_CREDENTIALS_PATH: z.string().min(1).optional(),
|
|
12090
12055
|
/** A session token passed explicitly instead of a cached `cow login`. */
|
|
12091
12056
|
COW_TOKEN: z.string().min(1).optional(),
|
|
12092
|
-
/** An org
|
|
12093
|
-
|
|
12057
|
+
/** An org pipeline key for CI; when set it wins over the cached session (ticket 05 reads it). */
|
|
12058
|
+
COW_PIPELINE_KEY: z.string().min(1).optional(),
|
|
12094
12059
|
/** The environment admin calls select; `--env` overrides it, production is the default. */
|
|
12095
12060
|
COW_ENVIRONMENT: environmentSchema.optional(),
|
|
12096
12061
|
/**
|