@mittwald/cli 1.9.0 → 1.10.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.
@@ -33,11 +33,11 @@ export class Download extends ExecRenderBaseCommand {
33
33
  static examples = [
34
34
  {
35
35
  description: "Download entire app to current working directory",
36
- command: "$ <%= config.bin %> <%= command.id %> .",
36
+ command: "$ <%= config.bin %> <%= command.id %> --target .",
37
37
  },
38
38
  {
39
39
  description: "Download only shared dir from a deployer-managed app",
40
- command: "<%= config.bin %> <%= command.id %> --remote-sub-directory=shared .",
40
+ command: "<%= config.bin %> <%= command.id %> --remote-sub-directory=shared --target .",
41
41
  },
42
42
  ];
43
43
  async exec() {
@@ -0,0 +1,17 @@
1
+ import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
2
+ export default class Exec extends ExtendedBaseCommand<typeof Exec> {
3
+ static summary: string;
4
+ static description: string;
5
+ static args: {
6
+ command: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ "installation-id": import("@oclif/core/interfaces").OptionFlag<string>;
13
+ "ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ "ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ };
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,71 @@
1
+ import * as child_process from "child_process";
2
+ import { Args, Flags } from "@oclif/core";
3
+ import { ExtendedBaseCommand } from "../../lib/basecommands/ExtendedBaseCommand.js";
4
+ import { sshConnectionFlags } from "../../lib/resources/ssh/flags.js";
5
+ import { sshUsageDocumentation } from "../../lib/resources/ssh/doc.js";
6
+ import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
7
+ import { appInstallationFlags } from "../../lib/resources/app/flags.js";
8
+ import { getSSHConnectionForAppInstallation } from "../../lib/resources/ssh/appinstall.js";
9
+ import shellEscape from "shell-escape";
10
+ import { prepareEnvironmentVariables } from "../../lib/resources/ssh/environment.js";
11
+ export default class Exec extends ExtendedBaseCommand {
12
+ static summary = "Execute a command in an app installation via SSH non-interactively.";
13
+ static description = sshUsageDocumentation;
14
+ static args = {
15
+ command: Args.string({
16
+ description: "Command to execute in the app installation",
17
+ required: true,
18
+ }),
19
+ };
20
+ static flags = {
21
+ ...sshConnectionFlags,
22
+ ...appInstallationFlags,
23
+ workdir: Flags.string({
24
+ char: "w",
25
+ summary: "working directory where the command will be executed",
26
+ default: undefined,
27
+ }),
28
+ env: Flags.string({
29
+ char: "e",
30
+ summary: "environment variables to set for the command (format: KEY=VALUE)",
31
+ multiple: true,
32
+ multipleNonGreedy: true,
33
+ }),
34
+ quiet: Flags.boolean({
35
+ char: "q",
36
+ summary: "disable informational output, only show command results",
37
+ default: false,
38
+ }),
39
+ };
40
+ async run() {
41
+ const { args, flags } = await this.parse(Exec);
42
+ const appInstallationId = await this.withAppInstallationId(Exec);
43
+ const { host, user, directory } = await getSSHConnectionForAppInstallation(this.apiClient, appInstallationId, flags["ssh-user"]);
44
+ if (!flags.quiet) {
45
+ this.log("executing command on %s as %s", host, user);
46
+ }
47
+ const command = args.command;
48
+ const workdir = flags.workdir ?? directory;
49
+ // Build the command to execute
50
+ let execCommand = "";
51
+ // Add environment variables if provided
52
+ if (flags.env && flags.env.length > 0) {
53
+ execCommand += prepareEnvironmentVariables(flags.env);
54
+ }
55
+ // Change to working directory if specified, otherwise use app directory
56
+ execCommand += `cd ${shellEscape([workdir])} && `;
57
+ // Add the actual command
58
+ execCommand += command;
59
+ const sshArgs = buildSSHClientFlags(user, host, flags, {
60
+ interactive: false,
61
+ });
62
+ const wrappedExecCommand = shellEscape(["/bin/bash", "-c", execCommand]);
63
+ this.debug("running ssh %o, with command %o", sshArgs, wrappedExecCommand);
64
+ const result = child_process.spawnSync("/usr/bin/ssh", [...sshArgs, wrappedExecCommand], {
65
+ stdio: "inherit",
66
+ });
67
+ if (result.status !== 0) {
68
+ this.error(`Command failed with exit code ${result.status}`);
69
+ }
70
+ }
71
+ }
@@ -10,16 +10,10 @@ export default class Exec extends ExtendedBaseCommand<typeof Exec> {
10
10
  workdir: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
11
  env: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
12
  shell: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
+ quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
14
  "project-id": import("@oclif/core/interfaces").OptionFlag<string>;
14
15
  "ssh-user": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
16
  "ssh-identity-file": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
16
17
  };
17
- /**
18
- * Prepare environment variables for the SSH command
19
- *
20
- * @param envVars Array of environment variables in KEY=VALUE format
21
- * @returns Formatted string with export commands
22
- */
23
- private prepareEnvironmentVariables;
24
18
  run(): Promise<void>;
25
19
  }
@@ -8,6 +8,7 @@ import { buildSSHClientFlags } from "../../lib/resources/ssh/connection.js";
8
8
  import { withContainerAndStackId } from "../../lib/resources/container/flags.js";
9
9
  import { projectFlags } from "../../lib/resources/project/flags.js";
10
10
  import shellEscape from "shell-escape";
11
+ import { prepareEnvironmentVariables } from "../../lib/resources/ssh/environment.js";
11
12
  export default class Exec extends ExtendedBaseCommand {
12
13
  static summary = "Execute a command in a container via SSH non-interactively.";
13
14
  static description = sshUsageDocumentation;
@@ -33,44 +34,32 @@ export default class Exec extends ExtendedBaseCommand {
33
34
  char: "e",
34
35
  summary: "environment variables to set for the command (format: KEY=VALUE)",
35
36
  multiple: true,
37
+ multipleNonGreedy: true,
36
38
  }),
37
39
  shell: Flags.string({
38
40
  summary: "shell to use for the SSH connection",
39
41
  default: "/bin/sh",
40
42
  }),
43
+ quiet: Flags.boolean({
44
+ char: "q",
45
+ summary: "disable informational output, only show command results",
46
+ default: false,
47
+ }),
41
48
  };
42
- /**
43
- * Prepare environment variables for the SSH command
44
- *
45
- * @param envVars Array of environment variables in KEY=VALUE format
46
- * @returns Formatted string with export commands
47
- */
48
- prepareEnvironmentVariables(envVars) {
49
- return (envVars
50
- .map((env) => {
51
- const eqIdx = env.indexOf("=");
52
- if (eqIdx === -1) {
53
- // If no '=', treat the whole string as key with empty value
54
- return `export ${shellEscape([env])}=`;
55
- }
56
- const key = env.slice(0, eqIdx);
57
- const value = env.slice(eqIdx + 1);
58
- return `export ${shellEscape([key])}=${shellEscape([value])}`;
59
- })
60
- .join("; ") + "; ");
61
- }
62
49
  async run() {
63
50
  const { args, flags } = await this.parse(Exec);
64
51
  const [containerId, stackId] = await withContainerAndStackId(this.apiClient, Exec, flags, this.args, this.config);
65
52
  const { host, user } = await getSSHConnectionForContainer(this.apiClient, containerId, stackId, flags["ssh-user"]);
66
- this.log("executing command on %s as %s", host, user);
53
+ if (!flags.quiet) {
54
+ this.log("executing command on %s as %s", host, user);
55
+ }
67
56
  const command = args.command;
68
57
  const workdir = flags.workdir;
69
58
  // Build the command to execute
70
59
  let execCommand = "";
71
60
  // Add environment variables if provided
72
61
  if (flags.env && flags.env.length > 0) {
73
- execCommand += this.prepareEnvironmentVariables(flags.env);
62
+ execCommand += prepareEnvironmentVariables(flags.env);
74
63
  }
75
64
  // Change to working directory if specified
76
65
  if (workdir !== undefined) {
@@ -0,0 +1,3 @@
1
+ import { Hook } from "@oclif/core";
2
+ declare const hook: Hook<"prerun">;
3
+ export default hook;
@@ -0,0 +1,19 @@
1
+ import { execSync } from "child_process";
2
+ const hook = async function (opts) {
3
+ const isInstalledWithBrew = () => {
4
+ try {
5
+ const cellar = execSync("brew --cellar", { encoding: "utf8" });
6
+ return opts.config.root.startsWith(cellar.trim());
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ };
12
+ if (opts.Command.id === "update") {
13
+ if (isInstalledWithBrew()) {
14
+ opts.context.warn("installed with brew.\nUse `brew upgrade mw` to update to the newest version");
15
+ opts.context.exit(1);
16
+ }
17
+ }
18
+ };
19
+ export default hook;
@@ -3,7 +3,7 @@ import { cwd } from "process";
3
3
  import path from "path";
4
4
  import { pathExists } from "../util/fs/pathExists.js";
5
5
  function overrideIDFromState(state, type) {
6
- const instances = state.resources?.find((r) => r.type === type)?.instances;
6
+ const instances = state.resources?.find((r) => r.type === type && r.mode === "managed")?.instances;
7
7
  if (instances === undefined) {
8
8
  return undefined;
9
9
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Prepare environment variables for SSH command execution
3
+ *
4
+ * @param envVars Array of environment variables in KEY=VALUE format
5
+ * @returns Formatted string with export commands
6
+ */
7
+ export declare function prepareEnvironmentVariables(envVars: string[]): string;
@@ -0,0 +1,21 @@
1
+ import shellEscape from "shell-escape";
2
+ /**
3
+ * Prepare environment variables for SSH command execution
4
+ *
5
+ * @param envVars Array of environment variables in KEY=VALUE format
6
+ * @returns Formatted string with export commands
7
+ */
8
+ export function prepareEnvironmentVariables(envVars) {
9
+ return (envVars
10
+ .map((env) => {
11
+ const eqIdx = env.indexOf("=");
12
+ if (eqIdx === -1) {
13
+ // If no '=', treat the whole string as key with empty value
14
+ return `export ${shellEscape([env])}=`;
15
+ }
16
+ const key = env.slice(0, eqIdx);
17
+ const value = env.slice(eqIdx + 1);
18
+ return `export ${shellEscape([key])}=${shellEscape([value])}`;
19
+ })
20
+ .join("; ") + "; ");
21
+ }
@@ -1,6 +1,6 @@
1
1
  import { BooleanFlag, OptionFlag } from "@oclif/core/interfaces";
2
2
  import { ListColumns } from "./Table.js";
3
- export { ListColumn, ListColumns } from "./Table.js";
3
+ export type { ListColumn, ListColumns } from "./Table.js";
4
4
  type ListFormatterFlags = {
5
5
  output: OptionFlag<OutputFormat>;
6
6
  extended: BooleanFlag<boolean>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -281,6 +281,9 @@
281
281
  "bucket": "mittwald-cli",
282
282
  "host": "https://mittwald-cli.s3.eu-central-1.amazonaws.com"
283
283
  }
284
+ },
285
+ "hooks": {
286
+ "prerun": "./dist/hooks/prerun/update-brew-check"
284
287
  }
285
288
  },
286
289
  "packageManager": "yarn@3.6.1"