@cowliss/cli 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { n as journeyStepOutputSchema, r as manifestOutputSchema, t as guestInputSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
  import { CapabilityError } from "./journeys.js";
3
3
  import { z } from "zod";
4
4
 
@@ -1,6 +1,6 @@
1
- import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { i as templateRenderOutputSchema, t as guestInputSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
  import { CapabilityError } from "./journeys.js";
3
- import { n as readManifest, t as asTemplate } from "./driver-B_pjmy5g.js";
3
+ import { n as readManifest, t as asTemplate } from "./driver-BtQYaFXP.js";
4
4
  import { createElement } from "react";
5
5
  import { renderToStaticMarkup } from "react-dom/server.browser";
6
6
 
@@ -1,5 +1,5 @@
1
1
  import { c as TemplateRenderOutput, o as JourneyStepOutput, s as ManifestOutput } from "./index-DADBwPaG.js";
2
- import { n as Template } from "./emails-5GindgMQ.js";
2
+ import { n as Template } from "./emails-DUwxKLGO.js";
3
3
  //#region src/guest/driver.d.ts
4
4
  /**
5
5
  * The in-process driver: JSON in, JSON out (spec: Guest protocol). One
@@ -1,3 +1,3 @@
1
- import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-B_pjmy5g.js";
1
+ import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-BtQYaFXP.js";
2
2
 
3
3
  export { asTemplate, readManifest, runGuest };
@@ -23,7 +23,7 @@ type SendClass = (typeof SEND_CLASSES)[number];
23
23
  type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
24
24
  /**
25
25
  * The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
26
- * into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
26
+ * into JSON Schema in the manifest and into `types.d.ts`), `subject`,
27
27
  * and the optional metadata below it; the sandbox renders `default`.
28
28
  */
29
29
  type Template<Schema extends z.ZodType = z.ZodType> = {
@@ -1,2 +1,2 @@
1
- import { n as Template, r as SendClass, t as Subject } from "./emails-5GindgMQ.js";
1
+ import { n as Template, r as SendClass, t as Subject } from "./emails-DUwxKLGO.js";
2
2
  export { type SendClass, Subject, Template };
@@ -513,7 +513,7 @@ function uniqueKeys(items, ctx, path) {
513
513
  }
514
514
  }
515
515
  /**
516
- * What `cow build` writes to `.cow/build/manifest.json`, and the shape a
516
+ * What `cow build` writes to `.cowliss/build/manifest.json`, and the shape a
517
517
  * push row stores for good, entry by entry, on the versions it creates.
518
518
  *
519
519
  * `protocol` is any positive integer rather than the current constant on
@@ -33,7 +33,7 @@ declare class CapabilityError extends Error {
33
33
  }
34
34
  /**
35
35
  * The repository's templates, keyed by template key. `cow build` writes the
36
- * real interface into `.cow/types.d.ts` and TypeScript merges it into this
36
+ * real interface into `types.d.ts` and TypeScript merges it into this
37
37
  * one, which is what types `api.send.email`. Empty here on purpose: a
38
38
  * repository that has not built yet still compiles, with string keys.
39
39
  */
@@ -1,4 +1,4 @@
1
- import { a as manifestJourneySchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { a as manifestJourneySchema } from "./journeys-v2-Cndh4CAr.js";
2
2
 
3
3
  //#region src/guest/journeys.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { o as manifestSegmentSchema } from "./journeys-v2-Djgs8U91.js";
1
+ import { o as manifestSegmentSchema } from "./journeys-v2-Cndh4CAr.js";
2
2
 
3
3
  //#region src/guest/segments.ts
4
4
  const segmentConfigSchema = manifestSegmentSchema.pick({
package/dist/index.js CHANGED
@@ -8,13 +8,13 @@ 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
10
  import { execFile, spawn } from "node:child_process";
11
+ import { existsSync, readFileSync } from "node:fs";
11
12
  import { fileURLToPath, pathToFileURL } from "node:url";
12
13
  import { promisify } from "node:util";
13
14
  import { MessagePort } from "node:worker_threads";
14
15
  import { build, formatMessagesSync } from "esbuild";
15
16
  import { create, extract } from "tar";
16
17
  import { parse } from "@babel/parser";
17
- import { existsSync, readFileSync } from "node:fs";
18
18
  import { createServer } from "node:http";
19
19
  import { createInterface } from "node:readline/promises";
20
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -173,6 +173,8 @@ const SYSTEM_EVENT_PREFIX = "system.";
173
173
  * system.profile_deleted is the provider-recorded fact that a provider
174
174
  * (e.g. Clerk `user.deleted`) deleted the user upstream — a marker event
175
175
  * only, with no deletion semantics (hard-delete belongs to GDPR erasure);
176
+ * system.signed_up and system.logged_in are the same shape for the other end
177
+ * of that lifecycle (Clerk `user.created` and `session.created`);
176
178
  * system.email_clicked is a link click reported by SES feedback.
177
179
  *
178
180
  * Rows carrying one of these names get `system: true` so the usage meter
@@ -187,6 +189,17 @@ const SYSTEM_EVENTS = {
187
189
  consentRevoked: "system.consent_revoked",
188
190
  profileDeleted: "system.profile_deleted",
189
191
  /**
192
+ * The provider's account lifecycle, as facts rather than as profile state:
193
+ * a profile can predate its `user.created` (an SDK identify or a
194
+ * membership delivery may have created it first), so these name what the
195
+ * provider reported, not what Cowliss did. `signedUp` is the trigger for a
196
+ * welcome journey fed by a provider source, which `emailRegistered` only
197
+ * approximates (it fires on an address change too, and never for a
198
+ * profile with no address). `loggedIn` is one row per session start.
199
+ */
200
+ signedUp: "system.signed_up",
201
+ loggedIn: "system.logged_in",
202
+ /**
190
203
  * A write's identifiers named more than one profile and Cowliss merged
191
204
  * them (ticket 69). Lands on the survivor's timeline carrying
192
205
  * `mergedProfileIds`, the ids folded into it.
@@ -5160,7 +5173,7 @@ function uniqueKeys(items, ctx, path) {
5160
5173
  }
5161
5174
  }
5162
5175
  /**
5163
- * What `cow build` writes to `.cow/build/manifest.json`, and the shape a
5176
+ * What `cow build` writes to `.cowliss/build/manifest.json`, and the shape a
5164
5177
  * push row stores for good, entry by entry, on the versions it creates.
5165
5178
  *
5166
5179
  * `protocol` is any positive integer rather than the current constant on
@@ -7873,6 +7886,36 @@ const updateWebhookBodySchema = z.object({ data: z.object({
7873
7886
  /** `q` is a substring search over the webhook's name. */
7874
7887
  const listWebhooksQuerySchema = paginationQuerySchema.extend({ q: searchQuerySchema });
7875
7888
 
7889
+ //#endregion
7890
+ //#region ../../packages/shared/src/whoami.ts
7891
+ /**
7892
+ * What an ingestion key plus one source id write into. An org API key is
7893
+ * org-scoped and carries no app of its own (`ApiKeyClaims`), so any live
7894
+ * `src_` in the organization is accepted by the write path: a well-formed id
7895
+ * belonging to another app lands profiles there with a 200. The id the
7896
+ * caller is about to send is therefore the input, and this answers where it
7897
+ * goes before anything is written.
7898
+ *
7899
+ * Scoped to that one source's app on purpose: the key stays a write
7900
+ * credential, and a leaked one must not enumerate the org's other apps
7901
+ * (.scratch/24-onboarding-dx/spec.md).
7902
+ */
7903
+ const whoamiQuerySchema = z.object({ sourceId: sourceIdSchema });
7904
+ const whoamiSchema = z.object({
7905
+ orgId: z.string(),
7906
+ app: z.object({
7907
+ id: z.string(),
7908
+ name: z.string()
7909
+ }),
7910
+ /** Every live pipe into that app, so "a Clerk source already feeds this" is visible. */
7911
+ sources: z.array(sourceSchema.pick({
7912
+ id: true,
7913
+ kind: true,
7914
+ configured: true,
7915
+ lastReceivedAt: true
7916
+ }))
7917
+ });
7918
+
7876
7919
  //#endregion
7877
7920
  //#region ../../packages/api-client/src/client.ts
7878
7921
  var ApiError = class extends Error {
@@ -7897,7 +7940,7 @@ async function send(baseUrl, init) {
7897
7940
  body: typeof init.body === "string" || init.body === null ? init.body : Uint8Array.from(init.body)
7898
7941
  });
7899
7942
  } catch {
7900
- throw new ApiError("network_error", `Could not reach the API at ${baseUrl}. Is apps/api running?`, 0);
7943
+ throw new ApiError("network_error", `Could not reach the API at ${baseUrl}. Check the address, or set COW_API_URL to the one you meant.`, 0);
7901
7944
  }
7902
7945
  const requestId = response.headers.get("X-Request-Id") ?? void 0;
7903
7946
  if (init.binary && response.ok) return {
@@ -8475,8 +8518,8 @@ function isNode(value) {
8475
8518
  */
8476
8519
  const execFileAsync$1 = promisify(execFile);
8477
8520
  /** Where build output lives, relative to the project root. */
8478
- const BUILD_DIR = join(".cow", "build");
8479
- const TYPES_FILE = join(".cow", "types.d.ts");
8521
+ const BUILD_DIR = "build";
8522
+ const TYPES_FILE = "types.d.ts";
8480
8523
  /**
8481
8524
  * The config files the source tarball carries besides `journeys/` and
8482
8525
  * `emails/`. Always the committed `cow.json`, never a `--config` override: a
@@ -8597,7 +8640,7 @@ function assertSegmentResolves(relPath, trigger, segmentKeys) {
8597
8640
  throw new Error(`${relPath}: this journey enrolls from the segment "${trigger.segment}", and there is no segments/${trigger.segment}.ts. ${available} Add that file, or write the predicate list in the trigger.`);
8598
8641
  }
8599
8642
  /**
8600
- * `.cow/types.d.ts`: one `typeof import(...)` per template, merged into the
8643
+ * `types.d.ts`: one `typeof import(...)` per template, merged into the
8601
8644
  * SDK's `CowTemplates`, which is what types `api.send.email`. Written before
8602
8645
  * the typecheck, because the typecheck is what it exists for.
8603
8646
  */
@@ -8607,7 +8650,7 @@ import type { z } from "zod";
8607
8650
 
8608
8651
  declare module "@cowliss/cli/journeys" {
8609
8652
  interface CowTemplates {
8610
- ${templates.map((template) => ` "${template.key}": z.infer<typeof import("../${template.relPath.replace(/\.tsx$/, "")}").props>;`).join("\n")}
8653
+ ${templates.map((template) => ` "${template.key}": z.infer<typeof import("./${template.relPath.replace(/\.tsx$/, "")}").props>;`).join("\n")}
8611
8654
  }
8612
8655
  }
8613
8656
 
@@ -8851,6 +8894,21 @@ function setConfigFile(name) {
8851
8894
  function projectConfigFile() {
8852
8895
  return configFile;
8853
8896
  }
8897
+ /**
8898
+ * The directory a cow project lives in: `.cowliss/` under the repo root, so
8899
+ * a host repository gets one directory instead of four plus a config file.
8900
+ * Standing inside the project directory itself (a `cow.json` right here) is
8901
+ * the other answer, which is how the project directory is also a directory
8902
+ * you can cd into and run `cow` from.
8903
+ *
8904
+ * A missing `.cowliss/` still resolves to it rather than to the cwd: the
8905
+ * error then names the path a project would be at, and `cow init` and
8906
+ * `cow pull` create it.
8907
+ */
8908
+ const PROJECT_DIR = ".cowliss";
8909
+ function resolveProjectDir(cwd = process.cwd()) {
8910
+ return existsSync(join(cwd, projectConfigFile())) ? cwd : join(cwd, PROJECT_DIR);
8911
+ }
8854
8912
  /** The project's config; throws unless `projectDir` holds a valid one. */
8855
8913
  async function assertCowConfig(projectDir) {
8856
8914
  const name = projectConfigFile();
@@ -8888,9 +8946,9 @@ async function configuredApp(projectDir) {
8888
8946
  }
8889
8947
  }
8890
8948
  /**
8891
- * Builds the project at `projectDir`. Writes `.cow/types.d.ts` first (the
8949
+ * Builds the project at `projectDir`. Writes `types.d.ts` first (the
8892
8950
  * typecheck needs it), then the bundles and the source tarball, and
8893
- * `.cow/build/manifest.json` last, so a failed build leaves no manifest and
8951
+ * `build/manifest.json` last, so a failed build leaves no manifest and
8894
8952
  * the next `cow test` rebuilds.
8895
8953
  */
8896
8954
  async function buildProject(projectDir) {
@@ -9199,7 +9257,7 @@ async function copyBlueprint(name, dir, force) {
9199
9257
  }
9200
9258
  function registerAdd(program, io) {
9201
9259
  program.command("add").description("add gallery blueprint code to a cow repo").command("blueprint <name>").description("copy a blueprint's journeys, emails, segments, and scenarios into this repo (local: no API call)").option("--force", "overwrite files that already exist").action(async (name, opts) => {
9202
- const dir = process.cwd();
9260
+ const dir = resolveProjectDir();
9203
9261
  await assertCowConfig(dir);
9204
9262
  const files = await copyBlueprint(name, dir, opts.force === true);
9205
9263
  emit({ data: {
@@ -9211,170 +9269,6 @@ function registerAdd(program, io) {
9211
9269
  });
9212
9270
  }
9213
9271
 
9214
- //#endregion
9215
- //#region src/lib/login.ts
9216
- /**
9217
- * Login via the dashboard's /cli-auth bridge: start a loopback server, open
9218
- * the web app there, and the signed-in browser POSTs the Clerk session token
9219
- * back. `--token <jwt>` bypasses the browser entirely (agents, CI).
9220
- */
9221
- async function login(env, options) {
9222
- let token;
9223
- if (options.token !== void 0) {
9224
- token = options.token.trim();
9225
- if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
9226
- } else token = await collectTokenViaBrowser(resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl, options.webUrl), 3e5, options.noOpen === true);
9227
- const claims = decodeSessionToken(token);
9228
- if (claims === null) throw new Error("Token is not a decodable JWT");
9229
- if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
9230
- const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
9231
- return {
9232
- credentialsPath: await writeCredentials(env, {
9233
- token,
9234
- apiUrl
9235
- }),
9236
- apiUrl,
9237
- userId: claims.userId,
9238
- orgId: claims.orgId,
9239
- role: claims.role,
9240
- hasOrg: claims.orgId !== null
9241
- };
9242
- }
9243
- /** Open the dashboard bridge and await one POST of { token }. */
9244
- function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
9245
- return new Promise((resolve, reject) => {
9246
- let server = null;
9247
- const timer = setTimeout(() => {
9248
- server?.close();
9249
- reject(/* @__PURE__ */ new Error(`Timed out waiting for the browser to sign in (${Math.round(timeoutMs / 1e3)}s). Or paste a token: \`cow login --token <jwt>\`.`));
9250
- }, timeoutMs);
9251
- server = createServer((req, res) => {
9252
- res.setHeader("Access-Control-Allow-Origin", "*");
9253
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
9254
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
9255
- if (req.method === "OPTIONS") {
9256
- res.writeHead(204).end();
9257
- return;
9258
- }
9259
- if (req.method !== "POST") {
9260
- res.writeHead(405).end();
9261
- return;
9262
- }
9263
- const chunks = [];
9264
- req.on("data", (chunk) => chunks.push(chunk));
9265
- req.on("end", () => {
9266
- clearTimeout(timer);
9267
- try {
9268
- const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
9269
- const field = (name) => typeof body === "object" && body !== null && name in body ? body[name] : void 0;
9270
- if (field("denied") === true) {
9271
- res.writeHead(200, { "Content-Type": "application/json" });
9272
- res.end(JSON.stringify({ ok: true }));
9273
- reject(/* @__PURE__ */ new Error("Sign-in was denied in the browser. No access was granted."));
9274
- return;
9275
- }
9276
- const token = field("token");
9277
- if (typeof token !== "string" || token.length === 0) {
9278
- res.writeHead(400, { "Content-Type": "application/json" });
9279
- res.end(JSON.stringify({ error: "missing token" }));
9280
- reject(/* @__PURE__ */ new Error("The browser bridge sent no token"));
9281
- return;
9282
- }
9283
- res.writeHead(200, { "Content-Type": "application/json" });
9284
- res.end(JSON.stringify({ ok: true }));
9285
- resolve(token);
9286
- } catch (error) {
9287
- reject(error instanceof Error ? error : new Error(String(error)));
9288
- } finally {
9289
- server?.close();
9290
- }
9291
- });
9292
- });
9293
- server.listen(0, "127.0.0.1", () => {
9294
- const address = server?.address();
9295
- if (address === null || typeof address !== "object") {
9296
- reject(/* @__PURE__ */ new Error("Could not bind a loopback port"));
9297
- return;
9298
- }
9299
- const callbackUrl = `http://127.0.0.1:${address.port}/cb`;
9300
- const target = `${webUrl}/cli-auth?cb=${encodeURIComponent(callbackUrl)}`;
9301
- if (!noOpen) openBrowser(target);
9302
- process.stderr.write(`Waiting for sign-in at:\n ${target}\n`);
9303
- });
9304
- server.on("error", (error) => {
9305
- clearTimeout(timer);
9306
- reject(error);
9307
- });
9308
- });
9309
- }
9310
- function openBrowser(url) {
9311
- const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
9312
- if (cmd === null) return;
9313
- spawn(cmd, [url], {
9314
- stdio: "ignore",
9315
- detached: true
9316
- }).unref();
9317
- }
9318
-
9319
- //#endregion
9320
- //#region src/commands/auth.ts
9321
- /** login/logout/whoami: session-token management, no contract route. */
9322
- function registerAuth(program, env, io) {
9323
- program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a session token instead of the browser flow").option("--web <url>", `dashboard URL; overrides COW_WEB_URL and webUrl in cow.json (default ${DEFAULT_WEB_URL})`).option("--no-open", "do not open the browser; print the URL and wait (headless/agent use)").action(async (opts) => {
9324
- const outcome = await login(env, {
9325
- token: opts.token,
9326
- webUrl: opts.web,
9327
- apiFlag: opts.api,
9328
- noOpen: opts.open === false
9329
- });
9330
- emit({ data: {
9331
- ...outcome,
9332
- warnings: outcome.hasOrg ? void 0 : ["The token carries no active organization. Select one in the dashboard and log in again."]
9333
- } }, io, opts.json);
9334
- });
9335
- program.command("logout").description("delete the cached session token").action(async (opts) => {
9336
- const removed = await clearCredentials(env);
9337
- emit({ data: { removed } }, io, opts.json);
9338
- });
9339
- program.command("whoami").description("show which credential commands will use, and for a session its subject, org, role, and expiry").action(async (opts) => {
9340
- const credentials = await readCredentials(env);
9341
- const { kind } = resolveCredential(env, credentials);
9342
- if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set COW_PIPELINE_KEY.");
9343
- if (kind === "pipelineKey") {
9344
- emit({ data: {
9345
- credential: kind,
9346
- source: "COW_PIPELINE_KEY",
9347
- note: "A pipeline key may push, enable, disable, and read executions."
9348
- } }, io, opts.json);
9349
- return;
9350
- }
9351
- const claims = decodeSessionToken(credentials?.token ?? "");
9352
- if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
9353
- const projectApiUrl = (await readCowConfig(process.cwd()))?.apiUrl;
9354
- const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
9355
- const mintedFor = credentials?.apiUrl;
9356
- emit({ data: {
9357
- credential: kind,
9358
- apiUrl,
9359
- mintedFor: mintedFor ?? null,
9360
- ...mintedFor !== void 0 && mintedFor !== apiUrl ? { warning: `This token was minted for ${mintedFor}, but commands here talk to ${apiUrl}. Run \`cow login\` again from this directory.` } : {},
9361
- ...claims
9362
- } }, io, opts.json);
9363
- });
9364
- }
9365
-
9366
- //#endregion
9367
- //#region src/commands/build.ts
9368
- /**
9369
- * `cow build`: typecheck, bundle, and write `.cow/build` for the project in
9370
- * the current directory. Local only, no API call and no network.
9371
- */
9372
- function registerBuild(program, io) {
9373
- program.command("build").description("typecheck and bundle the repo in the current directory, writing .cow/build (local: no API call)").action(async (opts) => {
9374
- emit({ data: await buildProject(process.cwd()) }, io, opts.json);
9375
- });
9376
- }
9377
-
9378
9272
  //#endregion
9379
9273
  //#region ../../node_modules/.pnpm/@asteasolutions+zod-to-openapi@9.1.0_zod@4.4.3/node_modules/@asteasolutions/zod-to-openapi/dist/index.cjs
9380
9274
  var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -10659,6 +10553,19 @@ const ingestion = defineModule(defineRoute({
10659
10553
  200: envelope(batchResultDtoSchema, "Per-item outcomes, positionally aligned"),
10660
10554
  ...ingestionErrors
10661
10555
  }
10556
+ }), defineRoute({
10557
+ method: "get",
10558
+ path: "/v1/whoami",
10559
+ operationId: "ingestion.whoami",
10560
+ tags: ["ingestion"],
10561
+ summary: "Which app a source id writes into",
10562
+ security: API_KEY_AUTH,
10563
+ surfaces: HIDDEN_FROM_TOOLS,
10564
+ request: { query: whoamiQuerySchema },
10565
+ responses: {
10566
+ 200: envelope(whoamiSchema, "The source's org, app, and sibling sources"),
10567
+ ...errors("invalid_key", "not_found", "validation_failed", "rate_limited")
10568
+ }
10662
10569
  }));
10663
10570
 
10664
10571
  //#endregion
@@ -11887,6 +11794,180 @@ const allRoutes = Object.values(contract).flatMap((module) => Object.values(modu
11887
11794
  /** The tags in the order their modules are declared. */
11888
11795
  const tags = [...new Set(allRoutes.flatMap((route) => route.tags ?? []))];
11889
11796
 
11797
+ //#endregion
11798
+ //#region src/lib/login.ts
11799
+ /**
11800
+ * Login via the dashboard's /cli-auth bridge: start a loopback server, open
11801
+ * the web app there, and the signed-in browser POSTs the Clerk session token
11802
+ * back. `--token <jwt>` bypasses the browser entirely (agents, CI).
11803
+ */
11804
+ async function login(env, options) {
11805
+ let token;
11806
+ if (options.token !== void 0) {
11807
+ token = options.token.trim();
11808
+ if (decodeTokenPayload(token) === null) throw new Error("The provided --token value is not a JWT");
11809
+ } else token = await collectTokenViaBrowser(resolveWebUrl(env, (await readCowConfig(process.cwd()))?.webUrl, options.webUrl), 3e5, options.noOpen === true);
11810
+ const claims = decodeSessionToken(token);
11811
+ if (claims === null) throw new Error("Token is not a decodable JWT");
11812
+ if (claims.userId === null) throw new Error("Token has no subject claim. Is it a Clerk session token?");
11813
+ const apiUrl = options.apiFlag ?? resolveApiUrl(env, null, void 0, (await readCowConfig(process.cwd()))?.apiUrl);
11814
+ return {
11815
+ credentialsPath: await writeCredentials(env, {
11816
+ token,
11817
+ apiUrl
11818
+ }),
11819
+ apiUrl,
11820
+ userId: claims.userId,
11821
+ orgId: claims.orgId,
11822
+ role: claims.role,
11823
+ hasOrg: claims.orgId !== null
11824
+ };
11825
+ }
11826
+ /** Open the dashboard bridge and await one POST of { token }. */
11827
+ function collectTokenViaBrowser(webUrl, timeoutMs = 18e4, noOpen = false) {
11828
+ return new Promise((resolve, reject) => {
11829
+ let server = null;
11830
+ const timer = setTimeout(() => {
11831
+ server?.close();
11832
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for the browser to sign in (${Math.round(timeoutMs / 1e3)}s). Or paste a token: \`cow login --token <jwt>\`.`));
11833
+ }, timeoutMs);
11834
+ server = createServer((req, res) => {
11835
+ res.setHeader("Access-Control-Allow-Origin", "*");
11836
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
11837
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
11838
+ if (req.method === "OPTIONS") {
11839
+ res.writeHead(204).end();
11840
+ return;
11841
+ }
11842
+ if (req.method !== "POST") {
11843
+ res.writeHead(405).end();
11844
+ return;
11845
+ }
11846
+ const chunks = [];
11847
+ req.on("data", (chunk) => chunks.push(chunk));
11848
+ req.on("end", () => {
11849
+ clearTimeout(timer);
11850
+ try {
11851
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
11852
+ const field = (name) => typeof body === "object" && body !== null && name in body ? body[name] : void 0;
11853
+ if (field("denied") === true) {
11854
+ res.writeHead(200, { "Content-Type": "application/json" });
11855
+ res.end(JSON.stringify({ ok: true }));
11856
+ reject(/* @__PURE__ */ new Error("Sign-in was denied in the browser. No access was granted."));
11857
+ return;
11858
+ }
11859
+ const token = field("token");
11860
+ if (typeof token !== "string" || token.length === 0) {
11861
+ res.writeHead(400, { "Content-Type": "application/json" });
11862
+ res.end(JSON.stringify({ error: "missing token" }));
11863
+ reject(/* @__PURE__ */ new Error("The browser bridge sent no token"));
11864
+ return;
11865
+ }
11866
+ res.writeHead(200, { "Content-Type": "application/json" });
11867
+ res.end(JSON.stringify({ ok: true }));
11868
+ resolve(token);
11869
+ } catch (error) {
11870
+ reject(error instanceof Error ? error : new Error(String(error)));
11871
+ } finally {
11872
+ server?.close();
11873
+ }
11874
+ });
11875
+ });
11876
+ server.listen(0, "127.0.0.1", () => {
11877
+ const address = server?.address();
11878
+ if (address === null || typeof address !== "object") {
11879
+ reject(/* @__PURE__ */ new Error("Could not bind a loopback port"));
11880
+ return;
11881
+ }
11882
+ const callbackUrl = `http://127.0.0.1:${address.port}/cb`;
11883
+ const target = `${webUrl}/cli-auth?cb=${encodeURIComponent(callbackUrl)}`;
11884
+ if (!noOpen) openBrowser(target);
11885
+ process.stderr.write(`Waiting for sign-in at:\n ${target}\n`);
11886
+ });
11887
+ server.on("error", (error) => {
11888
+ clearTimeout(timer);
11889
+ reject(error);
11890
+ });
11891
+ });
11892
+ }
11893
+ function openBrowser(url) {
11894
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
11895
+ if (cmd === null) return;
11896
+ spawn(cmd, [url], {
11897
+ stdio: "ignore",
11898
+ detached: true
11899
+ }).unref();
11900
+ }
11901
+
11902
+ //#endregion
11903
+ //#region src/commands/auth.ts
11904
+ /** login/logout/whoami: session-token management, no contract route. */
11905
+ function registerAuth(program, env, io) {
11906
+ program.command("login").description("authenticate via the dashboard browser flow (or --token) and cache the session token").option("--token <jwt>", "paste a session token instead of the browser flow").option("--web <url>", `dashboard URL; overrides COW_WEB_URL and webUrl in cow.json (default ${DEFAULT_WEB_URL})`).option("--no-open", "do not open the browser; print the URL and wait (headless/agent use)").action(async (opts) => {
11907
+ const outcome = await login(env, {
11908
+ token: opts.token,
11909
+ webUrl: opts.web,
11910
+ apiFlag: opts.api,
11911
+ noOpen: opts.open === false
11912
+ });
11913
+ emit({ data: {
11914
+ ...outcome,
11915
+ warnings: outcome.hasOrg ? void 0 : ["The token carries no active organization. Select one in the dashboard and log in again."]
11916
+ } }, io, opts.json);
11917
+ });
11918
+ program.command("logout").description("delete the cached session token").action(async (opts) => {
11919
+ const removed = await clearCredentials(env);
11920
+ emit({ data: { removed } }, io, opts.json);
11921
+ });
11922
+ program.command("whoami").description("show which credential commands will use, and for a session its subject, org, role, and expiry").option("--key <ak_...>", "ask the API what an ingestion key writes into, instead of reading the cached session").option("--source <src_...>", "the source id that key is about to send to (required with --key)").action(async (opts) => {
11923
+ if (opts.key) {
11924
+ if (!opts.source) throw new Error("`--key` needs `--source <src_...>`: the id you are about to send to is what this checks.");
11925
+ const client = createClient({
11926
+ baseUrl: resolveApiUrl(env, await readCredentials(env), opts.api, (await readCowConfig(resolveProjectDir()))?.apiUrl),
11927
+ auth: () => opts.key ?? null,
11928
+ headers: { [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` }
11929
+ });
11930
+ emit(await client.request(contract.ingestion["ingestion.whoami"], { query: { sourceId: opts.source } }), io, opts.json);
11931
+ return;
11932
+ }
11933
+ const credentials = await readCredentials(env);
11934
+ const { kind } = resolveCredential(env, credentials);
11935
+ if (kind === "none") throw new Error("Not logged in. Run `cow login` first, or set COW_PIPELINE_KEY.");
11936
+ if (kind === "pipelineKey") {
11937
+ emit({ data: {
11938
+ credential: kind,
11939
+ source: "COW_PIPELINE_KEY",
11940
+ note: "A pipeline key may push, enable, disable, and read executions."
11941
+ } }, io, opts.json);
11942
+ return;
11943
+ }
11944
+ const claims = decodeSessionToken(credentials?.token ?? "");
11945
+ if (!claims) throw new Error("Cached token is not decodable. Run `cow login` again.");
11946
+ const projectApiUrl = (await readCowConfig(resolveProjectDir()))?.apiUrl;
11947
+ const apiUrl = resolveApiUrl(env, credentials, opts.api, projectApiUrl);
11948
+ const mintedFor = credentials?.apiUrl;
11949
+ emit({ data: {
11950
+ credential: kind,
11951
+ apiUrl,
11952
+ mintedFor: mintedFor ?? null,
11953
+ ...mintedFor !== void 0 && mintedFor !== apiUrl ? { warning: `This token was minted for ${mintedFor}, but commands here talk to ${apiUrl}. Run \`cow login\` again from this directory.` } : {},
11954
+ ...claims
11955
+ } }, io, opts.json);
11956
+ });
11957
+ }
11958
+
11959
+ //#endregion
11960
+ //#region src/commands/build.ts
11961
+ /**
11962
+ * `cow build`: typecheck, bundle, and write `build/` for the project in
11963
+ * the current directory. Local only, no API call and no network.
11964
+ */
11965
+ function registerBuild(program, io) {
11966
+ program.command("build").description("typecheck and bundle the repo in the current directory, writing .cowliss/build (local: no API call)").action(async (opts) => {
11967
+ emit({ data: await buildProject(resolveProjectDir()) }, io, opts.json);
11968
+ });
11969
+ }
11970
+
11890
11971
  //#endregion
11891
11972
  //#region ../../packages/shared/src/contract/surfaces.ts
11892
11973
  /** camelCase field name → kebab-case flag body (`sourceId` → `source-id`). */
@@ -12358,7 +12439,7 @@ function flipSummary(journeys, status) {
12358
12439
  * segment's own key joins the set the events are filtered by.
12359
12440
  */
12360
12441
  async function unseenForKeys(client, keys) {
12361
- const dir = process.cwd();
12442
+ const dir = resolveProjectDir();
12362
12443
  const project = await assertCowConfig(dir).then(async (config) => ({
12363
12444
  config,
12364
12445
  built: await buildProject(dir)
@@ -12384,7 +12465,7 @@ function register(program, clientFor, io, verb, status) {
12384
12465
  };
12385
12466
  if (merged.all === true && keys.length > 0) throw new Error(`Name the journeys or pass --all, not both: cow ${verb} welcome.`);
12386
12467
  if (merged.all !== true && keys.length === 0) throw new Error(`Name at least one journey, or pass --all: cow ${verb} welcome.`);
12387
- const selector = merged.all === true ? { appId: (await assertCowConfig(process.cwd())).appId } : { keys };
12468
+ const selector = merged.all === true ? { appId: (await assertCowConfig(resolveProjectDir())).appId } : { keys };
12388
12469
  const client = await clientFor(merged);
12389
12470
  const updated = await setStatus(client, selector, status);
12390
12471
  if (merged.json === true) {
@@ -12484,7 +12565,7 @@ async function chooseApp(client, webUrl, flag, org, io, skipPrompts) {
12484
12565
  return { appId: await ask("App id", only ?? "", io, false) };
12485
12566
  }
12486
12567
  function registerInit(program, clientFor, env, io) {
12487
- program.command("init").argument("[dir]", "directory to create the repo in (default: here)").description("create a cow repo: cow.json, journeys/, emails/, segments/, and the files to build them").option("--org <id>", "organization id (default: the logged-in org)").option("--app <id>", "app id this repository pushes to (default: ask, or the org's only one)").option("--yes", "accept the defaults instead of prompting").option("--blueprint <name>", "start from a gallery blueprint").action(async (dirArg, opts) => {
12568
+ program.command("init").argument("[dir]", "repository to create the project in (default: here)").description("create a cow project in .cowliss/: cow.json, journeys/, emails/, segments/, and the files to build them").option("--org <id>", "organization id (default: the logged-in org)").option("--app <id>", "app id this repository pushes to (default: ask, or the org's only one)").option("--yes", "accept the defaults instead of prompting").option("--blueprint <name>", "start from a gallery blueprint").action(async (dirArg, opts) => {
12488
12569
  const merged = {
12489
12570
  ...program.opts(),
12490
12571
  ...opts
@@ -12493,7 +12574,8 @@ function registerInit(program, clientFor, env, io) {
12493
12574
  const cwd = process.cwd();
12494
12575
  const defaultName = basename(dirArg ? resolve(cwd, dirArg) : cwd);
12495
12576
  const name = await ask("Repo name", defaultName, io, skipPrompts);
12496
- const dir = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
12577
+ const repo = dirArg ? resolve(cwd, dirArg) : name === defaultName ? cwd : resolve(cwd, name);
12578
+ const dir = join(repo, PROJECT_DIR);
12497
12579
  if (existsSync(join(dir, "cow.json"))) throw new Error(`"${dir}" already holds a cow.json. Use \`cow add blueprint\` to add code to it.`);
12498
12580
  const credentials = await readCredentials(env);
12499
12581
  const claimedOrg = credentials ? decodeSessionToken(credentials.token)?.orgId ?? null : null;
@@ -12523,10 +12605,10 @@ function registerInit(program, clientFor, env, io) {
12523
12605
  "journeys",
12524
12606
  "emails",
12525
12607
  "segments",
12526
- ".cow/types.d.ts"
12608
+ "types.d.ts"
12527
12609
  ]
12528
12610
  }),
12529
- ".gitignore": ".cow/\nnode_modules/\n",
12611
+ ".gitignore": "build/\ntypes.d.ts\nnode_modules/\n",
12530
12612
  "journeys/.gitkeep": "",
12531
12613
  "emails/.gitkeep": "",
12532
12614
  "segments/.gitkeep": ""
@@ -12548,7 +12630,7 @@ function registerInit(program, clientFor, env, io) {
12548
12630
  const journey = scenario.slice(10).split(".")[0] ?? "abandoned-checkout";
12549
12631
  const appLine = app.appId === "" ? `No app named yet (${app.reason}). Put its id in cow.json's "appId"; you will find it on the app's page at ${webUrl}/apps.` : `Pushes to ${app.appId}`;
12550
12632
  io.stderr([
12551
- `Created a cow repo in ${here}`,
12633
+ `Created a cow project in ${here}`,
12552
12634
  appLine,
12553
12635
  "",
12554
12636
  "Next steps:",
@@ -12700,8 +12782,8 @@ function registerStatus(program, clientFor, io) {
12700
12782
  ...program.opts(),
12701
12783
  ...opts
12702
12784
  };
12703
- const config = await assertCowConfig(process.cwd());
12704
- const built = await buildProject(process.cwd()).catch(() => null);
12785
+ const config = await assertCowConfig(resolveProjectDir());
12786
+ const built = await buildProject(resolveProjectDir()).catch(() => null);
12705
12787
  const client = await clientFor(merged);
12706
12788
  const status = await readStatus(client, config.appId);
12707
12789
  if (merged.json === true) {
@@ -12939,7 +13021,7 @@ function registerPush(program, clientFor, env, io) {
12939
13021
  ...opts
12940
13022
  };
12941
13023
  const client = await clientFor(merged);
12942
- const projectDir = process.cwd();
13024
+ const projectDir = resolveProjectDir();
12943
13025
  const json = merged.json === true;
12944
13026
  const { plan, built } = await planProject({
12945
13027
  client,
@@ -13007,7 +13089,7 @@ function registerPush(program, clientFor, env, io) {
13007
13089
  async function projectPlan(client) {
13008
13090
  const { plan } = await planProject({
13009
13091
  client,
13010
- projectDir: process.cwd()
13092
+ projectDir: resolveProjectDir()
13011
13093
  });
13012
13094
  return { data: plan };
13013
13095
  }
@@ -13020,7 +13102,7 @@ function registerPlan(program, clientFor, io) {
13020
13102
  const client = await clientFor(merged);
13021
13103
  const { plan, built } = await planProject({
13022
13104
  client,
13023
- projectDir: process.cwd()
13105
+ projectDir: resolveProjectDir()
13024
13106
  });
13025
13107
  if (merged.json === true) {
13026
13108
  emit({ data: plan }, io, true);
@@ -13168,7 +13250,7 @@ const execFileAsync = promisify(execFile);
13168
13250
  *
13169
13251
  * Untracked files count: a journey the developer wrote and has not committed
13170
13252
  * is exactly what the tarball's copy of the same path would bury, and it is
13171
- * the only copy. `.cow/` and `node_modules/` are gitignored by `cow init`, so
13253
+ * the only copy. `build/` and `node_modules/` are gitignored by `cow init`, so
13172
13254
  * they never show up here.
13173
13255
  */
13174
13256
  async function dirtyPaths(dir) {
@@ -13217,6 +13299,7 @@ async function pullPush({ client, projectDir, pushId, force }) {
13217
13299
  }
13218
13300
  const tarball = await client.request(contract.pushes["pushes.source"], { params: { id: push.id } });
13219
13301
  const files = [];
13302
+ await mkdir(projectDir, { recursive: true });
13220
13303
  const unpack = extract({
13221
13304
  cwd: projectDir,
13222
13305
  onReadEntry: (entry) => files.push(entry.path)
@@ -13239,7 +13322,7 @@ function registerPull(program, clientFor, io) {
13239
13322
  };
13240
13323
  const outcome = await pullPush({
13241
13324
  client: await clientFor(merged),
13242
- projectDir: process.cwd(),
13325
+ projectDir: resolveProjectDir(),
13243
13326
  pushId: opts.push,
13244
13327
  force: opts.force === true
13245
13328
  });
@@ -13494,7 +13577,7 @@ function registerTest(program, io) {
13494
13577
  }
13495
13578
  const parsed = journeyScenarioSchema.safeParse(json);
13496
13579
  if (!parsed.success) throw new Error(`Scenario file "${opts.scenario}" is not a valid scenario: ${firstIssueMessage(parsed.error)}`);
13497
- const projectDir = process.cwd();
13580
+ const projectDir = resolveProjectDir();
13498
13581
  await buildIfStale(projectDir);
13499
13582
  const result = await runScenario(projectDir, journey, parsed.data);
13500
13583
  emit({ data: result }, io, opts.json);
@@ -13519,7 +13602,7 @@ function buildProgram(env, io) {
13519
13602
  const clientFor = async (opts) => {
13520
13603
  const credentials = await readCredentials(env);
13521
13604
  return createClient({
13522
- baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(process.cwd()))?.apiUrl),
13605
+ baseUrl: resolveApiUrl(env, credentials, typeof opts.api === "string" ? opts.api : void 0, (await readCowConfig(resolveProjectDir()))?.apiUrl),
13523
13606
  auth: async () => resolveCredential(env, await readCredentials(env)).token,
13524
13607
  headers: async () => ({ [CLIENT_HEADER]: `@cowliss/cli/${(await readCliPackage()).version}` })
13525
13608
  });
@@ -13591,7 +13674,7 @@ function errorMessage(error) {
13591
13674
  code: error.code,
13592
13675
  message: error.message
13593
13676
  } }, null, 2);
13594
- if (error.code === "invalid_key") return `${envelope}\n(hint: run \`cow login\` to refresh the session token)`;
13677
+ if (error.code === "invalid_key" && !process.argv.includes("--key")) return `${envelope}\n(hint: run \`cow login\` to refresh the session token)`;
13595
13678
  return envelope;
13596
13679
  }
13597
13680
  if (error instanceof Error) return `error: ${error.message}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cowliss/cli",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "cow": "./dist/index.js"