@lionad/port-key 0.1.2 → 0.1.4

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 CHANGED
@@ -5,4 +5,3 @@ Copyright (c) 2026 Lionad
5
5
  Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
6
 
7
7
  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
8
-
package/bin/port-key.js CHANGED
File without changes
package/locales/cn.json CHANGED
@@ -18,5 +18,8 @@
18
18
  "invalidDigits": "无效的位数。必须为 4 或 5。",
19
19
  "missingLang": "缺少 --lang 的值",
20
20
  "invalidLang": "不支持的语言。仅支持 \"en\" 或 \"cn\"。",
21
- "noValidPort": "无法从输入生成有效端口。"
21
+ "noValidPort": "无法从输入生成有效端口。",
22
+ "configReadFailed": "[info] 配置文件 {path} 似乎无法读取或不是有效的 JSON 格式。将使用默认配置。",
23
+ "firstRunNoConfig": "[info] 欢迎使用 PortKey!\n\n你可以在 {configDir} 目录下创建 config.json 文件来自定义配置。\n\n配置示例请参考:\n{readmeUrl}",
24
+ "readmeConfigExampleLink": "https://github.com/Lionad-Morotar/port-key#config"
22
25
  }
package/locales/en.json CHANGED
@@ -18,5 +18,8 @@
18
18
  "invalidDigits": "Invalid digit count. Must be 4 or 5.",
19
19
  "missingLang": "Missing value for --lang",
20
20
  "invalidLang": "Unsupported language. Only \"en\" or \"cn\" are available.",
21
- "noValidPort": "No valid port could be generated from input."
21
+ "noValidPort": "No valid port could be generated from input.",
22
+ "configReadFailed": "[info] It seems the config file at {path} could not be read or is not valid JSON. Using default config.",
23
+ "firstRunNoConfig": "[info] Welcome to PortKey!\n\nYou can create a config.json file in {configDir} to customize your settings.\n\nFor config examples, see:\n{readmeUrl}",
24
+ "readmeConfigExampleLink": "https://github.com/Lionad-Morotar/port-key#config"
22
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lionad/port-key",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A simple, practical port naming strategy",
5
5
  "type": "module",
6
6
  "exports": {
@@ -46,5 +46,8 @@
46
46
  "devDependencies": {
47
47
  "vitest": "4.0.16"
48
48
  },
49
- "dependencies": {}
49
+ "dependencies": {},
50
+ "publishConfig": {
51
+ "registry": "https://registry.npmjs.org/"
52
+ }
50
53
  }
package/src/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  import { DEFAULT_MAP, mapToPort, parseUserMap } from './port-key.js';
4
- import { loadUserConfigSync, mergeConfig } from './config.js';
4
+ import { loadUserConfigSync, mergeConfig, getPortKeyDirPath, loadRunCount, incrementRunCount } from './config.js';
5
5
  import { getLangOrDefault, loadMessages } from './i18n.js';
6
6
 
7
7
  function formatHelp(lang = 'cn') {
@@ -78,7 +78,6 @@ function parseArgv(argv) {
78
78
  continue;
79
79
  }
80
80
 
81
- // Unknown option: treat as positional (so "portkey -foo" still works when user passes "--")
82
81
  positionals.push(token);
83
82
  i += 1;
84
83
  }
@@ -93,7 +92,24 @@ function parseArgv(argv) {
93
92
  }
94
93
 
