@getjack/jack 0.1.14 → 0.1.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getjack/jack",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Ship before you forget why you started. The vibecoder's deployment CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "bun-types": "latest"
42
42
  },
43
43
  "dependencies": {
44
- "@inquirer/prompts": "^7.0.0",
44
+ "@clack/prompts": "^0.11.0",
45
45
  "@modelcontextprotocol/sdk": "^1.25.1",
46
46
  "archiver": "^7.0.1",
47
47
  "fflate": "^0.8.2",
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { select } from "@inquirer/prompts";
3
+ import { isCancel, select } from "@clack/prompts";
4
4
  import { fetchProjectTags } from "../lib/control-plane.ts";
5
5
  import { formatSize } from "../lib/format.ts";
6
6
  import { box, error, info, spinner, success } from "../lib/output.ts";
@@ -35,14 +35,14 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
35
35
  // Prompt user for action
36
36
  const action = await select({
37
37
  message: `Directory ${flags.as ?? projectName} already exists. What would you like to do?`,
38
- choices: [
39
- { value: "overwrite", name: "Overwrite (delete and recreate)" },
40
- { value: "merge", name: "Merge (keep existing files)" },
41
- { value: "cancel", name: "Cancel" },
38
+ options: [
39
+ { value: "overwrite", label: "Overwrite (delete and recreate)" },
40
+ { value: "merge", label: "Merge (keep existing files)" },
41
+ { value: "cancel", label: "Cancel" },
42
42
  ],
43
43
  });
44
44
 
45
- if (action === "cancel") {
45
+ if (isCancel(action) || action === "cancel") {
46
46
  info("Clone cancelled");
47
47
  process.exit(0);
48
48
  }
@@ -72,7 +72,8 @@ export default async function clone(projectName?: string, flags: CloneFlags = {}
72
72
 
73
73
  if (!result.success) {
74
74
  downloadSpin.error("Clone failed");
75
- error(result.error || "Unknown error");
75
+ error(result.error || "Could not download project files");
76
+ info("Check your network connection and try again");
76
77
  process.exit(1);
77
78
  }
78
79
 
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { getAuthState } from "../lib/auth/index.ts";
2
3
  import {
3
4
  checkWorkerExists,
4
5
  deleteDatabase,
@@ -99,10 +100,10 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
99
100
  }
100
101
  }
101
102
 
103
+ // Track auth state for final messaging
104
+ let authState: Awaited<ReturnType<typeof getAuthState>> | null = null;
102
105
  if (!resolved && !link) {
103
- // Not found anywhere
104
- warn(`Project '${name}' not found`);
105
- info("Will attempt to undeploy if deployed");
106
+ authState = await getAuthState();
106
107
  }
107
108
 
108
109
  // Check if this is a managed project (from link or resolved data)
@@ -135,8 +136,14 @@ export default async function down(projectName?: string, flags: DownFlags = {}):
135
136
 
136
137
  if (!workerExists) {
137
138
  console.error("");
138
- warn(`'${name}' is not deployed`);
139
- info("Nothing to undeploy");
139
+ error(`Project '${name}' not found`);
140
+ if (authState === "session-expired") {
141
+ info("Session expired. Run: jack login");
142
+ } else if (authState === "not-logged-in") {
143
+ info("Not logged in. Run: jack login");
144
+ } else {
145
+ info("Run jack projects to see your projects");
146
+ }
140
147
  return;
141
148
  }
142
149
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { existsSync } from "node:fs";
11
- import { select } from "@inquirer/prompts";
11
+ import { isCancel, select } from "@clack/prompts";
12
12
  import { isLoggedIn } from "../lib/auth/index.ts";
13
13
  import {
14
14
  type ManagedProject,
@@ -46,11 +46,11 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
46
46
  // BYO mode - generate local ID
47
47
  if (flags.byo) {
48
48
  const projectId = generateByoProjectId();
49
- output.start("Creating BYO link...");
49
+ output.start("Linking to your Cloudflare account...");
50
50
  await linkProject(process.cwd(), projectId, "byo");
51
51
  await registerPath(projectId, process.cwd());
52
52
  output.stop();
53
- success("Linked as BYO project");
53
+ success("Linked to your Cloudflare account");
54
54
  info(`Project ID: ${projectId}`);
55
55
  return;
56
56
  }
@@ -62,7 +62,7 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
62
62
  // Not logged in and no project name - suggest options
63
63
  error("Not logged in to jack cloud");
64
64
  info("Login with: jack login");
65
- info("Or create a BYO link: jack link --byo");
65
+ info("Or link to your Cloudflare account: jack link --byo");
66
66
  process.exit(1);
67
67
  }
68
68
 
@@ -118,21 +118,26 @@ export default async function link(projectName?: string, flags: LinkFlags = {}):
118
118
  output.stop();
119
119
 
120
120
  if (projects.length === 0) {
121
- error("No managed projects found");
121
+ error("No projects found");
122
122
  info("Create one with: jack new");
123
- info("Or link as BYO: jack link --byo");
123
+ info("Or link to your Cloudflare account: jack link --byo");
124
124
  process.exit(1);
125
125
  }
126
126
 
127
127
  console.error("");
128
128
  const choice = await select({
129
129
  message: "Select a project to link:",
130
- choices: projects.map((p) => ({
130
+ options: projects.map((p) => ({
131
131
  value: p.id,
132
- name: `${p.slug} (${p.status})`,
132
+ label: `${p.slug} (${p.status})`,
133
133
  })),
134
134
  });
135
135
 
136
+ if (isCancel(choice)) {
137
+ info("Cancelled");
138
+ process.exit(0);
139
+ }
140
+
136
141
  const selected = projects.find((p) => p.id === choice);
137
142
  if (!selected) {
138
143
  error("No project selected");
@@ -448,7 +448,7 @@ async function removeProjectEntry(args: string[]): Promise<void> {
448
448
  // Warn if still deployed
449
449
  if (project.status === "live") {
450
450
  console.error("");
451
- warn("Project is still deployed; removal does not undeploy the worker");
451
+ warn("Project is still live; this only removes it from jack, not from Cloudflare");
452
452
  }
453
453
 
454
454
  console.error("");
@@ -496,7 +496,7 @@ async function removeProjectEntry(args: string[]): Promise<void> {
496
496
  // Hint about undeploying
497
497
  if (project.status === "live") {
498
498
  console.error("");
499
- info(`To undeploy the worker, run: jack down ${name}`);
499
+ info(`To take it offline: jack down ${name}`);
500
500
  }
501
501
  }
502
502
 
@@ -12,8 +12,8 @@ export default async function publish(): Promise<void> {
12
12
  }
13
13
 
14
14
  if (link.deploy_mode !== "managed") {
15
- output.error("Only managed projects can be published");
16
- output.info("Projects deployed with BYOC (bring your own cloud) cannot be published");
15
+ output.error("Only jack cloud projects can be published");
16
+ output.info("Projects on your own Cloudflare account cannot be published");
17
17
  process.exit(1);
18
18
  }
19
19
 
@@ -6,7 +6,7 @@
6
6
  * For BYO projects: uses wrangler secret commands.
7
7
  */
8
8
 
9
- import { password as passwordPrompt } from "@inquirer/prompts";
9
+ import { isCancel, password as passwordPrompt } from "@clack/prompts";
10
10
  import { $ } from "bun";
11
11
  import { getControlApiUrl } from "../lib/control-plane.ts";
12
12
  import { JackError, JackErrorCode } from "../lib/errors.ts";
@@ -96,22 +96,19 @@ async function resolveProjectContext(options: SecretsOptions): Promise<{
96
96
 
97
97
  /**
98
98
  * Read a secret value interactively without echoing
99
- * Uses @inquirer/prompts password for robust handling of typing, pasting, and TTY
99
+ * Uses @clack/prompts password for robust handling of typing, pasting, and TTY
100
100
  */
101
101
  async function readSecretInteractive(keyName: string): Promise<string> {
102
- try {
103
- const value = await passwordPrompt({
104
- message: `Enter value for ${keyName}`,
105
- mask: "*",
106
- });
107
- return value;
108
- } catch (err) {
109
- // Handle Ctrl+C / Escape - inquirer throws ExitPromptError
110
- if (err instanceof Error && err.name === "ExitPromptError") {
111
- throw new Error("Cancelled");
112
- }
113
- throw err;
102
+ const value = await passwordPrompt({
103
+ message: `Enter value for ${keyName}`,
104
+ mask: "*",
105
+ });
106
+
107
+ if (isCancel(value)) {
108
+ throw new Error("Cancelled");
114
109
  }
110
+
111
+ return value;
115
112
  }
116
113
 
117
114
  /**
@@ -1,10 +1,11 @@
1
1
  import { join } from "node:path";
2
2
  import { fetchProjectResources } from "../lib/control-plane.ts";
3
3
  import { formatSize } from "../lib/format.ts";
4
- import { promptSelect } from "../lib/hooks.ts";
5
4
  import { error, info, item, output as outputSpinner, success, warn } from "../lib/output.ts";
6
5
  import { readProjectLink } from "../lib/project-link.ts";
7
6
  import { parseWranglerResources } from "../lib/resources.ts";
7
+ import { createDatabase } from "../lib/services/db-create.ts";
8
+ import { listDatabases } from "../lib/services/db-list.ts";
8
9
  import {
9
10
  deleteDatabase,
10
11
  exportDatabase,
@@ -12,6 +13,7 @@ import {
12
13
  getDatabaseInfo as getWranglerDatabaseInfo,
13
14
  } from "../lib/services/db.ts";
14
15
  import { getProjectNameFromDir } from "../lib/storage/index.ts";
16
+ import { Events, track } from "../lib/telemetry.ts";
15
17
 
16
18
  /**
17
19
  * Database info from control plane or wrangler config
@@ -115,19 +117,45 @@ function showHelp(): void {
115
117
  console.error("");
116
118
  }
117
119
 
120
+ function showDbHelp(): void {
121
+ console.error("");
122
+ info("jack services db - Manage databases");
123
+ console.error("");
124
+ console.error("Actions:");
125
+ console.error(" info Show database information (default)");
126
+ console.error(" create Create a new database");
127
+ console.error(" list List all databases in the project");
128
+ console.error(" export Export database to SQL file");
129
+ console.error(" delete Delete a database");
130
+ console.error("");
131
+ console.error("Examples:");
132
+ console.error(" jack services db Show info about the default database");
133
+ console.error(" jack services db create Create a new database");
134
+ console.error(" jack services db list List all databases");
135
+ console.error("");
136
+ }
137
+
118
138
  async function dbCommand(args: string[], options: ServiceOptions): Promise<void> {
119
139
  const action = args[0] || "info"; // Default to info
120
140
 
121
141
  switch (action) {
142
+ case "--help":
143
+ case "-h":
144
+ case "help":
145
+ return showDbHelp();
122
146
  case "info":
123
147
  return await dbInfo(options);
148
+ case "create":
149
+ return await dbCreate(args.slice(1), options);
150
+ case "list":
151
+ return await dbList(options);
124
152
  case "export":
125
153
  return await dbExport(options);
126
154
  case "delete":
127
155
  return await dbDelete(options);
128
156
  default:
129
157
  error(`Unknown action: ${action}`);
130
- info("Available: info, export, delete");
158
+ info("Available: info, create, list, export, delete");
131
159
  process.exit(1);
132
160
  }
133
161
  }
@@ -158,9 +186,8 @@ async function dbInfo(options: ServiceOptions): Promise<void> {
158
186
 
159
187
  if (!dbInfo) {
160
188
  console.error("");
161
- error("No database found for this project.");
162
- info("For managed projects, create a database with: jack services db create");
163
- info("For BYO projects, add d1_databases to your wrangler.jsonc");
189
+ error("No database found for this project");
190
+ info("Create one with: jack services db create");
164
191
  console.error("");
165
192
  return;
166
193
  }
@@ -200,9 +227,8 @@ async function dbExport(options: ServiceOptions): Promise<void> {
200
227
 
201
228
  if (!dbInfo) {
202
229
  console.error("");
203
- error("No database found for this project.");
204
- info("For managed projects, create a database with: jack services db create");
205
- info("For BYO projects, add d1_databases to your wrangler.jsonc");
230
+ error("No database found for this project");
231
+ info("Create one with: jack services db create");
206
232
  console.error("");
207
233
  return;
208
234
  }
@@ -239,9 +265,8 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
239
265
 
240
266
  if (!dbInfo) {
241
267
  console.error("");
242
- error("No database found for this project.");
243
- info("For managed projects, create a database with: jack services db create");
244
- info("For BYO projects, add d1_databases to your wrangler.jsonc");
268
+ error("No database found for this project");
269
+ info("Create one with: jack services db create");
245
270
  console.error("");
246
271
  return;
247
272
  }
@@ -263,10 +288,12 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
263
288
  console.error("");
264
289
 
265
290
  // Confirm deletion
266
- console.error(` Delete database '${dbInfo.name}'?\n`);
267
- const choice = await promptSelect(["Yes", "No"]);
291
+ const { confirm } = await import("@clack/prompts");
292
+ const shouldDelete = await confirm({
293
+ message: `Delete database '${dbInfo.name}'?`,
294
+ });
268
295
 
269
- if (choice !== 0) {
296
+ if (shouldDelete !== true) {
270
297
  info("Cancelled");
271
298
  return;
272
299
  }
@@ -287,3 +314,116 @@ async function dbDelete(options: ServiceOptions): Promise<void> {
287
314
  process.exit(1);
288
315
  }
289
316
  }
317
+
318
+ /**
319
+ * Parse --name flag from args
320
+ * Supports: --name foo, --name=foo
321
+ */
322
+ function parseNameFlag(args: string[]): string | undefined {
323
+ for (let i = 0; i < args.length; i++) {
324
+ const arg = args[i];
325
+ if (arg === "--name" && args[i + 1]) {
326
+ return args[i + 1];
327
+ }
328
+ if (arg.startsWith("--name=")) {
329
+ return arg.slice("--name=".length);
330
+ }
331
+ }
332
+ return undefined;
333
+ }
334
+
335
+ /**
336
+ * Create a new database
337
+ */
338
+ async function dbCreate(args: string[], options: ServiceOptions): Promise<void> {
339
+ // Parse --name flag
340
+ const name = parseNameFlag(args);
341
+
342
+ outputSpinner.start("Creating database...");
343
+ try {
344
+ const result = await createDatabase(process.cwd(), {
345
+ name,
346
+ interactive: true,
347
+ });
348
+ outputSpinner.stop();
349
+
350
+ // Track telemetry
351
+ track(Events.SERVICE_CREATED, {
352
+ service_type: "d1",
353
+ binding_name: result.bindingName,
354
+ created: result.created,
355
+ });
356
+
357
+ console.error("");
358
+ if (result.created) {
359
+ success(`Database created: ${result.databaseName}`);
360
+ } else {
361
+ success(`Using existing database: ${result.databaseName}`);
362
+ }
363
+ console.error("");
364
+ item(`Binding: ${result.bindingName}`);
365
+ item(`ID: ${result.databaseId}`);
366
+ console.error("");
367
+
368
+ // Prompt to deploy
369
+ const { confirm } = await import("@clack/prompts");
370
+ const shouldDeploy = await confirm({
371
+ message: "Deploy now?",
372
+ });
373
+
374
+ if (shouldDeploy === true) {
375
+ const { deployProject } = await import("../lib/project-operations.ts");
376
+ await deployProject(process.cwd(), { interactive: true });
377
+ } else {
378
+ console.error("");
379
+ info("Run 'jack ship' when ready to deploy");
380
+ console.error("");
381
+ }
382
+ } catch (err) {
383
+ outputSpinner.stop();
384
+ console.error("");
385
+ error(`Failed to create database: ${err instanceof Error ? err.message : String(err)}`);
386
+ process.exit(1);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * List all databases in the project
392
+ */
393
+ async function dbList(options: ServiceOptions): Promise<void> {
394
+ outputSpinner.start("Fetching databases...");
395
+ try {
396
+ const databases = await listDatabases(process.cwd());
397
+ outputSpinner.stop();
398
+
399
+ if (databases.length === 0) {
400
+ console.error("");
401
+ info("No databases found in this project.");
402
+ console.error("");
403
+ info("Create one with: jack services db create");
404
+ console.error("");
405
+ return;
406
+ }
407
+
408
+ console.error("");
409
+ success(`Found ${databases.length} database${databases.length === 1 ? "" : "s"}:`);
410
+ console.error("");
411
+
412
+ for (const db of databases) {
413
+ item(`${db.name} (${db.binding})`);
414
+ if (db.sizeBytes !== undefined) {
415
+ item(` Size: ${formatSize(db.sizeBytes)}`);
416
+ }
417
+ if (db.numTables !== undefined) {
418
+ item(` Tables: ${db.numTables}`);
419
+ }
420
+ item(` ID: ${db.id}`);
421
+ console.error("");
422
+ }
423
+ } catch (err) {
424
+ outputSpinner.stop();
425
+ console.error("");
426
+ error(`Failed to list databases: ${err instanceof Error ? err.message : String(err)}`);
427
+ process.exit(1);
428
+ }
429
+ }
@@ -20,8 +20,8 @@ export default async function sync(flags: SyncFlags = {}): Promise<void> {
20
20
 
21
21
  // Check for wrangler config
22
22
  if (!hasWranglerConfig()) {
23
- error("No wrangler config found in current directory");
24
- error("Run: jack new <project-name>");
23
+ error("Not in a project directory");
24
+ info("Run jack new <name> to create a project");
25
25
  process.exit(1);
26
26
  }
27
27
 
@@ -39,10 +39,12 @@ export default async function sync(flags: SyncFlags = {}): Promise<void> {
39
39
  const result = await syncToCloud(projectDir, { force, dryRun, verbose });
40
40
 
41
41
  if (!result.success) {
42
- syncSpin.error("Sync failed");
42
+ syncSpin.stop();
43
+ error("Sync failed");
43
44
  if (result.error) {
44
- error(result.error);
45
+ info(result.error);
45
46
  }
47
+ info("Check your network connection and try again");
46
48
  process.exit(1);
47
49
  }
48
50
 
@@ -0,0 +1,53 @@
1
+ /**
2
+ * jack update - Self-update to the latest version
3
+ */
4
+
5
+ import { error, info, success, warn } from "../lib/output.ts";
6
+ import {
7
+ checkForUpdate,
8
+ getCurrentVersion,
9
+ isRunningViaBunx,
10
+ performUpdate,
11
+ } from "../lib/version-check.ts";
12
+
13
+ export default async function update(): Promise<void> {
14
+ const currentVersion = getCurrentVersion();
15
+
16
+ // Check if running via bunx
17
+ if (isRunningViaBunx()) {
18
+ info(`Running via bunx (current: v${currentVersion})`);
19
+ info("bunx automatically uses cached packages.");
20
+ info("To get the latest version, run:");
21
+ info(" bunx @getjack/jack@latest <command>");
22
+ info("");
23
+ info("Or install globally:");
24
+ info(" bun add -g @getjack/jack");
25
+ return;
26
+ }
27
+
28
+ info(`Current version: v${currentVersion}`);
29
+
30
+ // Check for updates
31
+ const latestVersion = await checkForUpdate();
32
+
33
+ if (!latestVersion) {
34
+ success("You're on the latest version!");
35
+ return;
36
+ }
37
+
38
+ info(`New version available: v${latestVersion}`);
39
+ info("Updating...");
40
+
41
+ const result = await performUpdate();
42
+
43
+ if (result.success) {
44
+ success(`Updated to v${result.version ?? latestVersion}`);
45
+ info("Restart your terminal to use the new version.");
46
+ } else {
47
+ error("Update failed");
48
+ if (result.error) {
49
+ warn(result.error);
50
+ }
51
+ info("Try manually: bun add -g @getjack/jack@latest");
52
+ }
53
+ }
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ const cli = meow(
35
35
  login Sign in
36
36
  logout Sign out
37
37
  whoami Show current user
38
+ update Update jack to latest version
38
39
 
39
40
  Project Management
40
41
  link [name] Link directory to a project
@@ -168,6 +169,20 @@ identify({
168
169
  ...getEnvironmentProps(),
169
170
  });
170
171
 
172
+ // Start non-blocking version check (skip for update command, CI, and help)
173
+ const skipVersionCheck =
174
+ !command ||
175
+ command === "update" ||
176
+ command === "upgrade" ||
177
+ process.env.CI ||
178
+ process.env.JACK_NO_UPDATE_CHECK;
179
+
180
+ let updateCheckPromise: Promise<string | null> | null = null;
181
+ if (!skipVersionCheck) {
182
+ const { checkForUpdate } = await import("./lib/version-check.ts");
183
+ updateCheckPromise = checkForUpdate().catch(() => null);
184
+ }
185
+
171
186
  try {
172
187
  switch (command) {
173
188
  case "init": {
@@ -344,6 +359,12 @@ try {
344
359
  await withTelemetry("whoami", whoami)();
345
360
  break;
346
361
  }
362
+ case "update":
363
+ case "upgrade": {
364
+ const { default: update } = await import("./commands/update.ts");
365
+ await withTelemetry("update", update)();
366
+ break;
367
+ }
347
368
  case "feedback": {
348
369
  const { default: feedback } = await import("./commands/feedback.ts");
349
370
  await withTelemetry("feedback", feedback)();
@@ -362,6 +383,16 @@ try {
362
383
  default:
363
384
  cli.showHelp(command ? 1 : 0);
364
385
  }
386
+
387
+ // Show update notification if available (non-blocking check completed)
388
+ if (updateCheckPromise) {
389
+ const latestVersion = await updateCheckPromise;
390
+ if (latestVersion) {
391
+ info("");
392
+ info(`Update available: v${pkg.version} → v${latestVersion}`);
393
+ info("Run: jack update");
394
+ }
395
+ }
365
396
  } catch (err) {
366
397
  if (isJackError(err)) {
367
398
  printError(err.message);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared login flow for CLI and programmatic use
3
3
  */
4
- import { input } from "@inquirer/prompts";
4
+ import { isCancel, text } from "@clack/prompts";
5
5
  import {
6
6
  checkUsernameAvailable,
7
7
  getCurrentUserProfile,
@@ -189,10 +189,18 @@ async function promptForUsername(email: string, firstName: string | null): Promi
189
189
 
190
190
  if (choice === options.length - 1) {
191
191
  // User chose to type custom username
192
- inputUsername = await input({
192
+ const customInput = await text({
193
193
  message: "Username:",
194
- validate: validateUsername,
194
+ validate: (value) => {
195
+ const result = validateUsername(value);
196
+ if (result !== true) return result;
197
+ },
195
198
  });
199
+ if (isCancel(customInput)) {
200
+ warn("Skipped username setup. You can set it later.");
201
+ return true;
202
+ }
203
+ inputUsername = customInput;
196
204
  } else {
197
205
  // User picked a suggestion (choice is guaranteed to be valid index)
198
206
  inputUsername = suggestions[choice] as string;
@@ -217,11 +217,11 @@ export async function buildProject(options: BuildOptions): Promise<BuildOutput>
217
217
 
218
218
  if (dryRunResult.exitCode !== 0) {
219
219
  reporter?.stop();
220
- reporter?.error("Worker build failed");
220
+ reporter?.error("Build failed");
221
221
  throw new JackError(
222
222
  JackErrorCode.BUILD_FAILED,
223
- "Worker build failed",
224
- "Check your wrangler.jsonc and worker code for errors",
223
+ "Build failed",
224
+ "Check your code for syntax errors",
225
225
  {
226
226
  exitCode: dryRunResult.exitCode,
227
227
  stderr: dryRunResult.stderr.toString(),