@cowliss/cli 0.2.0 → 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-D8cPyQgF.js → driver-M6LQVMr2.js} +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/{journeys-CR_wIAtP.js → journeys-BcbIxxI-.js} +7 -1
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +258 -121
- 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
|
@@ -90,7 +90,13 @@ const cowConfigSchema = z.strictObject({
|
|
|
90
90
|
*/
|
|
91
91
|
project: projectNameSchema,
|
|
92
92
|
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
93
|
-
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()
|
|
94
100
|
}).meta({
|
|
95
101
|
title: "cow.json",
|
|
96
102
|
description: "A cow project: the organization and the project it deploys to."
|
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";
|
|
@@ -4056,8 +4056,15 @@ const releaseStatusEnum = pgEnum("release_status", [
|
|
|
4056
4056
|
* it doubles as the tenancy key.
|
|
4057
4057
|
*
|
|
4058
4058
|
* Immutable except for the compile result, which is why there is no
|
|
4059
|
-
* `updatedAt`: `compiledAt` and `
|
|
4060
|
-
*
|
|
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.
|
|
4061
4068
|
*/
|
|
4062
4069
|
const releases$1 = pgTable("releases", {
|
|
4063
4070
|
id: text("id").primaryKey(),
|
|
@@ -4075,7 +4082,9 @@ const releases$1 = pgTable("releases", {
|
|
|
4075
4082
|
mode: "date",
|
|
4076
4083
|
precision: 3
|
|
4077
4084
|
}),
|
|
4078
|
-
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)
|
|
4079
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)]);
|
|
4080
4089
|
const selectReleaseSchema = createSelectSchema(releases$1);
|
|
4081
4090
|
const insertReleaseSchema = createInsertSchema(releases$1);
|
|
@@ -5294,6 +5303,16 @@ const createSenderDomainBodySchema = z.object({ data: senderDomainInputSchema })
|
|
|
5294
5303
|
const updateSenderDomainBodySchema = z.object({ data: z.object({ clickTracking: z.boolean() }) });
|
|
5295
5304
|
/** `q` is a substring search over the domain string. */
|
|
5296
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
|
+
});
|
|
5297
5316
|
|
|
5298
5317
|
//#endregion
|
|
5299
5318
|
//#region ../../packages/shared/src/patterns.ts
|
|
@@ -5934,7 +5953,13 @@ const cowConfigSchema = z.strictObject({
|
|
|
5934
5953
|
*/
|
|
5935
5954
|
project: projectNameSchema,
|
|
5936
5955
|
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
5937
|
-
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()
|
|
5938
5963
|
}).meta({
|
|
5939
5964
|
title: "cow.json",
|
|
5940
5965
|
description: "A cow project: the organization and the project it deploys to."
|
|
@@ -6045,11 +6070,19 @@ const putArtifactQuerySchema = z.object({ kind: z.enum(["bundle", "source"]) });
|
|
|
6045
6070
|
*
|
|
6046
6071
|
* `deployedIn` is not a column: it is the environments whose latest
|
|
6047
6072
|
* deployment names this release, computed on read, so it can never fall out
|
|
6048
|
-
* 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.
|
|
6049
6077
|
*/
|
|
6050
6078
|
const releaseSchema = selectReleaseSchema.extend({
|
|
6051
6079
|
manifest: z.union([compiledManifestSchema, manifestSchema]),
|
|
6052
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(),
|
|
6053
6086
|
createdAt: z.iso.datetime(),
|
|
6054
6087
|
compiledAt: z.iso.datetime().nullable()
|
|
6055
6088
|
});
|
|
@@ -6735,94 +6768,6 @@ async function clearCredentials$1(path = defaultCredentialsPath()) {
|
|
|
6735
6768
|
}
|
|
6736
6769
|
}
|
|
6737
6770
|
|
|
6738
|
-
//#endregion
|
|
6739
|
-
//#region src/lib/config.ts
|
|
6740
|
-
const DEFAULT_API_URL = "http://localhost:3400";
|
|
6741
|
-
const DEFAULT_WEB_URL = "http://localhost:5273";
|
|
6742
|
-
function credentialsPath(env) {
|
|
6743
|
-
return env.COW_CREDENTIALS_PATH ?? defaultCredentialsPath();
|
|
6744
|
-
}
|
|
6745
|
-
function readCredentials(env) {
|
|
6746
|
-
return readCredentials$1(credentialsPath(env));
|
|
6747
|
-
}
|
|
6748
|
-
function writeCredentials(env, credentials) {
|
|
6749
|
-
return writeCredentials$1(credentialsPath(env), credentials).then(() => credentialsPath(env));
|
|
6750
|
-
}
|
|
6751
|
-
function clearCredentials(env) {
|
|
6752
|
-
return clearCredentials$1(credentialsPath(env));
|
|
6753
|
-
}
|
|
6754
|
-
/**
|
|
6755
|
-
* The token every command sends, and where it came from. Precedence:
|
|
6756
|
-
* `COW_DEPLOY_KEY` > `COW_TOKEN` > the cached login. The deploy key wins
|
|
6757
|
-
* because a machine that has one is CI (or a long-running `cow dev`), and a
|
|
6758
|
-
* stale session left in `~/.cow` on that machine must not quietly become the
|
|
6759
|
-
* credential a pipeline runs as. `COW_TOKEN` is a session token handed over
|
|
6760
|
-
* explicitly, so it reports as one and outranks the cached file.
|
|
6761
|
-
*
|
|
6762
|
-
* A deploy key only reaches the release lifecycle, so a `cow users list` on
|
|
6763
|
-
* a box that exports one answers 401 rather than falling back to the
|
|
6764
|
-
* session. That is the honest failure: the two credentials are different
|
|
6765
|
-
* identities, and silently switching between them per command is how a
|
|
6766
|
-
* pipeline ends up passing locally and failing in CI.
|
|
6767
|
-
*/
|
|
6768
|
-
function resolveCredential(env, credentials) {
|
|
6769
|
-
if (env.COW_DEPLOY_KEY) return {
|
|
6770
|
-
token: env.COW_DEPLOY_KEY,
|
|
6771
|
-
kind: "deployKey"
|
|
6772
|
-
};
|
|
6773
|
-
if (env.COW_TOKEN) return {
|
|
6774
|
-
token: env.COW_TOKEN,
|
|
6775
|
-
kind: "session"
|
|
6776
|
-
};
|
|
6777
|
-
if (credentials?.token) return {
|
|
6778
|
-
token: credentials.token,
|
|
6779
|
-
kind: "session"
|
|
6780
|
-
};
|
|
6781
|
-
return {
|
|
6782
|
-
token: null,
|
|
6783
|
-
kind: "none"
|
|
6784
|
-
};
|
|
6785
|
-
}
|
|
6786
|
-
/** Precedence: --api flag > env > credentials (from login) > default. */
|
|
6787
|
-
function resolveApiUrl(env, credentials, flag) {
|
|
6788
|
-
return flag ?? env.COW_API_URL ?? credentials?.apiUrl ?? "http://localhost:3400";
|
|
6789
|
-
}
|
|
6790
|
-
/**
|
|
6791
|
-
* Decode a JWT payload without verification. Display only: the API is the
|
|
6792
|
-
* verifier; the CLI just shows what it is about to send.
|
|
6793
|
-
*/
|
|
6794
|
-
function decodeTokenPayload(token) {
|
|
6795
|
-
const parts = token.split(".");
|
|
6796
|
-
if (parts.length !== 3) return null;
|
|
6797
|
-
try {
|
|
6798
|
-
const json = Buffer.from(parts[1] ?? "", "base64url").toString("utf8");
|
|
6799
|
-
const payload = JSON.parse(json);
|
|
6800
|
-
if (typeof payload !== "object" || payload === null) return null;
|
|
6801
|
-
return payload;
|
|
6802
|
-
} catch {
|
|
6803
|
-
return null;
|
|
6804
|
-
}
|
|
6805
|
-
}
|
|
6806
|
-
function decodeSessionToken(token) {
|
|
6807
|
-
const payload = decodeTokenPayload(token);
|
|
6808
|
-
if (!payload) return null;
|
|
6809
|
-
const o = payload.o;
|
|
6810
|
-
return {
|
|
6811
|
-
userId: typeof payload.sub === "string" ? payload.sub : null,
|
|
6812
|
-
orgId: o && typeof o === "object" && typeof o.id === "string" ? o.id : typeof payload.org_id === "string" ? payload.org_id : null,
|
|
6813
|
-
role: o && typeof o === "object" && typeof o.rol === "string" ? `org:${o.rol}` : typeof payload.org_role === "string" ? payload.org_role : null,
|
|
6814
|
-
expiresAt: typeof payload.exp === "number" ? (/* @__PURE__ */ new Date(payload.exp * 1e3)).toISOString() : null
|
|
6815
|
-
};
|
|
6816
|
-
}
|
|
6817
|
-
|
|
6818
|
-
//#endregion
|
|
6819
|
-
//#region src/lib/package.ts
|
|
6820
|
-
let cached;
|
|
6821
|
-
function readCliPackage() {
|
|
6822
|
-
cached ??= readFile(new URL(import.meta.resolve("@cowliss/cli/package.json")), "utf8").then((text) => JSON.parse(text));
|
|
6823
|
-
return cached;
|
|
6824
|
-
}
|
|
6825
|
-
|
|
6826
6771
|
//#endregion
|
|
6827
6772
|
//#region ../../packages/shared/src/stable-json.ts
|
|
6828
6773
|
/**
|
|
@@ -6866,6 +6811,14 @@ function manifestDigestOf(manifest) {
|
|
|
6866
6811
|
return digestOf(stableJson(manifest));
|
|
6867
6812
|
}
|
|
6868
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
|
+
|
|
6869
6822
|
//#endregion
|
|
6870
6823
|
//#region src/build/index.ts
|
|
6871
6824
|
/**
|
|
@@ -6878,7 +6831,12 @@ const execFileAsync$1 = promisify(execFile);
|
|
|
6878
6831
|
/** Where build output lives, relative to the project root. */
|
|
6879
6832
|
const BUILD_DIR = join(".cow", "build");
|
|
6880
6833
|
const TYPES_FILE = join(".cow", "types.d.ts");
|
|
6881
|
-
/**
|
|
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
|
+
*/
|
|
6882
6840
|
const CONFIG_FILES$1 = [
|
|
6883
6841
|
"cow.json",
|
|
6884
6842
|
"package.json",
|
|
@@ -7254,18 +7212,41 @@ async function writeSourceTarball(projectDir, outFile) {
|
|
|
7254
7212
|
}, files);
|
|
7255
7213
|
return (await stat(outFile)).size;
|
|
7256
7214
|
}
|
|
7257
|
-
/** 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. */
|
|
7258
7230
|
async function assertCowConfig(projectDir) {
|
|
7231
|
+
const name = projectConfigFile();
|
|
7259
7232
|
let text;
|
|
7260
7233
|
try {
|
|
7261
|
-
text = await readFile(join(projectDir,
|
|
7234
|
+
text = await readFile(join(projectDir, name), "utf8");
|
|
7262
7235
|
} catch {
|
|
7263
|
-
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.`);
|
|
7264
7237
|
}
|
|
7265
7238
|
const parsed = cowConfigSchema.safeParse(JSON.parse(text));
|
|
7266
|
-
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("; ")}`);
|
|
7267
7240
|
return parsed.data;
|
|
7268
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
|
+
}
|
|
7269
7250
|
/**
|
|
7270
7251
|
* Which project of the org this directory belongs to: the `project` in its
|
|
7271
7252
|
* `cow.json`, or undefined when there is no `cow.json` here at all. The
|
|
@@ -7387,6 +7368,95 @@ async function buildIfStale(projectDir) {
|
|
|
7387
7368
|
if ((await Promise.all(sources.map((file) => stat(join(projectDir, file)).then((stats) => stats.mtimeMs, () => 0)))).some((time) => time > builtAt)) await buildProject(projectDir);
|
|
7388
7369
|
}
|
|
7389
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
|
+
|
|
7390
7460
|
//#endregion
|
|
7391
7461
|
//#region src/commands/add.ts
|
|
7392
7462
|
/**
|
|
@@ -7463,7 +7533,7 @@ async function login(env, options) {
|
|
|
7463
7533
|
if (options.token !== void 0) {
|
|
7464
7534
|
token = options.token.trim();
|
|
7465
7535
|
if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
|
|
7466
|
-
} 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);
|
|
7467
7537
|
const claims = decodeSessionToken(token);
|
|
7468
7538
|
if (claims === null) throw new Error("Token is not a decodable JWT");
|
|
7469
7539
|
if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
|
|
@@ -7503,7 +7573,14 @@ function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
|
|
|
7503
7573
|
clearTimeout(timer);
|
|
7504
7574
|
try {
|
|
7505
7575
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
7506
|
-
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");
|
|
7507
7584
|
if (typeof token !== "string" || token.length === 0) {
|
|
7508
7585
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
7509
7586
|
res.end(JSON.stringify({ error: "missing token" }));
|
|
@@ -7550,7 +7627,7 @@ function openBrowser(url) {
|
|
|
7550
7627
|
//#region src/commands/auth.ts
|
|
7551
7628
|
/** login/logout/whoami: session-token management, no contract route. */
|
|
7552
7629
|
function registerAuth(program, env, io) {
|
|
7553
|
-
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) => {
|
|
7554
7631
|
const outcome = await login(env, {
|
|
7555
7632
|
token: opts.token,
|
|
7556
7633
|
webUrl: opts.web,
|
|
@@ -8599,6 +8676,21 @@ const domains = defineModule(defineRoute({
|
|
|
8599
8676
|
...sessionErrors,
|
|
8600
8677
|
...errors("conflict", "validation_failed", "malformed_request", "dependency_unavailable")
|
|
8601
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
|
+
}
|
|
8602
8694
|
}), defineRoute({
|
|
8603
8695
|
method: "post",
|
|
8604
8696
|
path: "/v1/domains/{id}/verify",
|
|
@@ -10117,23 +10209,55 @@ async function latestRelease(client, project) {
|
|
|
10117
10209
|
} })).data[0] ?? null;
|
|
10118
10210
|
}
|
|
10119
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
|
+
};
|
|
10237
|
+
}
|
|
10238
|
+
/**
|
|
10120
10239
|
* Poll until the compile settles. Backs off from a quarter second to two,
|
|
10121
10240
|
* because a small project is ready almost at once and a large one is not
|
|
10122
10241
|
* worth asking about ten times a second.
|
|
10123
10242
|
*/
|
|
10124
|
-
async function awaitCompile(client, releaseId, sleep) {
|
|
10243
|
+
async function awaitCompile(client, releaseId, sleep, progress) {
|
|
10125
10244
|
let waited = 0;
|
|
10126
10245
|
let interval = POLL_START_MS;
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
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?.("");
|
|
10134
10258
|
}
|
|
10135
10259
|
}
|
|
10136
|
-
async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeout$1(ms) }) {
|
|
10260
|
+
async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeout$1(ms), progress }) {
|
|
10137
10261
|
const { manifest, config } = await buildProject(projectDir);
|
|
10138
10262
|
const project = await ensureProject(client, config.project);
|
|
10139
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.`);
|
|
@@ -10158,7 +10282,7 @@ async function pushProject({ client, projectDir, force, sleep = (ms) => setTimeo
|
|
|
10158
10282
|
release: await awaitCompile(client, (await client.request(contract.releases["releases.create"], { body: {
|
|
10159
10283
|
manifest,
|
|
10160
10284
|
project: project.name
|
|
10161
|
-
} })).data.id, sleep),
|
|
10285
|
+
} })).data.id, sleep, progress),
|
|
10162
10286
|
uploaded
|
|
10163
10287
|
};
|
|
10164
10288
|
}
|
|
@@ -10179,7 +10303,8 @@ function registerPush(program, clientFor, io) {
|
|
|
10179
10303
|
const outcome = await pushProject({
|
|
10180
10304
|
client: await clientFor(merged),
|
|
10181
10305
|
projectDir: process.cwd(),
|
|
10182
|
-
force: opts.force === true
|
|
10306
|
+
force: opts.force === true,
|
|
10307
|
+
progress: progressLineFor(io, merged.json === true)
|
|
10183
10308
|
});
|
|
10184
10309
|
if (merged.json === true) emit({ data: outcome.release }, io, true);
|
|
10185
10310
|
else io.stdout(`${pushSummary(outcome)}\n`);
|
|
@@ -10190,7 +10315,7 @@ function registerPush(program, clientFor, io) {
|
|
|
10190
10315
|
//#endregion
|
|
10191
10316
|
//#region src/commands/deploy.ts
|
|
10192
10317
|
/** The release to deploy, and the push message when a push produced it. */
|
|
10193
|
-
async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
|
|
10318
|
+
async function releaseToDeploy({ client, projectDir, releaseId, sleep, progress }) {
|
|
10194
10319
|
if (releaseId) {
|
|
10195
10320
|
const { data } = await client.request(contract.releases["releases.get"], { params: { id: releaseId } });
|
|
10196
10321
|
return { release: data };
|
|
@@ -10198,7 +10323,8 @@ async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
|
|
|
10198
10323
|
const pushed = await pushProject({
|
|
10199
10324
|
client,
|
|
10200
10325
|
projectDir,
|
|
10201
|
-
sleep
|
|
10326
|
+
sleep,
|
|
10327
|
+
progress
|
|
10202
10328
|
});
|
|
10203
10329
|
return {
|
|
10204
10330
|
release: pushed.release,
|
|
@@ -10207,7 +10333,7 @@ async function releaseToDeploy({ client, projectDir, releaseId, sleep }) {
|
|
|
10207
10333
|
}
|
|
10208
10334
|
async function deployProject(options) {
|
|
10209
10335
|
const { release, skipped } = await releaseToDeploy(options);
|
|
10210
|
-
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.`);
|
|
10211
10337
|
const { data } = await options.client.request(contract.deployments["deployments.create"], { body: { releaseId: release.id } });
|
|
10212
10338
|
return {
|
|
10213
10339
|
deployment: data,
|
|
@@ -10234,7 +10360,8 @@ function registerDeploy(program, clientFor, env, io) {
|
|
|
10234
10360
|
const outcome = await deployProject({
|
|
10235
10361
|
client: await clientFor(merged),
|
|
10236
10362
|
projectDir: process.cwd(),
|
|
10237
|
-
releaseId: opts.release
|
|
10363
|
+
releaseId: opts.release,
|
|
10364
|
+
progress: progressLineFor(io, merged.json === true)
|
|
10238
10365
|
});
|
|
10239
10366
|
if (merged.json === true) {
|
|
10240
10367
|
emit({ data: outcome.deployment }, io, true);
|
|
@@ -11236,11 +11363,15 @@ function emit(result, io, json) {
|
|
|
11236
11363
|
}
|
|
11237
11364
|
function buildProgram(env, io) {
|
|
11238
11365
|
const program = new Command();
|
|
11239
|
-
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
|
+
});
|
|
11240
11371
|
const clientFor = async (opts) => {
|
|
11241
11372
|
const credentials = await readCredentials(env);
|
|
11242
11373
|
return createClient({
|
|
11243
|
-
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),
|
|
11244
11375
|
auth: async () => resolveCredential(env, await readCredentials(env)).token,
|
|
11245
11376
|
headers: async () => ({
|
|
11246
11377
|
[ENVIRONMENT_HEADER]: environmentFor(env, opts),
|
|
@@ -11285,7 +11416,13 @@ const envSchema = z.object({
|
|
|
11285
11416
|
/** An org deploy key for CI; when set it wins over the cached session (ticket 05 reads it). */
|
|
11286
11417
|
COW_DEPLOY_KEY: z.string().min(1).optional(),
|
|
11287
11418
|
/** The environment admin calls select; `--env` overrides it, production is the default. */
|
|
11288
|
-
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()
|
|
11289
11426
|
});
|
|
11290
11427
|
function loadEnv() {
|
|
11291
11428
|
const parsed = envSchema.safeParse(process.env);
|