@deployfoundation/foundation-deploy 0.2.51 → 0.2.52

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
@@ -54,9 +54,12 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
54
54
  | `github-app-create` | Create the instance's GitHub App from the shipped manifest template. |
55
55
  | `slack-manifest` | Print the instance's Slack app manifest as JSON (the Slack CLI's `get-manifest` hook contract). |
56
56
  | `stage-customization` | Validate and stage a tenant-owned runtime config. |
57
+ | `crm-key` | Write the company CRM's `crm/1` key, piped in with `--stdin`, into `<secretPrefix>/crm/http` (`crm.provider` kind `http`). |
58
+ | `crm-check` | Run the `crm/1` conformance suite: the read cases against the instance's CRM with its stored key, or, with `--base-url --tenant --key-stdin` and no instance, any server, write cases included. |
59
+ | `crm-replay` | Replay the agent's CRM writes (JSONL) into the instance's own table after a rollback to the DynamoDB provider. |
57
60
 
58
61
  `--instance <path>` is required for every command (or `FOUNDATION_INSTANCE_FILE`
59
- in a pipeline). There is no instance *name*, no `instances/` directory and no
62
+ in a pipeline), except `crm-check --base-url`, which checks a server rather than a deployment. There is no instance *name*, no `instances/` directory and no
60
63
  default: a deploy that guessed its target would be a deploy into the wrong
61
64
  account.
62
65
 
package/dist/bin/app.js CHANGED
@@ -8,14 +8,17 @@ import {
8
8
  FoundationPages,
9
9
  FoundationPipeline,
10
10
  NewsletterStack
11
- } from "../chunk-efqdhd2e.js";
11
+ } from "../chunk-59dpqyap.js";
12
+ import"../chunk-pxhftba1.js";
13
+ import"../chunk-dz91avrb.js";
12
14
  import {
13
15
  adminsFor,
14
16
  instanceNames,
15
17
  loadInstanceFile,
16
18
  provisionsIntegration
17
- } from "../chunk-gnz747z2.js";
18
- import"../chunk-dz91avrb.js";
19
+ } from "../chunk-rxetj2p9.js";
20
+ import"../chunk-96fjaysn.js";
21
+ import"../chunk-5wgwscd0.js";
19
22
 
20
23
  // bin/app.ts
21
24
  import * as cdk from "aws-cdk-lib";
@@ -1,297 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
  // @bun
3
3
  import {
4
- BASE_SLACK_EVENTS,
5
- CAPABILITY_IDS,
6
- CUSTOMIZATION_ARTIFACT_NAME,
7
4
  RELEASE_BUCKET_ENV,
8
5
  RELEASE_VERSION_RE,
6
+ manifestKey,
7
+ releaseCacheDir,
8
+ skillsKey,
9
+ verifyManifest
10
+ } from "../chunk-pxhftba1.js";
11
+ import {
12
+ DEFAULT_RELEASE_BUCKET,
13
+ expectedKeyArn
14
+ } from "../chunk-dz91avrb.js";
15
+ import {
16
+ argv,
17
+ aws,
18
+ awsContext,
19
+ awsMutate,
20
+ callerAccountId,
21
+ cdkEnv,
22
+ createSecretString,
23
+ instanceBanner,
24
+ loadInstanceContext,
25
+ putSecretJson,
26
+ putSecretString,
27
+ readSecretJson,
28
+ readSecretString,
29
+ resolveInstanceFilePath,
30
+ run,
31
+ runCapture,
32
+ secretExists,
33
+ stackExists,
34
+ stackOutput
35
+ } from "../chunk-9zx95xs6.js";
36
+ import {
37
+ CUSTOMIZATION_ARTIFACT_NAME,
9
38
  instanceNames,
39
+ slackCommandPrefix,
40
+ versionedConfigKey
41
+ } from "../chunk-rxetj2p9.js";
42
+ import"../chunk-m76jt5rc.js";
43
+ import"../chunk-3614v9rr.js";
44
+ import {
45
+ BASE_SLACK_EVENTS,
46
+ CAPABILITY_IDS,
10
47
  isScheduledJob,
11
48
  isWebhookJob,
12
49
  jobWebhookKey,
13
- loadInstanceFile,
14
- manifestKey,
15
50
  parseInstanceConfig,
16
51
  parseSchedule,
17
- releaseCacheDir,
18
52
  requiredGithubPermissions,
19
53
  requiredSlackCommands,
20
- requiredSlackScopes,
21
- skillsKey,
22
- slackCommandPrefix,
23
- verifyManifest,
24
- versionedConfigKey
25
- } from "../chunk-gnz747z2.js";
54
+ requiredSlackScopes
55
+ } from "../chunk-96fjaysn.js";
26
56
  import {
27
- DEFAULT_RELEASE_BUCKET,
28
- expectedKeyArn
29
- } from "../chunk-dz91avrb.js";
57
+ __require
58
+ } from "../chunk-5wgwscd0.js";
30
59
 
