@samebase/alchemy-convex 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,39 @@ the end. Rows are in the tables `resources` and `outputs` in the dashboard, unde
119
119
  The store has no lock: run one deploy of a stage at a time, such as with a CI concurrency group.
120
120
  The names of the store functions are a wire contract: never renamed or removed.
121
121
 
122
+ ### What the output records about a run
123
+
124
+ Alchemy state holds no time, commit, branch, or run. `Convex.state()` adds them to the stage output:
125
+ the document that `setOutput` stores is the stack's output plus the key `samebase.run`. On GitHub
126
+ Actions the record comes from the run's variables and the event payload; on a laptop from git, which
127
+ needs a branch and an `origin` remote. It holds no secret.
128
+
129
+ ```json
130
+ {
131
+ "convexUrl": "https://modest-guanaco-252.convex.cloud",
132
+ "samebase.run": {
133
+ "version": 1,
134
+ "repository": "samebase/base-alchemy",
135
+ "stackFile": "alchemy.run.ts",
136
+ "branch": "feature",
137
+ "pullRequest": 42,
138
+ "headCommit": "8af3c5b7932c528939cbc7aa0597b1fc36c8dc83",
139
+ "runUrl": "https://github.com/samebase/base-alchemy/actions/runs/12345678901/attempts/1",
140
+ "source": "github-actions",
141
+ "recordedAt": "2026-10-21T10:00:00.000Z"
142
+ }
143
+ }
144
+ ```
145
+
146
+ `stackFile` is the working directory of the run below the repository root plus `alchemy.run.ts`.
147
+ `pullRequest` and `runUrl` are `null` for a push and for a laptop, where `source` is `laptop`. A
148
+ value that the stack itself returns under `samebase.run` stays. A checkout that gives no record,
149
+ such as a tarball without git or a detached HEAD, writes its outputs without one, and the log says
150
+ why. Alchemy writes the output once after a full successful deploy, so a failed deploy leaves the
151
+ previous record, and a deploy of selected resources writes none. A reader gets every stage of a
152
+ stack, with its resources and output, from the store function `state:readStack({ stack })` in one
153
+ call.
154
+
122
155
  ## Resources
123
156
 
124
157
  - `Convex.Project`: `{ team, name, deploymentRegion?, deploymentType? }` creates a project with its
package/lib/index.d.ts CHANGED
@@ -347,6 +347,54 @@ export declare class ProjectNotFound extends ProjectNotFound_base {
347
347
  get message(): string;
348
348
  }
349
349
  //#endregion
350
+ //#region src/RunRecord.d.ts
351
+ /** The key of the record in the stage output document. */
352
+ export declare const RUN_RECORD_KEY = "samebase.run";
353
+ /** Version 1 of the record. A reader validates `version` first. */
354
+ interface RunRecord {
355
+ readonly version: 1;
356
+ /** GitHub `owner/name`. */
357
+ readonly repository: string;
358
+ /** Path of the stack file from the repository root, POSIX separators, such as `apps/samebase/alchemy.run.ts`. */
359
+ readonly stackFile: string;
360
+ /** The branch that was deployed: the head branch of a pull request, else the pushed branch. */
361
+ readonly branch: string;
362
+ /** The pull request whose head was deployed, or null for a push and for a laptop. */
363
+ readonly pullRequest: number | null;
364
+ /** The commit that was deployed: the head of the pull request, else GITHUB_SHA, else git HEAD. */
365
+ readonly headCommit: string;
366
+ /** The GitHub Actions run and attempt, or null for a laptop. */
367
+ readonly runUrl: string | null;
368
+ readonly source: "github-actions" | "laptop";
369
+ /** ISO 8601, when the record was written. */
370
+ readonly recordedAt: string;
371
+ }
372
+ declare const RunRecordError_base: Schema.Class<RunRecordError, Schema.TaggedStruct<"RunRecordError", {
373
+ readonly reason: Schema.String;
374
+ }>, import("effect/Cause").YieldableError>;
375
+ export declare class RunRecordError extends RunRecordError_base {
376
+ get message(): string;
377
+ }
378
+ /** Where the run happens: the working directory of the stack and the process environment. */
379
+ interface RunEnvironment {
380
+ readonly cwd: string;
381
+ readonly env: Readonly<Record<string, string | undefined>>;
382
+ }
383
+ /**
384
+ * The record of this run: from the GitHub Actions variables and the event
385
+ * payload when GITHUB_ACTIONS is `true`, else from git in `cwd`, which needs
386
+ * a branch and an `origin` remote with `owner/name`. Fails when a value is
387
+ * missing; nothing is guessed.
388
+ */
389
+ export declare const runRecord: (where: RunEnvironment) => Effect.Effect<RunRecord, RunRecordError>;
390
+ /**
391
+ * The output document of a stage: the stack's output plus the record under
392
+ * {@link RUN_RECORD_KEY}, or undefined for an output with no place for the
393
+ * key: a string, an array, null, or an object that is not plain, such as a
394
+ * Date. A value that the stack itself put under the key stays.
395
+ */
396
+ export declare const withRunRecord: (output: unknown, record: RunRecord) => object | undefined;
397
+ //#endregion
350
398
  //#region src/State.d.ts
