@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.
- package/dist/guest/{driver-BOe7jBFw.js → driver-M6LQVMr2.js} +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{journeys-CxuS-mHn.js → journeys-BcbIxxI-.js} +24 -6
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +395 -168
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-
|
|
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";
|
package/dist/guest/driver.js
CHANGED
|
@@ -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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
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
|
package/dist/guest/journeys.js
CHANGED
package/dist/guest/wasi.js
CHANGED
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),
|
|
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.
|
|
3810
|
-
*
|
|
3811
|
-
*
|
|
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
|
|
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.
|
|
3936
|
-
*
|
|
3937
|
-
*
|
|
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("
|
|
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 `
|
|
4052
|
-
*
|
|
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
|
|
4091
|
-
* rollback is another deployment of an earlier
|
|
4092
|
-
* mutation. That is what makes the deployment history
|
|
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
|
-
*
|
|
5905
|
-
*
|
|
5906
|
-
*
|
|
5907
|
-
|
|
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:
|
|
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({
|
|
6031
|
-
|
|
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
|
-
/**
|
|
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
|
|
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,
|
|
7234
|
+
text = await readFile(join(projectDir, name), "utf8");
|
|
7220
7235
|
} catch {
|
|
7221
|
-
throw new Error(
|
|
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(
|
|
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
|
|
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:
|
|
8904
|
-
*
|
|
8905
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
9991
|
-
* first; a project pushed from a machine that
|
|
9992
|
-
* not, and creating it here is what makes that
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
10039
|
-
|
|
10040
|
-
|
|
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
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
|
|
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,
|
|
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: {
|
|
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
|
-
/**
|
|
10756
|
-
|
|
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);
|