31
60
  // src/deploy/cli.ts
32
61
  import { readFileSync as readFileSync10 } from "node:fs";
33
62
 
34
- // src/deploy/aws.ts
35
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
36
- import { tmpdir } from "node:os";
37
- import { join } from "node:path";
38
-
39
- // src/deploy/instance.ts
40
- import { dirname } from "node:path";
41
- var INSTANCE_FILE_ENV = "FOUNDATION_INSTANCE_FILE";
42
- function resolveInstanceFilePath(argv = [], env = {}) {
43
- for (let i = 0;i < argv.length; i++) {
44
- const arg = argv[i] ?? "";
45
- if (arg === "--instance") {
46
- const next = argv[i + 1];
47
- if (next === undefined || next.startsWith("-"))
48
- throw new Error("--instance needs a path, e.g. --instance .foundation/instance.yaml");
49
- return next;
50
- }
51
- if (arg.startsWith("--instance=")) {
52
- const value = arg.slice("--instance=".length);
53
- if (value === "")
54
- throw new Error("--instance needs a path");
55
- return value;
56
- }
57
- }
58
- const fromEnv = env[INSTANCE_FILE_ENV];
59
- if (fromEnv !== undefined && fromEnv !== "")
60
- return fromEnv;
61
- throw new Error(`--instance <path> is required (or set ${INSTANCE_FILE_ENV}); it is the path to the deployment's instance YAML`);
62
- }
63
- function loadInstanceContext(path, cwd = process.cwd()) {
64
- const file = loadInstanceFile(path, cwd);
65
- return { ...file, root: dirname(file.path) };
66
- }
67
- function loadInstanceFromArgs(argv = process.argv.slice(2), env = process.env) {
68
- return loadInstanceContext(resolveInstanceFilePath(argv, env));
69
- }
70
- function instanceBanner(context) {
71
- const { instance } = context;
72
- return `instance: ${instance.name} (account ${instance.aws.account}, region ${instance.aws.region})
73
- file: ${context.path}
74
- config: ${context.configPath}`;
75
- }
76
-
77
- // src/deploy/sh.ts
78
- import { spawn as nodeSpawn } from "node:child_process";
79
- function formatCommand(cmd, env) {
80
- const prefix = Object.entries(env ?? {}).map(([k, v]) => `${k}=${quote(v)}`).join(" ");
81
- const body = cmd.map(quote).join(" ");
82
- return prefix === "" ? body : `${prefix} ${body}`;
83
- }
84
- function quote(value) {
85
- return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
86
- }
87
- function start(cmd, opts, stdout) {
88
- const [bin, ...rest] = cmd;
89
- if (bin === undefined)
90
- throw new Error("run: empty command");
91
- const child = nodeSpawn(bin, rest, {
92
- cwd: opts.cwd,
93
- env: { ...process.env, ...opts.env },
94
- stdio: [
95
- opts.stdin === undefined ? "ignore" : "pipe",
96
- stdout,
97
- stdout === "inherit" ? "inherit" : "pipe"
98
- ]
99
- });
100
- if (opts.stdin !== undefined && child.stdin !== null) {
101
- child.stdin.end(opts.stdin);
102
- }
103
- return child;
104
- }
105
- function exited(child) {
106
- return new Promise((resolve, reject) => {
107
- child.once("error", reject);
108
- child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
109
- });
110
- }
111
- function collect(stream) {
112
- if (stream === null)
113
- return Promise.resolve("");
114
- return new Promise((resolve, reject) => {
115
- const chunks = [];
116
- stream.on("data", (c) => chunks.push(c));
117
- stream.once("error", reject);
118
- stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
119
- });
120
- }
121
- async function run(cmd, opts = {}) {
122
- if (opts.dryRun === true) {
123
- console.log(` $ ${formatCommand(cmd, opts.env)}`);
124
- return;
125
- }
126
- const code = await exited(start(cmd, opts, "inherit"));
127
- if (code !== 0)
128
- throw new Error(`${formatCommand(cmd)} exited ${code}`);
129
- }
130
- async function runCapture(cmd, opts = {}) {
131
- const child = start(cmd, opts, "pipe");
132
- const [out, err, code] = await Promise.all([
133
- collect(child.stdout),
134
- collect(child.stderr),
135
- exited(child)
136
- ]);
137
- if (code !== 0)
138
- throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
139
- return out.trim();
140
- }
141
-
142
- // src/deploy/aws.ts
143
- function resolveProfile(fallback, explicit, env = process.env) {
144
- if (env.FOUNDATION_NO_PROFILE === "1")
145
- return "";
146
- if (explicit !== undefined)
147
- return explicit;
148
- if (env.AWS_PROFILE !== undefined && env.AWS_PROFILE !== "")
149
- return env.AWS_PROFILE;
150
- if (env.GITHUB_ACTIONS === "true")
151
- return "";
152
- return fallback;
153
- }
154
- function awsContext(overrides = {}) {
155
- const env = overrides.env ?? process.env;
156
- const paths = overrides.paths ?? loadInstanceFromArgs();
157
- const instance = paths.instance;
158
- return {
159
- profile: resolveProfile(instance.aws.profile, overrides.profile, env),
160
- region: overrides.region ?? env.AWS_REGION ?? instance.aws.region,
161
- dryRun: overrides.dryRun ?? false,
162
- instance,
163
- paths,
164
- names: instanceNames(instance)
165
- };
166
- }
167
- function argv(ctx, args) {
168
- const profile = ctx.profile === "" ? [] : ["--profile", ctx.profile];
169
- return ["aws", ...args, ...profile, "--region", ctx.region];
170
- }
171
- function cdkEnv(ctx) {
172
- return {
173
- ...ctx.profile === "" ? {} : { AWS_PROFILE: ctx.profile },
174
- AWS_REGION: ctx.region,
175
- CDK_DEFAULT_REGION: ctx.region
176
- };
177
- }
178
- async function aws(ctx, args, stdin) {
179
- return runCapture(argv(ctx, args), { stdin });
180
- }
181
- async function awsMutate(ctx, args, stdin) {
182
- if (ctx.dryRun === true) {
183
- console.log(` $ ${argv(ctx, args).join(" ")}${stdin === undefined ? "" : " < (stdin)"}`);
184
- return;
185
- }
186
- await run(argv(ctx, args), { stdin });
187
- }
188
- async function stackOutput(ctx, stack, key) {
189
- const value = await aws(ctx, [
190
- "cloudformation",
191
- "describe-stacks",
192
- "--stack-name",
193
- stack,
194
- "--query",
195
- `Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue`,
196
- "--output",
197
- "text"
198
- ]);
199
- if (value === "" || value === "None")
200
- throw new Error(`stack ${stack} has no output ${key} — has it been deployed?`);
201
- return value;
202
- }
203
- async function stackExists(ctx, stack) {
204
- try {
205
- await aws(ctx, [
206
- "cloudformation",
207
- "describe-stacks",
208
- "--stack-name",
209
- stack,
210
- "--output",
211
- "text"
212
- ]);
213
- return true;
214
- } catch {
215
- return false;
216
- }
217
- }
218
- async function secretExists(ctx, secretId) {
219
- try {
220
- await aws(ctx, [
221
- "secretsmanager",
222
- "describe-secret",
223
- "--secret-id",
224
- secretId,
225
- "--output",
226
- "text"
227
- ]);
228
- return true;
229
- } catch {
230
- return false;
231
- }
232
- }
233
- async function readSecretString(ctx, secretId) {
234
- return aws(ctx, [
235
- "secretsmanager",
236
- "get-secret-value",
237
- "--secret-id",
238
- secretId,
239
- "--query",
240
- "SecretString",
241
- "--output",
242
- "text"
243
- ]);
244
- }
245
- async function readSecretJson(ctx, secretId) {
246
- return JSON.parse(await readSecretString(ctx, secretId));
247
- }
248
- async function putSecretString(ctx, secretId, value) {
249
- await throughSecretFile(value, (file) => awsMutate(ctx, [
250
- "secretsmanager",
251
- "put-secret-value",
252
- "--secret-id",
253
- secretId,
254
- "--secret-string",
255
- `file://${file}`
256
- ]));
257
- }
258
- async function createSecretString(ctx, secretId, value, description) {
259
- await throughSecretFile(value, (file) => awsMutate(ctx, [
260
- "secretsmanager",
261
- "create-secret",
262
- "--name",
263
- secretId,
264
- "--description",
265
- description,
266
- "--secret-string",
267
- `file://${file}`
268
- ]));
269
- }
270
- async function throughSecretFile(value, body) {
271
- const dir = mkdtempSync(join(tmpdir(), "foundation-secret-"));
272
- const file = join(dir, "value.json");
273
- try {
274
- writeFileSync(file, value, { mode: 384 });
275
- await body(file);
276
- } finally {
277
- rmSync(dir, { recursive: true, force: true });
278
- }
279
- }
280
- async function putSecretJson(ctx, secretId, value) {
281
- await putSecretString(ctx, secretId, JSON.stringify(value));
282
- }
283
- async function callerAccountId(ctx) {
284
- return aws(ctx, ["sts", "get-caller-identity", "--query", "Account", "--output", "text"]);
285
- }
286
-
287
63
  // src/deploy/config-sync.ts