95
94
  function runCli(argv, stdout = process.stdout, stderr = process.stderr, deps = {}) {
96
- const { config } = loadUserConfigSync(deps);
95
+ const { config, configReadError, path: configPath, configExists } = loadUserConfigSync(deps);
96
+ const { isFirstRun } = loadRunCount(deps);
97
+
98
+ if (configReadError && configPath) {
99
+ const lang = getLangOrDefault((config && config.lang) || 'cn');
100
+ const MSG = loadMessages(lang);
101
+ stderr.write(MSG.configReadFailed.replace('{path}', configPath) + '\n');
102
+ }
103
+
104
+ if (isFirstRun && !configExists) {
105
+ const lang = getLangOrDefault((config && config.lang) || 'cn');
106
+ const MSG = loadMessages(lang);
107
+ const portKeyDir = getPortKeyDirPath(deps);
108
+ stderr.write(MSG.firstRunNoConfig.replace('{configDir}', portKeyDir || '~/.port-key').replace('{readmeUrl}', MSG.readmeConfigExampleLink) + '\n');
109
+ }
110
+
111
+ incrementRunCount(deps);
112
+
97
113
  let parsed;
98
114
  try {
99
115
  parsed = parseArgv(argv);
package/src/config.js CHANGED
@@ -11,7 +11,6 @@ function getConfigPath(pathModule, osModule, env) {
11
11
  const newPath = pathToUse.join(home, '.port-key', 'config.json');
12
12
  const oldPath = pathToUse.join(home, '.portkey', 'config.json');
13
13
  try {
14
- // Prefer new path if exists; otherwise fall back to old path
15
14
  if (fs.existsSync(newPath)) return newPath;
16
15
  return oldPath;
17
16
  } catch {
@@ -26,20 +25,19 @@ function loadUserConfigSync(deps = {}) {
26
25
  const env = deps.env || process.env;
27
26
 
28
27
  const configPath = getConfigPath(pathModule, osModule, env);
29
- if (!configPath) return { path: null, config: {} };
28
+ if (!configPath) return { path: null, config: {}, configReadError: false, configExists: false };
30
29
 
31
30
  try {
32
- if (!fsModule.existsSync(configPath)) return { path: configPath, config: {} };
31
+ if (!fsModule.existsSync(configPath)) return { path: configPath, config: {}, configReadError: false, configExists: false };
33
32
  const raw = fsModule.readFileSync(configPath, 'utf8');
34
- if (!String(raw || '').trim()) return { path: configPath, config: {} };
33
+ if (!String(raw || '').trim()) return { path: configPath, config: {}, configReadError: false, configExists: true };
35
34
  const parsed = JSON.parse(raw);
36
35
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
37
- return { path: configPath, config: {} };
36
+ return { path: configPath, config: {}, configReadError: true, configExists: true };
38
37
  }
39
- return { path: configPath, config: parsed };
38
+ return { path: configPath, config: parsed, configReadError: false, configExists: true };
40
39
  } catch {
41
- // Ignore config errors; CLI should still work.
42
- return { path: configPath, config: {} };
40
+ return { path: configPath, config: {}, configReadError: true, configExists: true };
43
41
  }
44
42
  }
45
43
 
@@ -60,8 +58,75 @@ function mergeConfig(base, override) {
60
58
  };
61
59
  }
62
60
 
61
+ function getPortKeyDirPath(deps = {}) {
62
+ const osModule = deps.os || os;
63
+ const pathModule = deps.path || path;
64
+ const env = deps.env || process.env;
65
+
66
+ const home = (env && (env.PORTKEY_HOME || env.HOME)) || (osModule && osModule.homedir && osModule.homedir());
67
+ if (!home) return null;
68
+
69
+ const pathToUse = pathModule || path;
70
+ return pathToUse.join(home, '.port-key');
71
+ }
72
+
73
+ function getLogPath(deps = {}) {
74
+ const portKeyDir = getPortKeyDirPath(deps);
75
+ if (!portKeyDir) return null;
76
+ const pathModule = deps.path || path;
77
+ return pathModule.join(portKeyDir, 'log.json');
78
+ }
79
+
80
+ function loadRunCount(deps = {}) {
81
+ const fsModule = deps.fs || fs;
82
+ const logPath = getLogPath(deps);
83
+ if (!logPath) return { count: 0, isFirstRun: true, logPath: null };
84
+
85
+ try {
86
+ if (!fsModule.existsSync(logPath)) {
87
+ return { count: 0, isFirstRun: true, logPath };
88
+ }
89
+ const raw = fsModule.readFileSync(logPath, 'utf8');
90
+ const parsed = JSON.parse(raw);
91
+ const count = typeof parsed?.count === 'number' ? parsed.count : 0;
92
+ return { count, isFirstRun: count === 0, logPath };
93
+ } catch {
94
+ return { count: 0, isFirstRun: true, logPath };
95
+ }
96
+ }
97
+
98
+ function incrementRunCount(deps = {}) {
99
+ const fsModule = deps.fs || fs;
100
+ const pathModule = deps.path || path;
101
+ const logPath = getLogPath(deps);
102
+ const portKeyDir = getPortKeyDirPath(deps);
103
+
104
+ if (!logPath || !portKeyDir) return;
105
+
106
+ try {
107
+ if (!fsModule.existsSync(portKeyDir)) {
108
+ fsModule.mkdirSync(portKeyDir, { recursive: true });
109
+ }
110
+
111
+ let count = 0;
112
+ if (fsModule.existsSync(logPath)) {
113
+ const raw = fsModule.readFileSync(logPath, 'utf8');
114
+ const parsed = JSON.parse(raw);
115
+ count = typeof parsed?.count === 'number' ? parsed.count : 0;
116
+ }
117
+
118
+ count += 1;
119
+ fsModule.writeFileSync(logPath, JSON.stringify({ count }, null, 2), 'utf8');
120
+ } catch {
121
+ }
122
+ }
123
+
63
124
  export {
64
125
  getConfigPath,
65
126
  loadUserConfigSync,
66
127
  mergeConfig,
128
+ getPortKeyDirPath,
129
+ getLogPath,
130
+ loadRunCount,
131
+ incrementRunCount,
67
132
  };
package/src/port-key.js CHANGED
@@ -16,7 +16,6 @@ const DEFAULT_MAP = Object.freeze({
16
16
  const DEFAULT_BLOCKED_PORTS = Object.freeze(
17
17
  new Set([
18
18
  0,
19
- // common/system-ish
20
19
  20,
21
20
  21,
22
21
  22,
@@ -39,7 +38,6 @@ const DEFAULT_BLOCKED_PORTS = Object.freeze(
39
38
  636,
40
39
  993,
41
40
  995,
42
- // very common dev defaults
43
41
  3000,
44
42
  3001,
45
43
  5000,
@@ -51,7 +49,6 @@ const DEFAULT_BLOCKED_PORTS = Object.freeze(
51
49
  9000,
52
50
  27017,
53
51
  3306,
54
- // common dev port
55
52
  1234,
56
53
  ])
57
54
  );
@@ -125,16 +122,14 @@ function pickPortFromDigits(digits, options = {}) {
125
122
  const minPort = Number.isFinite(options.minPort) ? options.minPort : 0;
126
123
  const maxPort = Number.isFinite(options.maxPort) ? options.maxPort : 65535;
127
124
  const blockedPorts = options.blockedPorts || DEFAULT_BLOCKED_PORTS;
128
- const preferDigitCount = options.preferDigitCount || 4; // default to 4
125
+ const preferDigitCount = options.preferDigitCount || 4;
129
126
 
130
- // Prefer exact digit count using normalized prefix first, then normalized suffix
131
127
  const candidates = [];
132
128
  const normalized = raw.replace(/^0+/, '');
133
129
  if (preferDigitCount && normalized.length >= preferDigitCount) {
134
130
  candidates.push(normalized.slice(0, preferDigitCount));
135
131
  candidates.push(normalized.slice(normalized.length - preferDigitCount));
136
132
  } else {
137
- // Fallback only when digits are fewer than preferDigitCount
138
133
  for (let len = Math.min(normalized.length, preferDigitCount); len >= 2; len -= 1) {
139
134
  candidates.push(normalized.slice(0, len));
140
135
  }
@@ -180,7 +175,6 @@ function parseUserMap(mapString) {
180
175
  const raw = String(mapString || '').trim();
181
176
  if (!raw) throw new Error('Empty map string');
182
177
 
183
- // 1) Strict JSON: {"1":"qaz",...}
184
178
  try {
185
179
  const maybe = JSON.parse(raw);
186
180
  if (!isPlainObject(maybe)) throw new Error('Map must be an object');
@@ -200,11 +194,8 @@ function parseUserMap(mapString) {
200
194
  if (err.message === 'Keys must be digits, values must be mapped letters') {
201
195
  throw err;
202
196
  }
203
- // fall through
204
197
  }
205
198
 
206
- // 2) JS-ish object literal: { 1: 'qaz', 0: 'p' }
207
- // Extract digit keys and quoted string values.
208
199
  const extracted = {};
209
200
  const re = /([0-9])\s*:\s*(['"])(.*?)\2/g;
210
201
  let match;
package/README.md DELETED
@@ -1,91 +0,0 @@
1
- # PortKey
2
-
3
- <p align="center">
4
- <img width="200" src="/public/logo.png" />
5
- </p>
6
-
7
- <p align="center">
8
- <strong>PortKey:A Simple, Practical Port Naming Strategy</strong>
9
- </p>
10
-
11
- ## Brief
12
-
13
- Generate ports with a letter-to-number keyboard mapping
14
-
15
- When you’re running a bunch of projects locally, picking port numbers becomes annoying.
16
-
17
- - Over the last couple of years, there have been *so many* new projects. To really try them out, you often need to boot them locally—and then ports start colliding.
18
- - If you want to keep browser tabs (or bookmarks) stable, a project’s port shouldn’t keep changing.
19
-
20
- For example, I have more than ten Nuxt apps on my machine. If they all default to `3000`, that’s obviously not going to work. So I came up with a simple, consistent port naming rule to “assign” ports per project.
21
-
22
- [Source Blog Post](https://lionad.art/articles/simple-naming-method)
23
-
24
- ### Core idea
25
-
26
- Instead of picking random numbers, map the **project name to numbers based on the keyboard**, so the port is *readable* and *memorable*.
27
-
28
- As long as the result is within the valid port range (**0–65535**) and doesn’t hit reserved/system ports, you can just use it.
29
-
30
- More specifically: using a standard QWERTY keyboard, map each letter to a single digit based on its **row/column position**.
31
-
32
- Example:
33
-
34
- `"cfetch"` → `c(3) f(4) e(3) t(5) c(3) h(6)` → `34353`(port number)
35
-
36
- Then you can take the first 4 digits (e.g. `3453`), or keep more digits (e.g. `34353`). Either is fine.
37
-
38
- If a project needs multiple ports (frontend, backend, database, etc.), pick **one** of these two approaches:
39
-
40
- 1. Use the project prefix, then append a “role suffix”
41
- - For `"cfetch"`, take `3435` as the base
42
- - Frontend (`fe`, i.e. `43`) → `34354`
43
- - Backend (`server`) → `34352`
44
- - Database (`mongo`) → `34357`
45
- - …and so on
46
-
47
- 2. Use the project prefix, then assign sequential roles
48
- - For `"cfetch"`, take `3435` as the base
49
- - Web → `34351`
50
- - Backend → `34352`
51
- - Database → `34353`
52
- - …and so on
53
-
54
- ### Valid port range
55
-
56
- - Ports must be within **0–65535**.
57
- - For custom services, it’s usually best to use **1024–49151** (non-reserved) or **49152–65535** (private/dynamic).
58
- - As long as the mapped number stays under the limit, it’s valid.
59
-
60
- ---
61
-
62
- ## How to use
63
-
64
- ```
65
- npx @lionad/port-key <your-project-name>
66
- ```
67
-
68
- ### CLI options
69
-
70
- - `-m, --map <object>`: custom mapping (JSON or JS-like object literal)
71
- - `--lang <code>`: output language (currently only `en` and `cn`, default: `cn`)
72
- - `-d, --digits <count>`: preferred digit count for port (4 or 5, default: 4)
73
- - `-h, --help`: show help
74
-
75
- Examples:
76
-
77
- ```bash
78
- npx @lionad/port-key cfetch # -> 3435
79
- npx @lionad/port-key cfetch --digits 4 # -> 3435 (4-digit port)
80
- npx @lionad/port-key cfetch --digits 5 # -> 34353 (5-digit port)
81
- ```
82
-
83
- Notes:
84
- - Default log language is `cn`. Use `--lang en` to show English messages.
85
- - Use `-h` or `--help` to show help.
86
-
87
- ### Config
88
-
89
- PortKey reads optional user config from:
90
-
91
- - `~/.port-key/config.json`