@abhiseck/zssh 0.0.1
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/LICENSE +21 -0
- package/README.md +80 -0
- package/dist/commands/add.js +112 -0
- package/dist/commands/connect.js +97 -0
- package/dist/commands/list.js +68 -0
- package/dist/commands/remove.js +60 -0
- package/dist/commands/sync.js +145 -0
- package/dist/index.js +152 -0
- package/dist/lib/config.js +167 -0
- package/dist/lib/crypto.js +71 -0
- package/dist/lib/types.js +2 -0
- package/package.json +57 -0
- package/src/commands/add.ts +79 -0
- package/src/commands/connect.ts +85 -0
- package/src/commands/list.ts +37 -0
- package/src/commands/remove.ts +29 -0
- package/src/commands/sync.ts +138 -0
- package/src/index.ts +130 -0
- package/src/lib/config.ts +160 -0
- package/src/lib/crypto.ts +59 -0
- package/src/lib/types.ts +17 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import * as chalk from "chalk";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import { ConfigManager } from "../lib/config";
|
|
5
|
+
|
|
6
|
+
const GITHUB_API = "https://api.github.com";
|
|
7
|
+
|
|
8
|
+
async function getGithubToken(configManager: ConfigManager): Promise<string> {
|
|
9
|
+
const config = configManager.getConfig();
|
|
10
|
+
if (config.githubToken) {
|
|
11
|
+
return config.githubToken;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { token } = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: "password", // or input? token is long.
|
|
17
|
+
name: "token",
|
|
18
|
+
message: "Enter GitHub Personal Access Token (with gist scope):",
|
|
19
|
+
mask: "*",
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
configManager.setGithubToken(token);
|
|
24
|
+
return token;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveCommand(configManager: ConfigManager) {
|
|
28
|
+
const token = await getGithubToken(configManager);
|
|
29
|
+
const config = configManager.getConfig();
|
|
30
|
+
const encryptedContent = configManager.getEncryptedContent(); // We need a way to get raw encrypted string
|
|
31
|
+
|
|
32
|
+
if (!encryptedContent) {
|
|
33
|
+
console.log(chalk.red("Error: Could not retrieve encrypted config."));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = {
|
|
38
|
+
"sshc_config.lock": {
|
|
39
|
+
content: encryptedContent,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (config.gistId) {
|
|
45
|
+
// Update existing Gist
|
|
46
|
+
console.log(chalk.blue("Updating existing Gist..."));
|
|
47
|
+
await axios.patch(
|
|
48
|
+
`${GITHUB_API}/gists/${config.gistId}`,
|
|
49
|
+
{ files },
|
|
50
|
+
{
|
|
51
|
+
headers: { Authorization: `token ${token}` },
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
console.log(chalk.green("Config saved to Gist successfully."));
|
|
55
|
+
} else {
|
|
56
|
+
// Create new Gist
|
|
57
|
+
console.log(chalk.blue("Creating new Gist..."));
|
|
58
|
+
const response = await axios.post(
|
|
59
|
+
`${GITHUB_API}/gists`,
|
|
60
|
+
{
|
|
61
|
+
description: "SSHC Encrypted Config",
|
|
62
|
+
public: false,
|
|
63
|
+
files,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
headers: { Authorization: `token ${token}` },
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const gistId = response.data.id;
|
|
71
|
+
configManager.setGistId(gistId);
|
|
72
|
+
console.log(chalk.green(`Gist created with ID: ${gistId}`));
|
|
73
|
+
console.log(chalk.green("Config saved to Gist successfully."));
|
|
74
|
+
}
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
console.error(
|
|
77
|
+
chalk.red("Failed to save to Gist:"),
|
|
78
|
+
error.response?.data?.message || error.message,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function syncCommand(configManager: ConfigManager) {
|
|
84
|
+
const token = await getGithubToken(configManager);
|
|
85
|
+
let { gistId } = configManager.getConfig();
|
|
86
|
+
|
|
87
|
+
if (!gistId) {
|
|
88
|
+
const answer = await inquirer.prompt([
|
|
89
|
+
{
|
|
90
|
+
type: "input",
|
|
91
|
+
name: "gistId",
|
|
92
|
+
message: "Enter Gist ID to sync with:",
|
|
93
|
+
},
|
|
94
|
+
]);
|
|
95
|
+
gistId = answer.gistId as string;
|
|
96
|
+
configManager.setGistId(gistId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
console.log(chalk.blue("Fetching Gist..."));
|
|
101
|
+
const response = await axios.get(`${GITHUB_API}/gists/${gistId}`, {
|
|
102
|
+
headers: { Authorization: `token ${token}` },
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const file = response.data.files["sshc_config.lock"];
|
|
106
|
+
if (!file) {
|
|
107
|
+
console.log(chalk.red("Invalid Gist: sshc_config.lock not found."));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const encryptedContent = file.content;
|
|
112
|
+
|
|
113
|
+
// Merge strategy:
|
|
114
|
+
// 1. Decrypt remote content
|
|
115
|
+
// 2. Merge connections (Remote adds to Local)
|
|
116
|
+
// 3. Save merged to Local
|
|
117
|
+
// 4. (Optional) Push back to Remote? No, `sync` in this context usually means "pull and apply".
|
|
118
|
+
// Use `save` to push.
|
|
119
|
+
|
|
120
|
+
const merged = configManager.mergeEncrypted(encryptedContent);
|
|
121
|
+
if (merged) {
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.green("Sync completed. Remote changes merged into local config."),
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
console.log(
|
|
127
|
+
chalk.yellow(
|
|
128
|
+
"Sync completed but no changes were made or merge failed.",
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
console.error(
|
|
134
|
+
chalk.red("Failed to sync with Gist:"),
|
|
135
|
+
error.response?.data?.message || error.message,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as chalk from "chalk";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import inquirer from "inquirer";
|
|
5
|
+
import { ConfigManager } from "./lib/config";
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
const configManager = new ConfigManager();
|
|
9
|
+
|
|
10
|
+
async function promptForMasterPassword(
|
|
11
|
+
isNew: boolean = false,
|
|
12
|
+
): Promise<string> {
|
|
13
|
+
const { password } = await inquirer.prompt([
|
|
14
|
+
{
|
|
15
|
+
type: "password",
|
|
16
|
+
name: "password",
|
|
17
|
+
message: isNew
|
|
18
|
+
? "Set a master password for your encrypted config:"
|
|
19
|
+
: "Enter master password:",
|
|
20
|
+
mask: "*",
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
return password;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function initConfig() {
|
|
27
|
+
if (configManager.isConfigExists()) {
|
|
28
|
+
const password = await promptForMasterPassword();
|
|
29
|
+
configManager.setMasterKey(password);
|
|
30
|
+
try {
|
|
31
|
+
configManager.load();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.error(chalk.red("Invalid password or corrupted config."));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.log(chalk.yellow("No existing config found. Initializing..."));
|
|
38
|
+
const password = await promptForMasterPassword(true);
|
|
39
|
+
const { confirm } = await inquirer.prompt([
|
|
40
|
+
{
|
|
41
|
+
type: "password",
|
|
42
|
+
name: "confirm",
|
|
43
|
+
message: "Confirm master password:",
|
|
44
|
+
mask: "*",
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
if (password !== confirm) {
|
|
49
|
+
console.error(chalk.red("Passwords do not match."));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
configManager.setMasterKey(password);
|
|
54
|
+
configManager.save(); // Create empty encrypted file
|
|
55
|
+
console.log(chalk.green("Config initialized!"));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.name("zssh")
|
|
61
|
+
.description("SSH Chain - Encrypted SSH Connection Manager")
|
|
62
|
+
.version("1.0.0")
|
|
63
|
+
.addHelpText(
|
|
64
|
+
"after",
|
|
65
|
+
`
|
|
66
|
+
Example:
|
|
67
|
+
$ zssh add
|
|
68
|
+
$ zssh list
|
|
69
|
+
$ zssh connect my-server
|
|
70
|
+
$ zssh connect 5f3a1
|
|
71
|
+
$ zssh save
|
|
72
|
+
$ zssh sync
|
|
73
|
+
`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command("add")
|
|
78
|
+
.description("Add a new SSH connection")
|
|
79
|
+
.action(async () => {
|
|
80
|
+
await initConfig();
|
|
81
|
+
const { addCommand } = await import("./commands/add");
|
|
82
|
+
await addCommand(configManager);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.command("list")
|
|
87
|
+
.description("List all saved connections")
|
|
88
|
+
.action(async () => {
|
|
89
|
+
await initConfig();
|
|
90
|
+
const { listCommand } = await import("./commands/list");
|
|
91
|
+
await listCommand(configManager);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command("connect <idOrAlias>")
|
|
96
|
+
.description("Connect to a server")
|
|
97
|
+
.action(async (idOrAlias) => {
|
|
98
|
+
await initConfig();
|
|
99
|
+
const { connectCommand } = await import("./commands/connect");
|
|
100
|
+
await connectCommand(configManager, idOrAlias);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
program
|
|
104
|
+
.command("remove <idOrAlias>")
|
|
105
|
+
.description("Remove a connection")
|
|
106
|
+
.action(async (idOrAlias) => {
|
|
107
|
+
await initConfig();
|
|
108
|
+
const { removeCommand } = await import("./commands/remove");
|
|
109
|
+
await removeCommand(configManager, idOrAlias);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
program
|
|
113
|
+
.command("save")
|
|
114
|
+
.description("Save config to GitHub Gist")
|
|
115
|
+
.action(async () => {
|
|
116
|
+
await initConfig();
|
|
117
|
+
const { saveCommand } = await import("./commands/sync");
|
|
118
|
+
await saveCommand(configManager);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
program
|
|
122
|
+
.command("sync")
|
|
123
|
+
.description("Sync config from GitHub Gist")
|
|
124
|
+
.action(async () => {
|
|
125
|
+
await initConfig();
|
|
126
|
+
const { syncCommand } = await import("./commands/sync");
|
|
127
|
+
await syncCommand(configManager);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { decrypt, encrypt } from "./crypto";
|
|
5
|
+
import { Config, SSHConnection } from "./types";
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = path.join(os.homedir(), ".sshc.json");
|
|
8
|
+
|
|
9
|
+
export class ConfigManager {
|
|
10
|
+
private config: Config;
|
|
11
|
+
private masterKey: string | null = null;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.config = {
|
|
15
|
+
connections: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public setMasterKey(key: string) {
|
|
20
|
+
this.masterKey = key;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public load(): boolean {
|
|
24
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
25
|
+
return true; // New config
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const fileContent = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
30
|
+
|
|
31
|
+
// Try parsing as plain JSON first (migration or initial unencrypted)
|
|
32
|
+
try {
|
|
33
|
+
const plain = JSON.parse(fileContent);
|
|
34
|
+
if (plain.connections) {
|
|
35
|
+
this.config = plain;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Not plain JSON, likely encrypted string or object
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If we are here, it's likely encrypted.
|
|
43
|
+
// We expect the file to contain just the encrypted string or { data: '...' }
|
|
44
|
+
// For simplicity let's assume the file content IS the encrypted string if not JSON
|
|
45
|
+
// Or we wrap it. Let's assume we wrap it to be safe.
|
|
46
|
+
|
|
47
|
+
let encryptedData = fileContent;
|
|
48
|
+
try {
|
|
49
|
+
const wrapped = JSON.parse(fileContent);
|
|
50
|
+
if (wrapped.encrypted) {
|
|
51
|
+
encryptedData = wrapped.encrypted;
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// It's raw encrypted string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!this.masterKey) {
|
|
58
|
+
throw new Error("Master key required to decrypt config");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const decrypted = decrypt(encryptedData, this.masterKey);
|
|
62
|
+
this.config = JSON.parse(decrypted);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (
|
|
66
|
+
error instanceof Error &&
|
|
67
|
+
error.message.includes("Master key required")
|
|
68
|
+
) {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
// wrong password or corrupted
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Failed to load config: " +
|
|
74
|
+
(error instanceof Error ? error.message : String(error)),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public getConfig(): Config {
|
|
80
|
+
return this.config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public setGithubToken(token: string) {
|
|
84
|
+
this.config.githubToken = token;
|
|
85
|
+
this.save();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public setGistId(id: string) {
|
|
89
|
+
this.config.gistId = id;
|
|
90
|
+
this.save();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public getEncryptedContent(): string | null {
|
|
94
|
+
if (!this.masterKey) return null;
|
|
95
|
+
const json = JSON.stringify(this.config);
|
|
96
|
+
return encrypt(json, this.masterKey);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public mergeEncrypted(encryptedContent: string): boolean {
|
|
100
|
+
if (!this.masterKey) return false;
|
|
101
|
+
try {
|
|
102
|
+
const decrypted = decrypt(encryptedContent, this.masterKey);
|
|
103
|
+
const remoteConfig: Config = JSON.parse(decrypted);
|
|
104
|
+
|
|
105
|
+
let changed = false;
|
|
106
|
+
remoteConfig.connections.forEach((remoteConn) => {
|
|
107
|
+
if (!this.config.connections.find((c) => c.id === remoteConn.id)) {
|
|
108
|
+
this.config.connections.push(remoteConn);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
// Could add logic to update existing ones if newer
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (changed) {
|
|
115
|
+
this.save();
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
} catch (e: any) {
|
|
119
|
+
console.error("Failed to decrypt and merge remote config:", e.message);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public save() {
|
|
125
|
+
if (!this.masterKey) {
|
|
126
|
+
throw new Error("Master key required to save config");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const json = JSON.stringify(this.config);
|
|
130
|
+
const encrypted = encrypt(json, this.masterKey);
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify({ encrypted }), "utf-8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public getConnections(): SSHConnection[] {
|
|
136
|
+
return this.config.connections;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public addConnection(connection: SSHConnection) {
|
|
140
|
+
this.config.connections.push(connection);
|
|
141
|
+
this.save();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
public removeConnection(idOrAlias: string) {
|
|
145
|
+
this.config.connections = this.config.connections.filter(
|
|
146
|
+
(c) => c.id !== idOrAlias && c.alias !== idOrAlias,
|
|
147
|
+
);
|
|
148
|
+
this.save();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public getConnection(idOrAlias: string): SSHConnection | undefined {
|
|
152
|
+
return this.config.connections.find(
|
|
153
|
+
(c) => c.id === idOrAlias || c.alias === idOrAlias,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public isConfigExists(): boolean {
|
|
158
|
+
return fs.existsSync(CONFIG_PATH);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
2
|
+
|
|
3
|
+
const ALGORITHM = "aes-256-gcm";
|
|
4
|
+
const IV_LENGTH = 16;
|
|
5
|
+
const SALT_LENGTH = 64;
|
|
6
|
+
const TAG_LENGTH = 16;
|
|
7
|
+
const KEY_LENGTH = 32;
|
|
8
|
+
const ITERATIONS = 100000;
|
|
9
|
+
|
|
10
|
+
export function encrypt(text: string, masterKey: string): string {
|
|
11
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
12
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
13
|
+
|
|
14
|
+
const key = crypto.pbkdf2Sync(
|
|
15
|
+
masterKey,
|
|
16
|
+
salt,
|
|
17
|
+
ITERATIONS,
|
|
18
|
+
KEY_LENGTH,
|
|
19
|
+
"sha512",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
23
|
+
|
|
24
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
25
|
+
encrypted += cipher.final("hex");
|
|
26
|
+
|
|
27
|
+
const tag = cipher.getAuthTag();
|
|
28
|
+
|
|
29
|
+
// Format: salt:iv:tag:encrypted
|
|
30
|
+
return `${salt.toString("hex")}:${iv.toString("hex")}:${tag.toString("hex")}:${encrypted}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function decrypt(text: string, masterKey: string): string {
|
|
34
|
+
const parts = text.split(":");
|
|
35
|
+
if (parts.length !== 4) {
|
|
36
|
+
throw new Error("Invalid encrypted data format");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const salt = Buffer.from(parts[0], "hex");
|
|
40
|
+
const iv = Buffer.from(parts[1], "hex");
|
|
41
|
+
const tag = Buffer.from(parts[2], "hex");
|
|
42
|
+
const encryptedText = parts[3];
|
|
43
|
+
|
|
44
|
+
const key = crypto.pbkdf2Sync(
|
|
45
|
+
masterKey,
|
|
46
|
+
salt,
|
|
47
|
+
ITERATIONS,
|
|
48
|
+
KEY_LENGTH,
|
|
49
|
+
"sha512",
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
53
|
+
decipher.setAuthTag(tag);
|
|
54
|
+
|
|
55
|
+
let decrypted = decipher.update(encryptedText, "hex", "utf8");
|
|
56
|
+
decrypted += decipher.final("utf8");
|
|
57
|
+
|
|
58
|
+
return decrypted;
|
|
59
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SSHConnection {
|
|
2
|
+
id: string;
|
|
3
|
+
alias: string;
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
username: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
privateKeyPath?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Config {
|
|
14
|
+
connections: SSHConnection[];
|
|
15
|
+
gistId?: string;
|
|
16
|
+
githubToken?: string;
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
15
|
+
}
|