288
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync } from "node:fs";
289
- import { tmpdir as tmpdir2 } from "node:os";
290
- import { join as join3, resolve as resolve3 } from "node:path";
64
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, readdirSync } from "node:fs";
65
+ import { tmpdir } from "node:os";
66
+ import { join as join2, resolve as resolve3 } from "node:path";
291
67
 
292
68
  // src/deploy/paths.ts
293
69
  import { existsSync, readFileSync } from "node:fs";
294
- import { dirname as dirname2, resolve } from "node:path";
70
+ import { dirname, resolve } from "node:path";
295
71
  import { fileURLToPath } from "node:url";
296
72
  var PACKAGE_NAME = "@deployfoundation/foundation-deploy";
297
73
  function packageRoot(from) {
@@ -305,20 +81,20 @@ function packageRoot(from) {
305
81
  return dir;
306
82
  } catch {}
307
83
  }
308
- const parent = dirname2(dir);
84
+ const parent = dirname(dir);
309
85
  if (parent === dir)
310
86
  throw new Error(`cannot find the ${PACKAGE_NAME} package root above ${from}`);
311
87
  dir = parent;
312
88
  }
313
89
  }
314
- var INFRA_ROOT = packageRoot(dirname2(fileURLToPath(import.meta.url)));
90
+ var INFRA_ROOT = packageRoot(dirname(fileURLToPath(import.meta.url)));
315
91
  var FOUNDATION_ROOT = resolve(INFRA_ROOT, "..", "..");
