create-linkdesk-plugin 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -1,55 +1,62 @@
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
+ The LinkDesk plugin scaffold — one command generates your first plugin project (the `yo code` equivalent).
4
+
5
+ ```bash
6
+ npm create linkdesk-plugin my-cool-plugin
7
+ ```
8
+
9
+ Run it without a name and it asks interactively:
10
+
11
+ ```bash
12
+ npm create linkdesk-plugin
13
+ ```
14
+
15
+ ## What you get
16
+
17
+ The generated project has **the same shape as an official plugin** — README / CHANGELOG / resources / i18n, nothing missing:
18
+
19
+ ```
20
+ my-cool-plugin/
21
+ ├── plugin.json # plugin manifest (JSONC: comments + trailing commas allowed, sectioned example fields, VS Code $schema validation)
22
+ ├── package.json # scripts: dev / dev:real / build / publish / validate / lint / verify / test
23
+ ├── tsconfig.json # jsx: react-jsx + window.linkdesk.* types (@linkdesk/plugin-sdk)
24
+ ├── AGENTS.md # what this project is + the iron rules + where the docs are (for your AI assistant)
25
+ ├── .gitignore # node_modules / dist / *.linkdesk-plugin
26
+ ├── README.md # description — data source for the marketplace "Details" tab + the directory contract table
27
+ ├── CHANGELOG.md # release notes — data source for the marketplace "Changelog" tab
28
+ ├── .github/workflows/ci.yml # CI that runs `npm run verify` on every push
29
+ ├── scripts/ci-verify.mjs # the strict tier CI runs (lint + tests + declaration self-checks)
30
+ ├── .vscode/settings.json # plugin.json is treated as jsonc (comments do not light up red)
31
+ ├── resources/
32
+ │ └── icon.svg # placeholder icon — replace it with your own
33
+ ├── src/
34
+ │ ├── index.tsx # view component, default export — the shell renders it with { isActive, tabId?, sourceId? }
35
+ │ └── index.css # styling example — colors/font sizes via var(--xxx), spacing on a 4px grid
36
+ └── i18n/
37
+ └── en.json # English translations (key = the source string)
38
+ ```
39
+
40
+ > **No empty folders are pre-created** (git does not track them anyway) — "where does this go" is spelled out in the generated `README.md`'s directory contract table. Prose is clearer than a folder-shaped hint.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ cd my-cool-plugin
46
+ npm install
47
+
48
+ npm run dev # browser preview with hot reload (changes apply instantly)
49
+ npm run dev:real # real-device loop — writes into {userData}/plugins/<id> + CDP reload (for plugins needing real IPC / serial / LSP)
50
+ npm run validate # validate plugin.json / theme recipes
51
+ npm run lint # SDK rule tier (hard-coded colors / font sizes / spacing grid / eslint rules)
52
+ npm run test # unit tests (vitest)
53
+ npm run verify # full pre-delivery tier — what CI runs
54
+ npm run build # produce <pluginId>.linkdesk-plugin — installable in LinkDesk / publishable
55
+ npm run publish # one-shot publish (creates the GitHub Release + uploads + updates the catalog)
56
+ ```
57
+
58
+ > The full author documentation (dev preview / build / the whole publishing chain) lives in the LinkDesk repo:
59
+ > the **English tree** at <https://github.com/Encaron/linkdesk/tree/electron/docs/03-plugin-authoring> (start at `00-readme.md`),
60
+ > with the Chinese original at `docs/03-插件制造/`.
61
+
62
+ > 🔴 The generated `AGENTS.md` also carries this pointer, plus the iron rules inline — so the AI working in your project knows where to look without being told.
package/index.js CHANGED
@@ -1,266 +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
- * 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
- });
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-linkdesk-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "LinkDesk 插件脚手架——`npm create linkdesk-plugin my-cool-plugin` 一行生成你的第一个插件项目(对标 yo code)。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,52 @@
1
+ # {{displayName}} — a LinkDesk plugin
2
+
3
+ > **This file is for the AI working in this repository** (Claude Code / Codex / Cursor / …). Humans will prefer `README.md`.
4
+ > Generated by `create-linkdesk-plugin`. Plugin identity = the top-level `pluginId` in `plugin.json` (currently: `{{pluginName}}`).
5
+
6
+ ## 1. What this is
7
+
8
+ A **LinkDesk plugin** project. LinkDesk is a "everything is a plugin" desktop container — **the core is only tabs + split view + a data pipeline + registries, and the core does not know what your plugin does.**
9
+
10
+ Your code runs in a renderer process: use any JS library and any Web API (Canvas / WebGL / wasm / WebRTC / Web Audio / fetch are all open — **there is no API allowlist**). Only **system-level capabilities** (serial ports, files, configuration, dialogs, notifications) must go through `window.linkdesk.*`; do not reach for raw Node.js APIs.
11
+
12
+ ## 2. Iron rules (breaking these breaks the plugin — or the shell)
13
+
14
+ 1. **Colors always via `var(--xxx)`** — never hard-code hex, or your UI will not follow the user's theme.
15
+ 2. **UI text always via `t()`** (key = the source string; English translations go in `i18n/en.json`) — never hard-code display strings.
16
+ 3. **System capabilities only via `window.linkdesk.*`** — never `import` shell internals (`@src/core/...`); the SDK lint fails this at error level.
17
+ 4. **Plugin identity comes only from declarations in `plugin.json`** — never let anyone infer what your plugin is from a directory name or file location.
18
+ 5. **Context menus are declarative** (`contributes.menus` + `<ContextMenu>`); **dialogs portal into `document.body`**; **persistence goes through `window.linkdesk.configuration`** (never `localStorage`).
19
+ 6. **keep-alive architecture: every tab stays mounted** — do not blank content with `isActive`; it is only for gating "run this only while focused" side effects.
20
+
21
+ ## 3. Where to find the rules
22
+
23
+ - **Online (full author documentation, organized by "what I want to do")**: <https://github.com/Encaron/linkdesk/tree/electron/docs/03-plugin-authoring> — start at its `00-readme.md`.
24
+ - **Offline (always available, no network)**: `node_modules/@linkdesk/plugin-sdk/schemas/plugin.schema.json` — the **field-level authority**; `theme.schema.json` next to it does the same for themes.
25
+ - **Editor completion**: `plugin.json`'s `$schema` points at it, so you get completion and diagnostics as you type.
26
+ - **Self-check after editing**: `npm run validate` (manifest/format validity) + `npm run lint` (SDK rule tier).
27
+ - 中文版作者文档(Chinese docs, maintainer-facing original):<https://github.com/Encaron/linkdesk/tree/electron/docs/03-插件制造>
28
+
29
+ ## 4. What to do next (this project's commands, verbatim)
30
+
31
+ ```bash
32
+ npm install # dependencies (first run)
33
+ npm run dev # ① see it in the browser dev host — HMR on save
34
+ npm run dev:real # ② debug inside an installed LinkDesk (real IPC / serial / filesystem)
35
+ npm run validate # ③ validate plugin.json / theme recipes
36
+ npm run lint # ④ SDK rule tier (the mechanical backstop for the iron rules)
37
+ npm run test # ⑤ unit tests (vitest)
38
+ npm run verify # ⑥ full pre-delivery tier — this is what CI runs
39
+ npm run build # ⑦ produce <pluginId>.linkdesk-plugin (single-file zip)
40
+ npm run publish # ⑧ publish to your own GitHub repo (first step of listing)
41
+ ```
42
+
43
+ **🔴 Listing is two steps**: `publish` only completes **step one** (visible to people who manually added your repo as a marketplace source). **Step two is submitting to the official catalog** — only then can every user with default settings find it.
44
+
45
+ ## 5. Two toolchain boundaries
46
+
47
+ - **This project is its own git repo** (the scaffold ran `git init -b main` plus one initial commit). `publish` uses `origin` to create the Release ⇒ pushing to GitHub is just two commands: `git remote add origin <your repo>` + `git push -u origin main`.
48
+ - **Keep the version numbers in step**: `plugin.json`'s `version` and `package.json`'s `version` **must match**; and every bump needs a matching `## v<new version>(YYYY-MM-DD)` section in `CHANGELOG.md`, or the plugin's detail page will show "no changelog provided for this version".
49
+
50
+ ---
51
+
52
+ > Author: {{author}} | Generated: {{date}}
@@ -1,58 +1,56 @@
1
- # {{displayName}}
2
-
3
- > 一句话:这个插件做什么。(写在最上面——LinkDesk 市场的「详情」页签显示的就是本文件)
4
-
5
- <!-- 有场景封面时,把图放到 resources/cover.svg,再取消下面这行的注释:
6
- ![{{displayName}} 场景封面](resources/cover.svg) -->
7
-
8
- ## 怎么用
9
-
10
- 安装后在 LinkDesk 里怎么打开、点哪里、看到什么。写清楚「第一次用的人怎么走通」。
11
-
12
- ## 目录说明——东西该放哪
13
-
14
- 不必预建空文件夹(git 也不记录空目录)。**到需要时再建,位置按下表。**
15
-
16
- | 路径 | 放什么 | 什么时候有 |
17
- |:--|:--|:--|
18
- | `plugin.json` | 插件清单 | **必有** |
19
- | `README.md` | 说明——市场**详情**页签的数据源 | 强烈建议 |
20
- | `CHANGELOG.md` | 更新日志——市场**更改日志**页签的数据源 | 强烈建议 |
21
- | `resources/` | 资产:`icon.svg` / `cover.svg` / README 里引用的图 | 有图就有 |
22
- | `i18n/` | `en.json`(key = 中文原文;**不建 zh.json**) | 有 UI 文案就有 |
23
- | `themes/` · `languages/` · `snippets/` | 数据型插件的载荷 | 数据型才有 |
24
- | `src/index.tsx` | 入口(`plugin.json` 的 `entry`) | 视图插件必有 |
25
- | `src/views/` | 侧栏 / 面板视图组件(`contributes.views` 的 render 指向的文件) | 有视图时 |
26
- | `src/components/` | 本插件内部复用的组件 | 需要时 |
27
- | `src/services/` | 域逻辑 / IPC 封装 / 数据层 | 需要时 |
28
- | `src/styles/` | **多份** CSS 时统一放这(单份且与入口同夹也可) | 需要时 |
29
- | `src/__tests__/` | 单元测试(要测就自己 `npm i -D vitest`,脚手架不预装) | 需要时 |
30
-
31
- > 🔴 **共享的东西不进这里**——跨插件复用的组件 / hook 走 `@linkdesk/ui`(壳提供的公共包),**不要在插件里再写一份**;只属于本插件的域逻辑才留本地。
32
- > 🔴 **资产一律住 `resources/`,插件根不放散图**——能进安装包的是**被 README 引用过**或**被 `icon` / `marketIcon` 声明过**的文件,目录名本身没有魔法。
33
-
34
- ## 写这个插件的三条纪律
35
-
36
- 1. **颜色走主题变量**——CSS 里一律 `var(--xxx)`,**禁硬编码 hex**。理由:LinkDesk 支持整套主题替换,写死颜色 = 换主题后你的插件不跟着变。
37
- 2. **UI 文案走 `t()`**——`t("中文原文")`,英文放 `i18n/en.json`,**不建 `zh.json`**(中文 key 自带兜底)。**只加你真的用 `t()` 读过的 key**——没人读的 key 是死 key。代码标识符(`src/index.tsx` 这类)不是文案,别包进 `t()`。
38
- 3. **插件身份只来自 `plugin.json` 的声明字段**——需要什么能力就声明什么字段(`contributes` / `tabBehavior` / `icon` …),**不要靠目录名或文件位置让别人猜你的插件是什么**。
39
-
40
- ## 发布
41
-
42
- ```bash
43
- npm run publish # 建 GitHub Release + 上传 .linkdesk-plugin + 更新 catalog
44
- ```
45
-
46
- 首次发布需要 GitHub token(跑一次会引导你填,存在本机)。只预览不动作:`npm run publish -- --dry-run`。
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
-
58
- > 完整作者文档见 LinkDesk 仓库的 `docs/03-插件制造/`(API 契约 / 生命周期 / contributes / 分发 / UI 写法规约)。
1
+ # {{displayName}}
2
+
3
+ > One line: what this plugin does. (Keep it at the very top — this file is what the marketplace shows on the **Details** tab.)
4
+
5
+ <!-- When you have cover art, put the image at resources/cover.svg and uncomment the line below:
6
+ ![{{displayName}} cover](resources/cover.svg) -->
7
+
8
+ ## How to use
9
+
10
+ How to open it in LinkDesk, where to click, what you should see. Spell out the path a first-time user walks.
11
+
12
+ ## Directory layout — where things go
13
+
14
+ You do not need to pre-create empty folders (git does not track them). **Create them when you need them; the table below says where.**
15
+
16
+ | Path | What goes here | When it exists |
17
+ |:--|:--|:--|
18
+ | `plugin.json` | The plugin manifest | **Always** |
19
+ | `README.md` | Description — the data source for the marketplace **Details** tab | Strongly recommended |
20
+ | `CHANGELOG.md` | Release notes — the data source for the marketplace **Changelog** tab | Strongly recommended |
21
+ | `resources/` | Assets: `icon.svg` / `cover.svg` / images referenced from the README | Once you have images |
22
+ | `i18n/` | `en.json` (key = the source string; **do not create `zh.json`**) | Once you have UI text |
23
+ | `themes/` · `languages/` · `snippets/` | Payloads for data-only plugins | Data-only plugins |
24
+ | `src/index.tsx` | Entry (the `entry` in `plugin.json`) | Always for view plugins |
25
+ | `src/views/` | Sidebar / panel view components (the files `contributes.views` points at) | Once you have views |
26
+ | `src/components/` | Components reused inside this plugin | When needed |
27
+ | `src/services/` | Domain logic / IPC wrappers / data layer | When needed |
28
+ | `src/styles/` | **Multiple** CSS files — keep them together here (a single file next to the entry is fine too) | When needed |
29
+ | `src/__tests__/` | Unit tests (run `npm i -D vitest` yourself if you want them — the scaffold does not preinstall test tooling) | When needed |
30
+
31
+ > 🔴 **Shared things do not belong here** — components/hooks reused across plugins come from `@linkdesk/ui` (the public package the shell provides). **Do not write a second copy inside your plugin.** Only logic that belongs to this plugin stays local.
32
+ > 🔴 **Assets always live in `resources/` — no loose images in the plugin root.** What gets into the install package is what is **referenced by the README** or **declared by `icon` / `marketIcon`**; the directory name itself has no magic.
33
+
34
+ ## Three rules for this plugin
35
+
36
+ 1. **Colors come from theme variables** — always `var(--xxx)` in CSS, **never a hard-coded hex**. Reason: LinkDesk supports full theme replacement, so a fixed color means your plugin does not follow the theme.
37
+ 2. **UI text goes through `t()`** — `t("source string")`, with English in `i18n/en.json` and **no `zh.json`** (the source string is the key and is its own fallback). **Only add keys you actually read with `t()`** — an unread key is a dead key. Code identifiers (`src/index.tsx` and friends) are not copy — do not wrap them in `t()`.
38
+ 3. **Plugin identity comes only from declared fields in `plugin.json`** — declare whatever capability you need (`contributes` / `tabBehavior` / `icon` …). **Never make other people guess what your plugin is from a directory name or file location.**
39
+
40
+ ## Publishing
41
+
42
+ ```bash
43
+ npm run publish # create the GitHub Release + upload the .linkdesk-plugin + update the catalog
44
+ ```
45
+
46
+ The first publish needs a GitHub token (the command walks you through it once and stores it locally). To see what it would do without doing it: `npm run publish -- --dry-run`.
47
+
48
+ Publishing also requires this project to be **pushed to GitHub** (`publish` uses your project's `origin` to create the Release):
49
+
50
+ ```bash
51
+ git remote add origin git@github.com:<you>/<repo>.git
52
+ git push -u origin main
53
+ ```
54
+
55
+ > The scaffold already created this repository for you (`main` branch + one initial commit), so this step is only about wiring the remote.
56
+ > If you generated with `--no-git`, run `git init -b main` and commit first.
@@ -1,26 +1,26 @@
1
- {
2
- "name": "{{pluginName}}",
3
- "version": "0.1.0",
4
- "private": true,
5
- "description": "{{displayName}}——LinkDesk 插件(由 create-linkdesk-plugin 生成)",
6
- "type": "module",
7
- "scripts": {
8
- "dev": "linkdesk-plugin-sdk dev",
9
- "dev:real": "linkdesk-plugin-sdk dev --real",
10
- "build": "linkdesk-plugin-sdk build",
11
- "publish": "linkdesk-plugin-sdk publish",
12
- "validate": "linkdesk-plugin-sdk validate",
13
- "lint": "linkdesk-plugin-sdk lint",
14
- "verify": "node scripts/ci-verify.mjs",
15
- "test": "vitest run"
16
- },
17
- "devDependencies": {
18
- "@linkdesk/plugin-sdk": "^0.1.0",
19
- "@testing-library/react": "^16.3.2",
20
- "@types/react": "^18.3.12",
21
- "jsdom": "^29.1.1",
22
- "jsonc-parser": "^3.3.1",
23
- "typescript": "^5.6.3",
24
- "vitest": "^4.1.10"
25
- }
26
- }
1
+ {
2
+ "name": "{{pluginName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "{{displayName}}——LinkDesk 插件(由 create-linkdesk-plugin 生成)",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "linkdesk-plugin-sdk dev",
9
+ "dev:real": "linkdesk-plugin-sdk dev --real",
10
+ "build": "linkdesk-plugin-sdk build",
11
+ "publish": "linkdesk-plugin-sdk publish",
12
+ "validate": "linkdesk-plugin-sdk validate",
13
+ "lint": "linkdesk-plugin-sdk lint",
14
+ "verify": "node scripts/ci-verify.mjs",
15
+ "test": "vitest run"
16
+ },
17
+ "devDependencies": {
18
+ "@linkdesk/plugin-sdk": "^0.1.0",
19
+ "@testing-library/react": "^16.3.2",
20
+ "@types/react": "^18.3.12",
21
+ "jsdom": "^29.1.1",
22
+ "jsonc-parser": "^3.3.1",
23
+ "typescript": "^5.6.3",
24
+ "vitest": "^4.1.10"
25
+ }
26
+ }
@@ -1,41 +1,41 @@
1
- {
2
- // ─────────────────────────────────────────────────────────────────────────
3
- // LinkDesk 插件清单 plugin.json(由 create-linkdesk-plugin 生成)
4
- //
5
- // · 本文件是 JSONC:可写注释、可尾逗号(LinkDesk 与 SDK 都按 jsonc 解析,对标 VS Code package.json)。
6
- // · 完整字段以 plugin.schema.json 为准——VS Code 读 $schema 自动补全 + 校验。
7
- // · $schema 指向本工程 node_modules/@linkdesk/plugin-sdk 随包拷贝(npm install 后生效)。
8
- // ─────────────────────────────────────────────────────────────────────────
9
- "$schema": "./node_modules/@linkdesk/plugin-sdk/schemas/plugin.schema.json",
10
-
11
- // ── 插件身份 ──
12
- // 🔴 插件 ID:**显式声明**。它是安装目录名 / 卸载墓碑 / 更新对账的唯一键,**发布后永不可变**。
13
- // 它与插件所在的目录名**无关**(目录可以随便改)——所以别拿目录名当它的替身。
14
- "pluginId": "{{pluginName}}",
15
- "name": "{{displayName}}", // 显示名——标签页 / 插件详情等 UI 出现处
16
- "version": "0.1.0", // 语义化版本 x.y.z——市场更新比较靠它;+1 时务必同笔补 CHANGELOG.md 的新段
17
- "description": "{{displayName}}——我的第一个 LinkDesk 插件", // 一句话描述(插件详情页展示)
18
- "author": "{{author}}", // 作者名
19
- "icon": "resources/icon.svg", // 图标——图标栏 / 标签页 / 市场里显示的就是它(resources/icon.svg 是占位图,换成你的)
20
-
21
- // ── 入口(视图插件 = 此文件 default 导出一个 React 组件)──
22
- "entry": "src/index.tsx",
23
-
24
- // ── 出现位置:可作为主区标签页打开 ──
25
- "appearsIn": { "tabBar": true },
26
- "tabBehavior": { "singleton": true }, // 全局只开一个实例,避免重复标签
27
-
28
- // ── 贡献点(contributes:全部可选,按需增删)──
29
- "contributes": {
30
- // 自带翻译:key=语言码, value=相对插件根的 JSON 文件。UI 文案用 t() 读这里;无需 zh.json——中文 key 原文自带兜底。
31
- "i18n": { "en": "i18n/en.json" },
32
- // ── 需要「侧栏 / 底部面板 / 辅助侧栏」分区视图时:取消注释,在 src/views/ 放对应组件,
33
- // 容器 key 与 view id 用你的 pluginId 做前缀防撞(对标 plugins/panel-demo)──
34
- // "viewsContainers": { "{{pluginName}}-sidebar": { "title": "{{displayName}}", "location": "sidebar" } },
35
- // "views": {
36
- // "{{pluginName}}-sidebar": [
37
- // { "id": "main", "title": "{{displayName}}", "render": "src/views/MainView.tsx", "order": 0 }
38
- // ]
39
- // }
40
- },
41
- }
1
+ {
2
+ // ─────────────────────────────────────────────────────────────────────────
3
+ // LinkDesk 插件清单 plugin.json(由 create-linkdesk-plugin 生成)
4
+ //
5
+ // · 本文件是 JSONC:可写注释、可尾逗号(LinkDesk 与 SDK 都按 jsonc 解析,对标 VS Code package.json)。
6
+ // · 完整字段以 plugin.schema.json 为准——VS Code 读 $schema 自动补全 + 校验。
7
+ // · $schema 指向本工程 node_modules/@linkdesk/plugin-sdk 随包拷贝(npm install 后生效)。
8
+ // ─────────────────────────────────────────────────────────────────────────
9
+ "$schema": "./node_modules/@linkdesk/plugin-sdk/schemas/plugin.schema.json",
10
+
11
+ // ── 插件身份 ──
12
+ // 🔴 插件 ID:**显式声明**。它是安装目录名 / 卸载墓碑 / 更新对账的唯一键,**发布后永不可变**。
13
+ // 它与插件所在的目录名**无关**(目录可以随便改)——所以别拿目录名当它的替身。
14
+ "pluginId": "{{pluginName}}",
15
+ "name": "{{displayName}}", // 显示名——标签页 / 插件详情等 UI 出现处
16
+ "version": "0.1.0", // 语义化版本 x.y.z——市场更新比较靠它;+1 时务必同笔补 CHANGELOG.md 的新段
17
+ "description": "{{displayName}}——我的第一个 LinkDesk 插件", // 一句话描述(插件详情页展示)
18
+ "author": "{{author}}", // 作者名
19
+ "icon": "resources/icon.svg", // 图标——图标栏 / 标签页 / 市场里显示的就是它(resources/icon.svg 是占位图,换成你的)
20
+
21
+ // ── 入口(视图插件 = 此文件 default 导出一个 React 组件)──
22
+ "entry": "src/index.tsx",
23
+
24
+ // ── 出现位置:可作为主区标签页打开 ──
25
+ "appearsIn": { "tabBar": true },
26
+ "tabBehavior": { "singleton": true }, // 全局只开一个实例,避免重复标签
27
+
28
+ // ── 贡献点(contributes:全部可选,按需增删)──
29
+ "contributes": {
30
+ // 自带翻译:key=语言码, value=相对插件根的 JSON 文件。UI 文案用 t() 读这里;无需 zh.json——中文 key 原文自带兜底。
31
+ "i18n": { "en": "i18n/en.json" },
32
+ // ── 需要「侧栏 / 底部面板 / 辅助侧栏」分区视图时:取消注释,在 src/views/ 放对应组件,
33
+ // 容器 key 与 view id 用你的 pluginId 做前缀防撞(对标 plugins/panel-demo)──
34
+ // "viewsContainers": { "{{pluginName}}-sidebar": { "title": "{{displayName}}", "location": "sidebar" } },
35
+ // "views": {
36
+ // "{{pluginName}}-sidebar": [
37
+ // { "id": "main", "title": "{{displayName}}", "render": "src/views/MainView.tsx", "order": 0 }
38
+ // ]
39
+ // }
40
+ },
41
+ }
@@ -7,7 +7,7 @@
7
7
  * 搬走的一共六类检查:编译图 / eslint(含 linkdesk/* 自定义规则)/ vitest / 体量 / i18n / 主题审计。
8
8
  * 本脚本 + ci.yml + vitest 配置 = 给插件仓装回来的那一份,否则「独立」就是拿「质量真空」换的。
9
9
  *
10
- * ── 四段(每段独立判红;**没有对象也要说话**,不许静默绿)──
10
+ * ── 五段(每段独立判红;**没有对象也要说话**,不许静默绿)──
11
11
  * ① lint 严格腿 —— `@linkdesk/plugin-sdk` 的 eslint 规则腿 + css/font-scale/spacing 三条扫描腿。
12
12
  * 🔴 SDK 的 `npm run lint` 是 **WARN 级、永不 fail**(07 §六·三档:警告不是封锁,
13
13
  * 作者本地不被拦——那是刻意的)。CI 要的是**拦截**,所以本段把同一份报告按
@@ -24,6 +24,11 @@
24
24
  * 文件在;`contributes.themes` / `iconThemes` 的数据文件在且过各自的 schema;
25
25
  * 主题 recipe 引用的 `linkdesk://<id>/…` 资产在(且 id 就是本插件);floatingPanel
26
26
  * 三向自洽(viewId ↔ views[].id ↔ render)。
27
+ * ⑤ 目录条目形态 —— **发布产物** `marketplace.json` 里出现的图标字段(`icon` / `marketIcon`)必须是
28
+ * 绝对 URL + 来源标 `"url"`(E6#106)。理由:目录条目是**未装用户**看图时的唯一数据源,
29
+ * 而包内相对路径(`resources/icon.svg`)在未装态恒 404(`linkdesk://` 只在本地已装的
30
+ * 插件根里找文件)。`publish` 自 E6#106 起自动 URL 化;本段是那条纪律的机械兜底——
31
+ * 它看不见「谁是图标栏插件」(不看插件类型,只看字段形态,硬约束 10 零 ID 知识)。
27
32
  *
28
33
  * ── 为什么 ③ 的覆盖度只能黄灯(不是漏做)──
29
34
  * `t()` 的 key 可以合法地住在**应用级字典**里(`lang-defaults` 插件,运行时由它经 LanguageRegistry
@@ -32,7 +37,7 @@
32
37
  * 同款理由)。所以:字典**文件本身**的问题判红(③ 上半),**跨仓才能回答**的覆盖度只报告。
33
38
  *
34
39
  * 用法:node scripts/ci-verify.mjs (工程根 = cwd)
35
- * 退出码 0 = 四段全过;1 = 有红灯(逐条打印缺什么)
40
+ * 退出码 0 = 五段全过;1 = 有红灯(逐条打印缺什么)
36
41
  */
37
42
  import { existsSync, readFileSync, readdirSync } from "node:fs";
38
43
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
@@ -128,7 +133,7 @@ const legCount = (label) => report?.legs.find((l) => l.label === label)?.violati
128
133
  const allRows = report?.eslintRows ?? [];
129
134
  const ruleNotFound = allRows.filter((r) => RULE_NOT_FOUND_RE.test(r.message));
130
135
  const strictEslintRows = allRows.filter((r) => !RULE_NOT_FOUND_RE.test(r.message));
131
- /** 判红的三样:真 eslint 偏离 + css 硬编码腿(硬约束 1 的 .css 半边,eslint 到不了 .css)+ 见下 ②③④ */
136
+ /** 判红的三样:真 eslint 偏离 + css 硬编码腿(硬约束 1 的 .css 半边,eslint 到不了 .css)+ 见下 ②③④⑤ */
132
137
  const cssLegViolations = legCount("check-css-hardcode");
133
138
  const strictLintViolations = strictEslintRows.length + cssLegViolations;
134
139
  /** 只报告不拦的两条腿:字号度量与 4px 节奏——属「审美校准」(SDK 07 §六),存量偏离多且修它们要动插件源码 */
@@ -427,6 +432,60 @@ if (!manifest) {
427
432
  }
428
433
  if (themeAssetRefs.size > 0) ok.push(`${themeAssetRefs.size} 处 linkdesk:// 资产引用`);
429
434
 
435
+ /* ⑤ E6#106:目录条目的图标字段必须是**未装态可解析**的形态(绝对 URL)。
436
+ *
437
+ * 为什么这条能是纯字段断言、不需要知道「谁是图标栏插件」:无论哪种插件,**未装用户**看市场行时
438
+ * 目录条目是唯一数据源,而包内相对路径(`resources/icon.svg`)在未装态恒 404——「目录里存相对路径」
439
+ * 这件事本身就不成立,与插件类型无关。故断言只取形态,零插件 ID 知识(硬约束 10)。
440
+ *
441
+ * 为什么归 CI 而不是壳仓门禁:条目是**各仓自己的产物**,壳仓的 `npm run check` 够不着别人的仓。
442
+ * 与 ④ 段其它腿不同,本腿的对象是 `marketplace.json`(发布产物)——未发布过(无该文件)即跳过。 */
443
+ const catalogPath = resolve(ROOT, "marketplace.json");
444
+ if (!existsSync(catalogPath)) {
445
+ line(" ℹ ⑤ 目录条目图标形态:本仓无 marketplace.json(尚未发布过)——跳过。");
446
+ } else {
447
+ const catProblems = [];
448
+ const catOk = [];
449
+ try {
450
+ const cat = JSON.parse(readFileSync(catalogPath, "utf8"));
451
+ const entries = Array.isArray(cat) ? cat : (cat.plugins ?? []);
452
+ for (const e of entries) {
453
+ if (!e || typeof e !== "object") continue;
454
+ for (const key of ["icon", "marketIcon"]) {
455
+ const v = e[key];
456
+ if (v === undefined) continue;
457
+ if (/^https?:\/\//i.test(v)) {
458
+ // 形态对:绝对 URL。再钉一句「来源必须显式标 url」——消费端 resolvePluginIcon 对
459
+ // 「无 source 的绝对 URL」会当包内路径拼出 linkdesk://(两处判据不同源就会出这种错)。
460
+ const srcKey = key === "icon" ? "iconSource" : "marketIconSource";
461
+ if (e[srcKey] !== "url") {
462
+ catProblems.push(
463
+ `${key} 是绝对 URL 但 ${srcKey} ≠ "url"(现为 ${JSON.stringify(e[srcKey])})——` +
464
+ `未装端会把它当包内路径拼 linkdesk://`,
465
+ );
466
+ } else {
467
+ catOk.push(key);
468
+ }
469
+ } else {
470
+ catProblems.push(
471
+ `${key} = ${JSON.stringify(v)} 是**包内相对路径**——目录条目是未装用户的唯一图源,` +
472
+ `相对路径在未装态恒 404。跑 \`npm run publish\` 让 SDK 自动转绝对 URL(E6#106)。`,
473
+ );
474
+ }
475
+ }
476
+ }
477
+ } catch (err) {
478
+ catProblems.push(`marketplace.json 解析失败:${err instanceof Error ? err.message : String(err)}`);
479
+ }
480
+ if (catProblems.length > 0) {
481
+ fail(`⑤ 目录条目图标形态:${catProblems.length} 处不可达/不合规\n${catProblems.map((p) => ` · ${p}`).join("\n")}`);
482
+ } else {
483
+ line(
484
+ ` ✅ ⑤ 目录条目图标形态:${catOk.length > 0 ? `${catOk.join("、")} 均为绝对 URL(未装态可达)` : "本仓条目未声明图标(零图可发,允许)"}`,
485
+ );
486
+ }
487
+ }
488
+
430
489
  // floatingPanel 声明自洽(壳侧 floatingPanelDeclarers.test.ts 的仓内等价物)
431
490
  const fp = manifest.contributes?.floatingPanel;
432
491
  if (fp && typeof fp === "object") {
@@ -479,5 +538,5 @@ if (failures.length > 0) {
479
538
  console.error(` eslint-disable 注释 + 理由(见上面报告尾部),别把检查删了。`);
480
539
  process.exitCode = 1;
481
540
  } else {
482
- console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 / 声明自洽四段。`);
541
+ console.log(`✅ 插件仓自检全过(${pluginId})——lint / 跨插件 / 字典 / 声明自洽 / 目录条目形态五段。`);
483
542
  }