create-linkdesk-plugin 0.1.1 → 0.1.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Encaron
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 CHANGED
@@ -1,55 +1,55 @@
1
- # create-linkdesk-plugin
2
-
3
- LinkDesk 插件脚手架——一行命令生成你的第一个插件项目(对标 `yo code`)。
4
-
5
- ```bash
6
- npm create linkdesk-plugin my-cool-plugin
7
- ```
8
-
9
- 不带名字则交互式询问:
10
-
11
- ```bash
12
- npm create linkdesk-plugin
13
- ```
14
-
15
- ## 生成什么
16
-
17
- 生成物的形状**与官方插件一致**——README / CHANGELOG / resources / i18n 一个不少:
18
-
19
- ```
20
- my-cool-plugin/
21
- ├── plugin.json # 插件清单(JSONC:可注释/尾逗号,字段分节示范,VS Code $schema 校验)
22
- ├── package.json # scripts: dev / dev:real / build / publish / validate / lint
23
- ├── tsconfig.json # jsx: react-jsx + window.linkdesk.* 类型(@linkdesk/plugin-sdk)
24
- ├── .gitignore # node_modules / dist / *.linkdesk-plugin
25
- ├── README.md # 说明——市场「详情」页签的数据源 + 目录契约表
26
- ├── CHANGELOG.md # 更新日志——市场「更改日志」页签的数据源
27
- ├── .vscode/settings.json # plugin.json 按 jsonc 识别(注释不标红)
28
- ├── resources/
29
- │ └── icon.svg # 图标占位图——换成你自己的
30
- ├── src/
31
- │ ├── index.tsx # 视图组件 default 导出——壳以 { isActive, tabId?, sourceId? } 渲染
32
- │ └── index.css # 样式示例——颜色 / 字号走 var(--xxx),间距走 4px 节奏
33
- └── i18n/
34
- └── en.json # 英文译文(key = 中文原文;不建 zh.json)
35
- ```
36
-
37
- > **不预建空文件夹**(git 本来也不记录空目录)——「东西该放哪」写在生成的 `README.md` 的目录契约表里,
38
- > 用文字说清比用空夹暗示更清楚。
39
-
40
- ## 命令
41
-
42
- ```bash
43
- cd my-cool-plugin
44
- npm install
45
-
46
- npm run dev # 浏览器热重载预览(改代码即时生效)
47
- npm run dev:real # 真机环——直写 {userData}/plugins/<id> + CDP reload(真 IPC / 串口 / LSP 类插件用)
48
- npm run validate # 校验 plugin.json
49
- npm run build # 产出 <pluginId>.linkdesk-plugin——可装进 LinkDesk / 发布
50
- npm run lint # 门禁自检(硬编码颜色 / 字号 / 间距网格 / eslint 规则)
51
- npm run publish # 一键发布(建 GitHub Release + 上传 + 更新 catalog)
52
- ```
53
-
54
- > 说明:插件作者工作流(dev 热预览 / build / 发布全链路)的完整文档见
55
- > [00-第三方作者旅程](../../docs/02-Electron架构/E6_插件生态与发布/05-文档与发布/00-第三方作者旅程.md)。
1
+ # create-linkdesk-plugin
2
+
3
+ LinkDesk 插件脚手架——一行命令生成你的第一个插件项目(对标 `yo code`)。
4
+
5
+ ```bash
6
+ npm create linkdesk-plugin my-cool-plugin
7
+ ```
8
+
9
+ 不带名字则交互式询问:
10
+
11
+ ```bash
12
+ npm create linkdesk-plugin
13
+ ```
14
+
15
+ ## 生成什么
16
+
17
+ 生成物的形状**与官方插件一致**——README / CHANGELOG / resources / i18n 一个不少:
18
+
19
+ ```
20
+ my-cool-plugin/
21
+ ├── plugin.json # 插件清单(JSONC:可注释/尾逗号,字段分节示范,VS Code $schema 校验)
22
+ ├── package.json # scripts: dev / dev:real / build / publish / validate / lint
23
+ ├── tsconfig.json # jsx: react-jsx + window.linkdesk.* 类型(@linkdesk/plugin-sdk)
24
+ ├── .gitignore # node_modules / dist / *.linkdesk-plugin
25
+ ├── README.md # 说明——市场「详情」页签的数据源 + 目录契约表
26
+ ├── CHANGELOG.md # 更新日志——市场「更改日志」页签的数据源
27
+ ├── .vscode/settings.json # plugin.json 按 jsonc 识别(注释不标红)
28
+ ├── resources/
29
+ │ └── icon.svg # 图标占位图——换成你自己的
30
+ ├── src/
31
+ │ ├── index.tsx # 视图组件 default 导出——壳以 { isActive, tabId?, sourceId? } 渲染
32
+ │ └── index.css # 样式示例——颜色 / 字号走 var(--xxx),间距走 4px 节奏
33
+ └── i18n/
34
+ └── en.json # 英文译文(key = 中文原文;不建 zh.json)
35
+ ```
36
+
37
+ > **不预建空文件夹**(git 本来也不记录空目录)——「东西该放哪」写在生成的 `README.md` 的目录契约表里,
38
+ > 用文字说清比用空夹暗示更清楚。
39
+
40
+ ## 命令
41
+
42
+ ```bash
43
+ cd my-cool-plugin
44
+ npm install
45
+
46
+ npm run dev # 浏览器热重载预览(改代码即时生效)
47
+ npm run dev:real # 真机环——直写 {userData}/plugins/<id> + CDP reload(真 IPC / 串口 / LSP 类插件用)
48
+ npm run validate # 校验 plugin.json
49
+ npm run build # 产出 <pluginId>.linkdesk-plugin——可装进 LinkDesk / 发布
50
+ npm run lint # 门禁自检(硬编码颜色 / 字号 / 间距网格 / eslint 规则)
51
+ npm run publish # 一键发布(建 GitHub Release + 上传 + 更新 catalog)
52
+ ```
53
+
54
+ > 说明:插件作者工作流(dev 热预览 / build / 发布全链路)的完整文档见
55
+ > [00-第三方作者旅程](../../docs/02-Electron架构/E6_插件生态与发布/05-文档与发布/00-第三方作者旅程.md)。
package/index.js CHANGED
@@ -1,136 +1,266 @@
1
- #!/usr/bin/env node
2
- /**
3
- * create-linkdesk-plugin——LinkDesk 插件脚手架 CLI(纯 Node,零依赖,对标 yo code)。
4
- *
5
- * 用法:
6
- * npm create linkdesk-plugin my-cool-plugin # 直接给名字(kebab-case,非交互)
7
- * npm create linkdesk-plugin # 不带参数 → 交互式询问插件名
8
- *
9
- * 行为:把同目录 template/ 复制到 <cwd>/<name>,把占位符替换成真实值,打印下一步提示。
10
- * 占位符:{{pluginName}} {{displayName}} {{author}} {{date}}(递归替换所有模板文件)。
11
- * {{date}} 注入 CHANGELOG.md 的初始段标题——格式必须是 `## v<版本>(YYYY-MM-DD)`,
12
- * 那是市场「更改日志」页签切段的解析依据(见 docs/02-Electron架构/.../插件规范化层/02)。
13
- *
14
- * 生成产物契约:见 docs/02-Electron架构/E6_插件生态与发布/02-插件开发工具链/01-create-linkdesk-plugin脚手架.md。
15
- */
16
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
17
- import { spawnSync } from "node:child_process";
18
- import { createInterface } from "node:readline";
19
- import { dirname, join } from "node:path";
20
- import { fileURLToPath } from "node:url";
21
-
22
- const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
23
-
24
- /** kebab-case——同时满足插件 id / viewsContainers key / npm 包名惯例(SAFE_PLUGIN_ID 的形状子集) */
25
- const NAME_RE = /^[a-z][a-z0-9-]*$/;
26
-
27
- /** 本地日期 YYYY-MM-DD(不用 toISOString——那是 UTC,跨时区会差一天) */
28
- function todayLocal() {
29
- const d = new Date();
30
- const p = (n) => String(n).padStart(2, "0");
31
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
32
- }
33
-
34
- /** my-cool-plugin → My Cool Plugin */
35
- function toDisplayName(name) {
36
- return name
37
- .split("-")
38
- .map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
39
- .join(" ");
40
- }
41
-
42
- /** 作者默认值 = git config user.name;读不到(无 git/无配置)→ "you"(作者生成后自改) */
43
- function gitUserName() {
44
- try {
45
- const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8", timeout: 3000 });
46
- const v = (r.stdout || "").trim();
47
- return v || "you";
48
- } catch {
49
- return "you";
50
- }
51
- }
52
-
53
- /** 交互式单问——返回去除首尾空白的答案 */
54
- function ask(question) {
55
- const rl = createInterface({ input: process.stdin, output: process.stdout });
56
- return new Promise((resolve) => {
57
- rl.question(question, (answer) => {
58
- rl.close();
59
- resolve(answer.trim());
60
- });
61
- });
62
- }
63
-
64
- function replacePlaceholders(file, values) {
65
- let text = readFileSync(file, "utf8");
66
- for (const [key, value] of Object.entries(values)) {
67
- text = text.split(`{{${key}}}`).join(value);
68
- }
69
- writeFileSync(file, text);
70
- }
71
-
72
- /** 递归替换目录内全部文件(模板全是文本文件,无需跳过二进制) */
73
- function walkReplace(dir, values) {
74
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
75
- const full = join(dir, entry.name);
76
- if (entry.isDirectory()) walkReplace(full, values);
77
- else replacePlaceholders(full, values);
78
- }
79
- }
80
-
81
- async function main() {
82
- let name = (process.argv[2] || "").trim();
83
- if (!name) {
84
- name = await ask("插件名(kebab-case,如 my-cool-plugin): ");
85
- }
86
- name = name.trim();
87
- if (!NAME_RE.test(name)) {
88
- console.error(`✖ 插件名须为 kebab-case(小写字母/数字/连字符),收到:${JSON.stringify(name)}`);
89
- process.exit(1);
90
- }
91
-
92
- const target = join(process.cwd(), name);
93
- if (existsSync(target) && readdirSync(target).length > 0) {
94
- console.error(`✖ ${name}/ 已存在且非空——换个名字,或清空后重跑`);
95
- process.exit(1);
96
- }
97
-
98
- mkdirSync(target, { recursive: true });
99
- cpSync(TEMPLATE_DIR, target, { recursive: true });
100
-
101
- // 🔥 模板里存的是 `gitignore`(无点),生成时才改名为 `.gitignore`。
102
- // 原因:**npm 打包恒定丢弃名为 `.gitignore` 的文件**(npm-packlist 排除表;实测
103
- // `template/.gitignoreprobe` 与 `template/probe.txt` 都能进 tarball,唯独 `.gitignore` 不能)。
104
- // 若模板里直接放 `.gitignore`,仓内生成(读模板目录)一切正常,**但发布后的
105
- // `npm create linkdesk-plugin` 生成的工程会没有 .gitignore**——作者第一次 `git add .`
106
- // 就把 node_modules/ 和 dist/ 全提交了。生成物契约见 check-scaffold.mjs 断言 8。
107
- const tplGitignore = join(target, "gitignore");
108
- if (existsSync(tplGitignore)) renameSync(tplGitignore, join(target, ".gitignore"));
109
-
110
- const values = {
111
- pluginName: name,
112
- displayName: toDisplayName(name),
113
- author: gitUserName(),
114
- date: todayLocal(),
115
- };
116
- walkReplace(target, values);
117
-
118
- console.log("");
119
- console.log(`✔ ${name}/ 已创建`);
120
- console.log("");
121
- console.log(" 接下来:");
122
- console.log(` cd ${name}`);
123
- console.log(" npm install");
124
- console.log(" npm run dev # 浏览器热重载预览(改代码即时生效)");
125
- console.log(" npm run validate # 校验 plugin.json($schema / 字段 / i18n 文件)");
126
- console.log(" npm run build # 打包出 <pluginId>.linkdesk-plugin,可装进 LinkDesk / 发布");
127
- console.log("");
128
- console.log(" 然后:先读 README.md —— 目录该放哪、三条纪律、怎么发布都在里面。");
129
- console.log(" plugin.json 的 name / description / author 是你的身份信息,src/index.tsx 是插件本体。");
130
- console.log(" 完整插件能力(侧栏视图 / 命令 / 设置 / 协议……)见 docs/03-插件制造/。");
131
- }
132
-
133
- main().catch((err) => {
134
- console.error(err);
135
- process.exit(1);
136
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-linkdesk-plugin——LinkDesk 插件脚手架 CLI(纯 Node,零依赖,对标 yo code)。
4
+ *
5
+ * 用法:
6
+ * npm create linkdesk-plugin my-cool-plugin # 直接给名字(kebab-case,非交互)
7
+ * npm create linkdesk-plugin # 不带参数 → 交互式询问插件名
8
+ * npm create linkdesk-plugin my-cool-plugin --no-git # 跳过建仓
9
+ *
10
+ * 行为:把同目录 template/ 复制到 <cwd>/<name>,占位符替换成真实值,**按 `cargo new` 的语义决定
11
+ * 建不建 git 仓**,再打印下一步提示。
12
+ * 占位符:{{pluginName}} {{displayName}} {{author}} {{date}}(递归替换所有模板文件)。
13
+ * {{date}} 注入 CHANGELOG.md 的初始段标题——格式必须是 `## v<版本>(YYYY-MM-DD)`,
14
+ * 那是市场「更改日志」页签切段的解析依据(见 docs/02-Electron架构/.../插件规范化层/02)。
15
+ *
16
+ * 🔴 建仓三语义(E6#103 · L7 第 7.6 轮)——照抄 `cargo new`,**不是「一律 git init」**:
17
+ * ① 目标目录**已在某个 git 仓内** ⇒ 不 init(防嵌套仓——在容器目录里生成插件正是这种情形)
18
+ * ② 不在任何 git 仓内 ⇒ `git init -b main`
19
+ * ③ `--no-git` ⇒ 不建仓(逃生口,对标 `cargo new --vcs none`)
20
+ * 比 cargo 多一步:建完仓**顺手做一次初始提交**——模板自带 `.gitignore`,作者第一步看到的
21
+ * 就不是满屏 untracked,`git log` 也立刻有一笔可回退的基线。不想要仓的人用 `--no-git`。
22
+ *
23
+ * 生成产物契约:见 docs/02-Electron架构/E6_插件生态与发布/02-插件开发工具链/01-create-linkdesk-plugin脚手架.md。
24
+ */
25
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
26
+ import { spawnSync } from "node:child_process";
27
+ import { createInterface } from "node:readline";
28
+ import { dirname, join } from "node:path";
29
+ import { fileURLToPath } from "node:url";
30
+
31
+ const TEMPLATE_DIR = join(dirname(fileURLToPath(import.meta.url)), "template");
32
+
33
+ /** kebab-case——同时满足插件 id / viewsContainers key / npm 包名惯例(SAFE_PLUGIN_ID 的形状子集) */
34
+ const NAME_RE = /^[a-z][a-z0-9-]*$/;
35
+
36
+ /** 认识的旗标——**显式白名单**:`--nogit` 这种拼错不能静默略过(作者会以为仓建好了) */
37
+ const KNOWN_FLAGS = ["--no-git", "--help", "-h"];
38
+
39
+ const USAGE = [
40
+ "用法:",
41
+ " npm create linkdesk-plugin <name> 生成 <name>/ 插件工程(kebab-case)",
42
+ " npm create linkdesk-plugin 交互式询问插件名",
43
+ "选项:",
44
+ " --no-git 不建 git 仓(对标 cargo new --vcs none)",
45
+ " --help, -h 显示本说明",
46
+ ].join("\n");
47
+
48
+ /** 本地日期 YYYY-MM-DD(不用 toISOString——那是 UTC,跨时区会差一天) */
49
+ function todayLocal() {
50
+ const d = new Date();
51
+ const p = (n) => String(n).padStart(2, "0");
52
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
53
+ }
54
+
55
+ /** my-cool-plugin → My Cool Plugin */
56
+ function toDisplayName(name) {
57
+ return name
58
+ .split("-")
59
+ .map((s) => (s ? s[0].toUpperCase() + s.slice(1) : s))
60
+ .join(" ");
61
+ }
62
+
63
+ /** 作者默认值 = git config user.name;读不到(无 git/无配置)→ "you"(作者生成后自改) */
64
+ function gitUserName() {
65
+ try {
66
+ const r = spawnSync("git", ["config", "user.name"], { encoding: "utf8", timeout: 3000 });
67
+ const v = (r.stdout || "").trim();
68
+ return v || "you";
69
+ } catch {
70
+ return "you";
71
+ }
72
+ }
73
+
74
+ // ─────────────────────────── git(§〇 建仓三语义) ───────────────────────────
75
+
76
+ /** 跑一条 git 命令——不抛,失败由调用方看 status(没装 git 时 status=null 且 error 有值) */
77
+ function git(args, cwd) {
78
+ try {
79
+ return spawnSync("git", args, { cwd, encoding: "utf8", timeout: 15000 });
80
+ } catch (err) {
81
+ return { status: null, stdout: "", stderr: String(err?.message ?? err) };
82
+ }
83
+ }
84
+
85
+ /** git 在不在 PATH 上——不在就不建仓(骨架照常给,不拿「没装 git」卡住作者) */
86
+ function gitAvailable() {
87
+ return git(["--version"]).status === 0;
88
+ }
89
+
90
+ /**
91
+ * 目标目录是否**已在某个 git 仓内**——`cargo new` 的「不造嵌套仓」判据。
92
+ *
93
+ * 在**目标目录里**问(不是 cwd):目录此时已建出来,向上找仓根正是 `cargo new` 的做法。
94
+ * `rev-parse --show-toplevel` 在仓外会 exit 128 ⇒ 那就是「不在仓内」。
95
+ */
96
+ function insideGitRepo(dir) {
97
+ const r = git(["rev-parse", "--show-toplevel"], dir);
98
+ return r.status === 0 ? (r.stdout || "").trim() : "";
99
+ }
100
+
101
+ /** stderr 摘要(给作者看的失败原因,最多三行) */
102
+ function why(r) {
103
+ return ((r.stderr || r.stdout || "").trim().split("\n").filter(Boolean).slice(0, 3).join(" / ")) || "(无输出)";
104
+ }
105
+
106
+ /**
107
+ * 建仓——返回结果对象给 CLI 输出(**作者必须知道到底建没建、为什么**,不许自己猜)。
108
+ *
109
+ * 🔴 任何一步失败都**不终止脚手架**:骨架已经生成好了,建仓是加分项,不是前置条件。
110
+ */
111
+ function setupGit(target, { noGit }) {
112
+ if (noGit) return { kind: "skipped-flag" };
113
+ if (!gitAvailable()) return { kind: "no-git-binary" };
114
+
115
+ const top = insideGitRepo(target);
116
+ if (top) return { kind: "skipped-inside-repo", top };
117
+
118
+ const init = git(["init", "-b", "main"], target);
119
+ if (init.status !== 0) {
120
+ // `-b` 是 git 2.28+ 才有的旗标;更老的 git 退回 init + 显式把 HEAD 指到 main
121
+ // (不这么做,作者第一次 push 会撞上默认分支叫 master 的提示,与 GitHub 默认也不一致)
122
+ if (git(["init"], target).status !== 0) return { kind: "failed", step: "git init", detail: why(init) };
123
+ git(["symbolic-ref", "HEAD", "refs/heads/main"], target);
124
+ }
125
+
126
+ if (git(["add", "-A"], target).status !== 0) return { kind: "init-only", step: "git add" };
127
+
128
+ // `--no-verify`:这是**一个新仓的初始提交**,不该被使用者全局 core.hooksPath 上的
129
+ // commit-msg / pre-commit 钩子审(那些钩子是给别的仓立的规矩)
130
+ const commit = git(["commit", "--no-verify", "-m", "chore: 初始骨架(create-linkdesk-plugin 生成)"], target);
131
+ if (commit.status !== 0) return { kind: "init-only", step: "git commit", detail: why(commit) };
132
+
133
+ return { kind: "created" };
134
+ }
135
+
136
+ /** 建仓结果 → CLI 那一行(`git` 那步发生了什么,说全) */
137
+ function gitLine(res) {
138
+ switch (res.kind) {
139
+ case "created":
140
+ return " ✔ 已建 git 仓(main 分支 + 一次初始提交)";
141
+ case "skipped-flag":
142
+ return " · --no-git:未建 git 仓(发布前需要自己 git init)";
143
+ case "skipped-inside-repo":
144
+ return ` · 已在 git 仓内(${res.top})——按 cargo new 语义不建嵌套仓`;
145
+ case "no-git-binary":
146
+ return " ⚠️ 找不到 git(不在 PATH)——未建仓;装上 git 后进目录自己 git init -b main";
147
+ case "init-only":
148
+ return ` ⚠️ 仓已建,但初始提交没成(${res.step}):${res.detail}\n 多半是没配 git 身份 → git config --global user.name "你" && git config --global user.email "you@example.com",再进目录 git commit -m "初始骨架"`;
149
+ default:
150
+ return ` ⚠️ 建仓失败(${res.step}):${res.detail}——进目录自己 git init -b main`;
151
+ }
152
+ }
153
+
154
+ /** 交互式单问——返回去除首尾空白的答案 */
155
+ function ask(question) {
156
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
157
+ return new Promise((resolve) => {
158
+ rl.question(question, (answer) => {
159
+ rl.close();
160
+ resolve(answer.trim());
161
+ });
162
+ });
163
+ }
164
+
165
+ function replacePlaceholders(file, values) {
166
+ let text = readFileSync(file, "utf8");
167
+ for (const [key, value] of Object.entries(values)) {
168
+ text = text.split(`{{${key}}}`).join(value);
169
+ }
170
+ writeFileSync(file, text);
171
+ }
172
+
173
+ /** 递归替换目录内全部文件(模板全是文本文件,无需跳过二进制) */
174
+ function walkReplace(dir, values) {
175
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
176
+ const full = join(dir, entry.name);
177
+ if (entry.isDirectory()) walkReplace(full, values);
178
+ else replacePlaceholders(full, values);
179
+ }
180
+ }
181
+
182
+ async function main() {
183
+ const argv = process.argv.slice(2);
184
+ const flags = argv.filter((a) => a.startsWith("-"));
185
+ const positional = argv.filter((a) => !a.startsWith("-"));
186
+
187
+ if (flags.includes("--help") || flags.includes("-h")) {
188
+ console.log(USAGE);
189
+ return;
190
+ }
191
+ const unknown = flags.filter((f) => !KNOWN_FLAGS.includes(f));
192
+ if (unknown.length > 0) {
193
+ console.error(`✖ 不认识的选项:${unknown.join("、")}\n\n${USAGE}`);
194
+ process.exit(1);
195
+ }
196
+ if (positional.length > 1) {
197
+ console.error(`✖ 只接受一个插件名,收到 ${positional.length} 个:${positional.join("、")}\n\n${USAGE}`);
198
+ process.exit(1);
199
+ }
200
+ const noGit = flags.includes("--no-git");
201
+
202
+ let name = (positional[0] || "").trim();
203
+ if (!name) {
204
+ name = await ask("插件名(kebab-case,如 my-cool-plugin): ");
205
+ }
206
+ name = name.trim();
207
+ if (!NAME_RE.test(name)) {
208
+ console.error(`✖ 插件名须为 kebab-case(小写字母/数字/连字符),收到:${JSON.stringify(name)}`);
209
+ process.exit(1);
210
+ }
211
+
212
+ const target = join(process.cwd(), name);
213
+ if (existsSync(target) && readdirSync(target).length > 0) {
214
+ console.error(`✖ ${name}/ 已存在且非空——换个名字,或清空后重跑`);
215
+ process.exit(1);
216
+ }
217
+
218
+ mkdirSync(target, { recursive: true });
219
+ cpSync(TEMPLATE_DIR, target, { recursive: true });
220
+
221
+ // 🔥 模板里存的是 `gitignore`(无点),生成时才改名为 `.gitignore`。
222
+ // 原因:**npm 打包恒定丢弃名为 `.gitignore` 的文件**(npm-packlist 排除表;实测
223
+ // `template/.gitignoreprobe` 与 `template/probe.txt` 都能进 tarball,唯独 `.gitignore` 不能)。
224
+ // 若模板里直接放 `.gitignore`,仓内生成(读模板目录)一切正常,**但发布后的
225
+ // `npm create linkdesk-plugin` 生成的工程会没有 .gitignore**——作者第一次 `git add .`
226
+ // 就把 node_modules/ 和 dist/ 全提交了。生成物契约见 check-scaffold.mjs 断言 8。
227
+ const tplGitignore = join(target, "gitignore");
228
+ if (existsSync(tplGitignore)) renameSync(tplGitignore, join(target, ".gitignore"));
229
+
230
+ const values = {
231
+ pluginName: name,
232
+ displayName: toDisplayName(name),
233
+ author: gitUserName(),
234
+ date: todayLocal(),
235
+ };
236
+ walkReplace(target, values);
237
+
238
+ // 建仓放在**最后**——此刻工作区已是终态,初始提交提交的就是作者拿到的那个骨架
239
+ const repo = setupGit(target, { noGit });
240
+
241
+ console.log("");
242
+ console.log(`✔ ${name}/ 已创建`);
243
+ console.log(gitLine(repo));
244
+ console.log("");
245
+ console.log(" 接下来:");
246
+ console.log(` cd ${name}`);
247
+ console.log(" npm install");
248
+ console.log(" npm run dev # 浏览器热重载预览(改代码即时生效)");
249
+ console.log(" npm run validate # 校验 plugin.json($schema / 字段 / i18n 文件)");
250
+ console.log(" npm run build # 打包出 <pluginId>.linkdesk-plugin,可装进 LinkDesk / 发布");
251
+ if (repo.kind === "created") {
252
+ console.log("");
253
+ console.log(" 要发布(npm run publish)时还需要一个 GitHub 远端:");
254
+ console.log(" git remote add origin git@github.com:<你>/<仓库>.git");
255
+ console.log(" git push -u origin main");
256
+ }
257
+ console.log("");
258
+ console.log(" 然后:先读 README.md —— 目录该放哪、三条纪律、怎么发布都在里面。");
259
+ console.log(" plugin.json 的 name / description / author 是你的身份信息,src/index.tsx 是插件本体。");
260
+ console.log(" 完整插件能力(侧栏视图 / 命令 / 设置 / 协议……)见 docs/03-插件制造/。");
261
+ }
262
+
263
+ main().catch((err) => {
264
+ console.error(err);
265
+ process.exit(1);
266
+ });
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-linkdesk-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "LinkDesk 插件脚手架——`npm create linkdesk-plugin my-cool-plugin` 一行生成你的第一个插件项目(对标 yo code)。",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-linkdesk-plugin": "./index.js"
7
+ "create-linkdesk-plugin": "index.js"
8
8
  },
9
9
  "files": [
10
10
  "index.js",
@@ -0,0 +1,45 @@
1
+ # LinkDesk 插件仓 CI(E6#102)——「搬出去不等于脱管」。
2
+ #
3
+ # 为什么有这份文件:插件源码住在**自己的仓**里,壳仓的 `npm run check` 够不着它(那是壳的门禁)。
4
+ # 类型 / lint / 测试 / i18n / 主题审计这些检查必须在**插件仓自己**跑一遍,否则「独立」是拿
5
+ # 「质量真空」换的。本文件 + `npm run verify` 就是把那一份装回来。
6
+ #
7
+ # 四步各自对应一类「本该在提交前拦住」的错误:
8
+ # validate —— plugin.json 过 schema(身份/contributes 形状)+ 声明的 i18n 字典文件真的在
9
+ # verify —— 严格门禁(内含 SDK lint 全腿;WARN 转拦)。见 scripts/ci-verify.mjs 头注
10
+ # build —— 真的能产出 .linkdesk-plugin(构建期错误在这里现形,不是等用户装的时候)
11
+ # test —— 工程自带测试(没写测试的工程自动跳过,不是失败)
12
+ #
13
+ # 🔴 这份文件**不用任何 `${{ … }}` 表达式**——它是被脚手架当模板复制出去的文本,而壳仓的
14
+ # `check-scaffold.mjs` 会把残留的 `{{…}}` 当「未替换占位符」判红。需要表达式时先去那道门禁
15
+ # 登记(它已对 `.github/**` 开了口子,说明见该脚本)。
16
+ #
17
+ # node 版本对齐壳仓 engines(>=24):插件与壳跑在同一个 Pool 里,别让语法/crypto 行为分叉。
18
+ name: ci
19
+
20
+ on:
21
+ push:
22
+ pull_request:
23
+
24
+ jobs:
25
+ check:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - uses: actions/setup-node@v4
31
+ with:
32
+ node-version: 24
33
+
34
+ # 依赖全部来自公开货架(@linkdesk/plugin-sdk 等作者面包)——本步骤不读 GitHub API,
35
+ # 故无需 GITHUB_TOKEN;将来若加了读 GitHub 的步骤,记得喂 token(共享 runner 的匿名
36
+ # 配额 60 次/时,匿名跑会把门禁打成 403 假红)。
37
+ - run: npm ci
38
+
39
+ - run: npm run validate
40
+ - run: npm run verify
41
+ - run: npm run build
42
+
43
+ # 没写测试的工程直接跳过(--if-present),不是失败——但**必须显式跳过**,
44
+ # 不许把「没跑」伪装成「跑了且绿」。
45
+ - run: npm test --if-present
@@ -1,8 +1,8 @@
1
- {
2
- // 让 VS Code 按 JSONC 解析 plugin.json —— 它允许 // 注释与尾逗号(对标 VS Code 自己的 package.json)。
3
- // 没有这一行,VS Code 会按严格 JSON 解析,本模板 plugin.json 里的注释会被标成满屏红波浪线。
4
- // 这只是编辑器提示,不影响运行:LinkDesk 与 SDK 本来就按 jsonc 解析。
5
- "files.associations": {
6
- "plugin.json": "jsonc"
7
- }
8
- }
1
+ {
2
+ // 让 VS Code 按 JSONC 解析 plugin.json —— 它允许 // 注释与尾逗号(对标 VS Code 自己的 package.json)。
3
+ // 没有这一行,VS Code 会按严格 JSON 解析,本模板 plugin.json 里的注释会被标成满屏红波浪线。
4
+ // 这只是编辑器提示,不影响运行:LinkDesk 与 SDK 本来就按 jsonc 解析。
5
+ "files.associations": {
6
+ "plugin.json": "jsonc"
7
+ }
8
+ }
@@ -45,4 +45,14 @@ npm run publish # 建 GitHub Release + 上传 .linkdesk-plugin + 更新 cata
45
45
 
46
46
  首次发布需要 GitHub token(跑一次会引导你填,存在本机)。只预览不动作:`npm run publish -- --dry-run`。
47
47
 
48
+ 发布还要求本工程**已经推到 GitHub**(`publish` 拿工程 origin 的仓库去建 Release):
49
+
50
+ ```bash
51
+ git remote add origin git@github.com:<你>/<仓库>.git
52
+ git push -u origin main
53
+ ```
54
+
55
+ > 脚手架生成时已替你建好本仓(`main` 分支 + 一次初始提交),所以这一步只是接远端。
56
+ > 若生成时带了 `--no-git`,则先自己 `git init -b main` 再提交。
57
+
48
58
  > 完整作者文档见 LinkDesk 仓库的 `docs/03-插件制造/`(API 契约 / 生命周期 / contributes / 分发 / UI 写法规约)。