316
92
  var AGENT_DOCKERFILE = "packages/infra/agent-image/Dockerfile";
317
93
  var PACKAGE_ASSETS = resolve(INFRA_ROOT, "src", "deploy", "assets");
318
94
 
319
95
  // src/deploy/release.ts
320
96
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
321
- import { dirname as dirname3, isAbsolute, join as join2, resolve as resolve2 } from "node:path";
97
+ import { dirname as dirname2, isAbsolute, join, resolve as resolve2 } from "node:path";
322
98
 
323
99
  // src/release/kms.ts
324
100
  import { KMSClient, SignCommand, VerifyCommand } from "@aws-sdk/client-kms";
@@ -357,14 +133,14 @@ function flag(args, name) {
357
133
  return value ?? (inline === undefined ? undefined : inline.slice(name.length + 1));
358
134
  }
359
135
  function toolVersion(infraRoot = INFRA_ROOT) {
360
- const { version } = JSON.parse(readFileSync2(join2(infraRoot, "package.json"), "utf8"));
136
+ const { version } = JSON.parse(readFileSync2(join(infraRoot, "package.json"), "utf8"));
361
137
  return version;
362
138
  }
363
139
  function foundationVersion(release) {
364
140
  return release?.version ?? `v${toolVersion()}`;
365
141
  }
