context101-cli 0.1.0 → 0.1.2

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/bin/context101.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context101-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "Context101 self-host CLI. Use `npx context101-cli` or the `context101` bin. The public npm package named context101 is Context7's MCP, not this tool.",
6
6
  "type": "module",
@@ -19,8 +19,5 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@inquirer/prompts": "^7.8.4"
22
- },
23
- "publishConfig": {
24
- "access": "public"
25
22
  }
26
23
  }
package/src/clone.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
- import { DEFAULT_AMPLIFY_REPO } from "./defaults.js";
4
+ import { DEFAULT_AMPLIFY_REPO, HOME_SRC_REL } from "./defaults.js";
4
5
  import { findRepoRoot } from "./repo.js";
5
6
 
6
7
  export const CLONE_URL = DEFAULT_AMPLIFY_REPO;
@@ -10,6 +11,10 @@ export function resolveCloneDir(cwd, dir) {
10
11
  return path.resolve(cwd, dir || DEFAULT_CLONE_DIR);
11
12
  }
12
13
 
14
+ export function homeSrcDir(homeDir = homedir()) {
15
+ return path.join(homeDir, HOME_SRC_REL);
16
+ }
17
+
13
18
  export function ensureRepoRoot({
14
19
  cwd,
15
20
  dir,
@@ -17,16 +22,27 @@ export function ensureRepoRoot({
17
22
  io,
18
23
  dryRun = false,
19
24
  exists = existsSync,
25
+ homeDir,
26
+ preferHomeClone = false,
20
27
  } = {}) {
21
28
  const existing = findRepoRoot(cwd, exists);
22
29
  if (existing) return { repoRoot: existing, cloned: false };
23
30
 
24
- const target = resolveCloneDir(cwd, dir);
25
- const already = findRepoRoot(target, exists);
26
- if (already) return { repoRoot: already, cloned: false };
31
+ const localTarget = resolveCloneDir(cwd, dir);
32
+ const alreadyLocal = findRepoRoot(localTarget, exists);
33
+ if (alreadyLocal) return { repoRoot: alreadyLocal, cloned: false };
34
+
35
+ const resolvedHome = homeDir ?? homedir();
36
+ const homeTarget = homeSrcDir(resolvedHome);
37
+ if (preferHomeClone) {
38
+ const alreadyHome = findRepoRoot(homeTarget, exists);
39
+ if (alreadyHome) return { repoRoot: alreadyHome, cloned: false };
40
+ }
41
+
42
+ const target = preferHomeClone && !dir ? homeTarget : localTarget;
27
43
 
28
44
  if (dryRun) {
29
- io?.write?.(`Would clone ${CLONE_URL} into ${path.relative(cwd, target) || target}`);
45
+ io?.write?.(`Would clone ${CLONE_URL} into ${displayCloneTarget(cwd, target, resolvedHome)}`);
30
46
  return { repoRoot: target, cloned: false, wouldClone: true };
31
47
  }
32
48
 
@@ -58,6 +74,13 @@ export function ensureRepoRoot({
58
74
  error: `cloned ${target} but it is not a Context101 checkout (needs cdk/ and web/)`,
59
75
  };
60
76
  }
61
- io?.ok?.(`cloned into ${path.relative(cwd, cloned) || cloned}`);
77
+ io?.ok?.(`cloned into ${displayCloneTarget(cwd, cloned, resolvedHome)}`);
62
78
  return { repoRoot: cloned, cloned: true };
63
79
  }
80
+
81
+ function displayCloneTarget(cwd, target, homeDir) {
82
+ if (homeDir && (target === homeDir || target.startsWith(homeDir + path.sep))) {
83
+ return `~${target.slice(homeDir.length)}`;
84
+ }
85
+ return path.relative(cwd, target) || target;
86
+ }
package/src/config.js CHANGED
@@ -52,6 +52,7 @@ export async function runConfig(opts, ctx) {
52
52
  envFile: opts.envFile,
53
53
  home: opts.home,
54
54
  cwd: ctx.cwd,
55
+ homeDir: ctx.homeDir,
55
56
  });
56
57
 
57
58
  if (opts.configAction === "set") {
@@ -85,7 +86,7 @@ async function writeConfig(opts, { io, filePath }) {
85
86
  return 1;
86
87
  }
87
88
  if (!filePath) {
88
- io.err("no deploy-env path. Re-run from a checkout or pass --deploy-env.");
89
+ io.err("no deploy-env path. Pass --home or --deploy-env.");
89
90
  return 1;
90
91
  }
91
92
 
package/src/defaults.js CHANGED
@@ -10,6 +10,7 @@ export const BILLING_ENABLED = "false";
10
10
  export const EXAMPLE_ENV_REL = "cdk/.deploy-env.example";
11
11
  export const REPO_ENV_REL = "cdk/.deploy-env";
12
12
  export const HOME_ENV_REL = ".context101/deploy-env";
13
+ export const HOME_SRC_REL = ".context101/src";
13
14
  export const DEPLOY_CLI = "context101 deploy";
14
15
  export const LIST_CLI = "context101 list";
15
16
  export const DESTROY_CLI = "context101 destroy";
@@ -39,23 +39,22 @@ export function findDeployEnvPath({
39
39
  home = false,
40
40
  cwd,
41
41
  exists = existsSync,
42
+ homeDir,
42
43
  } = {}) {
44
+ const resolvedHome = homeDir ?? homedir();
43
45
  if (envFile) {
44
46
  return path.isAbsolute(envFile)
45
47
  ? envFile
46
48
  : path.resolve(cwd ?? repoRoot ?? process.cwd(), envFile);
47
49
  }
48
- if (home) {
49
- const homePath = path.join(homedir(), HOME_ENV_REL);
50
- return exists(homePath) ? homePath : homePath;
51
- }
50
+ const homePath = path.join(resolvedHome, HOME_ENV_REL);
51
+ if (home) return homePath;
52
52
  if (repoRoot) {
53
53
  const repoPath = path.join(repoRoot, ...REPO_ENV_REL.split("/"));
54
54
  if (exists(repoPath)) return repoPath;
55
55
  }
56
- const homePath = path.join(homedir(), HOME_ENV_REL);
57
56
  if (exists(homePath)) return homePath;
58
- return repoRoot ? path.join(repoRoot, ...REPO_ENV_REL.split("/")) : null;
57
+ return repoRoot ? path.join(repoRoot, ...REPO_ENV_REL.split("/")) : homePath;
59
58
  }
60
59
 
61
60
  export function readDeployEnvFile(filePath, { readFile = readFileSync, exists = existsSync } = {}) {
package/src/init.js CHANGED
@@ -15,10 +15,7 @@ import {
15
15
  printChecks,
16
16
  runChecks,
17
17
  } from "./checks.js";
18
- import {
19
- formatAccessResult,
20
- requestEmbeddingModelAccess,
21
- } from "./bedrock-access.js";
18
+ import { requestEmbeddingModelAccess } from "./bedrock-access.js";
22
19
  import {
23
20
  isKnownEmbeddingModel,
24
21
  listEmbeddingModels,
@@ -267,11 +264,22 @@ export async function runInit(opts, ctx) {
267
264
  "no Amplify-capable GitHub PAT yet — set CTX_GH_TOKEN to a ghp_ or github_pat_ token before deploy"
268
265
  );
269
266
  }
270
- io.write("");
271
- io.write(nextSteps(plan));
272
- io.write("");
273
267
 
274
- if (!answers.deploy) return 0;
268
+ let deploy = Boolean(answers.deploy);
269
+ if (!deploy && !opts.yes && tty) {
270
+ deploy = Boolean(
271
+ await askDeployNow(ctx, {
272
+ createRds: Boolean(answers.createRds),
273
+ seed: Boolean(answers.seed),
274
+ repository: answers.repository || "",
275
+ })
276
+ );
277
+ }
278
+
279
+ if (!deploy) {
280
+ io.write(nextSteps({ seed: answers.seed }));
281
+ return 0;
282
+ }
275
283
 
276
284
  if (answers.repository && !githubReadyForAmplify(checks, answers)) {
277
285
  io.err(
@@ -391,15 +399,28 @@ function withAwsAuth(env, { profile, accessKeyId, secretAccessKey } = {}) {
391
399
  return next;
392
400
  }
393
401
 
402
+ async function askDeployNow(ctx, details) {
403
+ if (typeof ctx.confirmDeploy === "function") {
404
+ return ctx.confirmDeploy(details);
405
+ }
406
+ const { promptDeployNow } = await import("./prompt.js");
407
+ return promptDeployNow(details);
408
+ }
409
+
394
410
  function printBedrockAccess(results, io) {
395
- io.write("Bedrock embedding access:");
396
- for (const result of results) {
397
- const line = formatAccessResult(result);
398
- if (result.status === "needs-console" || result.status === "failed") {
399
- io.warn(line);
400
- } else {
401
- io.dim(` ${line}`);
402
- }
411
+ const problems = (results || []).filter(
412
+ (result) => result.status === "needs-console" || result.status === "failed"
413
+ );
414
+ const granted = (results || []).filter((result) => result.status === "granted").length;
415
+ if (problems.length) {
416
+ const ids = problems
417
+ .slice(0, 2)
418
+ .map((result) => result.id)
419
+ .join(", ");
420
+ const extra = problems.length > 2 ? ` (+${problems.length - 2})` : "";
421
+ io.warn(`Bedrock: enable ${ids}${extra} in console → Model access`);
422
+ } else if (granted) {
423
+ io.ok("Bedrock access granted");
403
424
  }
404
425
  }
405
426
 
package/src/main.js CHANGED
@@ -19,8 +19,10 @@ export async function main(argv, ctx) {
19
19
  throw error;
20
20
  }
21
21
 
22
- if (opts.help) {
23
- io.write(helpText());
22
+ if (opts.help || opts.command === "help") {
23
+ const topic =
24
+ opts.helpTopic ?? (opts.command !== "help" ? opts.command : null);
25
+ io.write(helpText(topic));
24
26
  return 0;
25
27
  }
26
28
 
package/src/parse-args.js CHANGED
@@ -1,84 +1,90 @@
1
1
  import { DRIVER_NEON, DRIVER_POSTGRES } from "./defaults.js";
2
2
 
3
- const FLAG_HELP = `
4
- Usage: context101 <command> [options]
5
-
6
- init write a local secrets file (default)
7
- deploy deploy the AWS stack (loads deploy-env, invokes cdk)
8
- diff cdk diff with the same context flags
9
- synth cdk synth with the same context flags
10
- list, ls list Context101 CloudFormation deployments
11
- destroy, remove, rm tear down a listed stack (name required)
12
- config show deploy-env keys (values redacted)
13
- config set KEY=value write one key (chmod 600; value is not printed)
14
-
15
- CDK fails closed: a bare \`cdk deploy\` without \`-c token=\` throws
16
- instead of deleting MCP / Amplify. The CLI is the front door.
17
-
18
- context101 init
19
- context101 deploy
20
- npx context101-cli init
21
- npx context101-cli deploy
22
-
23
- --dry-run print the plan; write nothing, deploy nothing
24
- --yes, -y accept defaults (creates RDS if no --database-url)
3
+ const COMMAND_LINES = [
4
+ ["init", "write deploy-env (default); TTY asks to deploy"],
5
+ ["deploy", "deploy the AWS stack"],
6
+ ["diff", "cdk diff with the same context"],
7
+ ["synth", "cdk synth with the same context"],
8
+ ["list", "list Context101 CloudFormation stacks"],
9
+ ["destroy", "tear down a listed stack (name required)"],
10
+ ["config", "show deploy-env keys (values redacted)"],
11
+ ["config set", "write one key (chmod 600; value is not printed)"],
12
+ ["help", "list commands"],
13
+ ];
14
+
15
+ const TOPIC_HELP = {
16
+ init: `init write deploy-env (default); TTY asks to deploy
17
+
18
+ --dry-run
19
+ --yes, -y accept defaults (creates RDS if no --database-url);
20
+ required --aws-profile when several exist
25
21
  --force overwrite an existing env file
26
- --dir <path> clone into this directory when not in a checkout
22
+ --dir <path> clone here when not in a checkout
27
23
  --deploy-env <path> default: <repo>/cdk/.deploy-env
28
- --home write ~/.context101/deploy-env instead
29
- --database-url <url> Postgres URL (also reads DATABASE_URL)
30
- --create-rds CDK provisions RDS Postgres (default when no URL)
24
+ --home ~/.context101/deploy-env
25
+ --database-url <url>
26
+ --create-rds default when no URL
31
27
  --database-driver ${DRIVER_NEON} | ${DRIVER_POSTGRES}
32
28
  --database-prepare true | false
33
- --aws-profile <name> also reads AWS_PROFILE; required with --yes
34
- when more than one profile exists
35
- --aws-access-key-id used when no profile exists (also AWS_ACCESS_KEY_ID)
29
+ --aws-profile <name>
30
+ --aws-access-key-id
36
31
  --aws-secret-access-key
37
- used when no profile exists (also AWS_SECRET_ACCESS_KEY)
38
- --repo <url> watch this GitHub repo with Amplify
39
- (default: skip Amplify, unless gh login is jginorio)
40
- --embed-model <id> optional CDK default embedding model
41
- (brains still pick any Titan/Cohere model in the app)
42
- --skip-bedrock-access do not request Bedrock model access during init
43
- --seed first deploy uploads knowledge/ once
44
- --deploy deploy after writing (combine with --yes)
45
-
46
- deploy / diff / synth:
47
- --seed upload knowledge/ once (first deploy only)
32
+ --repo <url> Amplify watch (skipped unless gh login is jginorio)
33
+ --embed-model <id>
34
+ --skip-bedrock-access
35
+ --seed
36
+ --deploy deploy after writing without asking`,
37
+
38
+ deploy: `deploy deploy the AWS stack
39
+
40
+ --seed
48
41
  --deploy-env <path>
49
42
  --home
50
- --dry-run print the command; invoke nothing
43
+ --dry-run`,
44
+
45
+ diff: `diff — cdk diff with the same context as deploy
46
+
47
+ --seed
48
+ --deploy-env <path>
49
+ --home
50
+ --dry-run`,
51
+
52
+ synth: `synth — cdk synth with the same context as deploy
53
+
54
+ --seed
55
+ --deploy-env <path>
56
+ --home
57
+ --dry-run`,
58
+
59
+ list: `list — list Context101 CloudFormation stacks (no checkout)
51
60
 
52
- list:
53
61
  --aws-profile <name>
54
62
  --aws-access-key-id
55
- --aws-secret-access-key
63
+ --aws-secret-access-key`,
56
64
 
57
- destroy <StackName>:
58
- --yes, -y skip the confirmation prompt
65
+ destroy: `destroy <name> — tear down a listed stack (clones if needed)
66
+
67
+ --yes, -y
59
68
  --aws-profile <name>
60
69
  --aws-access-key-id
61
70
  --aws-secret-access-key
71
+ --dir <path>
62
72
  --deploy-env <path>
63
73
  --home
64
- --dry-run print the plan; destroy nothing
74
+ --dry-run`,
75
+
76
+ config: `config — show deploy-env keys (values redacted)
65
77
 
66
- config:
67
78
  --deploy-env <path>
68
- --home
79
+ --home`,
69
80
 
70
- From this checkout (after npm install):
71
- npm run context101 -- init
72
- npm run context101 -- deploy
73
- npx context101-cli init
74
- npx context101-cli deploy
75
- context101 list
76
- context101 destroy Context101Stack
77
- context101 config
81
+ "config set": `config set KEY=value — write one key (chmod 600; value is not printed)
78
82
 
79
- npx context101 (unscoped) downloads Context7's MCP from npm — unrelated.
80
- The publishable CLI is context101-cli; the bin name is context101.
81
- `.trim();
83
+ --deploy-env <path>
84
+ --home`,
85
+
86
+ help: `help [command] — list commands, or flags for one command`,
87
+ };
82
88
 
83
89
  const INIT_ONLY = new Set([
84
90
  "--yes",
@@ -101,6 +107,7 @@ const INIT_ONLY = new Set([
101
107
  const DESTROY_FROM_INIT = new Set([
102
108
  "--yes",
103
109
  "-y",
110
+ "--dir",
104
111
  "--aws-profile",
105
112
  "--aws-access-key-id",
106
113
  "--aws-secret-access-key",
@@ -125,10 +132,22 @@ const COMMANDS = {
125
132
  remove: "destroy",
126
133
  rm: "destroy",
127
134
  config: "config",
135
+ help: "help",
128
136
  };
129
137
 
130
- export function helpText() {
131
- return FLAG_HELP;
138
+ export function helpText(topic) {
139
+ if (topic) {
140
+ const key = COMMANDS[topic] ?? topic;
141
+ return TOPIC_HELP[key] ?? helpText();
142
+ }
143
+ const nameW = Math.max(...COMMAND_LINES.map(([name]) => name.length));
144
+ return [
145
+ "Usage: context101 <command>",
146
+ "",
147
+ ...COMMAND_LINES.map(([name, desc]) => ` ${name.padEnd(nameW)} ${desc}`),
148
+ "",
149
+ "npx context101 (unscoped) is Context7's MCP — use context101-cli.",
150
+ ].join("\n");
132
151
  }
133
152
 
134
153
  export function parseArgs(argv) {
@@ -157,24 +176,30 @@ export function parseArgs(argv) {
157
176
  configAction: "show",
158
177
  configKey: null,
159
178
  configValue: null,
179
+ helpTopic: null,
160
180
  };
161
181
 
162
182
  const args = [...argv];
163
183
  if (args.length === 0) return opts;
164
184
 
165
185
  const first = args[0];
166
- if (COMMANDS[first]) {
186
+ if (first === "--help" || first === "-h") {
187
+ opts.command = "help";
188
+ opts.help = true;
189
+ args.shift();
190
+ } else if (COMMANDS[first]) {
167
191
  opts.command = COMMANDS[first];
168
192
  args.shift();
169
- } else if (first === "help" || first === "--help" || first === "-h") {
170
- opts.help = true;
171
- return opts;
172
193
  } else if (!first.startsWith("-")) {
173
194
  const err = new Error(`unknown command: ${first}`);
174
195
  err.code = "USAGE";
175
196
  throw err;
176
197
  }
177
198
 
199
+ if (opts.command === "help") {
200
+ return parseHelpArgs(opts, args);
201
+ }
202
+
178
203
  if (opts.command === "config" && args[0] === "set") {
179
204
  args.shift();
180
205
  const pair = args.shift();
@@ -283,6 +308,33 @@ export function parseArgs(argv) {
283
308
  return opts;
284
309
  }
285
310
 
311
+ function parseHelpArgs(opts, args) {
312
+ opts.help = true;
313
+ if (args[0] && !args[0].startsWith("-")) {
314
+ const topic = args.shift();
315
+ if (topic === "config" && args[0] === "set") {
316
+ args.shift();
317
+ opts.helpTopic = "config set";
318
+ } else if (COMMANDS[topic] && topic !== "help") {
319
+ opts.helpTopic = COMMANDS[topic];
320
+ } else if (topic === "help") {
321
+ opts.helpTopic = "help";
322
+ } else {
323
+ const err = new Error(`unknown command: ${topic}`);
324
+ err.code = "USAGE";
325
+ throw err;
326
+ }
327
+ }
328
+ while (args.length) {
329
+ const arg = args.shift();
330
+ if (arg === "--help" || arg === "-h") continue;
331
+ const err = new Error(`unknown flag: ${arg}`);
332
+ err.code = "USAGE";
333
+ throw err;
334
+ }
335
+ return opts;
336
+ }
337
+
286
338
  function flagAllowed(command, arg) {
287
339
  if (!INIT_ONLY.has(arg)) return true;
288
340
  if (command === "init") return true;
package/src/plan.js CHANGED
@@ -83,29 +83,5 @@ function bootstrapLabel(plan) {
83
83
  }
84
84
 
85
85
  export function nextSteps(plan) {
86
- const rds = plan.createRds
87
- ? "CDK will create RDS Postgres and apply the control-plane schema. Fetch the secret via ControlPlaneDbSecretArn — the password is never printed."
88
- : null;
89
- const next = [
90
- `Next: ${deployCommand(plan.seed)}`,
91
- plan.seed ? null : "First time? add --seed to upload the example knowledge/ files once.",
92
- rds,
93
- ];
94
- if (plan.repository) {
95
- return [
96
- ...next,
97
- "After Amplify is up (~4 min), open /setup on WebAppDefaultDomain (or your own domain).",
98
- "Leave BETTER_AUTH_URL / APP_URL unset unless you bring your own host. CDK fills the Amplify default.",
99
- ]
100
- .filter(Boolean)
101
- .join("\n");
102
- }
103
- return [
104
- ...next,
105
- "Amplify is skipped — this deploy is the AWS stack only (no GitHub-watched web app).",
106
- "Run the web app from web/, or re-run init with --repo when you want Amplify.",
107
- "Leave BETTER_AUTH_URL / APP_URL unset unless you bring your own host.",
108
- ]
109
- .filter(Boolean)
110
- .join("\n");
86
+ return deployCommand(plan.seed);
111
87
  }
package/src/prompt.js CHANGED
@@ -6,12 +6,24 @@ import {
6
6
  import { inferDriver, inferPrepare } from "./env-file.js";
7
7
  import { normalizeRepoUrl } from "./repo.js";
8
8
 
9
+ export function deployNowMessage({ createRds = false } = {}) {
10
+ return createRds
11
+ ? "Deploy the stack now? (CDK + Docker; creates RDS)"
12
+ : "Deploy the stack now?";
13
+ }
14
+
15
+ export async function promptDeployNow(details = {}) {
16
+ const { confirm } = await import("@inquirer/prompts");
17
+ return confirm({
18
+ message: deployNowMessage(details),
19
+ default: false,
20
+ });
21
+ }
22
+
9
23
  export async function promptAnswers({ defaults, io, exec, env }) {
10
24
  const { confirm, input, password, select } = await import("@inquirer/prompts");
11
25
 
12
- io.write("This writes a local secrets file. Deploy afterwards with:");
13
- io.write(" context101 deploy");
14
- io.write("Press ^C to quit.");
26
+ io.write("This writes a local secrets file. Press ^C to quit.");
15
27
  io.write("");
16
28
 
17
29
  const region = await input({
package/src/stacks.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEPLOY_CLI, DESTROY_CLI, LIST_CLI, SMOOTH_REGION } from "./defaults.js";
2
2
  import { runCdk } from "./cdk-invoke.js";
3
+ import { ensureRepoRoot } from "./clone.js";
3
4
  import { createExec } from "./exec.js";
4
- import { findRepoRoot } from "./repo.js";
5
5
  import { banner, writers } from "./style.js";
6
6
 
7
7
  export function parseStackSummaries(payload) {
@@ -70,12 +70,6 @@ export async function runList(opts, ctx) {
70
70
 
71
71
  banner(ctx);
72
72
 
73
- const repoRoot = findRepoRoot(ctx.cwd);
74
- if (!repoRoot) {
75
- io.err("run this from a Context101 checkout (needs cdk/ and web/).");
76
- return 1;
77
- }
78
-
79
73
  const env = withAwsAuth(ctx.env ?? {}, {
80
74
  profile: opts.awsProfile,
81
75
  accessKeyId: opts.awsAccessKeyId,
@@ -101,12 +95,6 @@ export async function runDestroy(opts, ctx) {
101
95
  io.write("");
102
96
  }
103
97
 
104
- const repoRoot = findRepoRoot(ctx.cwd);
105
- if (!repoRoot) {
106
- io.err("run this from a Context101 checkout (needs cdk/ and web/).");
107
- return 1;
108
- }
109
-
110
98
  const stackName = String(opts.stackName || "").trim();
111
99
  if (!stackName) {
112
100
  io.err(`destroy needs a stack name from \`${LIST_CLI}\`.`);
@@ -157,6 +145,25 @@ export async function runDestroy(opts, ctx) {
157
145
  io.warn(
158
146
  "Non-default brains are not in CloudFormation — delete them from /brains first."
159
147
  );
148
+
149
+ const checkout = ensureRepoRoot({
150
+ cwd: ctx.cwd,
151
+ dir: opts.dir,
152
+ exec,
153
+ io,
154
+ preferHomeClone: true,
155
+ homeDir: ctx.homeDir,
156
+ });
157
+ if (checkout.error) {
158
+ io.err(checkout.error);
159
+ return 1;
160
+ }
161
+ const repoRoot = checkout.repoRoot;
162
+ if (!repoRoot) {
163
+ io.err("could not find or clone a Context101 checkout (needs cdk/ and web/).");
164
+ return 1;
165
+ }
166
+
160
167
  io.write(`Destroying ${stackName}…`);
161
168
  return (ctx.runDeploy ?? runCdk)({
162
169
  repoRoot,