@cowliss/cli 0.1.1 → 0.3.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.
@@ -1,4 +1,4 @@
1
- import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-CxuS-mHn.js";
1
+ import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-BcbIxxI-.js";
2
2
  import { z } from "zod";
3
3
  import { createElement } from "react";
4
4
  import { renderToStaticMarkup } from "react-dom/server.browser";
@@ -1,3 +1,3 @@
1
- import { t as runGuest } from "./driver-BOe7jBFw.js";
1
+ import { t as runGuest } from "./driver-M6LQVMr2.js";
2
2
 
3
3
  export { runGuest };
@@ -69,19 +69,37 @@ const COW_CONFIG_SCHEMA_URL = `${DOCS_URL}${COW_CONFIG_SCHEMA_PATH}`;
69
69
  //#endregion
70
70
  //#region ../../packages/shared/src/journeys-v2/config.ts
71
71
  /**
72
- * `cow.json` (spec: Project layout): the org id and nothing more. The
73
- * folder name is the project's local name, the environment is always a
74
- * flag, and auth never lives in the project. Strict, so a typo'd key is a
75
- * build error rather than a silently ignored setting.
72
+ * A project's name: what `cow init` asked for, and what the `projects` row
73
+ * stores. Lives here rather than beside the release DTOs because `cow.json`
74
+ * is the file a developer types it into; `createProjectBodySchema` reuses it.
75
+ */
76
+ const projectNameSchema = z.string().trim().min(1).max(200);
77
+ /**
78
+ * `cow.json` (spec: Project layout): the org, and which of its projects this
79
+ * directory is. The environment is always a flag, and auth never lives in
80
+ * the project. Strict, so a typo'd key is a build error rather than a
81
+ * silently ignored setting.
76
82
  */
77
83
  const cowConfigSchema = z.strictObject({
78
84
  $schema: z.url().optional(),
79
85
  orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
86
+ /**
87
+ * Which project of the org this directory pushes to. One org can hold
88
+ * several, one per repo, each with its own release sequence and its own
89
+ * slice of the deployed journeys, so every project says which it is.
90
+ */
91
+ project: projectNameSchema,
80
92
  /** Overrides the API the CLI talks to; the hosted product needs none. */
81
- apiUrl: z.url().optional()
93
+ apiUrl: z.url().optional(),
94
+ /**
95
+ * Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
96
+ * login against one Cowliss's dashboard yields a token the other's API
97
+ * rejects, so a config that names an API names its dashboard too.
98
+ */
99
+ webUrl: z.url().optional()
82
100
  }).meta({
83
101
  title: "cow.json",
84
- description: "A cow project: the organization it deploys to."
102
+ description: "A cow project: the organization and the project it deploys to."
85
103
  });
86
104
 
87
105
  //#endregion
@@ -1,3 +1,3 @@
1
- import { n as defineJourney, t as CapabilityError } from "./journeys-CxuS-mHn.js";
1
+ import { n as defineJourney, t as CapabilityError } from "./journeys-BcbIxxI-.js";
2
2
 
3
3
  export { CapabilityError, defineJourney };
@@ -1,4 +1,4 @@
1
- import { t as runGuest } from "./driver-BOe7jBFw.js";
1
+ import { t as runGuest } from "./driver-M6LQVMr2.js";
2
2
 
3
3
  //#region src/guest/wasi.ts
4
4
  const STDIN = 0;
package/dist/index.js CHANGED
@@ -7,13 +7,13 @@ import { copyFile, mkdir, readFile, readdir, rm, stat, watch, writeFile } from "
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";
10
- import { existsSync, readFileSync } from "node:fs";
11
- import { fileURLToPath, pathToFileURL } from "node:url";
12
10
  import { execFile, spawn } from "node:child_process";
11
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
12
  import { promisify } from "node:util";
14
13
  import { MessagePort } from "node:worker_threads";
15
14
  import { build, formatMessagesSync } from "esbuild";
16
15
  import { create, extract } from "tar";
16
+ import { existsSync, readFileSync } from "node:fs";
17
17
  import { createServer } from "node:http";
18
18
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
19
19
  import { createInterface } from "node:readline/promises";
@@ -3802,13 +3802,18 @@ const insertJourneyRunSchema = createInsertSchema(journeyRuns);
3802
3802
  */
3803
3803
  const consentPurposeEnum = pgEnum("consent_purpose", ["emailMarketing", "dataProcessing"]);
3804
3804
  /**
3805
- * Derived journey rows: one per (org, environment, key), rebuilt from the
3805
+ * Derived journey rows: one per (org, environment, key), upserted from the
3806
3806
  * manifest inside the deploy transaction. Nothing here is authored through
3807
3807
  * the API, which is why there is no id of its own: the key is the name the
3808
3808
  * author gave the file, and the (org, environment, key) triple is the only
3809
- * identity a journey has. Deleting and reinserting these rows on every
3810
- * deploy is the point, so the row set always matches the environment's
3811
- * current release.
3809
+ * identity a journey has.
3810
+ *
3811
+ * A deploy only ever adds and updates: it owns the keys its release carries
3812
+ * and leaves every other row alone, because an org's journeys can come from
3813
+ * several projects and a developer may deploy a release holding only some of
3814
+ * the files. `projectId` records which project put the row here, which is
3815
+ * what makes a key another project owns a refused deploy rather than a silent
3816
+ * overwrite. Removing a journey is an explicit delete (ADR 0009).
3812
3817
  *
3813
3818
  * `active` is the author's own rollout gate, flattened from the manifest's
3814
3819
  * `environments` list into this row's environment. The operator's kill
@@ -3823,8 +3828,10 @@ const journeys$1 = pgTable("journeys", {
3823
3828
  orgId: text("org_id").notNull(),
3824
3829
  environment: environmentEnum("environment").notNull(),
3825
3830
  key: text("key").notNull(),
3826
- /** The release these rows were derived from; the environment's current one. */
3831
+ /** The release this row was derived from; the one its executions run. */
3827
3832
  releaseId: text("release_id").notNull(),
3833
+ /** The project whose deploy owns this key in this environment. */
3834
+ projectId: text("project_id").notNull(),
3828
3835
  /** The author's labels, from the manifest; the dashboard's only grouping. */
3829
3836
  tags: text("tags").array().notNull().default(sql`'{}'::text[]`),
3830
3837
  trigger: jsonb("trigger").$type().notNull(),
@@ -3932,9 +3939,10 @@ const insertOrgSettingsSchema = createInsertSchema(orgSettings);
3932
3939
  //#region ../../packages/db/src/schema/projects.ts