366
142
  function hasFoundationWorkspace(foundationRoot = FOUNDATION_ROOT) {
367
- return existsSync2(join2(foundationRoot, "packages", "gateway", "package.json"));
143
+ return existsSync2(join(foundationRoot, "packages", "gateway", "package.json"));
368
144
  }
369
145
  function releaseRequest(args, options = {}) {
370
146
  const env = options.env ?? process.env;
@@ -387,7 +163,7 @@ function releaseRequest(args, options = {}) {
387
163
  return { version, bucket };
388
164
  }
389
165
  async function s3Download(ctx, bucket, key, dest) {
390
- mkdirSync(dirname3(dest), { recursive: true });
166
+ mkdirSync(dirname2(dest), { recursive: true });
391
167
  await aws(ctx, ["s3", "cp", `s3://${bucket}/${key}`, dest]);
392
168
  }
393
169
  async function verifyRelease(ctx, request, deps = {}) {
@@ -397,7 +173,7 @@ async function verifyRelease(ctx, request, deps = {}) {
397
173
  const local = localManifestRef(request);
398
174
  const version = request.version;
399
175
  const cacheDir = deps.cacheDir ?? releaseCacheDir(version ?? "unversioned", env);
400
- const manifestPath = local ?? join2(cacheDir, "manifest.json");
176
+ const manifestPath = local ?? join(cacheDir, "manifest.json");
401
177
  if (local === undefined) {
402
178
  const source = s3ManifestRef(request);
403
179
  await download(ctx, source.bucket, source.key, manifestPath);
@@ -408,7 +184,7 @@ async function verifyRelease(ctx, request, deps = {}) {
408
184
  ...version === undefined ? {} : { expectVersion: version },
409
185
  ...deps.verifyArtifacts === false ? {} : {
410
186
  readArtifact: async (key) => {
411
- const dest = join2(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
187
+ const dest = join(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
412
188
  await download(ctx, request.bucket, key, dest);
413
189
  return readFileSync2(dest);
414
190
  }
@@ -425,7 +201,7 @@ function plannedRelease(request) {
425
201
  return {
426
202
  version,
427
203
  bucket: request.bucket,
428
- manifestPath: local ?? join2(releaseCacheDir(version), "manifest.json")
204
+ manifestPath: local ?? join(releaseCacheDir(version), "manifest.json")
429
205
  };
430
206
  }
431
207
  function localManifestRef(request) {
@@ -524,13 +300,13 @@ async function configSync(ctx, opts = {}) {
524
300
  }
525
301
  async function unpackReleaseSkills(ctx, release) {
526
302
  const key = release.manifest?.skills.key ?? skillsKey(release.version);
527
- const dir = mkdtempSync2(join3(tmpdir2(), "foundation-skills-"));
528
- const tarball = join3(dir, "skills.tar.gz");
303
+ const dir = mkdtempSync(join2(tmpdir(), "foundation-skills-"));
304
+ const tarball = join2(dir, "skills.tar.gz");
529
305
  await awsMutate(ctx, ["s3", "cp", `s3://${release.bucket}/${key}`, tarball]);
530
306
  return await extractSkillsTarball(tarball, dir, { dryRun: ctx.dryRun });
531
307
  }
532
308
  async function extractSkillsTarball(tarball, dir, opts = {}) {
533
- const contents = join3(dir, "skills");
309
+ const contents = join2(dir, "skills");
534
310
  await run(["tar", "-xzf", tarball, "-C", dir], { dryRun: opts.dryRun });
535
311
  if (opts.dryRun !== true && !existsSync3(contents))
536
312
  throw new Error(`${tarball} has no top-level skills/ directory`);
@@ -543,7 +319,7 @@ async function extractSkillsTarball(tarball, dir, opts = {}) {
543
319
  import { createHash } from "node:crypto";
544
320
  import { existsSync as existsSync4 } from "node:fs";
545
321
  import { createRequire } from "node:module";
546
- import { join as join5 } from "node:path";
322
+ import { join as join4 } from "node:path";
547
323
 
548
324
  // ../connectors/src/github.ts
549
325
  var GITHUB_TOKEN_RESPONSE_MAX_BYTES = 128 * 1024;
@@ -773,9 +549,6 @@ function isSafeOpaqueString(value, max) {
773
549
  function isPlainObject(value) {
774
550
  return typeof value === "object" && value !== null && !Array.isArray(value);
775
551
  }
776
- // ../connectors/src/crm.ts
777
- var CRM_MAX_REQUEST_BYTES = 512 * 1024;
778
- var CRM_MAX_RESPONSE_BYTES = 512 * 1024;
779
552
  // ../connectors/src/upwork.ts
780
553
  var UPWORK_MAX_RESPONSE_BYTES = 1024 * 1024;
781
554
  var UPWORK_MAX_ACCESS_TOKEN_TTL_SECONDS = 24 * 60 * 60;
@@ -962,8 +735,8 @@ function adminsCsvFromFile(path) {
962
735
 
963
736
  // src/deploy/endpoint.ts
964
737
  import { mkdtemp, readFile, rm } from "node:fs/promises";
965
- import { tmpdir as tmpdir3 } from "node:os";
966
- import { join as join4 } from "node:path";
738
+ import { tmpdir as tmpdir2 } from "node:os";
739
+ import { join as join3 } from "node:path";
967
740
  function cliRunner(ctx) {
968
741
  return {
969
742
  capture: (args) => aws(ctx, args),
@@ -1111,8 +884,8 @@ async function smokeInvoke(runner, opts) {
1111
884
  await runner.mutate([...args, "/dev/stdout"]);
1112
885
  return;
1113
886
  }
1114
- const dir = await mkdtemp(join4(tmpdir3(), "foundation-smoke-"));
1115
- const outFile = join4(dir, "response.json");
887
+ const dir = await mkdtemp(join3(tmpdir2(), "foundation-smoke-"));
888
+ const outFile = join3(dir, "response.json");
1116
889
  let out;
1117
890
  try {
1118
891
  const printed = await runner.capture([...args, outFile]);
@@ -1453,11 +1226,11 @@ import {
1453
1226
  mkdirSync as mkdirSync3,
1454
1227
  readFileSync as readFileSync5,
1455
1228
  realpathSync,
1456
- rmSync as rmSync2,
1229
+ rmSync,
1457
1230
  statSync,
1458
- writeFileSync as writeFileSync2
1231
+ writeFileSync
1459
1232
  } from "node:fs";
1460
- import { dirname as dirname4, isAbsolute as isAbsolute2, posix, relative, resolve as resolve4, sep } from "node:path";
1233
+ import { dirname as dirname3, isAbsolute as isAbsolute2, posix, relative, resolve as resolve4, sep } from "node:path";
1461
1234
  import { isDeepStrictEqual } from "node:util";
1462
1235
  function sourceRootFor(sourceDir) {
1463
1236
  if (sourceDir === undefined || sourceDir === "")
@@ -1561,7 +1334,7 @@ function stageCustomization(options) {
1561
1334
  const platformConfigPath = options.paths.configPath;
1562
1335
  const stagedPath = stagedCustomizationPath(options.paths);
1563
1336
  if (options.dryRun !== true)
1564
- rmSync2(stagedPath, { force: true });
1337
+ rmSync(stagedPath, { force: true });
1565
1338
  const customization = instance.customization;
1566
1339
  if (customization === undefined)
1567
1340
  return { status: "not-configured", runtimeConfigPath: platformConfigPath };
@@ -1579,8 +1352,8 @@ function stageCustomization(options) {
1579
1352
  assertValidTenantJobs(tenantConfig);
1580
1353
  assertProtectedPolicy(platformConfig, tenantConfig);
1581
1354
  if (options.dryRun !== true) {
1582
- mkdirSync3(dirname4(stagedPath), { recursive: true });
1583
- writeFileSync2(stagedPath, yaml);
1355
+ mkdirSync3(dirname3(stagedPath), { recursive: true });
1356
+ writeFileSync(stagedPath, yaml);
1584
1357
  }
1585
1358
  return {
1586
1359
  status: "staged",
@@ -1657,10 +1430,10 @@ function cdkCommand() {
1657
1430
  }
1658
1431
  }
1659
1432
  function cdkAppCommand(infraRoot = INFRA_ROOT) {
1660
- const source = join5(infraRoot, "bin", "app.ts");
1433
+ const source = join4(infraRoot, "bin", "app.ts");
1661
1434
  if (existsSync4(source))
1662
1435
  return `bun run ${source}`;
1663
- return `${process.execPath} ${join5(infraRoot, "dist", "bin", "app.js")}`;
1436
+ return `${process.execPath} ${join4(infraRoot, "dist", "bin", "app.js")}`;
1664
1437
  }
1665
1438
  async function cdkDeploy(ctx, opts) {
1666
1439
  const context = [
@@ -1931,9 +1704,9 @@ import { createServer } from "node:http";
1931
1704
 
1932
1705
  // src/deploy/github-app-manifest.ts
1933
1706
  import { readFileSync as readFileSync6 } from "node:fs";
1934
- import { join as join6 } from "node:path";
1707
+ import { join as join5 } from "node:path";
1935
1708
  import { parse as parseYaml } from "yaml";
1936
- var GITHUB_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "github-app-manifest.yml");
1709
+ var GITHUB_APP_MANIFEST_PATH = join5(PACKAGE_ASSETS, "github-app-manifest.yml");
1937
1710
  function manifestUrl(instance) {
1938
1711
  const repo = instance.github.defaultRepo ?? instanceNames(instance).defaultRepo;
1939
1712
  return `https://github.com/${repo === "" ? instance.github.org : repo}`;
@@ -2148,9 +1921,9 @@ import { resolve as resolve5 } from "node:path";
2148
1921
 
2149
1922
  // src/deploy/slack-manifest.ts
2150
1923
  import { readFileSync as readFileSync8 } from "node:fs";
2151
- import { join as join7 } from "node:path";
1924
+ import { join as join6 } from "node:path";
2152
1925
  import { parse as parse2 } from "yaml";
2153
- var SLACK_APP_MANIFEST_PATH = join7(PACKAGE_ASSETS, "slack-app-manifest.yml");
1926
+ var SLACK_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "slack-app-manifest.yml");
2154
1927
  function buildSlackManifest(template, instance, env) {
2155
1928
  const events = env.EVENTS_REQUEST_URL;
2156
1929
  const commands = env.COMMANDS_REQUEST_URL ?? events?.replace(/\/events$/, "/commands");
@@ -2598,7 +2371,10 @@ var COMMANDS = [
2598
2371
  "github-app-create",
2599
2372
  "slack-manifest",
2600
2373
  "stage-customization",
2601
- "job-key"
2374
+ "job-key",
2375
+ "crm-key",
2376
+ "crm-check",
2377
+ "crm-replay"
2602
2378
  ];
2603
2379
  var USAGE = `usage: foundation-deploy <command> --instance <path> [options]
2604
2380
 
@@ -2617,6 +2393,12 @@ commands
2617
2393
  slack-manifest print the instance's Slack app manifest as JSON
2618
2394
  stage-customization validate and stage a tenant-owned runtime config
2619
2395
  job-key <id> print a webhook job's URL and signing key as JSON
2396
+ crm-key write the company CRM's crm/1 key, read from stdin, into
2397
+ the instance's <secretPrefix>/crm/http secret
2398
+ crm-check run the crm/1 conformance suite against the instance's
2399
+ CRM (crm.provider kind http) with its stored key
2400
+ crm-replay replay the agent's CRM writes (JSONL) into the
2401
+ instance's own table after a rollback to dynamodb
2620
2402
 
2621
2403
  options
2622
2404
  --instance <path> REQUIRED: path to the deployment's instance YAML
@@ -2652,6 +2434,24 @@ job-key options
2652
2434
  --put-secret <id> write {url, key} into this Secrets Manager secret
2653
2435
  (created if absent) instead of printing the key
2654
2436
 
2437
+ crm-key options
2438
+ --stdin REQUIRED: read the key from a pipe, never argv, e.g.
2439
+ pbpaste | foundation-deploy crm-key --instance <path> --stdin
2440
+
2441
+ crm-check options
2442
+ --allow-skips pass despite cases only a person can set up (they need
2443
+ the CRM's own harness); without it they count as failed
2444
+ --base-url <url> check any crm/1 server instead of the instance's: needs
2445
+ --tenant <id> the tenant its /meta names, and the key on stdin
2446
+ --key-stdin (--key-stdin). No --instance in this form.
2447
+ --write with --base-url only: also run the write cases, which
2448
+ create records and spend the week's outreach allowance;
2449
+ point it at a sandbox tenant
2450
+
2451
+ crm-replay options
2452
+ --from <file> REQUIRED: the writes to replay, one JSON object per line
2453
+ (docs/crm-provider-contract.md); --dry-run validates only
2454
+
2655
2455
  github-app-create options
2656
2456
  --org <org> GitHub org (default: the instance's github.org)
2657
2457
  --port <n> local callback port (default 8765)
@@ -2731,6 +2531,21 @@ ${USAGE}`);
2731
2531
  console.log(JSON.stringify(result));
2732
2532
  return 0;
2733
2533
  }
2534
+ if (command === "crm-check" && args.includes("--base-url")) {
2535
+ if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance=")))
2536
+ throw new Error("crm-check takes --instance or --base-url, not both");
2537
+ const baseUrl = flag2(args, "--base-url");
2538
+ const tenantId = flag2(args, "--tenant");
2539
+ if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
2540
+ throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
2541
+ const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2542
+ return crmCheckTarget({
2543
+ baseUrl,
2544
+ tenantId,
2545
+ write: args.includes("--write"),
2546
+ allowSkips: args.includes("--allow-skips")
2547
+ }, defaultCrmCommandDeps());
2548
+ }
2734
2549
  if (command === "github-app-create") {
2735
2550
  const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
2736
2551
  console.log(instanceBanner(paths));
@@ -2795,6 +2610,25 @@ ${USAGE}`);
2795
2610
  console.log(` ${result.status}: ${result.runtimeConfigPath}`);
2796
2611
  return 0;
2797
2612
  }
2613
+ case "crm-key": {
2614
+ const { crmKey, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2615
+ await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
2616
+ return 0;
2617
+ }
2618
+ case "crm-check": {
2619
+ if (args.includes("--write"))
2620
+ throw new Error("crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant");
2621
+ const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2622
+ return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
2623
+ }
2624
+ case "crm-replay": {
2625
+ const from = flag2(args, "--from");
2626
+ if (from === undefined || from.startsWith("-"))
2627
+ throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
2628
+ const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-sgdcmme0.js");
2629
+ const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
2630
+ return result.refused.length > 0 ? 1 : 0;
2631
+ }
2798
2632
  default:
2799
2633
  console.error(`unhandled command: ${command}
2800
2634
 
@@ -2,6 +2,7 @@
2
2
  import {
3
3
  DEFAULT_RELEASE_BUCKET
4
4
  } from "../chunk-dz91avrb.js";
5
+ import"../chunk-5wgwscd0.js";
5
6
 
6
7
  // bin/release-account.ts
7
8
  import * as cdk2 from "aws-cdk-lib";