@mittwald/cli 1.0.0-alpha.32 → 1.0.0-alpha.33

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.
Files changed (36) hide show
  1. package/README.md +471 -436
  2. package/dist/esm/commands/app/create/node.d.ts +2 -2
  3. package/dist/esm/commands/app/create/node.js +5 -1
  4. package/dist/esm/commands/app/download.d.ts +17 -0
  5. package/dist/esm/commands/app/download.js +81 -0
  6. package/dist/esm/commands/app/get.js +1 -1
  7. package/dist/esm/commands/app/ssh.d.ts +1 -0
  8. package/dist/esm/commands/app/ssh.js +18 -26
  9. package/dist/esm/commands/database/mysql/dump.d.ts +3 -0
  10. package/dist/esm/commands/database/mysql/dump.js +56 -36
  11. package/dist/esm/commands/database/mysql/user/delete.d.ts +13 -0
  12. package/dist/esm/commands/database/mysql/user/delete.js +21 -0
  13. package/dist/esm/lib/app/Installer.d.ts +1 -0
  14. package/dist/esm/lib/app/Installer.js +6 -1
  15. package/dist/esm/lib/app/flags.d.ts +1 -0
  16. package/dist/esm/lib/app/flags.js +54 -42
  17. package/dist/esm/lib/database/mysql/connect.d.ts +4 -1
  18. package/dist/esm/lib/database/mysql/connect.js +2 -1
  19. package/dist/esm/lib/hasbin.d.ts +1 -0
  20. package/dist/esm/lib/hasbin.js +17 -0
  21. package/dist/esm/lib/ssh/appinstall.d.ts +3 -0
  22. package/dist/esm/lib/ssh/appinstall.js +25 -0
  23. package/dist/esm/lib/ssh/exec.d.ts +8 -0
  24. package/dist/esm/lib/ssh/exec.js +41 -0
  25. package/dist/esm/lib/ssh/project.d.ts +3 -0
  26. package/dist/esm/lib/ssh/project.js +15 -0
  27. package/dist/esm/lib/ssh/types.d.ts +5 -0
  28. package/dist/esm/lib/ssh/types.js +1 -0
  29. package/dist/esm/rendering/process/components/ProcessState.js +1 -1
  30. package/dist/esm/rendering/process/process.d.ts +13 -2
  31. package/dist/esm/rendering/process/process.js +18 -0
  32. package/dist/esm/rendering/process/process_fancy.d.ts +5 -2
  33. package/dist/esm/rendering/process/process_fancy.js +21 -4
  34. package/dist/esm/rendering/process/process_quiet.d.ts +5 -2
  35. package/dist/esm/rendering/process/process_quiet.js +13 -2
  36. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export declare function hasBinary(name: string): Promise<boolean>;