3933
3940
  /**
3934
3941
  * An org's cow project: the folder a developer runs `cow init` in, and the
3935
- * thing releases belong to. One per org for now (the unique index says so);
3936
- * the row exists rather than hanging releases off the org id directly so
3937
- * many projects per org is additive later without a migration of releases.
3942
+ * thing releases belong to. An org may hold several, one per repo, each with
3943
+ * its own release sequence, its own current release per environment, and its
3944
+ * own slice of the deployed journey rows. The name is how `cow.json` picks
3945
+ * one, so it is unique within the org.
3938
3946
  */
3939
3947
  const projects = pgTable("projects", {
3940
3948
  id: text("id").primaryKey(),
@@ -3942,7 +3950,7 @@ const projects = pgTable("projects", {
3942
3950
  name: text("name").notNull(),
3943
3951
  createdAt: createdAt(),
3944
3952
  updatedAt: updatedAt()
3945
- }, (table) => [uniqueIndex("projects_org_id_unique").on(table.orgId)]);
3953
+ }, (table) => [uniqueIndex("projects_org_id_name_unique").on(table.orgId, table.name)]);
3946
3954
  const selectProjectSchema = createSelectSchema(projects);
3947
3955
  const insertProjectSchema = createInsertSchema(projects);
3948
3956
 
@@ -4048,8 +4056,15 @@ const releaseStatusEnum = pgEnum("release_status", [
4048
4056
  * it doubles as the tenancy key.
4049
4057
  *
4050
4058
  * Immutable except for the compile result, which is why there is no
4051
- * `updatedAt`: `compiledAt` and `error` are the only fields that ever move,
4052
- * and they move exactly once.
4059
+ * `updatedAt`: `compiledAt`, `error` and `compiledEntries` are the only
4060
+ * fields that ever move. The first two move exactly once; `compiledEntries`
4061
+ * climbs while the compile runs, because a push is minutes of silence
4062
+ * otherwise and the manifest's own length is the denominator.
4063
+ *
4064
+ * A counter rather than a row per entry: the question both the CLI and the
4065
+ * dashboard ask is "how far along", which one integer answers at a fraction
4066
+ * of the write volume. Naming which entry is compiling needs per-entry rows
4067
+ * and is a different question.
4053
4068
  */
4054
4069
  const releases$1 = pgTable("releases", {
4055
4070
  id: text("id").primaryKey(),
@@ -4067,7 +4082,9 @@ const releases$1 = pgTable("releases", {
4067
4082
  mode: "date",
4068
4083
  precision: 3
4069
4084
  }),
4070
- error: text("error")
4085
+ error: text("error"),
4086
+ /** How many manifest entries have compiled and passed their checks. */
4087
+ compiledEntries: integer("compiled_entries").notNull().default(0)
4071
4088
  }, (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)]);
4072
4089
  const selectReleaseSchema = createSelectSchema(releases$1);
4073
4090
  const insertReleaseSchema = createInsertSchema(releases$1);
@@ -4087,19 +4104,23 @@ const releaseArtifacts = pgTable("release_artifacts", {
4087
4104
  table.digest
4088
4105
  ] })]);
4089
4106
  /**
4090
- * Append-only: an environment's current release is its latest row, and
4091
- * rollback is another deployment of an earlier release rather than a
4092
- * mutation. That is what makes the deployment history readable and makes
4093
- * "the last two deployments" the whole of `cow rollback`.
4107
+ * Append-only: an environment's current release, for one project, is that
4108
+ * project's latest row, and rollback is another deployment of an earlier
4109
+ * release rather than a mutation. That is what makes the deployment history
4110
+ * readable and makes "the last two deployments" the whole of `cow rollback`.
4111
+ *
4112
+ * `projectId` is the release's own project, denormalised so "this project's
4113
+ * latest deployment in this environment" is one indexed read.
4094
4114
  */
4095
4115
  const deployments$1 = pgTable("deployments", {
4096
4116
  id: text("id").primaryKey(),
4097
4117
  orgId: text("org_id").notNull(),
4098
4118
  environment: environmentEnum("environment").notNull(),
4099
4119
  releaseId: text("release_id").notNull().references(() => releases$1.id),
4120
+ projectId: text("project_id").notNull().references(() => projects.id),
4100
4121
  deployedBy: text("deployed_by").notNull(),
4101
4122
  createdAt: createdAt()
4102
- }, (table) => [index("deployments_org_id_environment_created_at_idx").on(table.orgId, table.environment, table.createdAt)]);
4123
+ }, (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)]);
4103
4124
  const selectDeploymentSchema = createSelectSchema(deployments$1);
4104
4125
  const insertDeploymentSchema = createInsertSchema(deployments$1);
4105
4126
 
@@ -5282,6 +5303,16 @@ const createSenderDomainBodySchema = z.object({ data: senderDomainInputSchema })
5282
5303
  const updateSenderDomainBodySchema = z.object({ data: z.object({ clickTracking: z.boolean() }) });
5283
5304
  /** `q` is a substring search over the domain string. */
5284
5305
  const listSenderDomainsQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
5306
+ /**
5307
+ * Where the one-click DNS setup sends the admin, or null when their DNS
5308
+ * provider does not offer it. Computed per request (it costs a DNS lookup
5309
+ * and two calls to the provider), never stored.
5310
+ */
5311
+ const domainDnsSetupSchema = z.object({
5312
+ url: z.url().nullable(),
5313
+ /** The DNS provider's own name for itself, for the button's label. */
5314
+ provider: z.string().nullable()
5315
+ });
5285
5316
 
5286
5317
  //#endregion
5287
5318
  //#region ../../packages/shared/src/patterns.ts
