stably 1.0.1 → 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 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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ npx lint-staged
@@ -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
- # stably
2
- Implement fault tolerance in node js. (Automatic restart, migration, fault notification)
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": "1.0.1",
4
- "main": "./stably.js",
5
- "description": "Implement fault tolerance in node js. (Automatic restart, migration, fault notification)",
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": "./bin/stably.js"
8
+ "stably": "./dist/index.js"
8
9
  },
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/hmmhmmhm/stably.git"
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
- "fault-tolerance",
15
- "restart",
16
- "reboot",
17
- "migration",
18
- "notification"
26
+ "cli"
19
27
  ],
20
- "author": "hmmhmmhm",
21
- "license": "MIT",
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
- "cancelable-event": "^1.0.2",
28
- "folder-logger": "^1.0.0",
29
- "fslogger": "^2.0.2",
30
- "mkdir-recursive": "^0.4.0",
31
- "nodemon": "^1.18.9",
32
- "string-error-parse": "^1.0.1",
33
- "yargs": "^12.0.5"
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,2 @@
1
+ export const STABLY_API_URL =
2
+ process.env.STABLY_API_URL || "https://api.stably.ai";
@@ -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
+ }
@@ -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,173 +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
- // Additional Exec(ts-node) support
54
- let parseExec = command.split(' ')
55
- if(parseExec.length >= 2 && (nodemonOption.script == parseExec[1]))
56
- nodemonOption.exec = parseExec[0]
57
-
58
- if(option.ignore) nodemonOption.ignore = ['*']
59
- nodemonOption.ext = option.external.split(',').join(' ')
60
- nodemonOption.stdout = false
61
- nodemon(nodemonOption)
62
-
63
- if(option.consoleLog || option.errorLog){
64
- if(logger === null)
65
- logger = new folderLogger(path.join(process.cwd(), option.logPath))
66
- }
67
-
68
- let restartSigned = false
69
-
70
- // Nodemon EventHandler
71
- nodemon.on('start', () => {
72
- if(option.terminalUse){
73
- logger.warn('App has been started.')
74
- console.log(' ')
75
- }
76
-
77
- }).on('quit', () => {
78
- if(option.terminalUse)
79
- logger.warn('Program terminated successfully (Quit).')
80
- process.exit()
81
-
82
- }).on('exit', ()=>{
83
- if(!restartSigned){
84
- if(option.terminalUse)
85
- logger.warn('Program terminated successfully. (Exit)')
86
-
87
- /**
88
- * @TODO 정상종료 이벤트 발생
89
- */
90
- process.exit()
91
- }else{
92
- restartSigned = false
93
- }
94
-
95
- }).on('restart', (files) => {
96
- if(option.terminalUse)
97
- logger.warn(`Program restarted due to: ${files}`)
98
- restartSigned = true
99
-
100
- }).on('crash', ()=>{
101
- nodemon.removeAllListeners()
102
-
103
- let throwCommand = lastError
104
- let parsedError = null
105
-
106
- try{
107
- parsedError = strErrParser(lastError)
108
- throwCommand = String(parsedError.main.text).toLowerCase()
109
- }catch(e){
110
- throwCommand = `${throwCommand.replace(/[\r\n\t\b]/gi, '')}`
111
- }
112
-
113
- let crashTimestamp = new Date().getTime()
114
- let crashTimeCheck = crashTimestamp - initTimestamp
115
- if(crashTimeCheck < option.min){
116
- logger.critical(String(lastError), {noFormat: true})
117
- if(option.terminalUse)
118
- logger.critical(`Program has been terminated, It failed fill in the minimum Op-time (${crashTimeCheck}ms < ${option.min}ms)`)
119
- setTimeout(()=>{
120
- process.exit()
121
- }, 3000)
122
- }else{
123
-
124
- // Processing App Commands.
125
- switch(throwCommand){
126
- case option.signalReboot:
127
- if(option.terminalUse)
128
- logger.warn('Program has requested a reboot...')
129
- break
130
- case option.signalShutdown:
131
- if(option.terminalUse){
132
- logger.warn('Program has requested a shutdown...')
133
- logger.warn('Program terminated successfully.')
134
- }
135
- process.exit()
136
-
137
- /**
138
- * @TODO 정상종료 이벤트
139
- */
140
- break
141
- default:
142
- if(option.terminalUse){
143
- logger.error('An error occurred in the program.')
144
- if(parsedError !== null){
145
- logger.error(`Error: ${parsedError.main.text}`)
146
- logger.error(`Line: ${parsedError.main.lineText}`)
147
- logger.error(`Location: ${parsedError.main.fileName}:${parsedError.main.lineNumber}\n`)
148
- }
149
- }
150
- logger.error(String(lastError), {noFormat: true, noPrint: true})
151
-
152
- /**
153
- * @TODO 크래시 이벤트
154
- */
155
- break
156
- }
157
-
158
- if(option.delay != 0){
159
- if(option.terminalUse)
160
- logger.warn(`App will be restart in ${option.delay} ms...`)
161
- }
162
-
163
- setTimeout(()=>{
164
- if(option.terminalUse)
165
- logger.warn('Reboot..')
166
- stably(command, option, program)
167
- }, option.delay)
168
- }
169
-
170
- })
171
- }
172
-
173
- 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')