@kevlns/v-cli 0.1.1-beta.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/cli.mjs +297 -0
  4. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kevlns <936277610@qq.com>
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/README.md ADDED
@@ -0,0 +1,156 @@
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/kevlns/v-cli/main/logo.png" alt="v-cli logo" width="320" />
4
+
5
+ # v-cli
6
+
7
+ **kevlns 的个人工具箱 CLI:插件化架构,内置命令随版本发布,本地插件放目录即生效。**
8
+
9
+ [![npm version](https://img.shields.io/npm/v/%40kevlns%2Fv-cli?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@kevlns/v-cli)
10
+ [![npm downloads](https://img.shields.io/npm/dm/%40kevlns%2Fv-cli?style=flat-square&color=4c8bf5)](https://www.npmjs.com/package/@kevlns/v-cli)
11
+ [![CI](https://img.shields.io/github/actions/workflow/status/kevlns/v-cli/test.yml?branch=main&style=flat-square&label=CI)](https://github.com/kevlns/v-cli/actions/workflows/test.yml)
12
+ [![license](https://img.shields.io/github/license/kevlns/v-cli?style=flat-square&color=2e8b57)](./LICENSE)
13
+
14
+ [Getting started](#getting-started) · [API](#api) · [Examples](#examples) · [Package family](#package-family)
15
+
16
+ </div>
17
+
18
+ ---
19
+
20
+ ## Why v-cli?
21
+
22
+ v-cli 让你**用一个命令沉淀所有个人小工具**,而不用为每个工具建仓库、发包、记命令。
23
+ 它被设计为**依赖极少、秒级启动、写个文件就能扩展**,与 kevlns 工具家族的其余部分可自由组合。
24
+
25
+ - **插件化架构** - 内置命令走注册表随版本发布;本地插件放进 `~/.v-cli/commands/` 立即生效,无需发版
26
+ - **容错加载** - 单个插件语法错误或形状不符只会被跳过并报告,绝不阻断其他命令
27
+ - **双通道输出** - 结果走 stdout(可管道、可脚本化),诊断走 stderr;全局 `--json` 任意位置生效
28
+ - **TypeScript-first** - 严格类型编写,tsup 将核心逻辑打包为单文件 ESM,仅保留 `commander` 运行依赖
29
+
30
+ ## Getting started
31
+
32
+ ### Install
33
+
34
+ ```bash
35
+ # beta 阶段请显式安装预发布版本
36
+ npm install -g @kevlns/v-cli@beta
37
+
38
+ # 或直接从 GitHub 仓库安装开发版本
39
+ npm install -g git+https://github.com/kevlns/v-cli.git
40
+ ```
41
+
42
+ 发布稳定版并设置 `latest` 标签后,可直接使用 `npm install -g @kevlns/v-cli`。
43
+
44
+ ### Quick start
45
+
46
+ ```bash
47
+ v-cli doctor # 环境体检:node/版本、主目录、配置、插件状态
48
+ v-cli plugin list # 列出内置命令与本地插件
49
+ v-cli ts 1710000000 # 时间戳互转
50
+ ```
51
+
52
+ > [!TIP]
53
+ > 所有命令都支持 `--json`(任意位置):`v-cli doctor --json` 输出机器可读结果,方便脚本消费。
54
+
55
+ ## Examples
56
+
57
+ ### 写一个本地插件
58
+
59
+ ```js
60
+ // ~/.v-cli/commands/hello.mjs
61
+ export default {
62
+ name: "hello",
63
+ description: "示例插件",
64
+ register(program, ctx) {
65
+ program.action(() => ctx.log.result("hello world"));
66
+ },
67
+ };
68
+ ```
69
+
70
+ 保存后立即生效:
71
+
72
+ ```bash
73
+ v-cli hello # hello world
74
+ v-cli plugin list # [local] hello 已出现
75
+ ```
76
+
77
+ ### 在脚本中消费输出
78
+
79
+ ```bash
80
+ v-cli ts 1710000000 --json | jq .seconds
81
+ ```
82
+
83
+ ## API
84
+
85
+ ### 内置命令
86
+
87
+ | 命令 | 说明 |
88
+ | --- | --- |
89
+ | `v-cli doctor` | 环境体检:node/v-cli 版本、主目录、config 可写性、本地插件与加载错误 |
90
+ | `v-cli plugin list` | 列出全部命令(标注 builtin/local 来源) |
91
+ | `v-cli plugin path` | 打印本地插件目录 |
92
+ | `v-cli ts [value]` | 无参=当前时间;数字(秒/毫秒自动识别)=转可读时间;日期串=转时间戳 |
93
+
94
+ ### 插件契约 `CliCommand`
95
+
96
+ ```ts
97
+ interface CliCommand {
98
+ name: string; // 子命令名
99
+ description: string;
100
+ hidden?: boolean;
101
+ register(program: Command, ctx: CliContext): void;
102
+ }
103
+
104
+ interface CliContext {
105
+ log: Logger; // result() 走 stdout,info/warn/error 走 stderr
106
+ config: ConfigStore; // <home>/config.json 惰性读写
107
+ json: boolean; // 全局 --json 开关,命令必须尊重
108
+ homeDir: string; // 主目录(默认 ~/.v-cli)
109
+ }
110
+ ```
111
+
112
+ #### 环境变量
113
+
114
+ | 变量 | 默认值 | 说明 |
115
+ | --- | --- | --- |
116
+ | `V_CLI_HOME` | `~/.v-cli` | 覆盖主目录(配置、本地插件都从这里找) |
117
+
118
+ ## Package family
119
+
120
+ kevlns 工具家族共享同一套发布约定(tag 驱动、CI 护栏、MIT)。
121
+
122
+ | Package | Purpose | Status |
123
+ | --- | --- | --- |
124
+ | [`v-cli`](https://github.com/kevlns/v-cli) | 个人工具箱 CLI(本仓库) | v0.1.1-beta.1 |
125
+ | [`xlmerge`](https://github.com/kevlns/xlmerge) | Git 中 .xlsx/.xlsm 冲突可视化解决工具 | v1.2.1-beta.1 |
126
+
127
+ ## Compatibility
128
+
129
+ | Runtime | Supported versions |
130
+ | --- | --- |
131
+ | Node.js | `18` and later |
132
+ | TypeScript | `5.6` and later(仅开发时) |
133
+
134
+ CLI 核心逻辑以单文件 ESM(`dist/cli.mjs`)分发,运行时仅依赖 `commander`。
135
+
136
+ ## Contributing
137
+
138
+ ```bash
139
+ git clone https://github.com/kevlns/v-cli.git
140
+ cd v-cli
141
+ npm install
142
+ npm run build && npm test
143
+ ```
144
+
145
+ For bugs and feature requests, use
146
+ [GitHub Issues](https://github.com/kevlns/v-cli/issues).
147
+
148
+ ## License
149
+
150
+ Released under the [MIT License](./LICENSE).
151
+
152
+ <div align="center">
153
+
154
+ Part of the **kevlns** tool family.
155
+
156
+ </div>
package/dist/cli.mjs ADDED
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command, CommanderError } from "commander";
5
+
6
+ // src/core/context.ts
7
+ import os from "os";
8
+ import path2 from "path";
9
+
10
+ // src/core/config.ts
11
+ import fs from "fs";
12
+ import path from "path";
13
+ var ConfigStore = class _ConfigStore {
14
+ constructor(file) {
15
+ this.file = file;
16
+ }
17
+ static fromHome(homeDir) {
18
+ return new _ConfigStore(path.join(homeDir, "config.json"));
19
+ }
20
+ get() {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(this.file, "utf-8"));
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+ set(updater) {
28
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
29
+ const cfg = updater(this.get());
30
+ fs.writeFileSync(this.file, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
31
+ return cfg;
32
+ }
33
+ };
34
+
35
+ // src/core/logger.ts
36
+ var Logger = class {
37
+ constructor(jsonMode = false) {
38
+ this.jsonMode = jsonMode;
39
+ }
40
+ /** 命令结果输出:--json 时输出结构化 JSON,否则原样输出 */
41
+ result(data) {
42
+ if (this.jsonMode) {
43
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
44
+ } else if (typeof data === "string") {
45
+ process.stdout.write(data + "\n");
46
+ } else {
47
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
48
+ }
49
+ }
50
+ info(msg) {
51
+ process.stderr.write(`[v-cli] ${msg}
52
+ `);
53
+ }
54
+ warn(msg) {
55
+ process.stderr.write(`[v-cli] \u8B66\u544A: ${msg}
56
+ `);
57
+ }
58
+ error(msg) {
59
+ process.stderr.write(`[v-cli] \u9519\u8BEF: ${msg}
60
+ `);
61
+ }
62
+ };
63
+
64
+ // src/core/context.ts
65
+ function createContext(homeDir, json) {
66
+ return {
67
+ log: new Logger(json),
68
+ config: ConfigStore.fromHome(homeDir),
69
+ json,
70
+ homeDir
71
+ };
72
+ }
73
+ function defaultHomeDir() {
74
+ return process.env.V_CLI_HOME || path2.join(os.homedir(), ".v-cli");
75
+ }
76
+
77
+ // src/core/loader.ts
78
+ import fs3 from "fs";
79
+ import path4 from "path";
80
+ import { pathToFileURL } from "url";
81
+
82
+ // src/core/command.ts
83
+ function isCliCommand(candidate) {
84
+ if (typeof candidate !== "object" || candidate === null) return false;
85
+ const cmd = candidate;
86
+ return typeof cmd.name === "string" && typeof cmd.description === "string" && typeof cmd.register === "function";
87
+ }
88
+
89
+ // src/commands/doctor.ts
90
+ import fs2 from "fs";
91
+
92
+ // src/version.ts
93
+ var VERSION = "0.1.1-beta.1";
94
+
95
+ // src/commands/doctor.ts
96
+ var doctor = {
97
+ name: "doctor",
98
+ description: "\u4F53\u68C0\uFF1Anode/v-cli \u7248\u672C\u3001homeDir\u3001config\u3001\u63D2\u4EF6\u72B6\u6001",
99
+ register(program, ctx) {
100
+ program.action(async () => {
101
+ const homeDir = ctx.homeDir;
102
+ const exists = fs2.existsSync(homeDir);
103
+ let configWritable = false;
104
+ try {
105
+ ctx.config.set((cfg) => cfg);
106
+ configWritable = true;
107
+ } catch {
108
+ configWritable = false;
109
+ }
110
+ const plugins = await loadLocalPlugins(ctx);
111
+ const list = plugins.map((p) => ({
112
+ source: p.source,
113
+ file: p.file,
114
+ error: p.error ?? null
115
+ }));
116
+ const report = {
117
+ version: VERSION,
118
+ node: process.version,
119
+ homeDir,
120
+ homeDirExists: exists,
121
+ configWritable,
122
+ localPluginCount: plugins.length,
123
+ localPlugins: list
124
+ };
125
+ if (ctx.json) {
126
+ ctx.log.result(report);
127
+ } else {
128
+ ctx.log.result(`vcli ${VERSION} (node ${process.version})`);
129
+ ctx.log.result(`homeDir: ${homeDir} ${exists ? "(\u5B58\u5728)" : "(\u4E0D\u5B58\u5728)"}`);
130
+ ctx.log.result(`config: ${configWritable ? "\u53EF\u8BFB\u5199" : "\u4E0D\u53EF\u5199"}`);
131
+ ctx.log.result(`\u672C\u5730\u63D2\u4EF6: ${plugins.length} \u4E2A`);
132
+ for (const p of plugins) {
133
+ ctx.log.result(` - ${p.file}${p.error ? ` [\u5931\u8D25: ${p.error}]` : ""}`);
134
+ }
135
+ }
136
+ });
137
+ }
138
+ };
139
+
140
+ // src/commands/plugin.ts
141
+ import path3 from "path";
142
+ var plugin = {
143
+ name: "plugin",
144
+ description: "\u63D2\u4EF6\u7BA1\u7406\uFF1Alist \u5217\u51FA\u547D\u4EE4\uFF0Cpath \u663E\u793A\u672C\u5730\u63D2\u4EF6\u76EE\u5F55",
145
+ register(program, ctx) {
146
+ program.command("list").description("\u5217\u51FA\u5185\u7F6E\u547D\u4EE4\u4E0E\u672C\u5730\u63D2\u4EF6").action(async () => {
147
+ const all = await loadAllCommands(ctx);
148
+ const rows = all.map((item) => ({
149
+ source: item.source,
150
+ name: item.command?.name ?? "(\u52A0\u8F7D\u5931\u8D25)",
151
+ description: item.command?.description ?? item.error ?? "",
152
+ file: item.file ?? ""
153
+ }));
154
+ if (ctx.json) {
155
+ ctx.log.result(rows);
156
+ } else {
157
+ for (const row of rows) {
158
+ ctx.log.result(`[${row.source}] ${row.name}: ${row.description}`);
159
+ }
160
+ }
161
+ });
162
+ program.command("path").description("\u6253\u5370\u672C\u5730\u63D2\u4EF6\u76EE\u5F55\uFF08\u653E\u5165 .mjs \u6587\u4EF6\u5373\u751F\u6548\uFF09").action(() => {
163
+ const dir = path3.join(ctx.homeDir, "commands");
164
+ if (ctx.json) {
165
+ ctx.log.result({ dir });
166
+ } else {
167
+ ctx.log.result(dir);
168
+ }
169
+ });
170
+ }
171
+ };
172
+
173
+ // src/commands/ts.ts
174
+ function formatTime(ms) {
175
+ const d = new Date(ms);
176
+ return d.toISOString().replace("T", " ").replace("Z", "");
177
+ }
178
+ function parseInput(input) {
179
+ if (/^\d+$/.test(input)) {
180
+ const n = Number(input);
181
+ if (n > 1e12) {
182
+ return { seconds: Math.floor(n / 1e3), milliseconds: n, raw: input };
183
+ }
184
+ return { seconds: n, milliseconds: n * 1e3, raw: input };
185
+ }
186
+ const parsed = Date.parse(input);
187
+ if (Number.isNaN(parsed)) {
188
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790\u65F6\u95F4: ${input}`);
189
+ }
190
+ return { seconds: Math.floor(parsed / 1e3), milliseconds: parsed, raw: input };
191
+ }
192
+ var ts = {
193
+ name: "ts",
194
+ description: "\u65F6\u95F4\u6233\u5DE5\u5177\uFF1A\u65F6\u95F4\u6233 <-> \u53EF\u8BFB\u65F6\u95F4\u4E92\u8F6C",
195
+ register(program, ctx) {
196
+ program.argument("[value]", "\u6570\u5B57\u65F6\u95F4\u6233\uFF08\u79D2\u6216\u6BEB\u79D2\uFF0C\u81EA\u52A8\u8BC6\u522B\uFF09\u6216\u65E5\u671F\u5B57\u7B26\u4E32").action((value) => {
197
+ if (!value || value === "now") {
198
+ const now = Date.now();
199
+ const report2 = {
200
+ seconds: Math.floor(now / 1e3),
201
+ milliseconds: now,
202
+ iso: new Date(now).toISOString(),
203
+ local: new Date(now).toString()
204
+ };
205
+ ctx.log.result(ctx.json ? report2 : report2.iso);
206
+ return;
207
+ }
208
+ const result = parseInput(value);
209
+ const report = {
210
+ input: result.raw,
211
+ seconds: result.seconds,
212
+ milliseconds: result.milliseconds,
213
+ iso: new Date(result.milliseconds).toISOString(),
214
+ local: formatTime(result.milliseconds)
215
+ };
216
+ ctx.log.result(ctx.json ? report : report.local);
217
+ });
218
+ }
219
+ };
220
+
221
+ // src/commands/index.ts
222
+ var builtinCommands = [doctor, plugin, ts];
223
+
224
+ // src/core/loader.ts
225
+ async function loadBuiltinCommands() {
226
+ return builtinCommands.map((command) => ({ command, source: "builtin" }));
227
+ }
228
+ async function loadLocalPlugins(ctx) {
229
+ const dir = path4.join(ctx.homeDir, "commands");
230
+ const loaded = [];
231
+ if (!fs3.existsSync(dir)) return loaded;
232
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".mjs")).sort();
233
+ for (const file of files) {
234
+ const abs = path4.join(dir, file);
235
+ try {
236
+ const mod = await import(
237
+ /* @vite-ignore */
238
+ pathToFileURL(abs).href
239
+ );
240
+ const cmd = mod.default ?? mod;
241
+ if (!isCliCommand(cmd)) {
242
+ loaded.push({ source: "local", file: abs, error: "\u9ED8\u8BA4\u5BFC\u51FA\u4E0D\u7B26\u5408 CliCommand \u5951\u7EA6" });
243
+ continue;
244
+ }
245
+ loaded.push({ command: cmd, source: "local", file: abs });
246
+ } catch (err) {
247
+ loaded.push({
248
+ source: "local",
249
+ file: abs,
250
+ error: err instanceof Error ? err.message : String(err)
251
+ });
252
+ }
253
+ }
254
+ return loaded;
255
+ }
256
+ async function loadAllCommands(ctx) {
257
+ return [...await loadBuiltinCommands(), ...await loadLocalPlugins(ctx)];
258
+ }
259
+
260
+ // src/cli.ts
261
+ async function main() {
262
+ const rawArgs = process.argv.slice(2);
263
+ const json = rawArgs.includes("--json");
264
+ const args = rawArgs.filter((a) => a !== "--json");
265
+ const ctx = createContext(defaultHomeDir(), json);
266
+ const program = new Command();
267
+ program.name("v-cli").description("kevlns \u7684\u4E2A\u4EBA\u5DE5\u5177\u7BB1").version(VERSION).option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").showHelpAfterError();
268
+ const loaded = await loadAllCommands(ctx);
269
+ for (const item of loaded) {
270
+ if (!item.command) {
271
+ ctx.log.warn(`${item.file ?? "?"} \u52A0\u8F7D\u5931\u8D25: ${item.error ?? "\u672A\u77E5\u9519\u8BEF"}`);
272
+ continue;
273
+ }
274
+ const cmd = item.command;
275
+ if (cmd.hidden) continue;
276
+ const sub = program.command(cmd.name).description(cmd.description);
277
+ cmd.register(sub, ctx);
278
+ }
279
+ try {
280
+ await program.parseAsync(args, { from: "user" });
281
+ return 0;
282
+ } catch (err) {
283
+ if (err instanceof CommanderError) {
284
+ return err.exitCode ?? 1;
285
+ }
286
+ ctx.log.error(err instanceof Error ? err.message : String(err));
287
+ return 1;
288
+ }
289
+ }
290
+ main().then(
291
+ (code) => process.exit(code),
292
+ (err) => {
293
+ process.stderr.write(`[v-cli] \u672A\u6355\u83B7\u5F02\u5E38: ${err instanceof Error ? err.stack ?? err.message : String(err)}
294
+ `);
295
+ process.exit(1);
296
+ }
297
+ );
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@kevlns/v-cli",
3
+ "version": "0.1.1-beta.1",
4
+ "description": "kevlns 的个人工具箱 CLI(插件化架构,内置 + 本地插件)+ doctor/plugin/ts 命令",
5
+ "type": "module",
6
+ "bin": {
7
+ "v-cli": "dist/cli.mjs"
8
+ },
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "prepack": "npm run build",
15
+ "test": "vitest run",
16
+ "typecheck": "tsc --noEmit",
17
+ "check": "npm run build && npm run typecheck && npm test"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "license": "MIT",
23
+ "author": "kevlns <936277610@qq.com>",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/kevlns/v-cli.git"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/kevlns/v-cli/issues"
30
+ },
31
+ "homepage": "https://github.com/kevlns/v-cli#readme",
32
+ "private": false,
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "keywords": [
37
+ "cli",
38
+ "toolbox",
39
+ "personal"
40
+ ],
41
+ "dependencies": {
42
+ "commander": "^12.1.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20.14.0",
46
+ "tsup": "^8.3.0",
47
+ "typescript": "^5.6.0",
48
+ "vitest": "^4.1.10"
49
+ },
50
+ "allowScripts": {
51
+ "esbuild@0.27.2": true
52
+ }
53
+ }