351
399
  /** The name of the state project in each team. */
352
400
  export declare const STORE_PROJECT_NAME = "alchemy-state";
@@ -362,5 +410,5 @@ export declare const state: (options?: {
362
410
  readonly team?: string | number;
363
411
  }) => Layer.Layer<State, never, ChildProcessSpawner.ChildProcessSpawner>;
364
412
  //#endregion
365
- export type { DeploymentAttributes, DeploymentProps, DeploymentRegion, EnvValue, ProjectAttributes, ProjectProps, Source };
413
+ export type { DeploymentAttributes, DeploymentProps, DeploymentRegion, EnvValue, ProjectAttributes, ProjectProps, RunRecord, Source };
366
414
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts","../src/State.ts"],"mappings":";;;;;;;;;;;;;UA6BiB;;WAEN,aAAa,SAAS;;WAEtB;;UAGM;;WAEN,aAAa,OAAO,OAAO;;WAE3B,gBAAgB,OAAO;;WAEvB,WAAW,OAAO,OAAO,SAAS;;;qBAGhC,oBAAoB;;qBAKpB;;qBAGA;;;;qBAOA,yBAAyB;;;;;;;;;qBAiCzB,kBAAiB,MAAM,MAAM;;qBA0B7B,aAAc;WAChB,cAAc,SAAS;WACvB,YAAY,SAAS;MAC5B,MAAM,MAAM;;;KC3GJ,mBAAmB,kBAAkB;KACrC,mBAAmB,kBAAkB;;UAGhC,YAAY;WAClB,OAAO;WACP;WACA,UAAU;;;;;;;;;;;;;qBAQR,uBAAuB;UAuBnB;;WAEN,UAAU,GACjB,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,cACP,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,0BACxC,OAAO,aAAa;;WAEhB,oBAAoB,GAC3B,mBACA,6BACA,WAAW,SAAS,kBACpB,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;;;;;;;WAQb,gBACP,sCACG,OAAO,eAAe;;;;;;;;WAQlB,oBAAoB,mBAAmB,OAAO;;;;;WAK9C,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBC1FtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,8CAAA,mCAAA,oDAAA;;;;KC+BlE,oBAAoB,SAAS;;;;;KAM7B;;WAIG;;;;;;;;WAQA;;;;;;;WAOA;;;;;;qBAkCF,0BAA0B;MAIxB;;;;;;qBAMF,0BAA0B;MAIxB;;;;;;;;;qBAMF,uBAAuB;MAMrB;;;;;;;qBAQF,2BAA2B;MAIzB;;;;;;;qBAMF,oCAAoC;MAIlC;;;;;;qBAMF,kCAAkC;MAIhC;;;qBAuCF,oBACX;EACG;EAAa;;;;UChIR;;WAEC;aAAoB;;;;;;;;WAOpB,SAAS;;WAET,MAAM,SAAS,eAAe;;KAG7B,mBACP;;WAEU;MAEV;;WAEU;;;;;WAKA;;;UAIL;WACC;WACA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;UAGM,6BAA6B;;WAEnC;;WAEA;;YAGC,aAAa,8BAEvB,iBACA,6BAEA;qBAEW,8BAAU,cAAA;;;;;;;;;qBAKV,iCAAiC;MAU/B;;;;;;qBAMF,qCAAqC;MAInC;;;;;;;;;qBAMF,4BAA4B;MAS1B;;;;;KC5IH,mBAAmB;KAEnB;;WAGG;;WAEA;;WAEA,mBAAmB;;WAEnB;;;WAIA;;WAEA;;UAGE;WACN;WACA;WACA;WACA;WACA;WACA;;YAGC,UAAU,2BAA2B,cAAc,0BAA0B;qBAC5E,2BAAO,cAAA;;;;;;;qBAGP,0BAA0B;MAIxB;;;;;;;;;;;;qBASF,gCAAgC;MAS9B;;;;;;;;qBAOF,yBAAyB;MAKvB;;;;;;qBAMF,+BAA+B;MAI7B;;;;;;;qBAMF,wBAAwB;MAItB;;;;;qBChDF;;;;;;;;;qBAiVA,QACX;WAAoB;MACnB,MAAM,MAAM,cAAc,oBAAoB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/Credentials.ts","../src/ManagementApi.ts","../src/Providers.ts","../src/Push.ts","../src/Deployment.ts","../src/Project.ts","../src/RunRecord.ts","../src/State.ts"],"mappings":";;;;;;;;;;;;;UA6BiB;;WAEN,aAAa,SAAS;;WAEtB;;UAGM;;WAEN,aAAa,OAAO,OAAO;;WAE3B,gBAAgB,OAAO;;WAEvB,WAAW,OAAO,OAAO,SAAS;;;qBAGhC,oBAAoB;;qBAKpB;;qBAGA;;;;qBAOA,yBAAyB;;;;;;;;;qBAiCzB,kBAAiB,MAAM,MAAM;;qBA0B7B,aAAc;WAChB,cAAc,SAAS;WACvB,YAAY,SAAS;MAC5B,MAAM,MAAM;;;KC3GJ,mBAAmB,kBAAkB;KACrC,mBAAmB,kBAAkB;;UAGhC,YAAY;WAClB,OAAO;WACP;WACA,UAAU;;;;;;;;;;;;;qBAQR,uBAAuB;UAuBnB;;WAEN,UAAU,GACjB,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;WAEb,cACP,mBACA,MAAM,QAAQ,qBAAqB,QAAQ,0BACxC,OAAO,aAAa;;WAEhB,oBAAoB,GAC3B,mBACA,6BACA,WAAW,SAAS,kBACpB,MAAM,QAAQ,qBAAqB,QAAQ,YAAY,QACpD,OAAO,OAAO,GAAG;;;;;;;;WAQb,gBACP,sCACG,OAAO,eAAe;;;;;;;;WAQlB,oBAAoB,mBAAmB,OAAO;;;;;WAK9C,YAAY,OAAO,SAAS,qBAAqB;;;qBAM/C,sBAAsB;;;;qBC1FtB,kBAAkB;;;;;;qBAOlB,YAAa,cAAa,MAAM,MAAM,iBAA2B,MAAA,MAAA,8CAAA,mCAAA,oDAAA;;;;KC+BlE,oBAAoB,SAAS;;;;;KAM7B;;WAIG;;;;;;;;WAQA;;;;;;;WAOA;;;;;;qBAkCF,0BAA0B;MAIxB;;;;;;qBAMF,0BAA0B;MAIxB;;;;;;;;;qBAMF,uBAAuB;MAMrB;;;;;;;qBAQF,2BAA2B;MAIzB;;;;;;;qBAMF,oCAAoC;MAIlC;;;;;;qBAMF,kCAAkC;MAIhC;;;qBAuCF,oBACX;EACG;EAAa;;;;UChIR;;WAEC;aAAoB;;;;;;;;WAOpB,SAAS;;WAET,MAAM,SAAS,eAAe;;KAG7B,mBACP;;WAEU;MAEV;;WAEU;;;;;WAKA;;;UAIL;WACC;WACA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;UAGM,6BAA6B;;WAEnC;;WAEA;;YAGC,aAAa,8BAEvB,iBACA,6BAEA;qBAEW,8BAAU,cAAA;;;;;;;;;qBAKV,iCAAiC;MAU/B;;;;;;qBAMF,qCAAqC;MAInC;;;;;;;;;qBAMF,4BAA4B;MAS1B;;;;;KC5IH,mBAAmB;KAEnB;;WAGG;;WAEA;;WAEA,mBAAmB;;WAEnB;;;WAIA;;WAEA;;UAGE;WACN;WACA;WACA;WACA;WACA;WACA;;YAGC,UAAU,2BAA2B,cAAc,0BAA0B;qBAC5E,2BAAO,cAAA;;;;;;;qBAGP,0BAA0B;MAIxB;;;;;;;;;;;;qBASF,gCAAgC;MAS9B;;;;;;;;qBAOF,yBAAyB;MAKvB;;;;;;qBAMF,+BAA+B;MAI7B;;;;;;;qBAMF,wBAAwB;MAItB;;;;;qBC5GF;;UAMI;WACN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;WACA;;WAEA;;;;;qBAGE,uBAAuB;MAGrB;;;UAME;WACN;WACA,KAAK,SAAS;;;;;;;;qBAuIZ,YAAa,OAAO,mBAAiB,OAAO,OAAO,WAAW;;;;;;;qBAgB9D,gBAAiB,iBAAiB,QAAQ;;;;qBCxH1C;;;;;;;;;qBAwWA,QACX;WAAoB;MACnB,MAAM,MAAM,cAAc,oBAAoB"}
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
5
  import * as Config from "effect/Config";