@@ -5901,19 +5932,37 @@ const dryRunJourneyBodySchema = z.object({ data: z.object({ profileId: z.string(
5901
5932
  //#endregion
5902
5933
  //#region ../../packages/shared/src/journeys-v2/config.ts
5903
5934
  /**
5904
- * `cow.json` (spec: Project layout): the org id and nothing more. The
5905
- * folder name is the project's local name, the environment is always a
5906
- * flag, and auth never lives in the project. Strict, so a typo'd key is a
5907
- * build error rather than a silently ignored setting.
5935
+ * A project's name: what `cow init` asked for, and what the `projects` row
5936
+ * stores. Lives here rather than beside the release DTOs because `cow.json`
5937
+ * is the file a developer types it into; `createProjectBodySchema` reuses it.
5938
+ */
5939
+ const projectNameSchema = z.string().trim().min(1).max(200);
5940
+ /**
5941
+ * `cow.json` (spec: Project layout): the org, and which of its projects this
5942
+ * directory is. The environment is always a flag, and auth never lives in
5943
+ * the project. Strict, so a typo'd key is a build error rather than a
5944
+ * silently ignored setting.
5908
5945
  */
5909
5946
  const cowConfigSchema = z.strictObject({
5910
5947
  $schema: z.url().optional(),
5911
5948
  orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
5949
+ /**
5950
+ * Which project of the org this directory pushes to. One org can hold
5951
+ * several, one per repo, each with its own release sequence and its own
5952
+ * slice of the deployed journeys, so every project says which it is.
5953
+ */
5954
+ project: projectNameSchema,
5912
5955
  /** Overrides the API the CLI talks to; the hosted product needs none. */
5913
- apiUrl: z.url().optional()
5956
+ apiUrl: z.url().optional(),
5957
+ /**
5958
+ * Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
5959
+ * login against one Cowliss's dashboard yields a token the other's API
5960
+ * rejects, so a config that names an API names its dashboard too.
5961
+ */
5962
+ webUrl: z.url().optional()
5914
5963
  }).meta({
5915
5964
  title: "cow.json",
5916
- description: "A cow project: the organization it deploys to."
5965
+ description: "A cow project: the organization and the project it deploys to."
5917
5966
  });
5918
5967
 
5919
5968
  //#endregion
@@ -6000,7 +6049,9 @@ const projectSchema = selectProjectSchema.extend({
6000
6049
  updatedAt: z.iso.datetime()
6001
6050
  });
6002
6051
  /** `cow init` names the project after the folder it created. */
6003
- const createProjectBodySchema = z.object({ data: z.object({ name: z.string().trim().min(1).max(200) }) });
6052
+ const createProjectBodySchema = z.object({ data: z.object({ name: projectNameSchema }) });
6053
+ /** Which project to read: the one the caller's `cow.json` names. */
6054
+ const getProjectQuerySchema = z.object({ name: projectNameSchema });
6004
6055
  const artifactDigestParams = z.object({ digest: digestSchema });
6005
6056
  /**
6006
6057
  * Which kind of artifact the uploaded bytes are. Named by the client rather
@@ -6019,16 +6070,30 @@ const putArtifactQuerySchema = z.object({ kind: z.enum(["bundle", "source"]) });
6019
6070
  *
6020
6071
  * `deployedIn` is not a column: it is the environments whose latest
6021
6072
  * deployment names this release, computed on read, so it can never fall out
6022
- * of sync with the append-only deployment log.
6073
+ * of sync with the append-only deployment log. Neither is `totalEntries`:
6074
+ * the manifest is already here and its journeys plus its templates are the
6075
+ * denominator, so storing it beside `compiledEntries` would only invite the
6076
+ * two to disagree.
6023
6077
  */
6024
6078
  const releaseSchema = selectReleaseSchema.extend({
6025
6079
  manifest: z.union([compiledManifestSchema, manifestSchema]),
6026
6080
  deployedIn: z.array(environmentSchema),
6081
+ /**
6082
+ * How many journeys and templates the manifest holds: the denominator
6083
+ * `compiledEntries` climbs towards, counted on read rather than stored.
6084
+ */
6085
+ totalEntries: z.number().int(),
6027
6086
  createdAt: z.iso.datetime(),
6028
6087
  compiledAt: z.iso.datetime().nullable()
6029
6088
  });
6030
- const createReleaseBodySchema = z.object({ data: z.object({ manifest: manifestSchema }) });
6031
- const listReleasesQuerySchema = paginationQuerySchema;
6089
+ const createReleaseBodySchema = z.object({ data: z.object({
6090
+ manifest: manifestSchema,
6091
+ /** The `cow.json` project this push belongs to. */
6092
+ project: projectNameSchema
6093
+ }) });
6094
+ const listReleasesQuerySchema = paginationQuerySchema.extend({
6095
+ /** One project's own releases; absent lists the org's. */
6096
+ project: projectNameSchema.optional() });
6032
6097
  /**
6033
6098
  * How many live executions the cancel sweep was asked to stop, and the
6034
6099
  * workflow doing the stopping. The count is taken when the request lands, so
@@ -6046,10 +6111,18 @@ const cancelExecutionsSchema = z.object({
6046
6111
  * A deployment, plus the warnings the deploy raised: segment names in
6047
6112
  * triggers and destination names in spines that do not exist in the target
6048
6113
  * environment. They never block, because either may be created afterwards.
6114
+ * `left` is the other half of the same conversation: what the deploy did not
6115
+ * touch, because a deploy only ever adds and updates (ADR 0009).
6049
6116
  */
6050
6117
  const deploymentSchema = selectDeploymentSchema.extend({
6051
6118
  createdAt: z.iso.datetime(),
6052
6119
  warnings: z.array(z.string()),
6120
+ /**
6121
+ * Journeys this project still runs in the environment that the deployed
6122
+ * release does not carry. A deploy never removes one, so these keep running
6123
+ * until someone deletes them; the CLI prints them and says how.
6124
+ */
6125
+ left: z.array(z.string()),
6053
6126
  /** The deployed release's per-project sequence number, its human handle. */
6054
6127
  releaseSeq: z.number().int()
6055
6128
  });
@@ -6057,7 +6130,9 @@ const createDeploymentBodySchema = z.object({ data: z.object({ releaseId: z.stri
6057
6130
  const listDeploymentsQuerySchema = paginationQuerySchema.extend({
6058
6131
  environment: environmentSchema.optional(),
6059
6132
  /** One release's own deployment history, across both environments. */
6060
- release: z.string().trim().min(1).max(200).optional()
6133
+ release: z.string().trim().min(1).max(200).optional(),
6134
+ /** One project's own deployments; absent lists the org's. */
6135
+ project: projectNameSchema.optional()
6061
6136
  });
6062
6137
 
6063
6138
  //#endregion
@@ -6693,94 +6768,6 @@ async function clearCredentials$1(path = defaultCredentialsPath()) {
6693
6768
  }
6694
6769
  }
6695
6770
 
6696
- //#endregion
6697
- //#region src/lib/config.ts
6698
- const DEFAULT_API_URL = "http://localhost:3400";
6699
- const DEFAULT_WEB_URL = "http://localhost:5273";
6700
- function credentialsPath(env) {
6701
- return env.COW_CREDENTIALS_PATH ?? defaultCredentialsPath();
6702
- }
6703
- function readCredentials(env) {
6704
- return readCredentials$1(credentialsPath(env));
6705
- }
6706
- function writeCredentials(env, credentials) {
6707
- return writeCredentials$1(credentialsPath(env), credentials).then(() => credentialsPath(env));
6708
- }
6709
- function clearCredentials(env) {
6710
- return clearCredentials$1(credentialsPath(env));
6711
- }
6712
- /**
6713
- * The token every command sends, and where it came from. Precedence:
6714
- * `COW_DEPLOY_KEY` > `COW_TOKEN` > the cached login. The deploy key wins
6715
- * because a machine that has one is CI (or a long-running `cow dev`), and a
6716
- * stale session left in `~/.cow` on that machine must not quietly become the
6717
- * credential a pipeline runs as. `COW_TOKEN` is a session token handed over
6718
- * explicitly, so it reports as one and outranks the cached file.
6719
- *
6720
- * A deploy key only reaches the release lifecycle, so a `cow users list` on
6721
- * a box that exports one answers 401 rather than falling back to the
6722
- * session. That is the honest failure: the two credentials are different
6723
- * identities, and silently switching between them per command is how a
6724
- * pipeline ends up passing locally and failing in CI.
6725
- */
6726
- function resolveCredential(env, credentials) {
6727
- if (env.COW_DEPLOY_KEY) return {
6728
- token: env.COW_DEPLOY_KEY,
6729
- kind: "deployKey"
6730
- };
6731
- if (env.COW_TOKEN) return {
6732
- token: env.COW_TOKEN,
6733
- kind: "session"
6734
- };
6735
- if (credentials?.token) return {
6736
- token: credentials.token,
6737
- kind: "session"
6738
- };
6739
- return {
6740
- token: null,
6741
- kind: "none"
6742
- };
6743
- }
6744
- /** Precedence: --api flag > env > credentials (from login) > default. */
6745
- function resolveApiUrl(env, credentials, flag) {
6746
- return flag ?? env.COW_API_URL ?? credentials?.apiUrl ?? "http://localhost:3400";
6747
- }
6748
- /**
6749
- * Decode a JWT payload without verification. Display only: the API is the
6750
- * verifier; the CLI just shows what it is about to send.
6751
- */
6752
- function decodeTokenPayload(token) {
6753
- const parts = token.split(".");
6754
- if (parts.length !== 3) return null;
6755
- try {
6756
- const json = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
6757
- const payload = JSON.parse(json);
6758
- if (typeof payload !== "object" || payload === null) return null;
6759
- return payload;
6760
- } catch {
6761
- return null;
6762
- }
6763
- }
6764
- function decodeSessionToken(token) {
6765
- const payload = decodeTokenPayload(token);
6766
- if (!payload) return null;
6767
- const o = payload.o;
6768
- return {
6769
- userId: typeof payload.sub === "string" ? payload.sub : null,
6770
- orgId: o && typeof o === "object" && typeof o.id === "string" ? o.id : typeof payload.org_id === "string" ? payload.org_id : null,
6771
- role: o && typeof o === "object" && typeof o.rol === "string" ? `org:${o.rol}` : typeof payload.org_role === "string" ? payload.org_role : null,
6772
- expiresAt: typeof payload.exp === "number" ? (/* @__PURE__ */ new Date(payload.exp * 1e3)).toISOString() : null
6773
- };
6774
- }
6775
-
6776
- //#endregion
6777
- //#region src/lib/package.ts
6778
- let cached;
6779
- function readCliPackage() {
6780
- cached ??= readFile(new URL(import.meta.resolve("@cowliss/cli/package.json")), "utf8").then((text) => JSON.parse(text));
6781
- return cached;
6782
- }
6783
-
6784
6771
  //#endregion
6785
6772
  //#region ../../packages/shared/src/stable-json.ts
6786
6773
  /**
@@ -6824,6 +6811,14 @@ function manifestDigestOf(manifest) {
6824
6811
  return digestOf(stableJson(manifest));
6825
6812
  }
6826
6813
 
6814
+ //#endregion
6815
+ //#region src/lib/package.ts
6816
+ let cached;
6817
+ function readCliPackage() {
6818
+ cached ??= readFile(new URL(import.meta.resolve("@cowliss/cli/package.json")), "utf8").then((text) => JSON.parse(text));
6819
+ return cached;
6820
+ }
6821
+
6827
6822
  //#endregion
6828
6823
  //#region src/build/index.ts
6829
6824
  /**
@@ -6836,7 +6831,12 @@ const execFileAsync$1 = promisify(execFile);
6836
6831
  /** Where build output lives, relative to the project root. */
6837
6832
  const BUILD_DIR = join(".cow", "build");
6838
6833
  const TYPES_FILE = join(".cow", "types.d.ts");
6839
- /** The config files the source tarball carries besides `journeys/` and `emails/`. */
6834
+ /**
6835
+ * The config files the source tarball carries besides `journeys/` and
6836
+ * `emails/`. Always the committed `cow.json`, never a `--config` override: a
6837
+ * release records the project as the repo declares it, not the local file
6838
+ * whoever pushed happened to point at.
6839
+ */
6840
6840
  const CONFIG_FILES$1 = [
6841
6841
  "cow.json",
6842
6842
  "package.json",
@@ -7212,18 +7212,54 @@ async function writeSourceTarball(projectDir, outFile) {
7212
7212
  }, files);
7213
7213
  return (await stat(outFile)).size;
7214
7214
  }
7215
- /** The project's cow.json; throws unless `projectDir` holds a valid one. */
7215
+ /** The project config a checkout carries by default. */
7216
+ const DEFAULT_CONFIG_FILE = "cow.json";
7217
+ /**
7218
+ * Which project config every command in this process reads. Resolved once
7219
+ * from `--config`/`COW_CONFIG` at startup rather than threaded through nine
7220
+ * call sites: the CLI is one shot, and the answer cannot change mid-run.
7221
+ */
7222
+ let configFile = DEFAULT_CONFIG_FILE;
7223
+ function setConfigFile(name) {
7224
+ configFile = name;
7225
+ }
7226
+ function projectConfigFile() {
7227
+ return configFile;
7228
+ }
7229
+ /** The project's config; throws unless `projectDir` holds a valid one. */
7216
7230
  async function assertCowConfig(projectDir) {
7231
+ const name = projectConfigFile();
7217
7232
  let text;
7218
7233
  try {
7219
- text = await readFile(join(projectDir, "cow.json"), "utf8");
7234
+ text = await readFile(join(projectDir, name), "utf8");
7220
7235
  } catch {
7221
- throw new Error(`No cow.json in "${projectDir}". Run \`cow init\` to create a project.`);
7236
+ throw new Error(name === "cow.json" ? `No ${name} in "${projectDir}". Run \`cow init\` to create a project.` : `No ${name} in "${projectDir}". That file was selected with --config or COW_CONFIG.`);
7222
7237
  }
7223
7238
  const parsed = cowConfigSchema.safeParse(JSON.parse(text));
7224
- if (!parsed.success) throw new Error(`cow.json is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
7239
+ if (!parsed.success) throw new Error(`${name} is invalid: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`);
7225
7240
  return parsed.data;
7226
7241
  }
7242
+ /** The project's config, or null when this directory has none. */
7243
+ async function readCowConfig(projectDir) {
7244
+ try {
7245
+ return await assertCowConfig(projectDir);
7246
+ } catch {
7247
+ return null;
7248
+ }
7249
+ }
7250
+ /**
7251
+ * Which project of the org this directory belongs to: the `project` in its
7252
+ * `cow.json`, or undefined when there is no `cow.json` here at all. The
7253
+ * undefined case is `cow pull` into an empty directory, which has no project
7254
+ * to scope by yet and reads the org's releases.
7255
+ */
7256
+ async function configuredProject(projectDir) {
7257
+ try {
7258
+ return (await assertCowConfig(projectDir)).project;
7259
+ } catch {
7260
+ return;
7261
+ }
7262
+ }
7227
7263
  /**
7228
7264
  * Builds the project at `projectDir`. Writes `.cow/types.d.ts` first (the
7229
7265
  * typecheck needs it), then the bundles and the source tarball, and
@@ -7332,6 +7368,95 @@ async function buildIfStale(projectDir) {
7332
7368
  if ((await Promise.all(sources.map((file) => stat(join(projectDir, file)).then((stats) => stats.mtimeMs, () => 0)))).some((time) => time > builtAt)) await buildProject(projectDir);
7333
7369
  }
7334
7370
 
7371
+ //#endregion
7372
+ //#region src/lib/config.ts
7373
+ const DEFAULT_API_URL = "http://localhost:3400";
7374
+ const DEFAULT_WEB_URL = "http://localhost:5273";
7375
+ function credentialsPath(env) {
7376
+ return env.COW_CREDENTIALS_PATH ?? defaultCredentialsPath();
7377
+ }
7378
+ function readCredentials(env) {
7379
+ return readCredentials$1(credentialsPath(env));
7380
+ }
7381
+ function writeCredentials(env, credentials) {
7382
+ return writeCredentials$1(credentialsPath(env), credentials).then(() => credentialsPath(env));
7383
+ }
7384
+ function clearCredentials(env) {
7385
+ return clearCredentials$1(credentialsPath(env));
7386
+ }
7387
+ /**
7388
+ * The token every command sends, and where it came from. Precedence:
7389
+ * `COW_DEPLOY_KEY` > `COW_TOKEN` > the cached login. The deploy key wins
7390
+ * because a machine that has one is CI (or a long-running `cow dev`), and a
7391
+ * stale session left in `~/.cow` on that machine must not quietly become the
7392
+ * credential a pipeline runs as. `COW_TOKEN` is a session token handed over
7393
+ * explicitly, so it reports as one and outranks the cached file.
7394
+ *
7395
+ * A deploy key only reaches the release lifecycle, so a `cow users list` on
7396
+ * a box that exports one answers 401 rather than falling back to the
7397
+ * session. That is the honest failure: the two credentials are different
7398
+ * identities, and silently switching between them per command is how a
7399
+ * pipeline ends up passing locally and failing in CI.
7400
+ */
7401
+ function resolveCredential(env, credentials) {
7402
+ if (env.COW_DEPLOY_KEY) return {
7403
+ token: env.COW_DEPLOY_KEY,
7404
+ kind: "deployKey"
7405
+ };
7406
+ if (env.COW_TOKEN) return {
7407
+ token: env.COW_TOKEN,
7408
+ kind: "session"
7409
+ };
7410
+ if (credentials?.token) return {
7411
+ token: credentials.token,
7412
+ kind: "session"
7413
+ };
7414
+ return {
7415
+ token: null,
7416
+ kind: "none"
7417
+ };
7418
+ }
7419
+ /**
7420
+ * Precedence: `--api` flag > env > the project config > credentials (from
7421
+ * login) > default.
7422
+ *
7423
+ * The project's own `apiUrl` outranks the credentials file because it is the
7424
+ * more specific answer: `cow.json` says where this project deploys, while the
7425
+ * credentials only remember where somebody last logged in. Without it a
7426
+ * checkout whose config names production silently fell through to
7427
+ * `DEFAULT_API_URL`, which is how a production deploy reaches localhost.
7428
+ */
7429
+ function resolveApiUrl(env, credentials, flag, projectApiUrl) {
7430
+ return flag ?? env.COW_API_URL ?? projectApiUrl ?? credentials?.apiUrl ?? "http://localhost:3400";
7431
+ }
7432
+ /**
7433
+ * Decode a JWT payload without verification. Display only: the API is the
7434
+ * verifier; the CLI just shows what it is about to send.
7435
+ */
7436
+ function decodeTokenPayload(token) {
7437
+ const parts = token.split(".");
7438
+ if (parts.length !== 3) return null;
7439
+ try {
7440
+ const json = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
7441
+ const payload = JSON.parse(json);
7442
+ if (typeof payload !== "object" || payload === null) return null;
7443
+ return payload;
7444
+ } catch {
7445
+ return null;
7446
+ }
7447
+ }
7448
+ function decodeSessionToken(token) {
7449
+ const payload = decodeTokenPayload(token);
7450
+ if (!payload) return null;
7451
+ const o = payload.o;
7452
+ return {
7453
+ userId: typeof payload.sub === "string" ? payload.sub : null,
7454
+ orgId: o && typeof o === "object" && typeof o.id === "string" ? o.id : typeof payload.org_id === "string" ? payload.org_id : null,
7455
+ role: o && typeof o === "object" && typeof o.rol === "string" ? `org:${o.rol}` : typeof payload.org_role === "string" ? payload.org_role : null,
7456
+ expiresAt: typeof payload.exp === "number" ? (/* @__PURE__ */ new Date(payload.exp * 1e3)).toISOString() : null
7457
+ };
7458
+ }
7459
+
7335
7460
  //#endregion
7336
7461
  //#region src/commands/add.ts
7337
7462
  /**
@@ -7408,7 +7533,7 @@ async function login(env, options) {
7408
7533
  if (options.token !== void 0) {
7409
7534
  token = options.token.trim();
7410
7535
  if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
7411
- } else token = await collectTokenViaBrowser(options.webUrl ?? env.COW_WEB_URL ?? "http://localhost:5273", 3e5, options.noOpen === true);
7536
+ } else token = await collectTokenViaBrowser(options.webUrl ?? env.COW_WEB_URL ?? (await readCowConfig(process.cwd()))?.webUrl ?? "http://localhost:5273", 3e5, options.noOpen === true);
7412
7537
  const claims = decodeSessionToken(token);
7413
7538
  if (claims === null) throw new Error("Token is not a decodable JWT");
7414
7539
  if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
@@ -7448,7 +7573,14 @@ function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
7448
7573
  clearTimeout(timer);
7449
7574
  try {
7450
7575
  const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7451
- const token = typeof body === "object" && body !== null && "token" in body ? body.token : void 0;
7576
+ const field = (name) => typeof body === "object" && body !== null && name in body ? body[name] : void 0;
7577
+ if (field("denied") === true) {
7578
+ res.writeHead(200, { "Content-Type": "application/json" });
7579
+ res.end(JSON.stringify({ ok: true }));
7580
+ reject(/* @__PURE__ */ new Error("Sign-in was denied in the browser. No access was granted."));
7581
+ return;
7582
+ }
7583
+ const token = field("token");
7452
7584
  if (typeof token !== "string" || token.length === 0) {
7453
7585
  res.writeHead(400, { "Content-Type": "application/json" });
7454
7586
  res.end(JSON.stringify({ error: "missing token" }));
@@ -7495,7 +7627,7 @@ function openBrowser(url) {
7495
7627
  //#region src/commands/auth.ts
7496
7628
  /** login/logout/whoami: session-token management, no contract route. */
7497
7629
  function registerAuth(program, env, io) {
7498
- program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a Clerk session token instead of the browser flow").option("--web <url>", `dashboard URL (default ${DEFAULT_WEB_URL})`).option("--no-open", "do not open the browser; print the URL and wait (headless/agent use)").action(async (opts) => {
7630
+ program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a Clerk session token instead of the browser flow").option("--web <url>", `dashboard URL; overrides COW_WEB_URL and webUrl in the project config (default ${DEFAULT_WEB_URL})`).option("--no-open", "do not open the browser; print the URL and wait (headless/agent use)").action(async (opts) => {
7499
7631
  const outcome = await login(env, {
7500
7632
  token: opts.token,
7501
7633
  webUrl: opts.web,
@@ -8544,6 +8676,21 @@ const domains = defineModule(defineRoute({
8544
8676
  ...sessionErrors,
8545
8677
  ...errors("conflict", "validation_failed", "malformed_request", "dependency_unavailable")
8546
8678
  }
8679
+ }), defineRoute({
8680
+ method: "get",
8681
+ path: "/v1/domains/{id}/dns-setup",
8682
+ operationId: "domains.dnsSetup",
8683
+ tags: ["domains"],
8684
+ summary: "Where the DNS provider can publish the records",
8685
+ 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.",
8686
+ security: SESSION_AUTH,
8687
+ surfaces: HIDDEN_FROM_TOOLS,
8688
+ request: { params: params$7 },
8689
+ responses: {
8690
+ 200: envelope(domainDnsSetupSchema),
8691
+ ...sessionErrors,
8692
+ ...errors("not_found")
8693
+ }
8547
8694
  }), defineRoute({
8548
8695
  method: "post",
8549
8696
  path: "/v1/domains/{id}/verify",
@@ -8879,6 +9026,19 @@ const journeys = defineModule(defineRoute({
8879
9026
  ...sessionErrors,
8880
9027
  ...errors("not_found", "validation_failed", "malformed_request")
8881
9028
  }
9029
+ }), defineRoute({
9030
+ method: "delete",
9031
+ path: "/v1/journeys/{key}",
9032
+ operationId: "journeys.delete",
9033
+ tags: ["journeys"],
9034
+ summary: "Remove a journey from the selected environment",
9035
+ security: SESSION_AUTH,
9036
+ request: { params: params$5 },
9037
+ responses: {
9038
+ 200: envelope(deletedSchema),
9039
+ ...sessionErrors,
9040
+ ...errors("not_found")
9041
+ }
8882
9042
  }), defineRoute({
8883
9043
  method: "post",
8884
9044
  path: "/v1/journeys/{key}/dry-run",
@@ -8900,28 +9060,29 @@ const journeys = defineModule(defineRoute({
8900
9060
  //#endregion
8901
9061
  //#region ../../packages/shared/src/contract/project.ts
8902
9062
  /**
8903
- * The org's project. Singular in the path on purpose: an org has exactly one
8904
- * for now, so there is no id to put in a URL, and many projects per org is a
8905
- * later, additive change (the `projects` row already exists so it can be).
9063
+ * The org's project. Singular in the path on purpose: a caller always means
9064
+ * one, the one its `cow.json` names, so the name is a query parameter and
9065
+ * there is no id to put in a URL. An org with a single project needs neither.
8906
9066
  */
8907
9067
  const project = defineModule(defineRoute({
8908
9068
  method: "get",
8909
9069
  path: "/v1/project",
8910
9070
  operationId: "project.get",
8911
9071
  tags: ["project"],
8912
- summary: "Get the organization's project",
9072
+ summary: "Get one of the organization's projects",
8913
9073
  security: RELEASE_AUTH,
9074
+ request: { query: getProjectQuerySchema },
8914
9075
  responses: {
8915
9076
  200: envelope(projectSchema),
8916
9077
  ...sessionErrors,
8917
- ...errors("project_missing")
9078
+ ...errors("project_missing", "validation_failed", "malformed_request")
8918
9079
  }
8919
9080
  }), defineRoute({
8920
9081
  method: "post",
8921
9082
  path: "/v1/project",
8922
9083
  operationId: "project.create",
8923
9084
  tags: ["project"],
8924
- summary: "Create the organization's project",
9085
+ summary: "Create a project in the organization",
8925
9086
  security: RELEASE_AUTH,
8926
9087
  request: { body: jsonBody(createProjectBodySchema) },
8927
9088
  responses: {
@@ -9987,18 +10148,19 @@ const POLL_BUDGET_MS = RELEASE_COMPILE_TIMEOUT_MS + 6e4;
9987
10148
  const POLL_START_MS = 250;
9988
10149
  const POLL_MAX_MS = 2e3;
9989
10150
  /**
9990
- * The org's project, created when it has none. `cow init` usually got there
9991
- * first; a project pushed from a machine that only ever cloned the repo has
9992
- * not, and creating it here is what makes that clone work.
10151
+ * This directory's project, created when the org has none by that name.
10152
+ * `cow init` usually got there first; a project pushed from a machine that
10153
+ * only ever cloned the repo has not, and creating it here is what makes that
10154
+ * clone work. The name is `cow.json`'s `project`.
9993
10155
  */
9994
- async function ensureProject(client, projectDir) {
10156
+ async function ensureProject(client, name) {
9995
10157
  try {
9996
- return (await client.request(contract.project["project.get"])).data;
10158
+ return (await client.request(contract.project["project.get"], { query: { name } })).data;
9997
10159
  } catch (error) {
9998
10160
  if (!(error instanceof ApiError) || error.code !== "project_missing") throw error;
9999
10161
  }
10000
10162
  try {
10001
- return (await client.request(contract.project["project.create"], { body: { name: basename(projectDir) } })).data;
10163
+ return (await client.request(contract.project["project.create"], { body: { name } })).data;
10002
10164
  } catch (error) {
10003
10165
  const reason = error instanceof Error ? error.message : String(error);
10004
10166
  throw new Error(`This organization has no project and one could not be created (${reason}). Run \`cow init\` first.`);
@@ -10035,33 +10197,72 @@ function artifactFiles(projectDir, manifest) {
10035
10197
  }
10036
10198
  ];
10037
10199
  }
10038
- /** The most recent release, or null when the project has none yet. */
10039
- async function latestRelease(client) {
10040
- return (await client.request(contract.releases["releases.list"], { query: { limit: 1 } })).data[0] ?? null;
10200
+ /**
10201
+ * This project's most recent release, or null when it has none yet. Scoped to
10202
+ * the project because a sibling repo's push must not make this one look
10203
+ * unchanged, or changed.
10204
+ */
10205
+ async function latestRelease(client, project) {
10206
+ return (await client.request(contract.releases["releases.list"], { query: {
10207
+ limit: 1,
10208
+ project
10209
+ } })).data[0] ?? null;
10210
+ }
10211
+ /**
10212
+ * How far a compile has got, as `18/26`. Only meaningful while the release is
10213
+ * pending: a `ready` release can legitimately sit below its total (every row
10214
+ * from before the counter existed reads 0, and the last progress write is
10215
+ * advisory), so completion is read from the status, never from this.
10216
+ */
10217
+ function compiledFraction(release) {
10218
+ return `${release.compiledEntries}/${release.totalEntries}`;
10219
+ }
10220
+ /**
10221
+ * The progress writer for a terminal, or nothing at all when there is no one
10222
+ * watching. It rewrites one line on stderr, so what a command prints to
10223
+ * stdout is the same bytes whether or not a terminal is attached.
10224
+ *
10225
+ * The flag is stdout's: a redirected push is a script reading the summary,
10226
+ * and it gets no cursor codes in either stream. `--json` is silent for the
10227
+ * same reason, since its whole output is one envelope.
10228
+ */
10229
+ function progressLineFor(io, json) {
10230
+ if (!io.isTTY || json === true) return;
10231
+ let drawn = false;
10232
+ return (text) => {
10233
+ if (text === "" && !drawn) return;
10234
+ drawn = text !== "";
10235
+ io.stderr(`\r\x1b[2K${text}`);
10236
+ };
10041
10237
  }
10042
10238
  /**
10043
10239
  * Poll until the compile settles. Backs off from a quarter second to two,
10044
10240
  * because a small project is ready almost at once and a large one is not
10045
10241
  * worth asking about ten times a second.
10046
10242
  */
10047
- async function awaitCompile(client, releaseId, sleep) {
10243
+ async function awaitCompile(client, releaseId, sleep, progress) {
10048
10244
  let waited = 0;
10049
10245
  let interval = POLL_START_MS;
10050
- for (;;) {
10051
- const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
10052
- if (data.status !== "pending") return data;
10053
- if (waited >= POLL_BUDGET_MS) throw new Error(`Release ${releaseId} is still compiling after ${Math.round(waited / 1e3)}s. Check \`cow releases get ${releaseId}\`.`);
10054
- await sleep(interval);
10055
- waited += interval;
10056
- interval = Math.min(interval * 2, POLL_MAX_MS);
10246
+ try {
10247
+ for (;;) {
10248
+ const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
10249
+ if (data.status !== "pending") return data;
10250
+ 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}\`.`);
10251
+ progress?.(`Compiling ${compiledFraction(data)}`);
10252
+ await sleep(interval);
10253
+ waited += interval;
10254
+ interval = Math.min(interval * 2, POLL_MAX_MS);
10255
+ }
10256
+ } finally {
10257
+ progress?.("");
10057
10258
  }
10058
10259
  }
10059
- async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeout$1(ms) }) {
10260
+ async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeout$1(ms), progress }) {
10060
10261
  const { manifest, config } = await buildProject(projectDir);
10061
- const project = await ensureProject(client, projectDir);
10262
+ const project = await ensureProject(client, config.project);
10062
10263
  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, or use its deploy key.`);
10063
10264
  const digest = manifestDigestOf(manifest);
10064
- const latest = await latestRelease(client);
10265
+ const latest = await latestRelease(client, project.name);
10065
10266
  if (latest && latest.manifestDigest === digest && !force) return {
10066
10267
  release: latest,
10067
10268
  uploaded: [],
@@ -10078,7 +10279,10 @@ async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeo
10078
10279
  uploaded.push(artifact.digest);
10079
10280
  }
10080
10281
  return {
10081
- release: await awaitCompile(client, (await client.request(contract.releases["releases.create"], { body: { manifest } })).data.id, sleep),
10282
+ release: await awaitCompile(client, (await client.request(contract.releases["releases.create"], { body: {
10283
+ manifest,
10284
+ project: project.name
10285
+ } })).data.id, sleep, progress),
10082
10286
  uploaded
10083
10287
  };
10084
10288
  }
@@ -10099,7 +10303,8 @@ function registerPush(program, clientFor, io) {
10099
10303
  const outcome = await pushProject({
10100
10304
  client: await clientFor(merged),
10101
10305
  projectDir: process.cwd(),
10102
- force: opts.force === true
10306
+ force: opts.force === true,
10307
+ progress: progressLineFor(io, merged.json === true)
10103
10308
  });
10104
10309
  if (merged.json === true) emit({ data: outcome.release }, io, true);
10105
10310
  else io.stdout(`${pushSummary(outcome)}\n`);
@@ -10110,7 +10315,7 @@ function registerPush(program, clientFor, io) {
10110
10315
  //#endregion
10111
10316
  //#region src/commands/deploy.ts
10112
10317
  /** The release to deploy, and the push message when a push produced it. */
10113
- async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
10318
+ async function releaseToDeploy({ client, projectDir, releaseId, sleep, progress }) {
10114
10319
  if (releaseId) {
10115
10320
  const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
10116
10321
  return { release: data };
@@ -10118,7 +10323,8 @@ async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
10118
10323
  const pushed = await pushProject({
10119
10324
  client,
10120
10325
  projectDir,
10121
- sleep
10326
+ sleep,
10327
+ progress
10122
10328
  });
10123
10329
  return {
10124
10330
  release: pushed.release,
@@ -10127,7 +10333,7 @@ async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
10127
10333
  }
10128
10334
  async function deployProject(options) {
10129
10335
  const { release, skipped } = await releaseToDeploy(options);
10130
- 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; wait for it to be ready.`);
10336
+ 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.`);
10131
10337
  const { data } = await options.client.request(contract.deployments["deployments.create"], { body: { releaseId: release.id } });
10132
10338
  return {
10133
10339
  deployment: data,
@@ -10141,6 +10347,7 @@ function deploySummary(outcome) {
10141
10347
  const lines = [`${release.id} #${release.seq} deployed to ${deployment.environment}.`];
10142
10348
  if (outcome.skipped) lines.unshift(outcome.skipped);
10143
10349
  for (const warning of deployment.warnings) lines.push(`Warning: ${warning}`);
10350
+ 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}\`.`);
10144
10351
  return lines.join("\n");
10145
10352
  }
10146
10353
  function registerDeploy(program, clientFor, env, io) {
@@ -10153,7 +10360,8 @@ function registerDeploy(program, clientFor, env, io) {
10153
10360
  const outcome = await deployProject({
10154
10361
  client: await clientFor(merged),
10155
10362
  projectDir: process.cwd(),
10156
- releaseId: opts.release
10363
+ releaseId: opts.release,
10364
+ progress: progressLineFor(io, merged.json === true)
10157
10365
  });
10158
10366
  if (merged.json === true) {
10159
10367
  emit({ data: outcome.deployment }, io, true);
@@ -10542,7 +10750,8 @@ function registerInit(program, env, io) {
10542
10750
  if (orgId === "") throw new Error("No organization id. Pass --org org_… or run `cow login` first.");
10543
10751
  const config = cowConfigSchema.parse({
10544
10752
  $schema: COW_CONFIG_SCHEMA_URL,
10545
- orgId
10753
+ orgId,
10754
+ project: name
10546
10755
  });
10547
10756
  const contents = {
10548
10757
  "cow.json": json(config),
@@ -10752,12 +10961,17 @@ function noGitHere(error) {
10752
10961
  const failure = error;
10753
10962
  return failure.code === "ENOENT" || String(failure.stderr ?? "").includes("not a git repository");
10754
10963
  }
10755
- /** The release to restore: the one named, the one an environment runs, else the newest that compiled. */
10756
- async function resolveRelease(client, releaseId, environment) {
10964
+ /**
10965
+ * The release to restore: the one named, the one an environment runs, else the
10966
+ * newest that compiled. Scoped to this directory's project, so a sibling
10967
+ * repo's release is never unpacked over this one's files.
10968
+ */
10969
+ async function resolveRelease(client, releaseId, environment, project) {
10757
10970
  if (releaseId) return (await client.request(contract.releases["releases.get"], { params: { id: releaseId } })).data;
10758
10971
  if (environment) {
10759
10972
  const current = (await client.request(contract.deployments["deployments.list"], { query: {
10760
10973
  environment,
10974
+ project,
10761
10975
  limit: 1
10762
10976
  } })).data[0];
10763
10977
  if (!current) throw new Error(`Nothing has been deployed to ${environment}, so there is no release to restore. Name one with --release.`);
@@ -10767,7 +10981,8 @@ async function resolveRelease(client, releaseId, environment) {
10767
10981
  do {
10768
10982
  const page = await client.request(contract.releases["releases.list"], { query: {
10769
10983
  limit: PAGE_SIZE,
10770
- cursor
10984
+ cursor,
10985
+ project
10771
10986
  } });
10772
10987
  const ready = page.data.find((release) => release.status === "ready");
10773
10988
  if (ready) return ready;
@@ -10776,7 +10991,7 @@ async function resolveRelease(client, releaseId, environment) {
10776
10991
  throw new Error("This organization has no release that compiled successfully. Run `cow push` first, or name one with --release.");
10777
10992
  }
10778
10993
  async function pullRelease({ client, projectDir, releaseId, environment, force }) {
10779
- const release = await resolveRelease(client, releaseId, environment);
10994
+ const release = await resolveRelease(client, releaseId, environment, await configuredProject(projectDir));
10780
10995
  if (!force) {
10781
10996
  const dirty = await dirtyPaths(projectDir);
10782
10997
  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.`);
@@ -10820,9 +11035,10 @@ function registerPull(program, clientFor, env, io) {
10820
11035
 
10821
11036
  //#endregion
10822
11037
  //#region src/commands/rollback.ts
10823
- async function rollbackEnvironment({ client, environment }) {
11038
+ async function rollbackEnvironment({ client, environment, project }) {
10824
11039
  const [current, previous] = (await client.request(contract.deployments["deployments.list"], { query: {
10825
11040
  environment,
11041
+ project,
10826
11042
  limit: 2
10827
11043
  } })).data;
10828
11044
  if (!current) throw new Error(`Nothing has been deployed to ${environment} yet, so there is nothing to roll back.`);
@@ -10849,7 +11065,8 @@ function registerRollback(program, clientFor, env, io) {
10849
11065
  };
10850
11066
  const outcome = await rollbackEnvironment({
10851
11067
  client: await clientFor(merged),
10852
- environment: requireEnvironment(env, merged)
11068
+ environment: requireEnvironment(env, merged),
11069
+ project: await configuredProject(process.cwd())
10853
11070
  });
10854
11071
  if (merged.json === true) {
10855
11072
  emit({ data: outcome.deployment }, io, true);
@@ -11146,11 +11363,15 @@ function emit(result, io, json) {
11146
11363
  }
11147
11364
  function buildProgram(env, io) {
11148
11365
  const program = new Command();
11149
- program.name("cow").description("Admin CLI for Cowliss: a thin client of the dashboard API").option("--api <url>", "API base URL (overrides env and saved config)").option("--env <environment>", `environment to act on: ${ENVIRONMENTS.join(" | ")} (overrides COW_ENVIRONMENT; default ${DEFAULT_ENVIRONMENT})`, parseEnvironment).option("--json", "force compact single-line JSON output");
11366
+ program.name("cow").description("Admin CLI for Cowliss: a thin client of the dashboard API").option("--api <url>", "API base URL (overrides env and saved config)").option("--env <environment>", `environment to act on: ${ENVIRONMENTS.join(" | ")} (overrides COW_ENVIRONMENT; default ${DEFAULT_ENVIRONMENT})`, parseEnvironment).option("--json", "force compact single-line JSON output").option("--config <file>", `project config to read instead of ${DEFAULT_CONFIG_FILE} (overrides COW_CONFIG)`);
11367
+ program.hook("preAction", () => {
11368
+ const flag = program.opts().config;
11369
+ setConfigFile((typeof flag === "string" ? flag : void 0) ?? env.COW_CONFIG ?? "cow.json");
11370
+ });
11150
11371
  const clientFor = async (opts) => {
11151
11372
  const credentials = await readCredentials(env);
11152
11373
  return createClient({
11153
- baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0),
11374
+ baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(process.cwd()))?.apiUrl),
11154
11375
  auth: async () => resolveCredential(env, await readCredentials(env)).token,
11155
11376
  headers: async () => ({
11156
11377
  [ENVIRONMENT_HEADER]: environmentFor(env, opts),
@@ -11195,7 +11416,13 @@ const envSchema = z.object({
11195
11416
  /** An org deploy key for CI; when set it wins over the cached session (ticket 05 reads it). */
11196
11417
  COW_DEPLOY_KEY: z.string().min(1).optional(),
11197
11418
  /** The environment admin calls select; `--env` overrides it, production is the default. */
11198
- COW_ENVIRONMENT: environmentSchema.optional()
11419
+ COW_ENVIRONMENT: environmentSchema.optional(),
11420
+ /**
11421
+ * The project config to read instead of `cow.json`, so one checkout can
11422
+ * hold both the committed production config and a local override.
11423
+ * `--config` overrides it.
11424
+ */
11425
+ COW_CONFIG: z.string().min(1).optional()
11199
11426
  });
11200
11427
  function loadEnv() {
11201
11428
  const parsed = envSchema.safeParse(process.env);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"