@@ -0,0 +1,17 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ export async function hasBinary(name) {
4
+ const p = process.env.PATH ?? "";
5
+ const pathItems = p.split(path.delimiter);
6
+ for (const item of pathItems) {
7
+ const fullPath = path.join(item, name);
8
+ try {
9
+ fs.statSync(fullPath);
10
+ return true;
11
+ }
12
+ catch (e) {
13
+ // ignore
14
+ }
15
+ }
16
+ return false;
17
+ }
@@ -0,0 +1,3 @@
1
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
+ import { SSHConnectionData } from "./types.js";
3
+ export declare function getSSHConnectionForAppInstallation(client: MittwaldAPIV2Client, appInstallationId: string): Promise<SSHConnectionData>;
@@ -0,0 +1,25 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ import path from "path";
3
+ export async function getSSHConnectionForAppInstallation(client, appInstallationId) {
4
+ const appInstallationResponse = await client.app.getAppinstallation({
5
+ appInstallationId,
6
+ });
7
+ assertStatus(appInstallationResponse, 200);
8
+ if (appInstallationResponse.data.projectId === undefined) {
9
+ throw new Error("Project ID of app must not be undefined");
10
+ }
11
+ const projectResponse = await client.project.getProject({
12
+ projectId: appInstallationResponse.data.projectId,
13
+ });
14
+ assertStatus(projectResponse, 200);
15
+ const userResponse = await client.user.getOwnAccount();
16
+ assertStatus(userResponse, 200);
17
+ const host = `ssh.${projectResponse.data.clusterID}.${projectResponse.data.clusterDomain}`;
18
+ const user = `${userResponse.data.email}@${appInstallationResponse.data.shortId}`;
19
+ const directory = path.join(projectResponse.data.directories["Web"], appInstallationResponse.data.installationPath);
20
+ return {
21
+ host,
22
+ user,
23
+ directory,
24
+ };
25
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
3
+ export type RunTarget = {
4
+ appInstallationId: string;
5
+ } | {
6
+ projectId: string;
7
+ };
8
+ export declare function executeViaSSH(client: MittwaldAPIV2Client, target: RunTarget, command: string, args: string[], output: NodeJS.WritableStream): Promise<void>;
@@ -0,0 +1,41 @@
1
+ import cp from "child_process";
2
+ import { getSSHConnectionForAppInstallation } from "./appinstall.js";
3
+ import { getSSHConnectionForProject } from "./project.js";
4
+ export async function executeViaSSH(client, target, command, args, output) {
5
+ const { user, host } = await connectionDataForTarget(client, target);
6
+ const sshArgs = ["-l", user, "-T", host, command, ...args];
7
+ const ssh = cp.spawn("ssh", sshArgs, {
8
+ stdio: ["ignore", "pipe", "pipe"],
9
+ });
10
+ let err = "";
11
+ ssh.stdout.pipe(output);
12
+ ssh.stderr.on("data", (data) => {
13
+ err += data.toString();
14
+ });
15
+ await new Promise((res, rej) => {
16
+ ssh.on("exit", (code) => {
17
+ const resolve = () => {
18
+ if (code === 0) {
19
+ res(undefined);
20
+ }
21
+ else {
22
+ rej(new Error(`ssh+${command} exited with code ${code}\n${err}`));
23
+ }
24
+ };
25
+ if (output === process.stdout) {
26
+ resolve();
27
+ }
28
+ else {
29
+ output.end(resolve);
30
+ }
31
+ });
32
+ });
33
+ }
34
+ async function connectionDataForTarget(client, target) {
35
+ if ("appInstallationId" in target) {
36
+ return getSSHConnectionForAppInstallation(client, target.appInstallationId);
37
+ }
38
+ else {
39
+ return getSSHConnectionForProject(client, target.projectId);
40
+ }
41
+ }
@@ -0,0 +1,3 @@
1
+ import { MittwaldAPIV2Client } from "@mittwald/api-client";
2
+ import { SSHConnectionData } from "./types.js";
3
+ export declare function getSSHConnectionForProject(client: MittwaldAPIV2Client, projectId: string): Promise<SSHConnectionData>;
@@ -0,0 +1,15 @@
1
+ import { assertStatus } from "@mittwald/api-client";
2
+ export async function getSSHConnectionForProject(client, projectId) {
3
+ const projectResponse = await client.project.getProject({ projectId });
4
+ assertStatus(projectResponse, 200);
5
+ const userResponse = await client.user.getOwnAccount();
6
+ assertStatus(userResponse, 200);
7
+ const host = `ssh.${projectResponse.data.clusterID}.${projectResponse.data.clusterDomain}`;
8
+ const user = `${userResponse.data.email}@${projectResponse.data.shortId}`;
9
+ const directory = projectResponse.data.directories["Web"];
10
+ return {
11
+ host,
12
+ user,
13
+ directory,
14
+ };
15
+ }
@@ -0,0 +1,5 @@
1
+ export interface SSHConnectionData {
2
+ host: string;
3
+ user: string;
4
+ directory: string;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -4,5 +4,5 @@ import { ProcessStateSummary } from "./ProcessStateSummary.js";
4
4
  import { ProcessError } from "./ProcessError.js";
5
5
  import { Box, Text } from "ink";
6
6
  export const ProcessState = ({ step }) => {
7
- return (_jsxs(_Fragment, { children: [_jsxs(Box, { marginX: 2, children: [_jsx(ProcessStateIcon, { step: step }), _jsx(Text, { children: step.title }), _jsx(ProcessStateSummary, { step: step })] }), step.type === "step" && step.error ? (_jsx(ProcessError, { err: step.error })) : null] }));
7
+ return (_jsxs(_Fragment, { children: [_jsxs(Box, { marginX: 2, children: [_jsx(ProcessStateIcon, { step: step }), _jsx(Text, { children: step.title }), _jsx(ProcessStateSummary, { step: step })] }), step.type === "step" && step.output ? (_jsx(Box, { marginX: 6, children: _jsx(Text, { color: "gray", children: step.output.split("\n").slice(-10).join("\n") }) })) : null, step.type === "step" && step.error ? (_jsx(ProcessError, { err: step.error })) : null] }));
8
8
  };
@@ -9,6 +9,7 @@ export type ProcessStepRunnable = {
9
9
  phase: "running" | "completed" | "failed" | "aborted";
10
10
  error?: unknown;
11
11
  progress?: string;
12
+ output?: string;
12
13
  };
13
14
  export type ProcessStepConfirm = {
14
15
  type: "confirm";
@@ -22,14 +23,23 @@ export type ProcessStepInput = {
22
23
  value?: string;
23
24
  };
24
25
  export type ProcessStep = ProcessStepInfo | ProcessStepRunnable | ProcessStepConfirm | ProcessStepInput;
26
+ export type CleanupFunction = {
27
+ title: ReactNode;
28
+ fn: () => Promise<unknown>;
29
+ };
25
30
  export declare class RunnableHandler {
26
31
  private readonly listener;
27
32
  private readonly processStep;
33
+ private readonly promise;
34
+ private resolve;
35
+ private reject;
28
36
  constructor(state: ProcessStepRunnable, l: () => void);
29
37
  get done(): boolean;
38
+ wait(): Promise<void>;
30
39
  abort(): void;
31
40
  complete(): void;
32
41
  progress(p: string): void;
42
+ appendOutput(o: string): void;
33
43
  error(err: unknown): void;
34
44
  }
35
45
  export interface ProcessRenderer {
@@ -39,6 +49,7 @@ export interface ProcessRenderer {
39
49
  addInfo(title: ReactElement): void;
40
50
  addConfirmation(question: ReactElement): Promise<boolean>;
41
51
  addInput(question: ReactNode, mask?: boolean): Promise<string>;
42
- complete(summary: ReactElement): void;
43
- error(err: unknown): void;
52
+ addCleanup(title: ReactNode, fn: () => Promise<unknown>): void;
53
+ complete(summary: ReactElement): Promise<void>;
54
+ error(err: unknown): Promise<void>;
44
55
  }
@@ -1,28 +1,46 @@
1
1
  export class RunnableHandler {
2
2
  listener;
3
3
  processStep;
4
+ promise;
5
+ resolve = () => { };
6
+ reject = () => { };
4
7
  constructor(state, l) {
5
8
  this.processStep = state;
6
9
  this.listener = l;
10
+ this.promise = new Promise((res, rej) => {
11
+ this.resolve = res;
12
+ this.reject = rej;
13
+ });
14
+ this.promise.catch(() => { });
7
15
  }
8
16
  get done() {
9
17
  return this.processStep.phase !== "running";
10
18
  }
19
+ wait() {
20
+ return this.promise;
21
+ }
11
22
  abort() {
12
23
  this.processStep.phase = "aborted";
13
24
  this.listener();
25
+ this.resolve();
14
26
  }
15
27
  complete() {
16
28
  this.processStep.phase = "completed";
17
29
  this.listener();
30
+ this.resolve();
18
31
  }
19
32
  progress(p) {
20
33
  this.processStep.progress = p;
21
34
  this.listener();
22
35
  }
36
+ appendOutput(o) {
37
+ this.processStep.output += o;
38
+ this.listener();
39
+ }
23
40
  error(err) {
24
41
  this.processStep.phase = "failed";
25
42
  this.processStep.error = err;
26
43
  this.listener();
44
+ this.reject(err);
27
45
  }
28
46
  }
@@ -4,6 +4,7 @@ export declare class FancyProcessRenderer implements ProcessRenderer {
4
4
  private readonly title;
5
5
  private started;
6
6
  private currentHandler;
7
+ private cleanupFns;
7
8
  constructor(title: string);
8
9
  start(): void;
9
10
  addStep(title: ReactNode): RunnableHandler;
@@ -11,7 +12,9 @@ export declare class FancyProcessRenderer implements ProcessRenderer {
11
12
  addInfo(title: ReactElement): void;
12
13
  addInput(question: React.ReactElement, mask?: boolean): Promise<string>;
13
14
  addConfirmation(question: ReactElement): Promise<boolean>;
14
- complete(summary: ReactElement): void;
15
- error(err: unknown): void;
15
+ complete(summary: ReactElement): Promise<void>;
16
+ error(err: unknown): Promise<void>;
16
17
  private renderStart;
18
+ addCleanup(title: ReactNode, fn: () => Promise<unknown>): void;
19
+ private cleanup;
17
20
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { RunnableHandler } from "./process.js";
2
+ import { RunnableHandler, } from "./process.js";
3
3
  import { Header } from "../react/components/Header.js";
4
4
  import { Box, render, Text } from "ink";
5
5
  import { ProcessState } from "./components/ProcessState.js";
@@ -9,6 +9,7 @@ export class FancyProcessRenderer {
9
9
  title;
10
10
  started = false;
11
11
  currentHandler = null;
12
+ cleanupFns = [];
12
13
  constructor(title) {
13
14
  this.title = title;
14
15
  }
@@ -43,7 +44,7 @@ export class FancyProcessRenderer {
43
44
  return result;
44
45
  }
45
46
  catch (err) {
46
- step.error(err);
47
+ await this.error(err);
47
48
  throw err;
48
49
  }
49
50
  }
@@ -99,21 +100,37 @@ export class FancyProcessRenderer {
99
100
  const renderHandle = render(_jsx(ProcessConfirmation, { step: state, onConfirm: onConfirm }));
100
101
  });
101
102
  }
102
- complete(summary) {
103
+ async complete(summary) {
103
104
  if (this.currentHandler) {
104
105
  this.currentHandler.complete();
105
106
  }
107
+ await this.cleanup();
106
108
  render(_jsx(Box, { marginY: 1, marginX: 2, children: summary })).unmount();
107
109
  }
108
- error(err) {
110
+ async error(err) {
109
111
  if (this.currentHandler) {
110
112
  this.currentHandler.error(err);
113
+ await this.cleanup();
111
114
  }
112
115
  else {
116
+ await this.cleanup();
113
117
  render(_jsx(Box, { marginY: 1, marginX: 2, borderStyle: "round", borderColor: "red", children: _jsxs(Text, { color: "red", children: ["Error: ", err?.toString()] }) })).unmount();
114
118
  }
115
119
  }
116
120
  renderStart() {
117
121
  return (_jsx(Box, { marginY: 1, marginX: 2, children: _jsx(Header, { title: this.title }) }));
118
122
  }
123
+ addCleanup(title, fn) {
124
+ this.cleanupFns.push({ title, fn });
125
+ }
126
+ async cleanup() {
127
+ if (this.cleanupFns.length === 0) {
128
+ return;
129
+ }
130
+ for (const { title, fn } of this.cleanupFns) {
131
+ await this.runStep(title, async () => {
132
+ await fn();
133
+ });
134
+ }
135
+ }
119
136
  }
@@ -1,12 +1,15 @@
1
1
  import { ReactNode } from "react";
2
2
  import { ProcessRenderer, RunnableHandler } from "./process.js";
3
3
  export declare class SilentProcessRenderer implements ProcessRenderer {
4
+ private cleanupFns;
4
5
  start(): void;
5
6
  addStep(title: ReactNode): RunnableHandler;
6
7
  runStep<TRes>(unusedTitle: ReactNode, fn: () => Promise<TRes>): Promise<TRes>;
7
8
  addInfo(): void;
8
- complete(): void;
9
- error(err: unknown): void;
9
+ complete(): Promise<void>;
10
+ error(err: unknown): Promise<void>;
10
11
  addConfirmation(): Promise<boolean>;
11
12
  addInput(): Promise<string>;
13
+ addCleanup(_: ReactNode, fn: () => Promise<unknown>): void;
14
+ private cleanup;
12
15
  }
@@ -1,5 +1,7 @@
1
1
  import { RunnableHandler } from "./process.js";
2
+ import * as console from "console";
2
3
  export class SilentProcessRenderer {
4
+ cleanupFns = [];
3
5
  start() {
4
6
  // 🤐
5
7
  }
@@ -14,11 +16,12 @@ export class SilentProcessRenderer {
14
16
  addInfo() {
15
17
  // 🤐
16
18
  }
17
- complete() {
18
- // 🤐
19
+ async complete() {
20
+ await this.cleanup();
19
21
  }
20
22
  error(err) {
21
23
  console.error(err);
24
+ return Promise.resolve();
22
25
  }
23
26
  addConfirmation() {
24
27
  return Promise.resolve(true);
@@ -26,4 +29,12 @@ export class SilentProcessRenderer {
26
29
  addInput() {
27
30
  throw new Error("no interactive input available in quiet mode");
28
31
  }
32
+ addCleanup(_, fn) {
33
+ this.cleanupFns.push(fn);
34
+ }
35
+ async cleanup() {
36
+ for (const fn of this.cleanupFns) {
37
+ await fn();
38
+ }
39
+ }
29
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mittwald/cli",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.33",
4
4
  "description": "Hand-crafted CLI for the mittwald API",
5
5
  "license": "MIT",
6
6
  "author": {