6
6
  import * as Context from "effect/Context";
7
7
  import * as Effect from "effect/Effect";
@@ -20,11 +20,12 @@ import { Stack } from "alchemy/Stack";
20
20
  import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
21
21
  import * as Schedule from "effect/Schedule";
22
22
  import { createHash } from "node:crypto";
23
- import { stripVTControlCharacters } from "node:util";
23
+ import { promisify, stripVTControlCharacters } from "node:util";
24
24
  import { hashDirectory } from "alchemy/Command/Memo";
25
25
  import * as Clock from "effect/Clock";
26
26
  import * as ChildProcess from "effect/process/ChildProcess";
27
27
  import * as Stream from "effect/Stream";
28
+ import { execFile } from "node:child_process";
28
29
  import { fileURLToPath } from "node:url";
29
30
  import * as Scope from "effect/Scope";
30
31
  import { STATE_STORE_VERSION, State, StateStoreError, encodeState, reviveState } from "alchemy/State";
@@ -1329,6 +1330,125 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
1329
1330
  */
1330
1331
  const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([Project, Deployment])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.orDie);
1331
1332
  //#endregion
1333
+ //#region src/RunRecord.ts
1334
+ /** The key of the record in the stage output document. */
1335
+ const RUN_RECORD_KEY = "samebase.run";
1336
+ /** The name of the stack file. Alchemy resolves `--config` against the working directory and does not expose it to a state layer, so the record assumes the default name. */
1337
+ const STACK_FILE_NAME = "alchemy.run.ts";
1338
+ var RunRecordError = class extends Schema.TaggedError()("RunRecordError", { reason: Schema.String }) {
1339
+ get message() {
1340
+ return `The run record of the stage output cannot be made: ${this.reason}`;
1341
+ }
1342
+ };
1343
+ /** The part of the GitHub Actions event payload that the record reads. A push event has no `pull_request`. */
1344
+ const EventPayload = Schema.Struct({ pull_request: Schema.optionalKey(Schema.Struct({
1345
+ number: Schema.Number,
1346
+ head: Schema.Struct({ sha: Schema.String })
1347
+ })) });
1348
+ /** `owner/name` of an https, ssh, or scp-like git URL with a host. A local path is not one. */
1349
+ const REMOTE_URL = /^(?:[a-z+]+:\/\/[^/]+\/|[^/@:]+@[^:/]+:)([^/]+)\/([^/]+?)(?:\.git)?\/?$/;
1350
+ const execFileAsync = promisify(execFile);
1351
+ const now = Clock.currentTimeMillis.pipe(Effect.map((millis) => new Date(millis).toISOString()));
1352
+ const git = (cwd, args) => Effect.tryPromise({
1353
+ try: async () => (await execFileAsync("git", [
1354
+ "-C",
1355
+ cwd,
1356
+ ...args
1357
+ ], { encoding: "utf8" })).stdout.trim(),
1358
+ catch: (error) => new RunRecordError({ reason: `git ${args.join(" ")} failed in ${cwd} (${error instanceof Error ? error.message.split("\n")[0] : String(error)})` })
1359
+ });
1360
+ /** The stack file from the repository root: the working directory below the git top level, plus {@link STACK_FILE_NAME}. */
1361
+ const stackFileOf = (cwd) => Effect.gen(function* () {
1362
+ const top = yield* git(cwd, ["rev-parse", "--show-toplevel"]);
1363
+ const [realTop, realCwd] = yield* Effect.tryPromise({
1364
+ try: () => Promise.all([realpath(top), realpath(cwd)]),
1365
+ catch: () => new RunRecordError({ reason: `${cwd} is gone` })
1366
+ });
1367
+ return [...relative(realTop, realCwd).split(sep).filter((part) => part !== ""), STACK_FILE_NAME].join("/");
1368
+ });
1369
+ const variable = (env, name) => Effect.suspend(() => {
1370
+ const value = env[name];
1371
+ return value === void 0 || value === "" ? Effect.fail(new RunRecordError({ reason: `GITHUB_ACTIONS is true, and ${name} is not set` })) : Effect.succeed(value);
1372
+ });
1373
+ const fromGitHubActions = (where) => Effect.gen(function* () {
1374
+ const { env } = where;
1375
+ const repository = yield* variable(env, "GITHUB_REPOSITORY");
1376
+ const refName = yield* variable(env, "GITHUB_REF_NAME");
1377
+ const sha = yield* variable(env, "GITHUB_SHA");
1378
+ const serverUrl = yield* variable(env, "GITHUB_SERVER_URL");
1379
+ const runId = yield* variable(env, "GITHUB_RUN_ID");
1380
+ const runAttempt = yield* variable(env, "GITHUB_RUN_ATTEMPT");
1381
+ const eventPath = env["GITHUB_EVENT_PATH"];
1382
+ const pullRequest = (eventPath === void 0 || eventPath === "" ? void 0 : yield* Effect.tryPromise({
1383
+ try: () => readFile(eventPath, "utf8"),
1384
+ catch: () => new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} cannot be read` })
1385
+ }).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Schema.fromJsonString(EventPayload))), Effect.mapError((error) => error instanceof RunRecordError ? error : new RunRecordError({ reason: `GITHUB_EVENT_PATH ${eventPath} is not an event payload` }))))?.pull_request;
1386
+ const headCommit = pullRequest?.head.sha ?? sha;
1387
+ const headRef = env["GITHUB_HEAD_REF"];
1388
+ const mergeRef = /^refs\/pull\/(\d+)\/merge$/.exec(env["GITHUB_REF"] ?? "");
1389
+ return {
1390
+ version: 1,
1391
+ repository,
1392
+ stackFile: yield* stackFileOf(where.cwd),
1393
+ branch: headRef === void 0 || headRef === "" ? refName : headRef,
1394
+ pullRequest: pullRequest?.number ?? (mergeRef === null ? null : Number(mergeRef[1])),
1395
+ headCommit,
1396
+ runUrl: `${serverUrl}/${repository}/actions/runs/${runId}/attempts/${runAttempt}`,
1397
+ source: "github-actions",
1398
+ recordedAt: yield* now
1399
+ };
1400
+ });
1401
+ const fromGit = (where) => Effect.gen(function* () {
1402
+ const { cwd } = where;
1403
+ const origin = yield* git(cwd, [
1404
+ "remote",
1405
+ "get-url",
1406
+ "origin"
1407
+ ]);
1408
+ const match = REMOTE_URL.exec(origin);
1409
+ if (match === null) return yield* new RunRecordError({ reason: `the origin remote of ${cwd} is not a git URL with a host and owner/name: ${origin}` });
1410
+ const branch = yield* git(cwd, [
1411
+ "rev-parse",
1412
+ "--abbrev-ref",
1413
+ "HEAD"
1414
+ ]);
1415
+ if (branch === "HEAD") return yield* new RunRecordError({ reason: `the checkout at ${cwd} is on no branch (detached HEAD)` });
1416
+ return {
1417
+ version: 1,
1418
+ repository: `${match[1]}/${match[2]}`,
1419
+ stackFile: yield* stackFileOf(cwd),
1420
+ branch,
1421
+ pullRequest: null,
1422
+ headCommit: yield* git(cwd, ["rev-parse", "HEAD"]),
1423
+ runUrl: null,
1424
+ source: "laptop",
1425
+ recordedAt: yield* now
1426
+ };
1427
+ });
1428
+ /**
1429
+ * The record of this run: from the GitHub Actions variables and the event
1430
+ * payload when GITHUB_ACTIONS is `true`, else from git in `cwd`, which needs
1431
+ * a branch and an `origin` remote with `owner/name`. Fails when a value is
1432
+ * missing; nothing is guessed.
1433
+ */
1434
+ const runRecord = (where) => where.env["GITHUB_ACTIONS"] === "true" ? fromGitHubActions(where) : fromGit(where);
1435
+ /** A plain object, which a spread keeps whole. A Date, a Duration, or a Redacted value is an object too, and a spread loses it. */
1436
+ const isPlainObject = (value) => {
1437
+ if (typeof value !== "object" || value === null) return false;
1438
+ const prototype = Object.getPrototypeOf(value);
1439
+ return prototype === Object.prototype || prototype === null;
1440
+ };
1441
+ /**
1442
+ * The output document of a stage: the stack's output plus the record under
1443
+ * {@link RUN_RECORD_KEY}, or undefined for an output with no place for the
1444
+ * key: a string, an array, null, or an object that is not plain, such as a
1445
+ * Date. A value that the stack itself put under the key stays.
1446
+ */
1447
+ const withRunRecord = (output, record) => isPlainObject(output) ? "samebase.run" in output ? output : {
1448
+ ...output,
1449
+ [RUN_RECORD_KEY]: record
1450
+ } : void 0;
1451
+ //#endregion
1332
1452
  //#region src/State.ts
1333
1453
  /** The name of the state project in each team. */
1334
1454
  const STORE_PROJECT_NAME = "alchemy-state";
@@ -1449,7 +1569,7 @@ const storeDirectory = () => join(dirname(fileURLToPath(import.meta.url)), "..",
1449
1569
  */
1450
1570
  const pushStore = (options, deployment) => Effect.gen(function* () {
1451
1571
  const cli = yield* installedCli(join(options.store, ".."));
1452
- yield* Effect.logInfo(`Convex state: pushing the store version 1 to ${deployment.name}`);
1572
+ yield* Effect.logInfo(`Convex state: pushing the store version 2 to ${deployment.name}`);
1453
1573
  yield* withPushKey(options.api, deployment.name, PUSH_KEY_ACTIONS, (key) => push(options.spawner, key, {
1454
1574
  deployment: deployment.name,
1455
1575
  url: deployment.deploymentUrl,
@@ -1512,16 +1632,31 @@ const makeConvexState = (options) => Effect.gen(function* () {
1512
1632
  return next.call(path, args);
1513
1633
  })))) };
1514
1634
  const version = yield* storeVersion(functions);
1515
- if (version === void 0 || version < 1) {
1635
+ if (version === void 0 || version < 2) {
1516
1636
  if (version === void 0 && deployment.lastDeployTime) return yield* failure(`project ${STORE_PROJECT_NAME} (${project.id}) of team ${team} has code that is not the state store, and Convex.state does not push over it. Rename that project in the Convex dashboard.`);
1517
1637
  const now = yield* storeVersion(functions);
1518
- if (now === void 0 || now < 1) {
1638
+ if (now === void 0 || now < 2) {
1519
1639
  yield* pushStore(options, deployment);
1520
1640
  const pushed = yield* storeVersion(functions);
1521
- if (pushed === void 0 || pushed < 1) return yield* failure(`${deployment.name} answers store version ${pushed ?? "none"} after the push of version 1.`);
1641
+ if (pushed === void 0 || pushed < 2) return yield* failure(`${deployment.name} answers store version ${pushed ?? "none"} after the push of version 2.`);
1522
1642
  }
1523
- } else if (version > 1) yield* Effect.logInfo(`Convex state: the store of team ${team} is version ${version}, newer than version 1 of this package, and it stays.`);
1643
+ } else if (version > 2) yield* Effect.logInfo(`Convex state: the store of team ${team} is version ${version}, newer than version 2 of this package, and it stays.`);
1524
1644
  const remote = remoteOf(functions);
1645
+ /** The stack's output plus the record, when the run has one and the output has a place for it. */
1646
+ const documentOf = (request) => Effect.gen(function* () {
1647
+ const { run } = options;
1648
+ if (run === void 0) return request.value;
1649
+ const recordedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
1650
+ const document = withRunRecord(request.value, {
1651
+ ...run,
1652
+ recordedAt
1653
+ });
1654
+ if (document === void 0) {
1655
+ yield* Effect.logWarning(`Convex state: the output of ${request.stack}/${request.stage} is not a plain object, so it carries no run record.`);
1656
+ return request.value;
1657
+ }
1658
+ return document;
1659
+ });
1525
1660
  return {
1526
1661
  id: "convex",
1527
1662
  getVersion: () => Effect.succeed(STATE_STORE_VERSION),
@@ -1534,7 +1669,15 @@ const makeConvexState = (options) => Effect.gen(function* () {
1534
1669
  set: (request) => remote.set(request).pipe(Effect.as(request.value)),
1535
1670
  delete: (request) => remote.delete(request).pipe(Effect.asVoid),
1536
1671
  deleteStack: (request) => remote.deleteStack(request).pipe(Effect.asVoid),
1537
- setOutput: (request) => remote.setOutput(request).pipe(Effect.as(request.value))
1672
+ setOutput: (request) => Effect.gen(function* () {
1673
+ const value = yield* documentOf(request);
1674
+ yield* remote.setOutput({
1675
+ stack: request.stack,
1676
+ stage: request.stage,
1677
+ value
1678
+ });
1679
+ return value;
1680
+ })
1538
1681
  };
1539
1682
  });
1540
1683
  /**
@@ -1551,16 +1694,21 @@ const state = (options = {}) => Layer.effect(State, Effect.gen(function* () {
1551
1694
  const context = yield* Effect.context();
1552
1695
  return yield* Effect.cached(Effect.gen(function* () {
1553
1696
  const api = yield* ManagementApi.pipe(Effect.provide(ManagementApiLive().pipe(Layer.provide(fromConfig()))));
1697
+ const run = yield* runRecord({
1698
+ cwd: process.cwd(),
1699
+ env: process.env
1700
+ }).pipe(Effect.catch((error) => Effect.logWarning(`Convex state: the stage outputs of this run carry no record. ${error.message}`).pipe(Effect.as(void 0))));
1554
1701
  return yield* makeConvexState({
1555
1702
  team: options.team,
1556
1703
  api,
1557
1704
  spawner,
1558
1705
  scope,
1559
- store: storeDirectory()
1706
+ store: storeDirectory(),
1707
+ run
1560
1708
  });
1561
1709
  }).pipe(Effect.provideContext(context), Effect.orDie));
1562
1710
  }));
1563
1711
  //#endregion
1564
- export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, state };
1712
+ export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, CodePushFailed, ConvexApiError, ConvexAppNotFound, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DEPLOY_KEY_ENV, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, ManagementApi, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectNotFound, ProjectTeamChange, Providers, PushTargetMismatch, RUN_RECORD_KEY, RunRecordError, STORE_PROJECT_NAME, VariablesOwnedBySomeoneElse, fromConfig, fromValues, parseDeployOutput, providers, runRecord, state, withRunRecord };
1565
1713
 
1566
1714
  //# sourceMappingURL=index.js.map