stably 1.0.0 → 2.0.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.
- package/.eslintrc.cjs +75 -0
- package/.eslintrc.json +20 -0
- package/.husky/pre-commit +2 -0
- package/CONTRIBUTING.md +43 -0
- package/README.md +75 -2
- package/dist/index.js +169 -0
- package/package.json +46 -25
- package/src/api/stably/constants.ts +2 -0
- package/src/api/stably/user.ts +22 -0
- package/src/auth/auth.ts +70 -0
- package/src/config/config.ts +34 -0
- package/src/dev/dev.ts +26 -0
- package/src/index.ts +28 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +14 -0
- package/LICENSE +0 -21
- package/bin/stably.js +0 -136
- package/stably.js +0 -168
- package/test.js +0 -14
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** @type {import("eslint").ESLint.ConfigData} */
|
|
2
|
+
const config = {
|
|
3
|
+
extends: ["plugin:@typescript-eslint/recommended", "prettier"],
|
|
4
|
+
plugins: ["unused-imports", "unicorn", "import"],
|
|
5
|
+
parser: "@typescript-eslint/parser",
|
|
6
|
+
parserOptions: { project: ["tsconfig.json"] },
|
|
7
|
+
root: true,
|
|
8
|
+
ignorePatterns: ["*", "!src/"],
|
|
9
|
+
rules: {
|
|
10
|
+
"@typescript-eslint/array-type": "error",
|
|
11
|
+
"@typescript-eslint/ban-ts-comment": "off",
|
|
12
|
+
"@typescript-eslint/consistent-indexed-object-style": "error",
|
|
13
|
+
"@typescript-eslint/consistent-type-assertions": ["error"],
|
|
14
|
+
"@typescript-eslint/consistent-type-imports": "error",
|
|
15
|
+
"@typescript-eslint/no-floating-promises": ["error"],
|
|
16
|
+
"@typescript-eslint/no-unnecessary-boolean-literal-compare": ["error"],
|
|
17
|
+
"@typescript-eslint/no-explicit-any": ["error", { ignoreRestArgs: true }],
|
|
18
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
19
|
+
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
|
|
20
|
+
"@typescript-eslint/no-unnecessary-template-expression": "error",
|
|
21
|
+
"@typescript-eslint/switch-exhaustiveness-check": ["error"],
|
|
22
|
+
"@typescript-eslint/prefer-includes": "error",
|
|
23
|
+
"@typescript-eslint/prefer-optional-chain": "error",
|
|
24
|
+
// temporary disable this causing issues with next upgrade
|
|
25
|
+
// '@typescript-eslint/prefer-readonly': 'error',
|
|
26
|
+
// TODO: consider enabling this
|
|
27
|
+
// 'no-unused-vars': 'off',
|
|
28
|
+
// '@typescript-eslint/no-use-before-define': ['error', { functions: false, typedefs: false }],
|
|
29
|
+
curly: "error",
|
|
30
|
+
"brace-style": ["error", "1tbs", { allowSingleLine: false }],
|
|
31
|
+
"import/newline-after-import": "error",
|
|
32
|
+
"import/no-anonymous-default-export": "off",
|
|
33
|
+
// We use a smarter no-used plugin: https://www.npmjs.com/package/eslint-plugin-unused-imports
|
|
34
|
+
"no-restricted-imports": [
|
|
35
|
+
"error",
|
|
36
|
+
{
|
|
37
|
+
paths: [
|
|
38
|
+
{
|
|
39
|
+
name: "inspector",
|
|
40
|
+
importNames: ["console"],
|
|
41
|
+
message:
|
|
42
|
+
"Likely an error. Please use the global console object instead",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "zod",
|
|
46
|
+
importNames: ["undefined"],
|
|
47
|
+
message: "Likely an error. Please use the global undefined instead",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
"no-unneeded-ternary": "error",
|
|
53
|
+
"no-unsafe-finally": "error",
|
|
54
|
+
"no-use-before-define": "off",
|
|
55
|
+
"object-shorthand": ["error", "always"],
|
|
56
|
+
"prefer-template": "error",
|
|
57
|
+
"unicorn/prefer-negative-index": "error",
|
|
58
|
+
"unicorn/prefer-ternary": "error",
|
|
59
|
+
"unused-imports/no-unused-imports": "error",
|
|
60
|
+
"unused-imports/no-unused-vars": [
|
|
61
|
+
"error",
|
|
62
|
+
{
|
|
63
|
+
caughtErrors: "none",
|
|
64
|
+
vars: "all",
|
|
65
|
+
varsIgnorePattern: "^_|z[A-Z]", // z[A-Z] is for zod objects
|
|
66
|
+
args: "after-used",
|
|
67
|
+
argsIgnorePattern: "^_",
|
|
68
|
+
ignoreRestSiblings: true,
|
|
69
|
+
destructuredArrayIgnorePattern: "^_",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = config;
|
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"plugins": ["@typescript-eslint"],
|
|
4
|
+
"extends": [
|
|
5
|
+
"eslint:recommended",
|
|
6
|
+
"plugin:@typescript-eslint/recommended"
|
|
7
|
+
],
|
|
8
|
+
"env": {
|
|
9
|
+
"node": true,
|
|
10
|
+
"es6": true
|
|
11
|
+
},
|
|
12
|
+
"parserOptions": {
|
|
13
|
+
"ecmaVersion": 2020,
|
|
14
|
+
"sourceType": "module"
|
|
15
|
+
},
|
|
16
|
+
"rules": {
|
|
17
|
+
"@typescript-eslint/explicit-function-return-type": "warn",
|
|
18
|
+
"@typescript-eslint/no-explicit-any": "warn"
|
|
19
|
+
}
|
|
20
|
+
}
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
This guide is for contributors who want to develop and contribute to the Stably CLI.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
1. Clone the repository
|
|
8
|
+
2. Install dependencies:
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
3. Build the project:
|
|
13
|
+
```bash
|
|
14
|
+
npm run build
|
|
15
|
+
```
|
|
16
|
+
4. Run in development mode:
|
|
17
|
+
```bash
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Available Scripts
|
|
22
|
+
|
|
23
|
+
- `npm run build` - Compile TypeScript to JavaScript
|
|
24
|
+
- `npm run start` - Run the compiled JavaScript
|
|
25
|
+
- `npm run dev` - Run TypeScript directly (for development)
|
|
26
|
+
- `npm run test` - Run tests
|
|
27
|
+
- `npm run lint` - Run ESLint
|
|
28
|
+
- `npm run format` - Format code with Prettier
|
|
29
|
+
|
|
30
|
+
## Contributing
|
|
31
|
+
|
|
32
|
+
When contributing to this project, please:
|
|
33
|
+
|
|
34
|
+
1. Follow the existing code style
|
|
35
|
+
2. Run tests before submitting pull requests
|
|
36
|
+
3. Update documentation as needed
|
|
37
|
+
|
|
38
|
+
For questions about development, please refer to the main [README.md](README.md) for general project information.
|
|
39
|
+
|
|
40
|
+
## Relevant ENV varialbes
|
|
41
|
+
| Variable | Description |
|
|
42
|
+
|------------------|---------------------------------------------|
|
|
43
|
+
| STABLY_API_URL | Base URL for the Stably API (override default endpoint) |
|
package/README.md
CHANGED
|
@@ -1,2 +1,75 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# Stably CLI
|
|
2
|
+
|
|
3
|
+
A command-line interface tool for interacting with Stably services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install globally
|
|
9
|
+
npm install -g stably
|
|
10
|
+
|
|
11
|
+
# Or use npx
|
|
12
|
+
npx stably
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
Before using the CLI, you need to authenticate with your Stably API key.
|
|
18
|
+
|
|
19
|
+
### Getting Your API Key
|
|
20
|
+
|
|
21
|
+
1. Visit the Stably API Keys page: https://auth.stably.ai/api_keys/personal
|
|
22
|
+
2. Create a new Personal API Key
|
|
23
|
+
3. Copy the generated API Key
|
|
24
|
+
|
|
25
|
+
### Authentication Methods
|
|
26
|
+
|
|
27
|
+
#### Using the auth command
|
|
28
|
+
```bash
|
|
29
|
+
# Authenticate with API key
|
|
30
|
+
stably auth --api-key YOUR_API_KEY
|
|
31
|
+
|
|
32
|
+
# Or let the CLI prompt you for the API key
|
|
33
|
+
stably auth
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The CLI will store your API key securely for future use.
|
|
37
|
+
|
|
38
|
+
## Development Server
|
|
39
|
+
|
|
40
|
+
Start a development server with a secure tunnel to expose your local environment.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Start development server on default port (3000)
|
|
44
|
+
stably dev
|
|
45
|
+
|
|
46
|
+
# Specify a custom port
|
|
47
|
+
stably dev --port 8080
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The CLI will:
|
|
51
|
+
1. Start a development server on the specified port
|
|
52
|
+
2. Create a secure tunnel to expose your local environment
|
|
53
|
+
3. Provide you with a public URL where your local environment is accessible
|
|
54
|
+
|
|
55
|
+
## Troubleshooting
|
|
56
|
+
|
|
57
|
+
### Authentication Issues
|
|
58
|
+
- If you receive an "Invalid Personal API Key" error, verify that:
|
|
59
|
+
1. The API key is correct and hasn't been revoked
|
|
60
|
+
2. You're using a Personal API Key, not an Organization API Key
|
|
61
|
+
3. The API key has the necessary permissions
|
|
62
|
+
|
|
63
|
+
### Development Server Issues
|
|
64
|
+
- If the development server fails to start:
|
|
65
|
+
1. Ensure the specified port is available
|
|
66
|
+
2. Check if you're authenticated (run `stably auth` if needed)
|
|
67
|
+
3. Verify your API key has the necessary permissions
|
|
68
|
+
|
|
69
|
+
## Contributing
|
|
70
|
+
|
|
71
|
+
For information on how to contribute to this project, including development setup and available scripts, please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
72
|
+
|
|
73
|
+
## Support
|
|
74
|
+
|
|
75
|
+
For additional help or to report issues, please contact the Stably support team.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// package.json
|
|
30
|
+
var version = "2.0.0";
|
|
31
|
+
|
|
32
|
+
// src/config/config.ts
|
|
33
|
+
var import_path = __toESM(require("path"));
|
|
34
|
+
var import_promises = require("fs/promises");
|
|
35
|
+
var import_os = __toESM(require("os"));
|
|
36
|
+
var import_v4 = __toESM(require("zod/v4"));
|
|
37
|
+
var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".stably");
|
|
38
|
+
var CONFIG_FILE = import_path.default.join(CONFIG_DIR, "config");
|
|
39
|
+
var zStablyUser = import_v4.default.object({
|
|
40
|
+
lastName: import_v4.default.string(),
|
|
41
|
+
organization: import_v4.default.string()
|
|
42
|
+
});
|
|
43
|
+
var zStablyConfig = import_v4.default.object({
|
|
44
|
+
apiKey: import_v4.default.string(),
|
|
45
|
+
user: zStablyUser.optional()
|
|
46
|
+
});
|
|
47
|
+
async function getConfig() {
|
|
48
|
+
try {
|
|
49
|
+
const configStr = await (0, import_promises.readFile)(CONFIG_FILE, "utf-8");
|
|
50
|
+
return zStablyConfig.parse(JSON.parse(configStr));
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function saveConfig(config) {
|
|
56
|
+
await (0, import_promises.mkdir)(import_path.default.dirname(CONFIG_FILE), { recursive: true });
|
|
57
|
+
await (0, import_promises.writeFile)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/auth/auth.ts
|
|
61
|
+
var import_prompts = require("@clack/prompts");
|
|
62
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
63
|
+
|
|
64
|
+
// src/api/stably/constants.ts
|
|
65
|
+
var STABLY_API_URL = process.env.STABLY_API_URL || "https://api.stably.ai";
|
|
66
|
+
|
|
67
|
+
// src/api/stably/user.ts
|
|
68
|
+
async function getUser(apiKey) {
|
|
69
|
+
const result = await fetch(`${STABLY_API_URL}/v1/user/info`, {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json"
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ apiKey })
|
|
75
|
+
});
|
|
76
|
+
if (result.status === 401) {
|
|
77
|
+
throw new Error("Invalid API Key");
|
|
78
|
+
}
|
|
79
|
+
if (!result.ok) {
|
|
80
|
+
throw new Error("Validating API Key failed");
|
|
81
|
+
}
|
|
82
|
+
return zStablyUser.parse(await result.json());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/auth/auth.ts
|
|
86
|
+
var import_v42 = __toESM(require("zod/v4"));
|
|
87
|
+
var zAuthOptions = import_v42.default.object({
|
|
88
|
+
apiKey: import_v42.default.string().optional()
|
|
89
|
+
});
|
|
90
|
+
async function auth(options) {
|
|
91
|
+
const config = await getAuthenticationConfig(options);
|
|
92
|
+
const validConfig = await validateConfig(config);
|
|
93
|
+
await saveConfig(validConfig);
|
|
94
|
+
(0, import_prompts.outro)(import_picocolors.default.green("Login successful"));
|
|
95
|
+
}
|
|
96
|
+
async function getAuthenticationConfig(options) {
|
|
97
|
+
if (options.apiKey) {
|
|
98
|
+
return { apiKey: options.apiKey };
|
|
99
|
+
}
|
|
100
|
+
const config = await getConfig();
|
|
101
|
+
if (!config?.apiKey) {
|
|
102
|
+
return await askForApiKey();
|
|
103
|
+
}
|
|
104
|
+
return config;
|
|
105
|
+
}
|
|
106
|
+
async function askForApiKey() {
|
|
107
|
+
import_prompts.log.info("Please visit the following URL to create a Personal API Key:");
|
|
108
|
+
import_prompts.log.info(import_picocolors.default.underline(import_picocolors.default.cyan("https://auth.stably.ai/api_keys/personal")));
|
|
109
|
+
const apiKey = await (0, import_prompts.password)({
|
|
110
|
+
message: "Enter your Personal API Key",
|
|
111
|
+
mask: "*"
|
|
112
|
+
});
|
|
113
|
+
if ((0, import_prompts.isCancel)(apiKey) || !apiKey) {
|
|
114
|
+
(0, import_prompts.outro)(import_picocolors.default.red("Login cancelled"));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
return { apiKey: apiKey.toString().trim() };
|
|
118
|
+
}
|
|
119
|
+
async function validateConfig(config) {
|
|
120
|
+
const s = (0, import_prompts.spinner)();
|
|
121
|
+
s.start("Validating Personal API Key");
|
|
122
|
+
if (!config?.apiKey) {
|
|
123
|
+
s.stop(import_picocolors.default.red("\u2717 No Personal API Key provided"));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const newConfig = await getUser(config.apiKey);
|
|
128
|
+
s.stop("\u2713 Personal API Key validated");
|
|
129
|
+
return { ...config, user: newConfig };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
s.stop(import_picocolors.default.red("\u2717 Invalid Personal API Key"));
|
|
132
|
+
(0, import_prompts.outro)(import_picocolors.default.red(`Error: ${error}`));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/dev/dev.ts
|
|
138
|
+
var import_runner_sdk = require("@stablyhq/runner-sdk");
|
|
139
|
+
var import_prompts2 = require("@clack/prompts");
|
|
140
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
141
|
+
async function dev(options) {
|
|
142
|
+
(0, import_prompts2.intro)(`Starting development server for port ${options.port}...`);
|
|
143
|
+
const config = await getConfig();
|
|
144
|
+
if (!config?.apiKey || !config.user) {
|
|
145
|
+
(0, import_prompts2.outro)(import_picocolors2.default.red("You are not authenticated. Run `stably auth`"));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const s = (0, import_prompts2.spinner)();
|
|
149
|
+
s.start("Starting development server...");
|
|
150
|
+
const tunnel = await (0, import_runner_sdk.startTunnel)(`http://localhost:${options.port}`, {
|
|
151
|
+
subdomain: `${config.user?.organization}-${config.user?.lastName}`.toLowerCase()
|
|
152
|
+
});
|
|
153
|
+
s.stop(
|
|
154
|
+
`Your local environment is available at: ${import_picocolors2.default.underline(
|
|
155
|
+
import_picocolors2.default.cyan(tunnel.url)
|
|
156
|
+
)}`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/index.ts
|
|
161
|
+
var program = new import_commander.Command();
|
|
162
|
+
program.name("stably").description("Stably CLI").version(version);
|
|
163
|
+
program.command("auth").description("Authenticate with Stably").option("-k, --api-key <string>", "API key for authentication").action(auth);
|
|
164
|
+
program.command("dev").description("Start the development server").option(
|
|
165
|
+
"-p, --port <number>",
|
|
166
|
+
"Port number for the development server",
|
|
167
|
+
"3000"
|
|
168
|
+
).option("-k, --api-key <string>", "API key for authentication").action(dev);
|
|
169
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,35 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stably",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Stably CLI",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"bin": {
|
|
7
|
-
"stably": "./
|
|
8
|
+
"stably": "./dist/index.js"
|
|
8
9
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "ts-node src/index.ts",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"lint": "eslint . --ext .ts --ignore-pattern 'dist/'",
|
|
16
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
17
|
+
"prepare": "husky"
|
|
18
|
+
},
|
|
19
|
+
"lint-staged": {
|
|
20
|
+
"*.{js,ts}": [
|
|
21
|
+
"eslint --fix",
|
|
22
|
+
"prettier --write"
|
|
23
|
+
]
|
|
12
24
|
},
|
|
13
25
|
"keywords": [
|
|
14
|
-
"
|
|
15
|
-
"restart",
|
|
16
|
-
"reboot",
|
|
17
|
-
"migration",
|
|
18
|
-
"notification"
|
|
26
|
+
"cli"
|
|
19
27
|
],
|
|
20
|
-
"author": "
|
|
21
|
-
"license": "
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/hmmhmmhm/stably/issues"
|
|
24
|
-
},
|
|
25
|
-
"homepage": "https://github.com/hmmhmmhm/stably#readme",
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "ISC",
|
|
26
30
|
"dependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
"@clack/prompts": "^0.11.0",
|
|
32
|
+
"@stablyhq/runner-sdk": "^1.0.8",
|
|
33
|
+
"commander": "^14.0.0",
|
|
34
|
+
"picocolors": "^1.1.1",
|
|
35
|
+
"zod": "^4.0.5"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@tsconfig/node20": "^20.1.6",
|
|
39
|
+
"@types/node": "^24.0.14",
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "^8.37.0",
|
|
41
|
+
"@typescript-eslint/parser": "^8.37.0",
|
|
42
|
+
"eslint": "8.57.0",
|
|
43
|
+
"eslint-config-prettier": "^9.1.0",
|
|
44
|
+
"eslint-plugin-import": "^2.32.0",
|
|
45
|
+
"eslint-plugin-unicorn": "^56.0.1",
|
|
46
|
+
"eslint-plugin-unused-imports": "^4.1.4",
|
|
47
|
+
"husky": "^9.1.7",
|
|
48
|
+
"jest": "^29.7.0",
|
|
49
|
+
"lint-staged": "^15.5.2",
|
|
50
|
+
"prettier": "3.6.2",
|
|
51
|
+
"ts-jest": "^29.4.0",
|
|
52
|
+
"ts-node": "^10.9.2",
|
|
53
|
+
"tsup": "^8.5.0",
|
|
54
|
+
"typescript": "^5.8.3"
|
|
34
55
|
}
|
|
35
56
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { zStablyUser, type StablyUser } from "~/config/config";
|
|
2
|
+
import { STABLY_API_URL } from "./constants";
|
|
3
|
+
|
|
4
|
+
export async function getUser(apiKey: string): Promise<StablyUser> {
|
|
5
|
+
const result = await fetch(`${STABLY_API_URL}/v1/user/info`, {
|
|
6
|
+
method: "POST",
|
|
7
|
+
headers: {
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
},
|
|
10
|
+
body: JSON.stringify({ apiKey }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (result.status === 401) {
|
|
14
|
+
throw new Error("Invalid API Key");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (!result.ok) {
|
|
18
|
+
throw new Error("Validating API Key failed");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return zStablyUser.parse(await result.json());
|
|
22
|
+
}
|
package/src/auth/auth.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { StablyConfig } from "~/config/config";
|
|
2
|
+
import { getConfig, saveConfig } from "~/config/config";
|
|
3
|
+
import { outro, password, spinner, log, isCancel } from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { getUser } from "~/api/stably/user";
|
|
6
|
+
import z from "zod/v4";
|
|
7
|
+
|
|
8
|
+
const zAuthOptions = z.object({
|
|
9
|
+
apiKey: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type AuthOptions = z.infer<typeof zAuthOptions>;
|
|
13
|
+
|
|
14
|
+
export async function auth(options: AuthOptions) {
|
|
15
|
+
const config = await getAuthenticationConfig(options);
|
|
16
|
+
const validConfig = await validateConfig(config);
|
|
17
|
+
await saveConfig(validConfig);
|
|
18
|
+
outro(pc.green("Login successful"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getAuthenticationConfig(options: AuthOptions) {
|
|
22
|
+
// apiKey is optional, so if not provided, we ask for it
|
|
23
|
+
// in an interactive way
|
|
24
|
+
if (options.apiKey) {
|
|
25
|
+
return { apiKey: options.apiKey };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = await getConfig();
|
|
29
|
+
if (!config?.apiKey) {
|
|
30
|
+
return await askForApiKey();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function askForApiKey(): Promise<StablyConfig> {
|
|
37
|
+
log.info("Please visit the following URL to create a Personal API Key:");
|
|
38
|
+
log.info(pc.underline(pc.cyan("https://auth.stably.ai/api_keys/personal")));
|
|
39
|
+
|
|
40
|
+
const apiKey = await password({
|
|
41
|
+
message: "Enter your Personal API Key",
|
|
42
|
+
mask: "*",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (isCancel(apiKey) || !apiKey) {
|
|
46
|
+
outro(pc.red("Login cancelled"));
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { apiKey: apiKey.toString().trim() };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function validateConfig(config?: StablyConfig): Promise<StablyConfig> {
|
|
54
|
+
const s = spinner();
|
|
55
|
+
s.start("Validating Personal API Key");
|
|
56
|
+
if (!config?.apiKey) {
|
|
57
|
+
s.stop(pc.red("✗ No Personal API Key provided"));
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const newConfig = await getUser(config.apiKey);
|
|
63
|
+
s.stop("✓ Personal API Key validated");
|
|
64
|
+
return { ...config, user: newConfig };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
s.stop(pc.red("✗ Invalid Personal API Key"));
|
|
67
|
+
outro(pc.red(`Error: ${error}`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readFile, mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import z from "zod/v4";
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), ".stably");
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config");
|
|
8
|
+
|
|
9
|
+
export const zStablyUser = z.object({
|
|
10
|
+
lastName: z.string(),
|
|
11
|
+
organization: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const zStablyConfig = z.object({
|
|
15
|
+
apiKey: z.string(),
|
|
16
|
+
user: zStablyUser.optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export type StablyUser = z.infer<typeof zStablyUser>;
|
|
20
|
+
export type StablyConfig = z.infer<typeof zStablyConfig>;
|
|
21
|
+
|
|
22
|
+
export async function getConfig(): Promise<StablyConfig | undefined> {
|
|
23
|
+
try {
|
|
24
|
+
const configStr = await readFile(CONFIG_FILE, "utf-8");
|
|
25
|
+
return zStablyConfig.parse(JSON.parse(configStr));
|
|
26
|
+
} catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function saveConfig(config: StablyConfig) {
|
|
32
|
+
await mkdir(path.dirname(CONFIG_FILE), { recursive: true });
|
|
33
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
34
|
+
}
|
package/src/dev/dev.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { startTunnel } from "@stablyhq/runner-sdk";
|
|
2
|
+
import { getConfig } from "../config/config";
|
|
3
|
+
import { spinner, intro, outro } from "@clack/prompts";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
|
|
6
|
+
export async function dev(options: { port: number; apiKey: string }) {
|
|
7
|
+
intro(`Starting development server for port ${options.port}...`);
|
|
8
|
+
|
|
9
|
+
const config = await getConfig();
|
|
10
|
+
if (!config?.apiKey || !config.user) {
|
|
11
|
+
outro(pc.red("You are not authenticated. Run `stably auth`"));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const s = spinner();
|
|
16
|
+
s.start("Starting development server...");
|
|
17
|
+
const tunnel = await startTunnel(`http://localhost:${options.port}`, {
|
|
18
|
+
subdomain:
|
|
19
|
+
`${config.user?.organization}-${config.user?.lastName}`.toLowerCase(),
|
|
20
|
+
});
|
|
21
|
+
s.stop(
|
|
22
|
+
`Your local environment is available at: ${pc.underline(
|
|
23
|
+
pc.cyan(tunnel.url),
|
|
24
|
+
)}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { version } from "../package.json";
|
|
3
|
+
|
|
4
|
+
import { auth } from "./auth/auth";
|
|
5
|
+
import { dev } from "./dev/dev";
|
|
6
|
+
|
|
7
|
+
const program = new Command();
|
|
8
|
+
|
|
9
|
+
program.name("stably").description("Stably CLI").version(version);
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command("auth")
|
|
13
|
+
.description("Authenticate with Stably")
|
|
14
|
+
.option("-k, --api-key <string>", "API key for authentication")
|
|
15
|
+
.action(auth);
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.command("dev")
|
|
19
|
+
.description("Start the development server")
|
|
20
|
+
.option(
|
|
21
|
+
"-p, --port <number>",
|
|
22
|
+
"Port number for the development server",
|
|
23
|
+
"3000",
|
|
24
|
+
)
|
|
25
|
+
.option("-k, --api-key <string>", "API key for authentication")
|
|
26
|
+
.action(dev);
|
|
27
|
+
|
|
28
|
+
program.parse();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/node20/tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"forceConsistentCasingInFileNames": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"baseUrl": ".",
|
|
14
|
+
"paths": {
|
|
15
|
+
"~/*": ["./src/*"]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["cjs"],
|
|
6
|
+
target: "node18",
|
|
7
|
+
outDir: "dist",
|
|
8
|
+
clean: true,
|
|
9
|
+
minify: false,
|
|
10
|
+
sourcemap: false,
|
|
11
|
+
banner: {
|
|
12
|
+
js: "#!/usr/bin/env node",
|
|
13
|
+
},
|
|
14
|
+
});
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2019 hmmhmmhm
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/bin/stably.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Load modules
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const stably = require('../stably')
|
|
7
|
-
const yargs = require('yargs/yargs')
|
|
8
|
-
|
|
9
|
-
// Differentiate between
|
|
10
|
-
// program options and commands.
|
|
11
|
-
let parsedOption = []
|
|
12
|
-
let slicedArgvs = process.argv.slice(2)
|
|
13
|
-
let optionHasExist = false
|
|
14
|
-
let optionCollectEnded = false
|
|
15
|
-
let parsedCommand = []
|
|
16
|
-
for(let slicedArgv of slicedArgvs){
|
|
17
|
-
if(!optionCollectEnded){
|
|
18
|
-
if(slicedArgv[0] == '-'){
|
|
19
|
-
optionHasExist = true
|
|
20
|
-
parsedOption.push(slicedArgv)
|
|
21
|
-
continue
|
|
22
|
-
}
|
|
23
|
-
if(optionHasExist){
|
|
24
|
-
parsedOption.push(slicedArgv)
|
|
25
|
-
optionHasExist = false
|
|
26
|
-
continue
|
|
27
|
-
}else{
|
|
28
|
-
optionCollectEnded = true
|
|
29
|
-
parsedCommand.push(slicedArgv)
|
|
30
|
-
}
|
|
31
|
-
}else{
|
|
32
|
-
parsedCommand.push(slicedArgv)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
parsedOption = parsedOption.join(' ')
|
|
36
|
-
parsedCommand = parsedCommand.join(' ')
|
|
37
|
-
const command = parsedCommand
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* @description
|
|
41
|
-
* https://github.com/yargs/yargs/blob/master/docs/api.md
|
|
42
|
-
*/
|
|
43
|
-
var option =
|
|
44
|
-
yargs(parsedOption)
|
|
45
|
-
.scriptName('stably')
|
|
46
|
-
.command('<script>', 'The command you want to execute (Ex. node test.js)')
|
|
47
|
-
.command('default', 'You can config the default set-up that you want to use as a default for the global module.')
|
|
48
|
-
.command('email', 'You can config the email information that you want to use as a default for the global module.')
|
|
49
|
-
.command('init', 'This command will be create an advanced settings file for stably module in the project folder.')
|
|
50
|
-
|
|
51
|
-
.alias('c', 'config')
|
|
52
|
-
.describe('c', 'This refers to the file name to be used as the stably setting code.')
|
|
53
|
-
.default('c', './stably.config.js')
|
|
54
|
-
|
|
55
|
-
.alias('m', 'min')
|
|
56
|
-
.describe('m', 'This means the minimum operating time. If the program fails to meet this minimum operating time and an error occurs, the program is completely stopped. (set milliseconds.)')
|
|
57
|
-
.default('m', 1000)
|
|
58
|
-
|
|
59
|
-
.alias('r', 'refresh')
|
|
60
|
-
.describe('r', 'You can set this param whether you want to restart the program if there are changes in the source code.')
|
|
61
|
-
.default('r', true)
|
|
62
|
-
|
|
63
|
-
.alias('d', 'delay')
|
|
64
|
-
.describe('d', 'You can set the param to this factor value to have the program turn on after a certain amount of time when it restarts. (set milliseconds.)')
|
|
65
|
-
.default('d', 0)
|
|
66
|
-
|
|
67
|
-
.alias('i', 'ignore')
|
|
68
|
-
.describe('i', 'If you enable this option, It will be run it right away without using stably.config.js in this project.')
|
|
69
|
-
.default('i', false)
|
|
70
|
-
|
|
71
|
-
.alias('e', 'external')
|
|
72
|
-
.describe('e', 'Sets the extension of the file to monitoring.')
|
|
73
|
-
.default('e', 'js,jsx,ts,tsx')
|
|
74
|
-
|
|
75
|
-
.alias('t', 'terminal-use')
|
|
76
|
-
.describe('t', 'Select whether to display module message on the terminal.')
|
|
77
|
-
.default('t', true)
|
|
78
|
-
|
|
79
|
-
.describe('error-log', 'Choose whether to collect errors that occurred while the program was running.')
|
|
80
|
-
.default('error-log', true)
|
|
81
|
-
|
|
82
|
-
.describe('console-log', 'Select whether to collect logs that occurred while the program was running.')
|
|
83
|
-
.default('console-log', false)
|
|
84
|
-
|
|
85
|
-
.describe('log-path', 'Set a location to store the logs that occurred while the program was running.')
|
|
86
|
-
.default('log-path', './_log')
|
|
87
|
-
|
|
88
|
-
.describe('signal-reboot', 'Rebootable signal that can be executed via an error message.')
|
|
89
|
-
.default('signal-reboot', 'reboot')
|
|
90
|
-
|
|
91
|
-
.describe('signal-shutdown', 'Shutdown signal that can be executed via an error message.')
|
|
92
|
-
.default('signal-shutdown', 'shutdown')
|
|
93
|
-
|
|
94
|
-
.example(`stably node test.js`, 'An example of a command to run test.js as the without option.\n')
|
|
95
|
-
.example(`stably -c ./stably.config.js -m 1000 -r true -d 0 node test.js`, 'An example of a command to run test.js as the option.')
|
|
96
|
-
|
|
97
|
-
.epilogue('For more information, find our manual at http://github.com/hmmhmmhm/stably')
|
|
98
|
-
.help('help')
|
|
99
|
-
|
|
100
|
-
.argv
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* @TODO
|
|
104
|
-
* Global Command Processing
|
|
105
|
-
*
|
|
106
|
-
* 1. 이메일 정보 입력받아서 중앙 모듈에 저장하기
|
|
107
|
-
* 2. 기본설정 옵션 입력받아서 중앙 모듈에 저장하기
|
|
108
|
-
*/
|
|
109
|
-
switch(command){
|
|
110
|
-
case 'default':
|
|
111
|
-
// TODO: 기본설정 옵션 입력받아서 중앙 모듈에 저장하기
|
|
112
|
-
return
|
|
113
|
-
case 'email':
|
|
114
|
-
// TODO: 이메일 정보 입력받아서 중앙 모듈에 저장하기
|
|
115
|
-
return
|
|
116
|
-
case 'init':
|
|
117
|
-
// TODO: 기본설정 파일 해당 모듈폴더에 저장하기
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Load user scripts
|
|
122
|
-
const projectPath = process.cwd()
|
|
123
|
-
const userScriptPath = path.join(projectPath, `/${option.config}`)
|
|
124
|
-
var program = null
|
|
125
|
-
if(!option.ignore){
|
|
126
|
-
if(fs.existsSync(userScriptPath)){
|
|
127
|
-
let userScript = require(userScriptPath)
|
|
128
|
-
if(typeof userScript == 'function'){
|
|
129
|
-
let userProgram = userScript(command, option)
|
|
130
|
-
if(typeof userProgram == 'function')
|
|
131
|
-
program = userProgram
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
stably(command, option, program)
|
package/stably.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @TODO
|
|
3
|
-
* 1. [v] -min 최소 작동시간 체크 후 유효값보다 작으면 프로그램 종료
|
|
4
|
-
* 2. [v] -refresh 소스코드 변경시 자동재시작 토글 기능 추가
|
|
5
|
-
* 3. [v] -delay 재시작시 기다리는 기능 추가
|
|
6
|
-
* 4. [v] 안전한 재부팅 시그널
|
|
7
|
-
* 5. [ ] -l --log
|
|
8
|
-
* 6. [ ] 이용자가 Ctrl+C 로 끌경우 감지(현재 맥만 작동)
|
|
9
|
-
* 7. [ ] 정기보고?
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs')
|
|
13
|
-
const path = require('path')
|
|
14
|
-
const nodemon = require('nodemon')
|
|
15
|
-
const cli = require('nodemon/lib/cli')
|
|
16
|
-
const bus = require('nodemon/lib/utils/bus')
|
|
17
|
-
const config = require('nodemon/lib/config')
|
|
18
|
-
const strErrParser = require('string-error-parse')
|
|
19
|
-
const folderLogger = require('folder-logger')
|
|
20
|
-
|
|
21
|
-
// Main Logger
|
|
22
|
-
var logger = null
|
|
23
|
-
|
|
24
|
-
// Here is the most recent error string.
|
|
25
|
-
var lastError = null
|
|
26
|
-
|
|
27
|
-
// Obtain stdio data from the child process.
|
|
28
|
-
bus.on('stdout', (text)=>{
|
|
29
|
-
if(logger === null){
|
|
30
|
-
console.log(String(text))
|
|
31
|
-
}else{
|
|
32
|
-
logger.info(String(text), {noFormat: true, noWrite: true})
|
|
33
|
-
logger.info(String(text), {noPrint: true})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @TODO 일반 로그 이벤트발생
|
|
38
|
-
*/
|
|
39
|
-
})
|
|
40
|
-
bus.on('stderr', (text)=>{
|
|
41
|
-
lastError = String(text)
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @TODO 크래시 로그 이벤트발생
|
|
45
|
-
*/
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// Stably Main Function
|
|
49
|
-
const stably = (command, option, program)=>{
|
|
50
|
-
let initTimestamp = new Date().getTime()
|
|
51
|
-
let nodemonOption = cli.parse(`nodemon ${command}`)
|
|
52
|
-
|
|
53
|
-
if(option.ignore) nodemonOption.ignore = ['*']
|
|
54
|
-
nodemonOption.ext = option.external.split(',').join(' ')
|
|
55
|
-
nodemonOption.stdout = false
|
|
56
|
-
nodemon(nodemonOption)
|
|
57
|
-
|
|
58
|
-
if(option.consoleLog || option.errorLog){
|
|
59
|
-
if(logger === null)
|
|
60
|
-
logger = new folderLogger(path.join(process.cwd(), option.logPath))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let restartSigned = false
|
|
64
|
-
|
|
65
|
-
// Nodemon EventHandler
|
|
66
|
-
nodemon.on('start', () => {
|
|
67
|
-
if(option.terminalUse){
|
|
68
|
-
logger.warn('App has been started.')
|
|
69
|
-
console.log(' ')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
}).on('quit', () => {
|
|
73
|
-
if(option.terminalUse)
|
|
74
|
-
logger.warn('Program terminated successfully (Quit).')
|
|
75
|
-
process.exit()
|
|
76
|
-
|
|
77
|
-
}).on('exit', ()=>{
|
|
78
|
-
if(!restartSigned){
|
|
79
|
-
if(option.terminalUse)
|
|
80
|
-
logger.warn('Program terminated successfully. (Exit)')
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @TODO 정상종료 이벤트 발생
|
|
84
|
-
*/
|
|
85
|
-
process.exit()
|
|
86
|
-
}else{
|
|
87
|
-
restartSigned = false
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
}).on('restart', (files) => {
|
|
91
|
-
if(option.terminalUse)
|
|
92
|
-
logger.warn(`Program restarted due to: ${files}`)
|
|
93
|
-
restartSigned = true
|
|
94
|
-
|
|
95
|
-
}).on('crash', ()=>{
|
|
96
|
-
nodemon.removeAllListeners()
|
|
97
|
-
|
|
98
|
-
let throwCommand = lastError
|
|
99
|
-
let parsedError = null
|
|
100
|
-
|
|
101
|
-
try{
|
|
102
|
-
parsedError = strErrParser(lastError)
|
|
103
|
-
throwCommand = String(parsedError.main.text).toLowerCase()
|
|
104
|
-
}catch(e){
|
|
105
|
-
throwCommand = `${throwCommand.replace(/[\r\n\t\b]/gi, '')}`
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let crashTimestamp = new Date().getTime()
|
|
109
|
-
let crashTimeCheck = crashTimestamp - initTimestamp
|
|
110
|
-
if(crashTimeCheck < option.min){
|
|
111
|
-
logger.critical(String(lastError), {noFormat: true})
|
|
112
|
-
if(option.terminalUse)
|
|
113
|
-
logger.critical(`Program has been terminated, It failed fill in the minimum Op-time (${crashTimeCheck}ms < ${option.min}ms)`)
|
|
114
|
-
setTimeout(()=>{
|
|
115
|
-
process.exit()
|
|
116
|
-
}, 3000)
|
|
117
|
-
}else{
|
|
118
|
-
|
|
119
|
-
// Processing App Commands.
|
|
120
|
-
switch(throwCommand){
|
|
121
|
-
case option.signalReboot:
|
|
122
|
-
if(option.terminalUse)
|
|
123
|
-
logger.warn('Program has requested a reboot...')
|
|
124
|
-
break
|
|
125
|
-
case option.signalShutdown:
|
|
126
|
-
if(option.terminalUse){
|
|
127
|
-
logger.warn('Program has requested a shutdown...')
|
|
128
|
-
logger.warn('Program terminated successfully.')
|
|
129
|
-
}
|
|
130
|
-
process.exit()
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* @TODO 정상종료 이벤트
|
|
134
|
-
*/
|
|
135
|
-
break
|
|
136
|
-
default:
|
|
137
|
-
if(option.terminalUse){
|
|
138
|
-
logger.error('An error occurred in the program.')
|
|
139
|
-
if(parsedError !== null){
|
|
140
|
-
logger.error(`Error: ${parsedError.main.text}`)
|
|
141
|
-
logger.error(`Line: ${parsedError.main.lineText}`)
|
|
142
|
-
logger.error(`Location: ${parsedError.main.fileName}:${parsedError.main.lineNumber}\n`)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
logger.error(String(lastError), {noFormat: true, noPrint: true})
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* @TODO 크래시 이벤트
|
|
149
|
-
*/
|
|
150
|
-
break
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if(option.delay != 0){
|
|
154
|
-
if(option.terminalUse)
|
|
155
|
-
logger.warn(`App will be restart in ${option.delay} ms...`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
setTimeout(()=>{
|
|
159
|
-
if(option.terminalUse)
|
|
160
|
-
logger.warn('Reboot..')
|
|
161
|
-
stably(command, option, program)
|
|
162
|
-
}, option.delay)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
module.exports = stably
|
package/test.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const FsLogger = require('fslogger')
|
|
2
|
-
const mkdirFull = require('mkdir-recursive')
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class FolderLogger {
|
|
6
|
-
constructor(fs){
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
const logger = new FsLogger(__dirname + '/testlog', '[STABLY]')
|
|
11
|
-
|
|
12
|
-
logger.setLogLevel(3)
|
|
13
|
-
logger.log(3, new Date(), 'INFO TEST')
|
|
14
|
-
logger.log(1, new Date(), 'ERROR TEST')
|