@vsceasy/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +9044 -0
- package/dist/cli.d.ts +3 -0
- package/dist/commands/command/add.d.ts +3 -0
- package/dist/commands/components/add.d.ts +3 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/crud/add.d.ts +3 -0
- package/dist/commands/db/init.d.ts +3 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/groups.d.ts +16 -0
- package/dist/commands/helper/add.d.ts +3 -0
- package/dist/commands/job/add.d.ts +3 -0
- package/dist/commands/menu/add.d.ts +3 -0
- package/dist/commands/menu/edit.d.ts +3 -0
- package/dist/commands/model/add.d.ts +3 -0
- package/dist/commands/panel/add.d.ts +3 -0
- package/dist/commands/publish/init.d.ts +3 -0
- package/dist/commands/rpc/add.d.ts +3 -0
- package/dist/commands/statusBar/add.d.ts +3 -0
- package/dist/commands/subpanel/add.d.ts +3 -0
- package/dist/commands/test/setup.d.ts +3 -0
- package/dist/commands/treeView/add.d.ts +3 -0
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/wizard.d.ts +3 -0
- package/dist/data/codicons.d.ts +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3169 -0
- package/dist/lib/command/add.d.ts +31 -0
- package/dist/lib/components/add.d.ts +20 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/crud/add.d.ts +19 -0
- package/dist/lib/crud/crudConfig.d.ts +37 -0
- package/dist/lib/crud/parseModel.d.ts +33 -0
- package/dist/lib/db/init.d.ts +16 -0
- package/dist/lib/db/wire.d.ts +10 -0
- package/dist/lib/doctor.d.ts +30 -0
- package/dist/lib/findProject.d.ts +10 -0
- package/dist/lib/helper/add.d.ts +14 -0
- package/dist/lib/iconPicker.d.ts +7 -0
- package/dist/lib/index.d.ts +46 -0
- package/dist/lib/interactive.d.ts +30 -0
- package/dist/lib/job/add.d.ts +24 -0
- package/dist/lib/menu/add.d.ts +13 -0
- package/dist/lib/menu/edit.d.ts +39 -0
- package/dist/lib/menuTree.d.ts +33 -0
- package/dist/lib/model/add.d.ts +27 -0
- package/dist/lib/model/parseFields.d.ts +14 -0
- package/dist/lib/panel/add.d.ts +29 -0
- package/dist/lib/publish/init.d.ts +12 -0
- package/dist/lib/rpc/add.d.ts +22 -0
- package/dist/lib/scaffold.d.ts +13 -0
- package/dist/lib/statusBar/add.d.ts +33 -0
- package/dist/lib/subpanel/add.d.ts +20 -0
- package/dist/lib/testSetup/index.d.ts +10 -0
- package/dist/lib/treeView/add.d.ts +13 -0
- package/dist/lib/upgrade.d.ts +22 -0
- package/dist/lib/validate.d.ts +14 -0
- package/dist/lib/wizard/run.d.ts +13 -0
- package/package.json +67 -0
- package/templates/_generators/command/command.ts.tpl +8 -0
- package/templates/_generators/components/Button.tsx.tpl +12 -0
- package/templates/_generators/components/Card.tsx.tpl +22 -0
- package/templates/_generators/components/Field.tsx.tpl +20 -0
- package/templates/_generators/components/Input.tsx.tpl +10 -0
- package/templates/_generators/components/List.tsx.tpl +29 -0
- package/templates/_generators/components/components.css.tpl +66 -0
- package/templates/_generators/components/index.ts.tpl +10 -0
- package/templates/_generators/crud/formApp.tsx.tpl +83 -0
- package/templates/_generators/crud/formNav.ts.tpl +19 -0
- package/templates/_generators/crud/formPanel.ts.tpl +32 -0
- package/templates/_generators/crud/listApp.tsx.tpl +84 -0
- package/templates/_generators/crud/listPanel.ts.tpl +30 -0
- package/templates/_generators/crud/main.tsx.tpl +6 -0
- package/templates/_generators/crud/service.ts.tpl +27 -0
- package/templates/_generators/helper/cache.ts.tpl +117 -0
- package/templates/_generators/helper/config.ts.tpl +36 -0
- package/templates/_generators/helper/db.ts.tpl +322 -0
- package/templates/_generators/helper/notifications.ts.tpl +45 -0
- package/templates/_generators/helper/secrets.ts.tpl +36 -0
- package/templates/_generators/helper/state.ts.tpl +44 -0
- package/templates/_generators/job/job.ts.tpl +10 -0
- package/templates/_generators/menu/menu.ts.tpl +21 -0
- package/templates/_generators/model/model.ts.tpl +17 -0
- package/templates/_generators/panel/App.tsx.tpl +10 -0
- package/templates/_generators/panel/main.tsx.tpl +6 -0
- package/templates/_generators/panel/panel.ts.tpl +5 -0
- package/templates/_generators/panel/templates/dashboard/App.tsx.tpl +41 -0
- package/templates/_generators/panel/templates/form/App.tsx.tpl +44 -0
- package/templates/_generators/panel/templates/list/App.tsx.tpl +40 -0
- package/templates/_generators/publish/CHANGELOG.md.tpl +8 -0
- package/templates/_generators/publish/README.md.tpl +23 -0
- package/templates/_generators/statusBar/statusBar.ts.tpl +7 -0
- package/templates/_generators/subpanel/App.tsx.tpl +10 -0
- package/templates/_generators/subpanel/main.tsx.tpl +6 -0
- package/templates/_generators/subpanel/subpanel.ts.tpl +6 -0
- package/templates/_generators/test/_helpers.ts.tpl +120 -0
- package/templates/_generators/test/sample.test.ts.tpl +38 -0
- package/templates/_generators/test/vitest.config.ts.tpl +23 -0
- package/templates/_generators/test/vscode.stub.ts.tpl +109 -0
- package/templates/_generators/treeView/treeView.ts.tpl +16 -0
- package/templates/react/.vscode/launch.json +34 -0
- package/templates/react/.vscode/tasks.json +32 -0
- package/templates/react/.vscodeignore +8 -0
- package/templates/react/README.md +50 -0
- package/templates/react/package.json +54 -0
- package/templates/react/scripts/gen.ts +395 -0
- package/templates/react/src/commands/hello.ts +6 -0
- package/templates/react/src/extension/extension.ts +5 -0
- package/templates/react/src/panels/dashboard.ts +21 -0
- package/templates/react/src/shared/api.ts +7 -0
- package/templates/react/src/shared/vsceasy/bootstrap.ts +657 -0
- package/templates/react/src/shared/vsceasy/client.ts +8 -0
- package/templates/react/src/shared/vsceasy/codiconNames.ts +196 -0
- package/templates/react/src/shared/vsceasy/define.ts +269 -0
- package/templates/react/src/shared/vsceasy/index.ts +13 -0
- package/templates/react/src/shared/vsceasy/rpc.ts +214 -0
- package/templates/react/src/webview/panels/dashboard/App.tsx +31 -0
- package/templates/react/src/webview/panels/dashboard/main.tsx +6 -0
- package/templates/react/src/webview/styles.css +33 -0
- package/templates/react/tsconfig.json +17 -0
- package/templates/react/vite.config.ts +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3169 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
8
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
9
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
10
|
+
for (let key of __getOwnPropNames(mod))
|
|
11
|
+
if (!__hasOwnProp.call(to, key))
|
|
12
|
+
__defProp(to, key, {
|
|
13
|
+
get: () => mod[key],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
19
|
+
var __toCommonJS = (from) => {
|
|
20
|
+
var entry = __moduleCache.get(from), desc;
|
|
21
|
+
if (entry)
|
|
22
|
+
return entry;
|
|
23
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function")
|
|
25
|
+
__getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
|
|
26
|
+
get: () => from[key],
|
|
27
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
28
|
+
}));
|
|
29
|
+
__moduleCache.set(from, entry);
|
|
30
|
+
return entry;
|
|
31
|
+
};
|
|
32
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
33
|
+
var __export = (target, all) => {
|
|
34
|
+
for (var name in all)
|
|
35
|
+
__defProp(target, name, {
|
|
36
|
+
get: all[name],
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
set: (newValue) => all[name] = () => newValue
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
43
|
+
|
|
44
|
+
// src/lib/config.ts
|
|
45
|
+
function configPath(projectRoot) {
|
|
46
|
+
for (const f of CONFIG_FILES) {
|
|
47
|
+
const p = path.join(projectRoot, f);
|
|
48
|
+
if (fs.existsSync(p))
|
|
49
|
+
return p;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function readConfig(projectRoot) {
|
|
54
|
+
const p = configPath(projectRoot);
|
|
55
|
+
if (!p)
|
|
56
|
+
return {};
|
|
57
|
+
try {
|
|
58
|
+
if (p.endsWith(".json")) {
|
|
59
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
const src = fs.readFileSync(p, "utf8");
|
|
62
|
+
return parseExportDefault(src);
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function parseExportDefault(src) {
|
|
68
|
+
let literal = extractBracedLiteral(src, /export\s+default\s*\{/);
|
|
69
|
+
if (!literal) {
|
|
70
|
+
literal = extractBracedLiteral(src, /(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*(?::\s*[^=]+)?=\s*\{/);
|
|
71
|
+
}
|
|
72
|
+
if (!literal)
|
|
73
|
+
return {};
|
|
74
|
+
try {
|
|
75
|
+
const jsonish = literal.replace(/\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1").replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":').replace(/'/g, '"');
|
|
76
|
+
return JSON.parse(jsonish);
|
|
77
|
+
} catch {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function extractBracedLiteral(src, head) {
|
|
82
|
+
const m = head.exec(src);
|
|
83
|
+
if (!m)
|
|
84
|
+
return null;
|
|
85
|
+
const openIdx = m.index + m[0].length - 1;
|
|
86
|
+
let depth = 0;
|
|
87
|
+
for (let i = openIdx;i < src.length; i++) {
|
|
88
|
+
const c = src[i];
|
|
89
|
+
if (c === "{")
|
|
90
|
+
depth++;
|
|
91
|
+
else if (c === "}") {
|
|
92
|
+
depth--;
|
|
93
|
+
if (depth === 0)
|
|
94
|
+
return src.slice(openIdx, i + 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function writeConfig(projectRoot, cfg) {
|
|
100
|
+
const target = path.join(projectRoot, "vsceasy.config.ts");
|
|
101
|
+
const body = `import type { VsceasyConfig } from 'vsceasy';
|
|
102
|
+
|
|
103
|
+
const config: VsceasyConfig = ${stringify(cfg)};
|
|
104
|
+
|
|
105
|
+
export default config;
|
|
106
|
+
`;
|
|
107
|
+
fs.writeFileSync(target, body);
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
function stringify(obj) {
|
|
111
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== undefined);
|
|
112
|
+
if (entries.length === 0)
|
|
113
|
+
return "{}";
|
|
114
|
+
const lines = entries.map(([k, v]) => ` ${k}: ${JSON.stringify(v)},`);
|
|
115
|
+
return `{
|
|
116
|
+
${lines.join(`
|
|
117
|
+
`)}
|
|
118
|
+
}`;
|
|
119
|
+
}
|
|
120
|
+
var fs, path, CONFIG_FILES;
|
|
121
|
+
var init_config = __esm(() => {
|
|
122
|
+
fs = __toESM(require("fs"));
|
|
123
|
+
path = __toESM(require("path"));
|
|
124
|
+
CONFIG_FILES = ["vsceasy.config.ts", "vsceasy.config.js", "vsceasy.config.json"];
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/lib/scaffold.ts
|
|
128
|
+
async function scaffold(opts) {
|
|
129
|
+
const src = path2.join(opts.templatesRoot, opts.ui);
|
|
130
|
+
if (!fs2.existsSync(src)) {
|
|
131
|
+
throw new Error(`Template not found: ${src}`);
|
|
132
|
+
}
|
|
133
|
+
if (fs2.existsSync(opts.targetDir) && fs2.readdirSync(opts.targetDir).length > 0) {
|
|
134
|
+
throw new Error(`Target directory not empty: ${opts.targetDir}`);
|
|
135
|
+
}
|
|
136
|
+
fs2.mkdirSync(opts.targetDir, { recursive: true });
|
|
137
|
+
const vars = buildVars(opts);
|
|
138
|
+
await copyTree(src, opts.targetDir, vars);
|
|
139
|
+
applyPreset(opts.targetDir, opts.preset ?? "full");
|
|
140
|
+
writeConfig(opts.targetDir, {
|
|
141
|
+
publisher: opts.publisher,
|
|
142
|
+
commandPrefix: vars.commandPrefix,
|
|
143
|
+
ui: opts.ui
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function applyPreset(targetDir, preset) {
|
|
147
|
+
if (preset === "full")
|
|
148
|
+
return;
|
|
149
|
+
const removals = [
|
|
150
|
+
"src/panels/dashboard.ts",
|
|
151
|
+
"src/webview/panels/dashboard"
|
|
152
|
+
];
|
|
153
|
+
for (const rel of removals) {
|
|
154
|
+
const abs = path2.join(targetDir, rel);
|
|
155
|
+
if (fs2.existsSync(abs))
|
|
156
|
+
fs2.rmSync(abs, { recursive: true, force: true });
|
|
157
|
+
}
|
|
158
|
+
const apiPath = path2.join(targetDir, "src", "shared", "api.ts");
|
|
159
|
+
if (fs2.existsSync(apiPath)) {
|
|
160
|
+
fs2.writeFileSync(apiPath, `// RPC contracts go here. One interface per panel/subpanel.
|
|
161
|
+
// Example:
|
|
162
|
+
// export interface DashboardApi {
|
|
163
|
+
// ping(): Promise<string>;
|
|
164
|
+
// }
|
|
165
|
+
`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function buildVars(opts) {
|
|
169
|
+
const simpleName = opts.name.replace(/^@[^/]+\//, "");
|
|
170
|
+
const commandPrefix = simpleName.replace(/[^a-zA-Z0-9]+/g, "");
|
|
171
|
+
return {
|
|
172
|
+
name: opts.name,
|
|
173
|
+
displayName: opts.displayName,
|
|
174
|
+
description: opts.description,
|
|
175
|
+
publisher: opts.publisher,
|
|
176
|
+
commandPrefix
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function copyTree(srcDir, destDir, vars) {
|
|
180
|
+
for (const entry of fs2.readdirSync(srcDir, { withFileTypes: true })) {
|
|
181
|
+
if (SKIP_NAMES.has(entry.name))
|
|
182
|
+
continue;
|
|
183
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
184
|
+
const destPath = path2.join(destDir, entry.name);
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
fs2.mkdirSync(destPath, { recursive: true });
|
|
187
|
+
await copyTree(srcPath, destPath, vars);
|
|
188
|
+
} else if (entry.isFile()) {
|
|
189
|
+
const ext = path2.extname(entry.name);
|
|
190
|
+
if (PLACEHOLDER_EXTS.has(ext) || entry.name.startsWith(".")) {
|
|
191
|
+
const content = fs2.readFileSync(srcPath, "utf8");
|
|
192
|
+
fs2.writeFileSync(destPath, substitute(content, vars));
|
|
193
|
+
} else {
|
|
194
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function substitute(input, vars) {
|
|
200
|
+
return input.replace(/\{\{(\w+)\}\}/g, (_m, key) => vars[key] ?? `{{${key}}}`);
|
|
201
|
+
}
|
|
202
|
+
var fs2, path2, PLACEHOLDER_EXTS, SKIP_NAMES;
|
|
203
|
+
var init_scaffold = __esm(() => {
|
|
204
|
+
init_config();
|
|
205
|
+
fs2 = __toESM(require("fs"));
|
|
206
|
+
path2 = __toESM(require("path"));
|
|
207
|
+
PLACEHOLDER_EXTS = new Set([
|
|
208
|
+
".ts",
|
|
209
|
+
".tsx",
|
|
210
|
+
".js",
|
|
211
|
+
".jsx",
|
|
212
|
+
".json",
|
|
213
|
+
".md",
|
|
214
|
+
".html",
|
|
215
|
+
".css",
|
|
216
|
+
".cjs",
|
|
217
|
+
".mjs",
|
|
218
|
+
".yml",
|
|
219
|
+
".yaml"
|
|
220
|
+
]);
|
|
221
|
+
SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// src/lib/validate.ts
|
|
225
|
+
function assertId(field, value) {
|
|
226
|
+
const trimmed = (value ?? "").trim();
|
|
227
|
+
if (!trimmed)
|
|
228
|
+
throw new Error(`${field} is required.`);
|
|
229
|
+
if (!ID_RE.test(trimmed)) {
|
|
230
|
+
throw new Error(`Invalid ${field}: "${trimmed}". Use camelCase starting with a lowercase letter (e.g. \`myPanel\`, \`doStuff\`).`);
|
|
231
|
+
}
|
|
232
|
+
return trimmed;
|
|
233
|
+
}
|
|
234
|
+
function assertNoOverwrite(projectRoot, target, kind) {
|
|
235
|
+
if (fs3.existsSync(target)) {
|
|
236
|
+
throw new Error(`${kind} already exists at \`${path3.relative(projectRoot, target)}\`. Pick a different name or delete the file first.`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function assertSiblingExists(projectRoot, kind, id) {
|
|
240
|
+
const dir = `${kind}s`;
|
|
241
|
+
const dirPath = path3.join(projectRoot, "src", dir);
|
|
242
|
+
const candidates = [
|
|
243
|
+
path3.join(dirPath, `${id}.ts`),
|
|
244
|
+
path3.join(dirPath, `${id}.tsx`)
|
|
245
|
+
];
|
|
246
|
+
for (const p of candidates)
|
|
247
|
+
if (fs3.existsSync(p))
|
|
248
|
+
return p;
|
|
249
|
+
const existing = listSiblings(dirPath);
|
|
250
|
+
const hint = existing.length ? ` Available ${kind}s: ${existing.map((n) => `"${n}"`).join(", ")}.` : ` No ${kind}s exist yet — run \`vsceasy ${kind} add --name <id>\` first.`;
|
|
251
|
+
throw new Error(`${kind} "${id}" not found at \`src/${dir}/${id}.ts\`.${hint}`);
|
|
252
|
+
}
|
|
253
|
+
function listSiblings(dirPath) {
|
|
254
|
+
if (!fs3.existsSync(dirPath))
|
|
255
|
+
return [];
|
|
256
|
+
return fs3.readdirSync(dirPath, { withFileTypes: true }).filter((e) => e.isFile() && /\.(ts|tsx)$/.test(e.name) && !e.name.startsWith("_")).map((e) => e.name.replace(/\.(ts|tsx)$/, "")).sort();
|
|
257
|
+
}
|
|
258
|
+
var fs3, path3, ID_RE;
|
|
259
|
+
var init_validate = __esm(() => {
|
|
260
|
+
fs3 = __toESM(require("fs"));
|
|
261
|
+
path3 = __toESM(require("path"));
|
|
262
|
+
ID_RE = /^[a-z][a-zA-Z0-9]*$/;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// src/lib/components/add.ts
|
|
266
|
+
function addComponents(opts) {
|
|
267
|
+
const genDir = path4.join(opts.templatesRoot, "_generators", "components");
|
|
268
|
+
const outDir = path4.join(opts.projectRoot, "src", "webview", "components");
|
|
269
|
+
fs4.mkdirSync(outDir, { recursive: true });
|
|
270
|
+
const created = [];
|
|
271
|
+
const skipped = [];
|
|
272
|
+
for (const file of COMPONENT_FILES) {
|
|
273
|
+
const src = path4.join(genDir, `${file}.tpl`);
|
|
274
|
+
const dest = path4.join(outDir, file);
|
|
275
|
+
if (fs4.existsSync(dest) && !opts.force) {
|
|
276
|
+
skipped.push(dest);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
fs4.writeFileSync(dest, fs4.readFileSync(src, "utf8"));
|
|
280
|
+
created.push(dest);
|
|
281
|
+
}
|
|
282
|
+
return { created, skipped, dir: outDir };
|
|
283
|
+
}
|
|
284
|
+
function componentsExist(projectRoot) {
|
|
285
|
+
return fs4.existsSync(path4.join(projectRoot, "src", "webview", "components", "index.ts"));
|
|
286
|
+
}
|
|
287
|
+
var fs4, path4, COMPONENT_FILES;
|
|
288
|
+
var init_add = __esm(() => {
|
|
289
|
+
fs4 = __toESM(require("fs"));
|
|
290
|
+
path4 = __toESM(require("path"));
|
|
291
|
+
COMPONENT_FILES = [
|
|
292
|
+
"Button.tsx",
|
|
293
|
+
"Input.tsx",
|
|
294
|
+
"Field.tsx",
|
|
295
|
+
"Card.tsx",
|
|
296
|
+
"List.tsx",
|
|
297
|
+
"index.ts",
|
|
298
|
+
"components.css"
|
|
299
|
+
];
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// src/lib/panel/add.ts
|
|
303
|
+
function addPanel(opts) {
|
|
304
|
+
const name = assertId("panel name", normalizeCamel(opts.name));
|
|
305
|
+
const Pascal = name.charAt(0).toUpperCase() + name.slice(1);
|
|
306
|
+
const title = opts.title ?? Pascal;
|
|
307
|
+
const template = opts.template ?? "blank";
|
|
308
|
+
if (!PANEL_TEMPLATES.includes(template)) {
|
|
309
|
+
throw new Error(`Unknown panel template "${template}". Available: ${PANEL_TEMPLATES.join(", ")}.`);
|
|
310
|
+
}
|
|
311
|
+
const withApi = template !== "blank" ? true : opts.withApi !== false;
|
|
312
|
+
const apiName = `${Pascal}Api`;
|
|
313
|
+
const panelTs = path5.join(opts.projectRoot, "src", "panels", `${name}.ts`);
|
|
314
|
+
const uiDir = path5.join(opts.projectRoot, "src", "webview", "panels", name);
|
|
315
|
+
const appTsx = path5.join(uiDir, "App.tsx");
|
|
316
|
+
const mainTsx = path5.join(uiDir, "main.tsx");
|
|
317
|
+
const apiPath = path5.join(opts.projectRoot, "src", "shared", "api.ts");
|
|
318
|
+
assertNoOverwrite(opts.projectRoot, panelTs, "Panel");
|
|
319
|
+
assertNoOverwrite(opts.projectRoot, uiDir, "Webview dir");
|
|
320
|
+
const genDir = path5.join(opts.templatesRoot, "_generators", "panel");
|
|
321
|
+
const readTpl = (n) => fs5.readFileSync(path5.join(genDir, n), "utf8");
|
|
322
|
+
const vars = {
|
|
323
|
+
name,
|
|
324
|
+
Name: Pascal,
|
|
325
|
+
title,
|
|
326
|
+
api: withApi ? apiName : "",
|
|
327
|
+
apiImport: withApi ? `import type { ${apiName} } from '../shared/api';
|
|
328
|
+
` : "",
|
|
329
|
+
apiGeneric: withApi ? `<${apiName}>` : "",
|
|
330
|
+
rpcBlock: withApi ? `
|
|
331
|
+
rpc: (vscode) => ({
|
|
332
|
+
// add RPC handlers here
|
|
333
|
+
}),` : "",
|
|
334
|
+
apiBlock: withApi ? `import { connectWebview } from '../../../shared/vsceasy/client';
|
|
335
|
+
import type { ${apiName} } from '../../../shared/api';
|
|
336
|
+
|
|
337
|
+
const api = connectWebview<${apiName}>();
|
|
338
|
+
void api;
|
|
339
|
+
` : ""
|
|
340
|
+
};
|
|
341
|
+
const created = [];
|
|
342
|
+
const modified = [];
|
|
343
|
+
const skipped = [];
|
|
344
|
+
if (template !== "blank" && !componentsExist(opts.projectRoot)) {
|
|
345
|
+
const comp = addComponents({ projectRoot: opts.projectRoot, templatesRoot: opts.templatesRoot });
|
|
346
|
+
created.push(...comp.created);
|
|
347
|
+
}
|
|
348
|
+
fs5.mkdirSync(path5.dirname(panelTs), { recursive: true });
|
|
349
|
+
fs5.writeFileSync(panelTs, substitute(readTpl("panel.ts.tpl"), vars));
|
|
350
|
+
created.push(panelTs);
|
|
351
|
+
fs5.mkdirSync(uiDir, { recursive: true });
|
|
352
|
+
const appSrc = template === "blank" ? readTpl("App.tsx.tpl") : fs5.readFileSync(path5.join(genDir, "templates", template, "App.tsx.tpl"), "utf8");
|
|
353
|
+
fs5.writeFileSync(appTsx, substitute(appSrc, { ...vars, ...TEMPLATE_VARS[template] }));
|
|
354
|
+
created.push(appTsx);
|
|
355
|
+
fs5.writeFileSync(mainTsx, substitute(readTpl("main.tsx.tpl"), vars));
|
|
356
|
+
created.push(mainTsx);
|
|
357
|
+
if (withApi) {
|
|
358
|
+
appendApi(apiPath, apiName, API_BODY[template], created, modified, skipped);
|
|
359
|
+
}
|
|
360
|
+
let genRan = false;
|
|
361
|
+
if (opts.runGen !== false) {
|
|
362
|
+
genRan = runGen(opts.projectRoot);
|
|
363
|
+
}
|
|
364
|
+
return { created, modified, skipped, genRan };
|
|
365
|
+
}
|
|
366
|
+
function appendApi(apiPath, apiName, body, created, modified, skipped) {
|
|
367
|
+
const decl = `export interface ${apiName} {${body}}
|
|
368
|
+
`;
|
|
369
|
+
if (!fs5.existsSync(apiPath)) {
|
|
370
|
+
fs5.mkdirSync(path5.dirname(apiPath), { recursive: true });
|
|
371
|
+
fs5.writeFileSync(apiPath, decl);
|
|
372
|
+
created.push(apiPath);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const current = fs5.readFileSync(apiPath, "utf8");
|
|
376
|
+
const re = new RegExp(`\\bexport\\s+interface\\s+${apiName}\\b`);
|
|
377
|
+
if (re.test(current)) {
|
|
378
|
+
skipped.push(`${apiPath} (interface ${apiName} already declared)`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const sep = current.endsWith(`
|
|
382
|
+
`) ? `
|
|
383
|
+
` : `
|
|
384
|
+
|
|
385
|
+
`;
|
|
386
|
+
fs5.writeFileSync(apiPath, current + sep + decl);
|
|
387
|
+
modified.push(apiPath);
|
|
388
|
+
}
|
|
389
|
+
function runGen(cwd) {
|
|
390
|
+
const tryRun = (cmd, args) => {
|
|
391
|
+
const r = import_child_process.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
392
|
+
return r.status === 0;
|
|
393
|
+
};
|
|
394
|
+
if (which("bun") && tryRun("bun", ["run", "gen"]))
|
|
395
|
+
return true;
|
|
396
|
+
if (which("npm") && tryRun("npm", ["run", "gen"]))
|
|
397
|
+
return true;
|
|
398
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` (or `npm run gen`) manually to wire up the new panel.\n');
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
function which(cmd) {
|
|
402
|
+
const r = import_child_process.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
403
|
+
return r.status === 0;
|
|
404
|
+
}
|
|
405
|
+
function normalizeCamel(s) {
|
|
406
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
407
|
+
if (!cleaned)
|
|
408
|
+
return "";
|
|
409
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
410
|
+
}
|
|
411
|
+
var fs5, path5, import_child_process, PANEL_TEMPLATES, API_BODY, TEMPLATE_VARS;
|
|
412
|
+
var init_add2 = __esm(() => {
|
|
413
|
+
init_scaffold();
|
|
414
|
+
init_validate();
|
|
415
|
+
init_add();
|
|
416
|
+
fs5 = __toESM(require("fs"));
|
|
417
|
+
path5 = __toESM(require("path"));
|
|
418
|
+
import_child_process = require("child_process");
|
|
419
|
+
PANEL_TEMPLATES = ["blank", "form", "list", "dashboard"];
|
|
420
|
+
API_BODY = {
|
|
421
|
+
blank: "",
|
|
422
|
+
form: `
|
|
423
|
+
save(input: { name: string; email: string }): Promise<void>;
|
|
424
|
+
`,
|
|
425
|
+
list: `
|
|
426
|
+
list(): Promise<{ id: string; label: string }[]>;
|
|
427
|
+
`,
|
|
428
|
+
dashboard: `
|
|
429
|
+
stats(): Promise<{ total: number; active: number; updatedAt: string }>;
|
|
430
|
+
`
|
|
431
|
+
};
|
|
432
|
+
TEMPLATE_VARS = {
|
|
433
|
+
blank: {},
|
|
434
|
+
form: { submitCall: "await api.save({ name, email });" },
|
|
435
|
+
list: { loadCall: "setRows(await api.list());" },
|
|
436
|
+
dashboard: { statsCall: "setStats(await api.stats());" }
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// src/lib/menu/add.ts
|
|
441
|
+
function addMenu(opts) {
|
|
442
|
+
const name = assertId("menu name", normalizeCamel2(opts.name));
|
|
443
|
+
const Pascal = name.charAt(0).toUpperCase() + name.slice(1);
|
|
444
|
+
const title = opts.title ?? Pascal;
|
|
445
|
+
const cfg = readConfig(opts.projectRoot);
|
|
446
|
+
const icon = opts.icon ?? cfg.defaultIcon ?? "symbol-misc";
|
|
447
|
+
const menuTs = path6.join(opts.projectRoot, "src", "menus", `${name}.ts`);
|
|
448
|
+
assertNoOverwrite(opts.projectRoot, menuTs, "Menu");
|
|
449
|
+
const tplPath = path6.join(opts.templatesRoot, "_generators", "menu", "menu.ts.tpl");
|
|
450
|
+
const body = substitute(fs6.readFileSync(tplPath, "utf8"), { name, Name: Pascal, title, icon });
|
|
451
|
+
fs6.mkdirSync(path6.dirname(menuTs), { recursive: true });
|
|
452
|
+
fs6.writeFileSync(menuTs, body);
|
|
453
|
+
const created = [menuTs];
|
|
454
|
+
let genRan = false;
|
|
455
|
+
if (opts.runGen !== false)
|
|
456
|
+
genRan = runGen2(opts.projectRoot);
|
|
457
|
+
return { created, genRan };
|
|
458
|
+
}
|
|
459
|
+
function runGen2(cwd) {
|
|
460
|
+
const tryRun = (cmd, args) => {
|
|
461
|
+
const r = import_child_process2.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
462
|
+
return r.status === 0;
|
|
463
|
+
};
|
|
464
|
+
if (which2("bun") && tryRun("bun", ["run", "gen"]))
|
|
465
|
+
return true;
|
|
466
|
+
if (which2("npm") && tryRun("npm", ["run", "gen"]))
|
|
467
|
+
return true;
|
|
468
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
function which2(cmd) {
|
|
472
|
+
const r = import_child_process2.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
473
|
+
return r.status === 0;
|
|
474
|
+
}
|
|
475
|
+
function normalizeCamel2(s) {
|
|
476
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
477
|
+
if (!cleaned)
|
|
478
|
+
return "";
|
|
479
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
480
|
+
}
|
|
481
|
+
var fs6, path6, import_child_process2;
|
|
482
|
+
var init_add3 = __esm(() => {
|
|
483
|
+
init_scaffold();
|
|
484
|
+
init_validate();
|
|
485
|
+
init_config();
|
|
486
|
+
fs6 = __toESM(require("fs"));
|
|
487
|
+
path6 = __toESM(require("path"));
|
|
488
|
+
import_child_process2 = require("child_process");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// src/lib/menu/edit.ts
|
|
492
|
+
function listMenus(projectRoot) {
|
|
493
|
+
const dir = path7.join(projectRoot, "src", "menus");
|
|
494
|
+
if (!fs7.existsSync(dir))
|
|
495
|
+
return [];
|
|
496
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".ts")).map((f) => f.replace(/\.ts$/, "")).sort();
|
|
497
|
+
}
|
|
498
|
+
function listPanels(projectRoot) {
|
|
499
|
+
const dir = path7.join(projectRoot, "src", "panels");
|
|
500
|
+
if (!fs7.existsSync(dir))
|
|
501
|
+
return [];
|
|
502
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".ts")).map((f) => f.replace(/\.ts$/, "")).sort();
|
|
503
|
+
}
|
|
504
|
+
function listCommands(projectRoot) {
|
|
505
|
+
const dir = path7.join(projectRoot, "src", "commands");
|
|
506
|
+
if (!fs7.existsSync(dir))
|
|
507
|
+
return [];
|
|
508
|
+
return fs7.readdirSync(dir).filter((f) => f.endsWith(".ts")).map((f) => f.replace(/\.ts$/, "")).sort();
|
|
509
|
+
}
|
|
510
|
+
function listGroups(src) {
|
|
511
|
+
const root = findArrayContents(src, findItemsArrayStart(src));
|
|
512
|
+
if (!root)
|
|
513
|
+
return [];
|
|
514
|
+
const inner = src.slice(root.openIdx + 1, root.closeIdx);
|
|
515
|
+
const groups = [];
|
|
516
|
+
const re = /\blabel\s*:\s*(['"`])([^'"`]+)\1[^{}]*?\bchildren\s*:\s*\[/g;
|
|
517
|
+
let m;
|
|
518
|
+
while ((m = re.exec(inner)) !== null) {
|
|
519
|
+
groups.push(m[2]);
|
|
520
|
+
}
|
|
521
|
+
return groups;
|
|
522
|
+
}
|
|
523
|
+
function editMenu(opts) {
|
|
524
|
+
const file = path7.join(opts.projectRoot, "src", "menus", `${opts.menuName}.ts`);
|
|
525
|
+
if (!fs7.existsSync(file)) {
|
|
526
|
+
throw new Error(`Menu not found: ${path7.relative(opts.projectRoot, file)}`);
|
|
527
|
+
}
|
|
528
|
+
const src = fs7.readFileSync(file, "utf8");
|
|
529
|
+
const updated = insertItem(src, opts.item);
|
|
530
|
+
fs7.writeFileSync(file, updated.source);
|
|
531
|
+
const genRan = opts.runGen === false ? false : runGen3(opts.projectRoot);
|
|
532
|
+
return { file, inserted: updated.inserted, genRan };
|
|
533
|
+
}
|
|
534
|
+
function runGen3(cwd) {
|
|
535
|
+
const tryRun = (cmd, args) => {
|
|
536
|
+
const r = import_child_process3.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
537
|
+
return r.status === 0;
|
|
538
|
+
};
|
|
539
|
+
if (which3("bun") && tryRun("bun", ["run", "gen"]))
|
|
540
|
+
return true;
|
|
541
|
+
if (which3("npm") && tryRun("npm", ["run", "gen"]))
|
|
542
|
+
return true;
|
|
543
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
function which3(cmd) {
|
|
547
|
+
const r = import_child_process3.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
548
|
+
return r.status === 0;
|
|
549
|
+
}
|
|
550
|
+
function insertItem(src, item) {
|
|
551
|
+
const target = item.parentLabel ? findGroupChildrenArray(src, item.parentLabel) : findArrayContents(src, findItemsArrayStart(src));
|
|
552
|
+
if (!target) {
|
|
553
|
+
throw new Error(item.parentLabel ? `Group "${item.parentLabel}" not found in menu` : "Could not locate items: [...] array in menu file");
|
|
554
|
+
}
|
|
555
|
+
const indent = detectArrayItemIndent(src, target);
|
|
556
|
+
const rendered = renderItem(item, indent);
|
|
557
|
+
const before = src.slice(0, target.closeIdx);
|
|
558
|
+
const after = src.slice(target.closeIdx);
|
|
559
|
+
const trimmedBefore = before.replace(/[ \t]*$/, "");
|
|
560
|
+
const insertion = (trimmedBefore.endsWith(`
|
|
561
|
+
`) ? "" : `
|
|
562
|
+
`) + rendered + `
|
|
563
|
+
` + " ".repeat(closeIndent(src, target.closeIdx));
|
|
564
|
+
return { source: trimmedBefore + insertion + after, inserted: rendered };
|
|
565
|
+
}
|
|
566
|
+
function renderItem(item, indent) {
|
|
567
|
+
const parts = [];
|
|
568
|
+
parts.push(`${indent}{`);
|
|
569
|
+
parts.push(`${indent} label: ${quote(item.label)},`);
|
|
570
|
+
if (item.icon)
|
|
571
|
+
parts.push(`${indent} icon: ${quote(item.icon)},`);
|
|
572
|
+
if (item.description)
|
|
573
|
+
parts.push(`${indent} description: ${quote(item.description)},`);
|
|
574
|
+
switch (item.kind) {
|
|
575
|
+
case "panel":
|
|
576
|
+
if (!item.target)
|
|
577
|
+
throw new Error("panel kind requires target");
|
|
578
|
+
parts.push(`${indent} panel: ${quote(item.target)},`);
|
|
579
|
+
break;
|
|
580
|
+
case "command":
|
|
581
|
+
if (!item.target)
|
|
582
|
+
throw new Error("command kind requires target");
|
|
583
|
+
parts.push(`${indent} command: ${quote(item.target)},`);
|
|
584
|
+
break;
|
|
585
|
+
case "url":
|
|
586
|
+
if (!item.target)
|
|
587
|
+
throw new Error("url kind requires target");
|
|
588
|
+
parts.push(`${indent} url: ${quote(item.target)},`);
|
|
589
|
+
break;
|
|
590
|
+
case "group":
|
|
591
|
+
parts.push(`${indent} children: [],`);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
parts.push(`${indent}},`);
|
|
595
|
+
return parts.join(`
|
|
596
|
+
`);
|
|
597
|
+
}
|
|
598
|
+
function quote(s) {
|
|
599
|
+
return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
600
|
+
}
|
|
601
|
+
function detectArrayItemIndent(src, arr) {
|
|
602
|
+
const inner = src.slice(arr.openIdx + 1, arr.closeIdx);
|
|
603
|
+
const m = inner.match(/\n([ \t]+)\{/);
|
|
604
|
+
if (m)
|
|
605
|
+
return m[1];
|
|
606
|
+
return " ".repeat(closeIndent(src, arr.closeIdx) + 2);
|
|
607
|
+
}
|
|
608
|
+
function closeIndent(src, closeIdx) {
|
|
609
|
+
let i = closeIdx - 1;
|
|
610
|
+
while (i >= 0 && src[i] !== `
|
|
611
|
+
`)
|
|
612
|
+
i--;
|
|
613
|
+
let n = 0;
|
|
614
|
+
let j = i + 1;
|
|
615
|
+
while (j < closeIdx && (src[j] === " " || src[j] === "\t")) {
|
|
616
|
+
n++;
|
|
617
|
+
j++;
|
|
618
|
+
}
|
|
619
|
+
return n;
|
|
620
|
+
}
|
|
621
|
+
function findItemsArrayStart(src) {
|
|
622
|
+
const re = /\bitems\s*:\s*\[/g;
|
|
623
|
+
const m = re.exec(src);
|
|
624
|
+
if (!m)
|
|
625
|
+
throw new Error("Menu file missing items: [...] array");
|
|
626
|
+
return m.index + m[0].length - 1;
|
|
627
|
+
}
|
|
628
|
+
function findArrayContents(src, openIdx) {
|
|
629
|
+
if (src[openIdx] !== "[")
|
|
630
|
+
return null;
|
|
631
|
+
const closeIdx = findMatching(src, openIdx);
|
|
632
|
+
if (closeIdx < 0)
|
|
633
|
+
return null;
|
|
634
|
+
return { openIdx, closeIdx };
|
|
635
|
+
}
|
|
636
|
+
function findGroupChildrenArray(src, label) {
|
|
637
|
+
const root = findArrayContents(src, findItemsArrayStart(src));
|
|
638
|
+
if (!root)
|
|
639
|
+
return null;
|
|
640
|
+
let i = root.openIdx + 1;
|
|
641
|
+
while (i < root.closeIdx) {
|
|
642
|
+
const ch = src[i];
|
|
643
|
+
if (ch === "{") {
|
|
644
|
+
const objEnd = findMatching(src, i);
|
|
645
|
+
if (objEnd < 0)
|
|
646
|
+
break;
|
|
647
|
+
const obj = src.slice(i, objEnd + 1);
|
|
648
|
+
const labelMatch = obj.match(/\blabel\s*:\s*(['"`])([^'"`]+)\1/);
|
|
649
|
+
if (labelMatch && labelMatch[2] === label) {
|
|
650
|
+
const childRe = /\bchildren\s*:\s*\[/;
|
|
651
|
+
const cm = childRe.exec(obj);
|
|
652
|
+
if (!cm)
|
|
653
|
+
return null;
|
|
654
|
+
const openInObj = cm.index + cm[0].length - 1;
|
|
655
|
+
const openIdx = i + openInObj;
|
|
656
|
+
const closeIdx = findMatching(src, openIdx);
|
|
657
|
+
if (closeIdx < 0)
|
|
658
|
+
return null;
|
|
659
|
+
return { openIdx, closeIdx };
|
|
660
|
+
}
|
|
661
|
+
i = objEnd + 1;
|
|
662
|
+
} else {
|
|
663
|
+
i++;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
function findMatching(src, openIdx) {
|
|
669
|
+
const open = src[openIdx];
|
|
670
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : open === "(" ? ")" : "";
|
|
671
|
+
if (!close)
|
|
672
|
+
return -1;
|
|
673
|
+
let depth = 0;
|
|
674
|
+
let i = openIdx;
|
|
675
|
+
const n = src.length;
|
|
676
|
+
while (i < n) {
|
|
677
|
+
const ch = src[i];
|
|
678
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
679
|
+
const quote2 = ch;
|
|
680
|
+
i++;
|
|
681
|
+
while (i < n && src[i] !== quote2) {
|
|
682
|
+
if (src[i] === "\\")
|
|
683
|
+
i += 2;
|
|
684
|
+
else if (quote2 === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
685
|
+
const end = findMatching(src, i + 1);
|
|
686
|
+
if (end < 0)
|
|
687
|
+
return -1;
|
|
688
|
+
i = end + 1;
|
|
689
|
+
} else
|
|
690
|
+
i++;
|
|
691
|
+
}
|
|
692
|
+
i++;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
696
|
+
while (i < n && src[i] !== `
|
|
697
|
+
`)
|
|
698
|
+
i++;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
702
|
+
i += 2;
|
|
703
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
|
|
704
|
+
i++;
|
|
705
|
+
i += 2;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (ch === open)
|
|
709
|
+
depth++;
|
|
710
|
+
else if (ch === close) {
|
|
711
|
+
depth--;
|
|
712
|
+
if (depth === 0)
|
|
713
|
+
return i;
|
|
714
|
+
}
|
|
715
|
+
i++;
|
|
716
|
+
}
|
|
717
|
+
return -1;
|
|
718
|
+
}
|
|
719
|
+
var fs7, path7, import_child_process3;
|
|
720
|
+
var init_edit = __esm(() => {
|
|
721
|
+
fs7 = __toESM(require("fs"));
|
|
722
|
+
path7 = __toESM(require("path"));
|
|
723
|
+
import_child_process3 = require("child_process");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// src/lib/command/add.ts
|
|
727
|
+
function addCommand(opts) {
|
|
728
|
+
const name = assertId("command name", normalizeCamel3(opts.name));
|
|
729
|
+
const Pascal = name.charAt(0).toUpperCase() + name.slice(1);
|
|
730
|
+
const title = (opts.title ?? Pascal).trim() || Pascal;
|
|
731
|
+
const file = path8.join(opts.projectRoot, "src", "commands", `${name}.ts`);
|
|
732
|
+
assertNoOverwrite(opts.projectRoot, file, "Command");
|
|
733
|
+
if (opts.menuEntry)
|
|
734
|
+
assertSiblingExists(opts.projectRoot, "menu", opts.menuEntry);
|
|
735
|
+
const tplPath = path8.join(opts.templatesRoot, "_generators", "command", "command.ts.tpl");
|
|
736
|
+
if (!fs8.existsSync(tplPath))
|
|
737
|
+
throw new Error(`Template missing: ${tplPath}`);
|
|
738
|
+
const cfg = readConfig(opts.projectRoot);
|
|
739
|
+
const category = opts.category ?? cfg.defaultCategory;
|
|
740
|
+
const vars = {
|
|
741
|
+
name,
|
|
742
|
+
Name: Pascal,
|
|
743
|
+
title,
|
|
744
|
+
categoryLine: category ? `
|
|
745
|
+
category: '${escapeQuotes(category)}',` : "",
|
|
746
|
+
keybindingLine: opts.keybinding ? `
|
|
747
|
+
keybinding: '${escapeQuotes(opts.keybinding)}',` : "",
|
|
748
|
+
whenLine: opts.when ? `
|
|
749
|
+
when: '${escapeQuotes(opts.when)}',` : ""
|
|
750
|
+
};
|
|
751
|
+
fs8.mkdirSync(path8.dirname(file), { recursive: true });
|
|
752
|
+
fs8.writeFileSync(file, substitute(fs8.readFileSync(tplPath, "utf8"), vars));
|
|
753
|
+
const created = [file];
|
|
754
|
+
const modified = [];
|
|
755
|
+
let menuUpdated = false;
|
|
756
|
+
if (opts.menuEntry) {
|
|
757
|
+
const result = editMenu({
|
|
758
|
+
projectRoot: opts.projectRoot,
|
|
759
|
+
menuName: opts.menuEntry,
|
|
760
|
+
runGen: false,
|
|
761
|
+
item: {
|
|
762
|
+
label: title,
|
|
763
|
+
kind: "command",
|
|
764
|
+
target: name,
|
|
765
|
+
icon: opts.icon || undefined,
|
|
766
|
+
parentLabel: opts.group || undefined
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
modified.push(result.file);
|
|
770
|
+
menuUpdated = true;
|
|
771
|
+
}
|
|
772
|
+
let genRan = false;
|
|
773
|
+
if (opts.runGen !== false)
|
|
774
|
+
genRan = runGen4(opts.projectRoot);
|
|
775
|
+
return { created, modified, menuUpdated, genRan };
|
|
776
|
+
}
|
|
777
|
+
function runGen4(cwd) {
|
|
778
|
+
const tryRun = (cmd, args) => {
|
|
779
|
+
const r = import_child_process4.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
780
|
+
return r.status === 0;
|
|
781
|
+
};
|
|
782
|
+
if (which4("bun") && tryRun("bun", ["run", "gen"]))
|
|
783
|
+
return true;
|
|
784
|
+
if (which4("npm") && tryRun("npm", ["run", "gen"]))
|
|
785
|
+
return true;
|
|
786
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
function which4(cmd) {
|
|
790
|
+
const r = import_child_process4.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
791
|
+
return r.status === 0;
|
|
792
|
+
}
|
|
793
|
+
function escapeQuotes(s) {
|
|
794
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
795
|
+
}
|
|
796
|
+
function normalizeCamel3(s) {
|
|
797
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
798
|
+
if (!cleaned)
|
|
799
|
+
return "";
|
|
800
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
801
|
+
}
|
|
802
|
+
var fs8, path8, import_child_process4;
|
|
803
|
+
var init_add4 = __esm(() => {
|
|
804
|
+
init_scaffold();
|
|
805
|
+
init_edit();
|
|
806
|
+
init_validate();
|
|
807
|
+
init_config();
|
|
808
|
+
fs8 = __toESM(require("fs"));
|
|
809
|
+
path8 = __toESM(require("path"));
|
|
810
|
+
import_child_process4 = require("child_process");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// src/lib/rpc/add.ts
|
|
814
|
+
function addRpcMethod(opts) {
|
|
815
|
+
const panelId = assertId("panel id", opts.panel.trim());
|
|
816
|
+
const method = assertId("method name", opts.method.trim());
|
|
817
|
+
assertSiblingExists(opts.projectRoot, "panel", panelId);
|
|
818
|
+
const panelFile = path9.join(opts.projectRoot, "src", "panels", `${panelId}.ts`);
|
|
819
|
+
const apiFile = path9.join(opts.projectRoot, "src", "shared", "api.ts");
|
|
820
|
+
if (!fs9.existsSync(apiFile)) {
|
|
821
|
+
throw new Error(`RPC contract file missing at \`src/shared/api.ts\`. Create it (an empty file is fine) and retry.`);
|
|
822
|
+
}
|
|
823
|
+
const Pascal = panelId.charAt(0).toUpperCase() + panelId.slice(1);
|
|
824
|
+
const apiName = `${Pascal}Api`;
|
|
825
|
+
const paramSig = (opts.paramSig ?? "").trim();
|
|
826
|
+
const rawReturn = (opts.returns ?? "void").trim() || "void";
|
|
827
|
+
const returnType = /^Promise<.+>$/.test(rawReturn) ? rawReturn : `Promise<${rawReturn}>`;
|
|
828
|
+
let apiSrc = fs9.readFileSync(apiFile, "utf8");
|
|
829
|
+
const ifaceRange = findInterfaceRange(apiSrc, apiName);
|
|
830
|
+
let interfaceCreated = false;
|
|
831
|
+
if (ifaceRange) {
|
|
832
|
+
const body = apiSrc.slice(ifaceRange.bodyStart, ifaceRange.bodyEnd);
|
|
833
|
+
if (methodAlreadyDeclared(body, method)) {
|
|
834
|
+
throw new Error(`Method "${method}" already declared in ${apiName}`);
|
|
835
|
+
}
|
|
836
|
+
const trimmedBody = body.replace(/\s+$/, "");
|
|
837
|
+
const newBody = `${trimmedBody}
|
|
838
|
+
${method}(${paramSig}): ${returnType};
|
|
839
|
+
`;
|
|
840
|
+
apiSrc = apiSrc.slice(0, ifaceRange.bodyStart) + newBody + apiSrc.slice(ifaceRange.bodyEnd);
|
|
841
|
+
} else {
|
|
842
|
+
interfaceCreated = true;
|
|
843
|
+
const block = `
|
|
844
|
+
export interface ${apiName} {
|
|
845
|
+
${method}(${paramSig}): ${returnType};
|
|
846
|
+
}
|
|
847
|
+
`;
|
|
848
|
+
apiSrc = apiSrc.replace(/\s*$/, "") + `
|
|
849
|
+
` + block;
|
|
850
|
+
}
|
|
851
|
+
fs9.writeFileSync(apiFile, apiSrc);
|
|
852
|
+
let panelSrc = fs9.readFileSync(panelFile, "utf8");
|
|
853
|
+
const argNames = parseArgNames(paramSig);
|
|
854
|
+
const handlerBody = ` async ${method}(${argNames.join(", ")}) {
|
|
855
|
+
` + ` // TODO: implement
|
|
856
|
+
` + ` throw new Error('Not implemented');
|
|
857
|
+
` + ` },`;
|
|
858
|
+
if (!new RegExp(`\\b${apiName}\\b`).test(panelSrc)) {
|
|
859
|
+
panelSrc = ensureApiImport(panelSrc, apiName);
|
|
860
|
+
panelSrc = ensurePanelGeneric(panelSrc, apiName);
|
|
861
|
+
}
|
|
862
|
+
let rpcBlockCreated = false;
|
|
863
|
+
const rpcRange = findRpcBodyRange(panelSrc);
|
|
864
|
+
if (rpcRange) {
|
|
865
|
+
const inner = panelSrc.slice(rpcRange.start, rpcRange.end).replace(/\s+$/, "");
|
|
866
|
+
const newInner = `${inner}
|
|
867
|
+
${handlerBody}
|
|
868
|
+
`;
|
|
869
|
+
panelSrc = panelSrc.slice(0, rpcRange.start) + newInner + panelSrc.slice(rpcRange.end);
|
|
870
|
+
} else {
|
|
871
|
+
rpcBlockCreated = true;
|
|
872
|
+
panelSrc = insertRpcBlock(panelSrc, handlerBody);
|
|
873
|
+
}
|
|
874
|
+
fs9.writeFileSync(panelFile, panelSrc);
|
|
875
|
+
const webviewSnippet = buildSnippet(method, argNames);
|
|
876
|
+
let genRan = false;
|
|
877
|
+
if (opts.runGen === true)
|
|
878
|
+
genRan = runGen5(opts.projectRoot);
|
|
879
|
+
return { apiFile, panelFile, interfaceCreated, rpcBlockCreated, webviewSnippet, genRan };
|
|
880
|
+
}
|
|
881
|
+
function findInterfaceRange(src, name) {
|
|
882
|
+
const re = new RegExp(`export\\s+interface\\s+${escapeRe(name)}\\b[^{]*\\{`, "m");
|
|
883
|
+
const m = re.exec(src);
|
|
884
|
+
if (!m)
|
|
885
|
+
return null;
|
|
886
|
+
const bodyStart = m.index + m[0].length;
|
|
887
|
+
const bodyEnd = findMatchingBrace(src, bodyStart);
|
|
888
|
+
if (bodyEnd === -1)
|
|
889
|
+
return null;
|
|
890
|
+
return { bodyStart, bodyEnd };
|
|
891
|
+
}
|
|
892
|
+
function findRpcBodyRange(src) {
|
|
893
|
+
const re = /\brpc\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/m;
|
|
894
|
+
const m = re.exec(src);
|
|
895
|
+
if (!m)
|
|
896
|
+
return null;
|
|
897
|
+
const start = m.index + m[0].length;
|
|
898
|
+
const end = findMatchingBrace(src, start);
|
|
899
|
+
if (end === -1)
|
|
900
|
+
return null;
|
|
901
|
+
return { start, end };
|
|
902
|
+
}
|
|
903
|
+
function findMatchingBrace(src, start) {
|
|
904
|
+
let depth = 1;
|
|
905
|
+
for (let i = start;i < src.length; i++) {
|
|
906
|
+
const c = src[i];
|
|
907
|
+
if (c === "{")
|
|
908
|
+
depth++;
|
|
909
|
+
else if (c === "}") {
|
|
910
|
+
depth--;
|
|
911
|
+
if (depth === 0)
|
|
912
|
+
return i;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return -1;
|
|
916
|
+
}
|
|
917
|
+
function methodAlreadyDeclared(interfaceBody, method) {
|
|
918
|
+
return new RegExp(`\\b${escapeRe(method)}\\s*\\(`).test(interfaceBody);
|
|
919
|
+
}
|
|
920
|
+
function escapeRe(s) {
|
|
921
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
922
|
+
}
|
|
923
|
+
function parseArgNames(sig) {
|
|
924
|
+
if (!sig)
|
|
925
|
+
return [];
|
|
926
|
+
return sig.split(",").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
927
|
+
const name = part.split(":")[0].trim();
|
|
928
|
+
return name.replace(/\?$/, "").replace(/^\.\.\./, "");
|
|
929
|
+
}).filter(Boolean);
|
|
930
|
+
}
|
|
931
|
+
function ensureApiImport(src, apiName) {
|
|
932
|
+
if (new RegExp(`from\\s+['"]\\.\\./shared/api['"]`).test(src)) {
|
|
933
|
+
return src.replace(/import\s+type\s+\{([^}]*)\}\s+from\s+['"]\.\.\/shared\/api['"];/, (_m, group) => {
|
|
934
|
+
const names = new Set(group.split(",").map((n) => n.trim()).filter(Boolean));
|
|
935
|
+
names.add(apiName);
|
|
936
|
+
return `import type { ${[...names].join(", ")} } from '../shared/api';`;
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const importLine = `import type { ${apiName} } from '../shared/api';
|
|
940
|
+
`;
|
|
941
|
+
const lastImport = src.match(/^(import[^\n]*\n)+/m);
|
|
942
|
+
if (lastImport) {
|
|
943
|
+
const end = lastImport.index + lastImport[0].length;
|
|
944
|
+
return src.slice(0, end) + importLine + src.slice(end);
|
|
945
|
+
}
|
|
946
|
+
return importLine + src;
|
|
947
|
+
}
|
|
948
|
+
function ensurePanelGeneric(src, apiName) {
|
|
949
|
+
return src.replace(/definePanel\s*(<[^>]+>)?\s*\(/, (_m, generic) => {
|
|
950
|
+
return generic ? `definePanel${generic}(` : `definePanel<${apiName}>(`;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
function insertRpcBlock(src, handlerBody) {
|
|
954
|
+
const block = `
|
|
955
|
+
rpc: (vscode) => ({
|
|
956
|
+
${handlerBody}
|
|
957
|
+
}),`;
|
|
958
|
+
if (/title\s*:\s*['"][^'"]*['"]\s*,/.test(src)) {
|
|
959
|
+
return src.replace(/(title\s*:\s*['"][^'"]*['"]\s*,)/, `$1${block}`);
|
|
960
|
+
}
|
|
961
|
+
return src.replace(/\}\s*\)\s*;?\s*$/, `${block}
|
|
962
|
+
});
|
|
963
|
+
`);
|
|
964
|
+
}
|
|
965
|
+
function buildSnippet(method, argNames) {
|
|
966
|
+
const args = argNames.length ? argNames.join(", ") : "";
|
|
967
|
+
return `const result = await api.${method}(${args});`;
|
|
968
|
+
}
|
|
969
|
+
function runGen5(cwd) {
|
|
970
|
+
const tryRun = (cmd, args) => {
|
|
971
|
+
const r = import_child_process5.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
972
|
+
return r.status === 0;
|
|
973
|
+
};
|
|
974
|
+
if (which5("bun") && tryRun("bun", ["run", "gen"]))
|
|
975
|
+
return true;
|
|
976
|
+
if (which5("npm") && tryRun("npm", ["run", "gen"]))
|
|
977
|
+
return true;
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
function which5(cmd) {
|
|
981
|
+
const r = import_child_process5.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
982
|
+
return r.status === 0;
|
|
983
|
+
}
|
|
984
|
+
var fs9, path9, import_child_process5;
|
|
985
|
+
var init_add5 = __esm(() => {
|
|
986
|
+
init_validate();
|
|
987
|
+
fs9 = __toESM(require("fs"));
|
|
988
|
+
path9 = __toESM(require("path"));
|
|
989
|
+
import_child_process5 = require("child_process");
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// src/lib/statusBar/add.ts
|
|
993
|
+
function addStatusBar(opts) {
|
|
994
|
+
const name = assertId("status bar name", normalizeCamel4(opts.name));
|
|
995
|
+
const file = path10.join(opts.projectRoot, "src", "statusBars", `${name}.ts`);
|
|
996
|
+
assertNoOverwrite(opts.projectRoot, file, "Status bar");
|
|
997
|
+
const tplPath = path10.join(opts.templatesRoot, "_generators", "statusBar", "statusBar.ts.tpl");
|
|
998
|
+
if (!fs10.existsSync(tplPath))
|
|
999
|
+
throw new Error(`Template missing: ${tplPath}`);
|
|
1000
|
+
let commandId = opts.command;
|
|
1001
|
+
let commandCreated;
|
|
1002
|
+
if (opts.newCommandTitle) {
|
|
1003
|
+
const cmdId = `${name}Action`;
|
|
1004
|
+
addCommand({
|
|
1005
|
+
name: cmdId,
|
|
1006
|
+
title: opts.newCommandTitle,
|
|
1007
|
+
projectRoot: opts.projectRoot,
|
|
1008
|
+
templatesRoot: opts.templatesRoot,
|
|
1009
|
+
runGen: false
|
|
1010
|
+
});
|
|
1011
|
+
commandId = cmdId;
|
|
1012
|
+
commandCreated = cmdId;
|
|
1013
|
+
}
|
|
1014
|
+
const text = opts.text ?? "";
|
|
1015
|
+
const alignment = opts.alignment ?? "left";
|
|
1016
|
+
const priority = String(opts.priority ?? 100);
|
|
1017
|
+
const vars = {
|
|
1018
|
+
name,
|
|
1019
|
+
text: escapeSingleQuotes(text),
|
|
1020
|
+
alignment,
|
|
1021
|
+
priority,
|
|
1022
|
+
iconLine: opts.icon ? `
|
|
1023
|
+
icon: '${escapeSingleQuotes(opts.icon)}',` : "",
|
|
1024
|
+
tooltipLine: opts.tooltipMarkdown ? `
|
|
1025
|
+
tooltipMarkdown: ${asBacktickString(opts.tooltipMarkdown)},` : opts.tooltip ? `
|
|
1026
|
+
tooltip: '${escapeSingleQuotes(opts.tooltip)}',` : "",
|
|
1027
|
+
commandLine: !opts.panel && !opts.menu && commandId ? `
|
|
1028
|
+
command: '${escapeSingleQuotes(commandId)}',` : "",
|
|
1029
|
+
panelLine: opts.panel && !opts.menu ? `
|
|
1030
|
+
panel: '${escapeSingleQuotes(opts.panel)}',` : ""
|
|
1031
|
+
};
|
|
1032
|
+
if (opts.menu && opts.menu.length > 0) {
|
|
1033
|
+
const itemLines = opts.menu.map((it) => {
|
|
1034
|
+
const fields = [`label: '${escapeSingleQuotes(it.label)}'`];
|
|
1035
|
+
if (it.description)
|
|
1036
|
+
fields.push(`description: '${escapeSingleQuotes(it.description)}'`);
|
|
1037
|
+
if (it.detail)
|
|
1038
|
+
fields.push(`detail: '${escapeSingleQuotes(it.detail)}'`);
|
|
1039
|
+
if (it.command)
|
|
1040
|
+
fields.push(`command: '${escapeSingleQuotes(it.command)}'`);
|
|
1041
|
+
if (it.panel)
|
|
1042
|
+
fields.push(`panel: '${escapeSingleQuotes(it.panel)}'`);
|
|
1043
|
+
if (it.url)
|
|
1044
|
+
fields.push(`url: '${escapeSingleQuotes(it.url)}'`);
|
|
1045
|
+
return ` { ${fields.join(", ")} },`;
|
|
1046
|
+
}).join(`
|
|
1047
|
+
`);
|
|
1048
|
+
vars.panelLine += `
|
|
1049
|
+
menu: [
|
|
1050
|
+
${itemLines}
|
|
1051
|
+
],`;
|
|
1052
|
+
}
|
|
1053
|
+
fs10.mkdirSync(path10.dirname(file), { recursive: true });
|
|
1054
|
+
fs10.writeFileSync(file, substitute(fs10.readFileSync(tplPath, "utf8"), vars));
|
|
1055
|
+
let genRan = false;
|
|
1056
|
+
if (opts.runGen !== false)
|
|
1057
|
+
genRan = runGen6(opts.projectRoot);
|
|
1058
|
+
return { created: [file], commandCreated, genRan };
|
|
1059
|
+
}
|
|
1060
|
+
function escapeSingleQuotes(s) {
|
|
1061
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1062
|
+
}
|
|
1063
|
+
function asBacktickString(s) {
|
|
1064
|
+
return "`" + s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${") + "`";
|
|
1065
|
+
}
|
|
1066
|
+
function runGen6(cwd) {
|
|
1067
|
+
const tryRun = (cmd, args) => {
|
|
1068
|
+
const r = import_child_process6.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
1069
|
+
return r.status === 0;
|
|
1070
|
+
};
|
|
1071
|
+
if (which6("bun") && tryRun("bun", ["run", "gen"]))
|
|
1072
|
+
return true;
|
|
1073
|
+
if (which6("npm") && tryRun("npm", ["run", "gen"]))
|
|
1074
|
+
return true;
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
function which6(cmd) {
|
|
1078
|
+
const r = import_child_process6.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
1079
|
+
return r.status === 0;
|
|
1080
|
+
}
|
|
1081
|
+
function normalizeCamel4(s) {
|
|
1082
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
1083
|
+
if (!cleaned)
|
|
1084
|
+
return "";
|
|
1085
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
1086
|
+
}
|
|
1087
|
+
var fs10, path10, import_child_process6;
|
|
1088
|
+
var init_add6 = __esm(() => {
|
|
1089
|
+
init_scaffold();
|
|
1090
|
+
init_add4();
|
|
1091
|
+
init_validate();
|
|
1092
|
+
fs10 = __toESM(require("fs"));
|
|
1093
|
+
path10 = __toESM(require("path"));
|
|
1094
|
+
import_child_process6 = require("child_process");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
// src/lib/subpanel/add.ts
|
|
1098
|
+
function addSubpanel(opts) {
|
|
1099
|
+
const name = assertId("subpanel name", normalizeCamel5(opts.name));
|
|
1100
|
+
const menu = assertId("menu id", normalizeCamel5(opts.menu ?? ""));
|
|
1101
|
+
assertSiblingExists(opts.projectRoot, "menu", menu);
|
|
1102
|
+
const Pascal = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1103
|
+
const title = opts.title ?? Pascal;
|
|
1104
|
+
const withApi = opts.withApi !== false;
|
|
1105
|
+
const apiName = `${Pascal}ViewApi`;
|
|
1106
|
+
const viewTs = path11.join(opts.projectRoot, "src", "subpanels", `${name}.ts`);
|
|
1107
|
+
const uiDir = path11.join(opts.projectRoot, "src", "webview", "subpanels", name);
|
|
1108
|
+
const appTsx = path11.join(uiDir, "App.tsx");
|
|
1109
|
+
const mainTsx = path11.join(uiDir, "main.tsx");
|
|
1110
|
+
const apiPath = path11.join(opts.projectRoot, "src", "shared", "api.ts");
|
|
1111
|
+
assertNoOverwrite(opts.projectRoot, viewTs, "Subpanel");
|
|
1112
|
+
assertNoOverwrite(opts.projectRoot, uiDir, "Webview dir");
|
|
1113
|
+
const genDir = path11.join(opts.templatesRoot, "_generators", "subpanel");
|
|
1114
|
+
const readTpl = (n) => fs11.readFileSync(path11.join(genDir, n), "utf8");
|
|
1115
|
+
const vars = {
|
|
1116
|
+
name,
|
|
1117
|
+
Name: Pascal,
|
|
1118
|
+
title,
|
|
1119
|
+
menu,
|
|
1120
|
+
apiImport: withApi ? `import type { ${apiName} } from '../shared/api';
|
|
1121
|
+
` : "",
|
|
1122
|
+
apiGeneric: withApi ? `<${apiName}>` : "",
|
|
1123
|
+
rpcBlock: withApi ? `
|
|
1124
|
+
rpc: (vscode) => ({
|
|
1125
|
+
// add RPC handlers here
|
|
1126
|
+
}),` : "",
|
|
1127
|
+
apiBlock: withApi ? `import { connectWebview } from '../../../shared/vsceasy/client';
|
|
1128
|
+
import type { ${apiName} } from '../../../shared/api';
|
|
1129
|
+
|
|
1130
|
+
const api = connectWebview<${apiName}>();
|
|
1131
|
+
void api;
|
|
1132
|
+
` : ""
|
|
1133
|
+
};
|
|
1134
|
+
const created = [];
|
|
1135
|
+
const modified = [];
|
|
1136
|
+
const skipped = [];
|
|
1137
|
+
fs11.mkdirSync(path11.dirname(viewTs), { recursive: true });
|
|
1138
|
+
fs11.writeFileSync(viewTs, substitute(readTpl("subpanel.ts.tpl"), vars));
|
|
1139
|
+
created.push(viewTs);
|
|
1140
|
+
fs11.mkdirSync(uiDir, { recursive: true });
|
|
1141
|
+
fs11.writeFileSync(appTsx, substitute(readTpl("App.tsx.tpl"), vars));
|
|
1142
|
+
created.push(appTsx);
|
|
1143
|
+
fs11.writeFileSync(mainTsx, substitute(readTpl("main.tsx.tpl"), vars));
|
|
1144
|
+
created.push(mainTsx);
|
|
1145
|
+
if (withApi) {
|
|
1146
|
+
appendApi2(apiPath, apiName, created, modified, skipped);
|
|
1147
|
+
}
|
|
1148
|
+
let genRan = false;
|
|
1149
|
+
if (opts.runGen !== false)
|
|
1150
|
+
genRan = runGen7(opts.projectRoot);
|
|
1151
|
+
return { created, modified, skipped, genRan };
|
|
1152
|
+
}
|
|
1153
|
+
function appendApi2(apiPath, apiName, created, modified, skipped) {
|
|
1154
|
+
if (!fs11.existsSync(apiPath)) {
|
|
1155
|
+
fs11.mkdirSync(path11.dirname(apiPath), { recursive: true });
|
|
1156
|
+
fs11.writeFileSync(apiPath, `export interface ${apiName} {}
|
|
1157
|
+
`);
|
|
1158
|
+
created.push(apiPath);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const current = fs11.readFileSync(apiPath, "utf8");
|
|
1162
|
+
const re = new RegExp(`\\bexport\\s+interface\\s+${apiName}\\b`);
|
|
1163
|
+
if (re.test(current)) {
|
|
1164
|
+
skipped.push(`${apiPath} (interface ${apiName} already declared)`);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const sep = current.endsWith(`
|
|
1168
|
+
`) ? `
|
|
1169
|
+
` : `
|
|
1170
|
+
|
|
1171
|
+
`;
|
|
1172
|
+
fs11.writeFileSync(apiPath, current + `${sep}export interface ${apiName} {}
|
|
1173
|
+
`);
|
|
1174
|
+
modified.push(apiPath);
|
|
1175
|
+
}
|
|
1176
|
+
function runGen7(cwd) {
|
|
1177
|
+
const tryRun = (cmd, args) => {
|
|
1178
|
+
const r = import_child_process7.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
1179
|
+
return r.status === 0;
|
|
1180
|
+
};
|
|
1181
|
+
if (which7("bun") && tryRun("bun", ["run", "gen"]))
|
|
1182
|
+
return true;
|
|
1183
|
+
if (which7("npm") && tryRun("npm", ["run", "gen"]))
|
|
1184
|
+
return true;
|
|
1185
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
function which7(cmd) {
|
|
1189
|
+
const r = import_child_process7.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
1190
|
+
return r.status === 0;
|
|
1191
|
+
}
|
|
1192
|
+
function normalizeCamel5(s) {
|
|
1193
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
1194
|
+
if (!cleaned)
|
|
1195
|
+
return "";
|
|
1196
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
1197
|
+
}
|
|
1198
|
+
var fs11, path11, import_child_process7;
|
|
1199
|
+
var init_add7 = __esm(() => {
|
|
1200
|
+
init_scaffold();
|
|
1201
|
+
init_validate();
|
|
1202
|
+
fs11 = __toESM(require("fs"));
|
|
1203
|
+
path11 = __toESM(require("path"));
|
|
1204
|
+
import_child_process7 = require("child_process");
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// src/lib/menuTree.ts
|
|
1208
|
+
function parseMenu(src) {
|
|
1209
|
+
const title = matchString(src, /\btitle\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1210
|
+
const icon = matchString(src, /\bicon\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1211
|
+
const items = parseItemsArray(src);
|
|
1212
|
+
return { title, icon, items };
|
|
1213
|
+
}
|
|
1214
|
+
function parseItemsArray(src) {
|
|
1215
|
+
const start = findItemsStart(src);
|
|
1216
|
+
if (start < 0)
|
|
1217
|
+
return [];
|
|
1218
|
+
const end = findMatching2(src, start);
|
|
1219
|
+
if (end < 0)
|
|
1220
|
+
return [];
|
|
1221
|
+
return parseArrayItems(src.slice(start + 1, end));
|
|
1222
|
+
}
|
|
1223
|
+
function parseArrayItems(inner) {
|
|
1224
|
+
const out = [];
|
|
1225
|
+
let i = 0;
|
|
1226
|
+
while (i < inner.length) {
|
|
1227
|
+
skipTrivia(inner, { idx: i });
|
|
1228
|
+
while (i < inner.length && /[\s,]/.test(inner[i]))
|
|
1229
|
+
i++;
|
|
1230
|
+
if (i >= inner.length)
|
|
1231
|
+
break;
|
|
1232
|
+
if (inner[i] === "/" && (inner[i + 1] === "/" || inner[i + 1] === "*")) {
|
|
1233
|
+
i = skipComment(inner, i);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (inner[i] !== "{") {
|
|
1237
|
+
i++;
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const end = findMatching2(inner, i);
|
|
1241
|
+
if (end < 0)
|
|
1242
|
+
break;
|
|
1243
|
+
const obj = inner.slice(i, end + 1);
|
|
1244
|
+
out.push(parseObject(obj));
|
|
1245
|
+
i = end + 1;
|
|
1246
|
+
}
|
|
1247
|
+
return out;
|
|
1248
|
+
}
|
|
1249
|
+
function parseObject(obj) {
|
|
1250
|
+
const label = matchString(obj, /\blabel\s*:\s*(['"`])([^'"`]+)\1/) ?? "(unnamed)";
|
|
1251
|
+
const icon = matchString(obj, /\bicon\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1252
|
+
const panel = matchString(obj, /\bpanel\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1253
|
+
const command = matchString(obj, /\bcommand\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1254
|
+
const url = matchString(obj, /\burl\s*:\s*(['"`])([^'"`]+)\1/);
|
|
1255
|
+
const childrenIdx = obj.search(/\bchildren\s*:\s*\[/);
|
|
1256
|
+
let children;
|
|
1257
|
+
if (childrenIdx >= 0) {
|
|
1258
|
+
const arrOpen = obj.indexOf("[", childrenIdx);
|
|
1259
|
+
if (arrOpen >= 0) {
|
|
1260
|
+
const arrClose = findMatching2(obj, arrOpen);
|
|
1261
|
+
if (arrClose >= 0) {
|
|
1262
|
+
children = parseArrayItems(obj.slice(arrOpen + 1, arrClose));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
let kind = "item";
|
|
1267
|
+
let target;
|
|
1268
|
+
if (children !== undefined) {
|
|
1269
|
+
kind = "group";
|
|
1270
|
+
} else if (panel) {
|
|
1271
|
+
kind = "panel";
|
|
1272
|
+
target = panel;
|
|
1273
|
+
} else if (command) {
|
|
1274
|
+
kind = "command";
|
|
1275
|
+
target = command;
|
|
1276
|
+
} else if (url) {
|
|
1277
|
+
kind = "url";
|
|
1278
|
+
target = url;
|
|
1279
|
+
}
|
|
1280
|
+
return { label, kind, target, icon, children };
|
|
1281
|
+
}
|
|
1282
|
+
function matchString(s, re) {
|
|
1283
|
+
const m = s.match(re);
|
|
1284
|
+
return m?.[2];
|
|
1285
|
+
}
|
|
1286
|
+
function findItemsStart(src) {
|
|
1287
|
+
const re = /\bitems\s*:\s*\[/g;
|
|
1288
|
+
const m = re.exec(src);
|
|
1289
|
+
if (!m)
|
|
1290
|
+
return -1;
|
|
1291
|
+
return m.index + m[0].length - 1;
|
|
1292
|
+
}
|
|
1293
|
+
function findMatching2(src, openIdx) {
|
|
1294
|
+
const open = src[openIdx];
|
|
1295
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : open === "(" ? ")" : "";
|
|
1296
|
+
if (!close)
|
|
1297
|
+
return -1;
|
|
1298
|
+
let depth = 0;
|
|
1299
|
+
let i = openIdx;
|
|
1300
|
+
const n = src.length;
|
|
1301
|
+
while (i < n) {
|
|
1302
|
+
const ch = src[i];
|
|
1303
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
1304
|
+
const q = ch;
|
|
1305
|
+
i++;
|
|
1306
|
+
while (i < n && src[i] !== q) {
|
|
1307
|
+
if (src[i] === "\\")
|
|
1308
|
+
i += 2;
|
|
1309
|
+
else
|
|
1310
|
+
i++;
|
|
1311
|
+
}
|
|
1312
|
+
i++;
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
1316
|
+
while (i < n && src[i] !== `
|
|
1317
|
+
`)
|
|
1318
|
+
i++;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
1322
|
+
i += 2;
|
|
1323
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
|
|
1324
|
+
i++;
|
|
1325
|
+
i += 2;
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (ch === open)
|
|
1329
|
+
depth++;
|
|
1330
|
+
else if (ch === close) {
|
|
1331
|
+
depth--;
|
|
1332
|
+
if (depth === 0)
|
|
1333
|
+
return i;
|
|
1334
|
+
}
|
|
1335
|
+
i++;
|
|
1336
|
+
}
|
|
1337
|
+
return -1;
|
|
1338
|
+
}
|
|
1339
|
+
function skipTrivia(_s, _ref) {}
|
|
1340
|
+
function skipComment(s, i) {
|
|
1341
|
+
if (s[i + 1] === "/") {
|
|
1342
|
+
while (i < s.length && s[i] !== `
|
|
1343
|
+
`)
|
|
1344
|
+
i++;
|
|
1345
|
+
return i;
|
|
1346
|
+
}
|
|
1347
|
+
if (s[i + 1] === "*") {
|
|
1348
|
+
i += 2;
|
|
1349
|
+
while (i < s.length && !(s[i] === "*" && s[i + 1] === "/"))
|
|
1350
|
+
i++;
|
|
1351
|
+
return i + 2;
|
|
1352
|
+
}
|
|
1353
|
+
return i + 1;
|
|
1354
|
+
}
|
|
1355
|
+
var ESC = "\x1B", DIM, BOLD, CYAN, GREEN, YELLOW, MAGENTA, RST, KIND_BADGE;
|
|
1356
|
+
var init_menuTree = __esm(() => {
|
|
1357
|
+
DIM = `${ESC}[2m`;
|
|
1358
|
+
BOLD = `${ESC}[1m`;
|
|
1359
|
+
CYAN = `${ESC}[36m`;
|
|
1360
|
+
GREEN = `${ESC}[32m`;
|
|
1361
|
+
YELLOW = `${ESC}[33m`;
|
|
1362
|
+
MAGENTA = `${ESC}[35m`;
|
|
1363
|
+
RST = `${ESC}[0m`;
|
|
1364
|
+
KIND_BADGE = {
|
|
1365
|
+
panel: `${MAGENTA}■${RST}`,
|
|
1366
|
+
command: `${GREEN}▶${RST}`,
|
|
1367
|
+
url: `${CYAN}↗${RST}`,
|
|
1368
|
+
group: `${YELLOW}▾${RST}`,
|
|
1369
|
+
item: `${DIM}·${RST}`
|
|
1370
|
+
};
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// src/data/codicons.ts
|
|
1374
|
+
function isKnownCodicon(name) {
|
|
1375
|
+
return CODICONS.some((c) => c.name === name);
|
|
1376
|
+
}
|
|
1377
|
+
var CODICONS;
|
|
1378
|
+
var init_codicons = __esm(() => {
|
|
1379
|
+
CODICONS = [
|
|
1380
|
+
{ name: "play", category: "actions" },
|
|
1381
|
+
{ name: "play-circle", category: "actions" },
|
|
1382
|
+
{ name: "debug-start", category: "actions" },
|
|
1383
|
+
{ name: "debug-stop", category: "actions" },
|
|
1384
|
+
{ name: "debug-restart", category: "actions" },
|
|
1385
|
+
{ name: "debug-pause", category: "actions" },
|
|
1386
|
+
{ name: "stop", category: "actions" },
|
|
1387
|
+
{ name: "stop-circle", category: "actions" },
|
|
1388
|
+
{ name: "refresh", category: "actions" },
|
|
1389
|
+
{ name: "sync", category: "actions" },
|
|
1390
|
+
{ name: "add", category: "actions" },
|
|
1391
|
+
{ name: "remove", category: "actions" },
|
|
1392
|
+
{ name: "trash", category: "actions" },
|
|
1393
|
+
{ name: "edit", category: "actions" },
|
|
1394
|
+
{ name: "save", category: "actions" },
|
|
1395
|
+
{ name: "save-all", category: "actions" },
|
|
1396
|
+
{ name: "cloud-upload", category: "actions" },
|
|
1397
|
+
{ name: "cloud-download", category: "actions" },
|
|
1398
|
+
{ name: "export", category: "actions" },
|
|
1399
|
+
{ name: "desktop-download", category: "actions" },
|
|
1400
|
+
{ name: "search", category: "actions" },
|
|
1401
|
+
{ name: "filter", category: "actions" },
|
|
1402
|
+
{ name: "replace", category: "actions" },
|
|
1403
|
+
{ name: "check", category: "actions" },
|
|
1404
|
+
{ name: "close", category: "actions" },
|
|
1405
|
+
{ name: "eye", category: "actions" },
|
|
1406
|
+
{ name: "eye-closed", category: "actions" },
|
|
1407
|
+
{ name: "lock", category: "actions" },
|
|
1408
|
+
{ name: "unlock", category: "actions" },
|
|
1409
|
+
{ name: "key", category: "actions" },
|
|
1410
|
+
{ name: "symbol-class", category: "symbols" },
|
|
1411
|
+
{ name: "symbol-method", category: "symbols" },
|
|
1412
|
+
{ name: "symbol-function", category: "symbols" },
|
|
1413
|
+
{ name: "symbol-variable", category: "symbols" },
|
|
1414
|
+
{ name: "symbol-constant", category: "symbols" },
|
|
1415
|
+
{ name: "symbol-property", category: "symbols" },
|
|
1416
|
+
{ name: "symbol-field", category: "symbols" },
|
|
1417
|
+
{ name: "symbol-event", category: "symbols" },
|
|
1418
|
+
{ name: "symbol-interface", category: "symbols" },
|
|
1419
|
+
{ name: "symbol-enum", category: "symbols" },
|
|
1420
|
+
{ name: "symbol-module", category: "symbols" },
|
|
1421
|
+
{ name: "symbol-namespace", category: "symbols" },
|
|
1422
|
+
{ name: "symbol-array", category: "symbols" },
|
|
1423
|
+
{ name: "symbol-boolean", category: "symbols" },
|
|
1424
|
+
{ name: "symbol-numeric", category: "symbols" },
|
|
1425
|
+
{ name: "symbol-string", category: "symbols" },
|
|
1426
|
+
{ name: "symbol-misc", category: "symbols" },
|
|
1427
|
+
{ name: "symbol-color", category: "symbols" },
|
|
1428
|
+
{ name: "symbol-key", category: "symbols" },
|
|
1429
|
+
{ name: "symbol-parameter", category: "symbols" },
|
|
1430
|
+
{ name: "symbol-snippet", category: "symbols" },
|
|
1431
|
+
{ name: "file", category: "files" },
|
|
1432
|
+
{ name: "file-code", category: "files" },
|
|
1433
|
+
{ name: "file-text", category: "files" },
|
|
1434
|
+
{ name: "file-media", category: "files" },
|
|
1435
|
+
{ name: "file-pdf", category: "files" },
|
|
1436
|
+
{ name: "file-zip", category: "files" },
|
|
1437
|
+
{ name: "file-binary", category: "files" },
|
|
1438
|
+
{ name: "file-symlink-file", category: "files" },
|
|
1439
|
+
{ name: "file-symlink-directory", category: "files" },
|
|
1440
|
+
{ name: "files", category: "files" },
|
|
1441
|
+
{ name: "folder", category: "files" },
|
|
1442
|
+
{ name: "folder-opened", category: "files" },
|
|
1443
|
+
{ name: "folder-active", category: "files" },
|
|
1444
|
+
{ name: "folder-library", category: "files" },
|
|
1445
|
+
{ name: "new-file", category: "files" },
|
|
1446
|
+
{ name: "new-folder", category: "files" },
|
|
1447
|
+
{ name: "archive", category: "files" },
|
|
1448
|
+
{ name: "home", category: "ui" },
|
|
1449
|
+
{ name: "dashboard", category: "ui" },
|
|
1450
|
+
{ name: "gear", category: "ui" },
|
|
1451
|
+
{ name: "settings", category: "ui" },
|
|
1452
|
+
{ name: "settings-gear", category: "ui" },
|
|
1453
|
+
{ name: "preferences", category: "ui" },
|
|
1454
|
+
{ name: "list-flat", category: "ui" },
|
|
1455
|
+
{ name: "list-tree", category: "ui" },
|
|
1456
|
+
{ name: "list-unordered", category: "ui" },
|
|
1457
|
+
{ name: "list-ordered", category: "ui" },
|
|
1458
|
+
{ name: "list-selection", category: "ui" },
|
|
1459
|
+
{ name: "layout", category: "ui" },
|
|
1460
|
+
{ name: "layout-sidebar-left", category: "ui" },
|
|
1461
|
+
{ name: "layout-sidebar-right", category: "ui" },
|
|
1462
|
+
{ name: "layout-panel", category: "ui" },
|
|
1463
|
+
{ name: "split-horizontal", category: "ui" },
|
|
1464
|
+
{ name: "split-vertical", category: "ui" },
|
|
1465
|
+
{ name: "window", category: "ui" },
|
|
1466
|
+
{ name: "browser", category: "ui" },
|
|
1467
|
+
{ name: "preview", category: "ui" },
|
|
1468
|
+
{ name: "bell", category: "ui" },
|
|
1469
|
+
{ name: "bell-dot", category: "ui" },
|
|
1470
|
+
{ name: "info", category: "ui" },
|
|
1471
|
+
{ name: "warning", category: "ui" },
|
|
1472
|
+
{ name: "error", category: "ui" },
|
|
1473
|
+
{ name: "question", category: "ui" },
|
|
1474
|
+
{ name: "git-branch", category: "git" },
|
|
1475
|
+
{ name: "git-commit", category: "git" },
|
|
1476
|
+
{ name: "git-merge", category: "git" },
|
|
1477
|
+
{ name: "git-compare", category: "git" },
|
|
1478
|
+
{ name: "git-pull-request", category: "git" },
|
|
1479
|
+
{ name: "git-fork", category: "git" },
|
|
1480
|
+
{ name: "source-control", category: "git" },
|
|
1481
|
+
{ name: "github", category: "git" },
|
|
1482
|
+
{ name: "github-alt", category: "git" },
|
|
1483
|
+
{ name: "github-inverted", category: "git" },
|
|
1484
|
+
{ name: "repo", category: "git" },
|
|
1485
|
+
{ name: "repo-clone", category: "git" },
|
|
1486
|
+
{ name: "repo-forked", category: "git" },
|
|
1487
|
+
{ name: "repo-pull", category: "git" },
|
|
1488
|
+
{ name: "repo-push", category: "git" },
|
|
1489
|
+
{ name: "cloud", category: "git" },
|
|
1490
|
+
{ name: "bug", category: "debug" },
|
|
1491
|
+
{ name: "debug", category: "debug" },
|
|
1492
|
+
{ name: "debug-alt", category: "debug" },
|
|
1493
|
+
{ name: "debug-console", category: "debug" },
|
|
1494
|
+
{ name: "debug-breakpoint", category: "debug" },
|
|
1495
|
+
{ name: "debug-step-over", category: "debug" },
|
|
1496
|
+
{ name: "debug-step-into", category: "debug" },
|
|
1497
|
+
{ name: "debug-step-out", category: "debug" },
|
|
1498
|
+
{ name: "debug-continue", category: "debug" },
|
|
1499
|
+
{ name: "debug-disconnect", category: "debug" },
|
|
1500
|
+
{ name: "beaker", category: "debug" },
|
|
1501
|
+
{ name: "terminal", category: "terminal" },
|
|
1502
|
+
{ name: "terminal-bash", category: "terminal" },
|
|
1503
|
+
{ name: "terminal-cmd", category: "terminal" },
|
|
1504
|
+
{ name: "terminal-powershell", category: "terminal" },
|
|
1505
|
+
{ name: "terminal-linux", category: "terminal" },
|
|
1506
|
+
{ name: "console", category: "terminal" },
|
|
1507
|
+
{ name: "output", category: "terminal" },
|
|
1508
|
+
{ name: "server", category: "terminal" },
|
|
1509
|
+
{ name: "server-environment", category: "terminal" },
|
|
1510
|
+
{ name: "server-process", category: "terminal" },
|
|
1511
|
+
{ name: "code", category: "editor" },
|
|
1512
|
+
{ name: "json", category: "editor" },
|
|
1513
|
+
{ name: "markdown", category: "editor" },
|
|
1514
|
+
{ name: "note", category: "editor" },
|
|
1515
|
+
{ name: "notebook", category: "editor" },
|
|
1516
|
+
{ name: "pencil", category: "editor" },
|
|
1517
|
+
{ name: "comment", category: "editor" },
|
|
1518
|
+
{ name: "comment-discussion", category: "editor" },
|
|
1519
|
+
{ name: "tag", category: "editor" },
|
|
1520
|
+
{ name: "bookmark", category: "editor" },
|
|
1521
|
+
{ name: "pin", category: "editor" },
|
|
1522
|
+
{ name: "pinned", category: "editor" },
|
|
1523
|
+
{ name: "history", category: "editor" },
|
|
1524
|
+
{ name: "clock", category: "editor" },
|
|
1525
|
+
{ name: "watch", category: "editor" },
|
|
1526
|
+
{ name: "wand", category: "editor" },
|
|
1527
|
+
{ name: "sparkle", category: "editor" },
|
|
1528
|
+
{ name: "lightbulb", category: "editor" },
|
|
1529
|
+
{ name: "image", category: "media" },
|
|
1530
|
+
{ name: "video", category: "media" },
|
|
1531
|
+
{ name: "play-circle", category: "media" },
|
|
1532
|
+
{ name: "record", category: "media" },
|
|
1533
|
+
{ name: "mute", category: "media" },
|
|
1534
|
+
{ name: "unmute", category: "media" },
|
|
1535
|
+
{ name: "megaphone", category: "media" },
|
|
1536
|
+
{ name: "broadcast", category: "media" },
|
|
1537
|
+
{ name: "rocket", category: "misc" },
|
|
1538
|
+
{ name: "star", category: "misc" },
|
|
1539
|
+
{ name: "star-empty", category: "misc" },
|
|
1540
|
+
{ name: "star-full", category: "misc" },
|
|
1541
|
+
{ name: "heart", category: "misc" },
|
|
1542
|
+
{ name: "flame", category: "misc" },
|
|
1543
|
+
{ name: "zap", category: "misc" },
|
|
1544
|
+
{ name: "globe", category: "misc" },
|
|
1545
|
+
{ name: "book", category: "misc" },
|
|
1546
|
+
{ name: "mortar-board", category: "misc" },
|
|
1547
|
+
{ name: "tools", category: "misc" },
|
|
1548
|
+
{ name: "package", category: "misc" },
|
|
1549
|
+
{ name: "database", category: "misc" },
|
|
1550
|
+
{ name: "extensions", category: "misc" },
|
|
1551
|
+
{ name: "plug", category: "misc" },
|
|
1552
|
+
{ name: "link", category: "misc" },
|
|
1553
|
+
{ name: "link-external", category: "misc" },
|
|
1554
|
+
{ name: "mail", category: "misc" },
|
|
1555
|
+
{ name: "person", category: "misc" },
|
|
1556
|
+
{ name: "organization", category: "misc" },
|
|
1557
|
+
{ name: "account", category: "misc" },
|
|
1558
|
+
{ name: "shield", category: "misc" },
|
|
1559
|
+
{ name: "verified", category: "misc" },
|
|
1560
|
+
{ name: "graph", category: "misc" },
|
|
1561
|
+
{ name: "pulse", category: "misc" },
|
|
1562
|
+
{ name: "flag", category: "misc" },
|
|
1563
|
+
{ name: "inbox", category: "misc" },
|
|
1564
|
+
{ name: "calendar", category: "misc" },
|
|
1565
|
+
{ name: "location", category: "misc" },
|
|
1566
|
+
{ name: "compass", category: "misc" }
|
|
1567
|
+
];
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
// src/lib/doctor.ts
|
|
1571
|
+
function runDoctor(opts) {
|
|
1572
|
+
const root = opts.projectRoot;
|
|
1573
|
+
const pkg = readPkg(root);
|
|
1574
|
+
const results = [];
|
|
1575
|
+
results.push(checkEngine(pkg));
|
|
1576
|
+
results.push(checkScripts(pkg));
|
|
1577
|
+
results.push(...checkPanels(root));
|
|
1578
|
+
results.push(...checkRpcContracts(root));
|
|
1579
|
+
results.push(...checkMenus(root));
|
|
1580
|
+
results.push(...checkStatusBars(root));
|
|
1581
|
+
results.push(...checkSubpanels(root));
|
|
1582
|
+
results.push(checkContributesSync(root, pkg));
|
|
1583
|
+
results.push(checkActivationEvents(pkg));
|
|
1584
|
+
results.push(checkMarketplaceIcon(root, pkg));
|
|
1585
|
+
results.push(checkGenScript(root));
|
|
1586
|
+
results.push(checkGitignore(root));
|
|
1587
|
+
const counts = { ok: 0, warn: 0, error: 0 };
|
|
1588
|
+
for (const r of results)
|
|
1589
|
+
counts[r.level]++;
|
|
1590
|
+
return {
|
|
1591
|
+
projectRoot: root,
|
|
1592
|
+
displayName: pkg?.displayName ?? pkg?.name ?? path12.basename(root),
|
|
1593
|
+
results,
|
|
1594
|
+
counts
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function checkEngine(pkg) {
|
|
1598
|
+
const engine = pkg?.engines?.vscode;
|
|
1599
|
+
if (!engine) {
|
|
1600
|
+
return { id: "engine", level: "error", message: "package.json missing engines.vscode" };
|
|
1601
|
+
}
|
|
1602
|
+
if (!/^[\^~>=]?\d+\.\d+/.test(engine)) {
|
|
1603
|
+
return { id: "engine", level: "warn", message: `engines.vscode looks malformed: ${engine}` };
|
|
1604
|
+
}
|
|
1605
|
+
return { id: "engine", level: "ok", message: `engines.vscode ${engine}` };
|
|
1606
|
+
}
|
|
1607
|
+
function checkScripts(pkg) {
|
|
1608
|
+
const scripts = pkg?.scripts ?? {};
|
|
1609
|
+
const required = ["gen", "dev", "launch", "build"];
|
|
1610
|
+
const missing = required.filter((s) => typeof scripts[s] !== "string");
|
|
1611
|
+
if (missing.length === 0) {
|
|
1612
|
+
return { id: "scripts", level: "ok", message: `scripts ${required.join(", ")}` };
|
|
1613
|
+
}
|
|
1614
|
+
return {
|
|
1615
|
+
id: "scripts",
|
|
1616
|
+
level: "error",
|
|
1617
|
+
message: `missing scripts: ${missing.join(", ")}`
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function checkPanels(root) {
|
|
1621
|
+
const panels = listPanels(root);
|
|
1622
|
+
if (panels.length === 0) {
|
|
1623
|
+
return [{ id: "panels", level: "ok", message: "panels none" }];
|
|
1624
|
+
}
|
|
1625
|
+
const out = [];
|
|
1626
|
+
for (const id of panels) {
|
|
1627
|
+
const file = path12.join(root, "src", "panels", `${id}.ts`);
|
|
1628
|
+
const src = fs12.readFileSync(file, "utf8");
|
|
1629
|
+
if (!/\bdefinePanel\b/.test(src)) {
|
|
1630
|
+
out.push({ id: `panel.${id}`, level: "error", message: `panel ${id}: missing definePanel export` });
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
const webviewDir = path12.join(root, "src", "webview", "panels", id);
|
|
1634
|
+
if (!fs12.existsSync(webviewDir)) {
|
|
1635
|
+
out.push({
|
|
1636
|
+
id: `panel.${id}.webview`,
|
|
1637
|
+
level: "warn",
|
|
1638
|
+
message: `panel ${id}: webview dir missing (src/webview/panels/${id}/)`
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (out.length === 0) {
|
|
1643
|
+
out.push({ id: "panels", level: "ok", message: `panels ${panels.length} valid` });
|
|
1644
|
+
}
|
|
1645
|
+
return out;
|
|
1646
|
+
}
|
|
1647
|
+
function checkRpcContracts(root) {
|
|
1648
|
+
const apiFile = path12.join(root, "src", "shared", "api.ts");
|
|
1649
|
+
if (!fs12.existsSync(apiFile))
|
|
1650
|
+
return [];
|
|
1651
|
+
const apiSrc = fs12.readFileSync(apiFile, "utf8");
|
|
1652
|
+
const interfaces = parseInterfaces(apiSrc);
|
|
1653
|
+
const out = [];
|
|
1654
|
+
const panels = listPanels(root);
|
|
1655
|
+
for (const id of panels) {
|
|
1656
|
+
const Pascal = id.charAt(0).toUpperCase() + id.slice(1);
|
|
1657
|
+
const apiName = `${Pascal}Api`;
|
|
1658
|
+
if (!interfaces[apiName])
|
|
1659
|
+
continue;
|
|
1660
|
+
const file = path12.join(root, "src", "panels", `${id}.ts`);
|
|
1661
|
+
const src = fs12.readFileSync(file, "utf8");
|
|
1662
|
+
const handlers = parseRpcHandlers(src);
|
|
1663
|
+
if (handlers === null) {
|
|
1664
|
+
out.push({
|
|
1665
|
+
id: `rpc.${id}`,
|
|
1666
|
+
level: "warn",
|
|
1667
|
+
message: `panel ${id}: ${apiName} interface declared but no rpc block`
|
|
1668
|
+
});
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
const declared = interfaces[apiName];
|
|
1672
|
+
const missing = declared.filter((m) => !handlers.includes(m));
|
|
1673
|
+
const extra = handlers.filter((m) => !declared.includes(m));
|
|
1674
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
1675
|
+
out.push({ id: `rpc.${id}`, level: "ok", message: `rpc ${id} ${declared.length} method(s) in sync` });
|
|
1676
|
+
} else {
|
|
1677
|
+
out.push({
|
|
1678
|
+
id: `rpc.${id}`,
|
|
1679
|
+
level: "error",
|
|
1680
|
+
message: `rpc ${id}: contract drift`,
|
|
1681
|
+
details: [
|
|
1682
|
+
...missing.map((m) => `missing handler: ${m}`),
|
|
1683
|
+
...extra.map((m) => `extra handler (not in api): ${m}`)
|
|
1684
|
+
],
|
|
1685
|
+
fix: missing.length > 0 ? () => {
|
|
1686
|
+
addRpcHandlerStubs(file, missing);
|
|
1687
|
+
return `added ${missing.length} handler stub(s) to ${path12.relative(root, file)}: ${missing.join(", ")}`;
|
|
1688
|
+
} : undefined
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return out;
|
|
1693
|
+
}
|
|
1694
|
+
function checkMenus(root) {
|
|
1695
|
+
const menus = listMenus(root);
|
|
1696
|
+
if (menus.length === 0)
|
|
1697
|
+
return [];
|
|
1698
|
+
const panels = new Set(listPanels(root));
|
|
1699
|
+
const commands = new Set(listCommands(root));
|
|
1700
|
+
const out = [];
|
|
1701
|
+
for (const id of menus) {
|
|
1702
|
+
const file = path12.join(root, "src", "menus", `${id}.ts`);
|
|
1703
|
+
const tree = parseMenu(fs12.readFileSync(file, "utf8"));
|
|
1704
|
+
const orphans = [];
|
|
1705
|
+
const badIcons = [];
|
|
1706
|
+
walk(tree.items, (n) => {
|
|
1707
|
+
if (n.kind === "panel" && n.target && !panels.has(n.target)) {
|
|
1708
|
+
orphans.push(`panel "${n.target}" (in "${n.label}")`);
|
|
1709
|
+
}
|
|
1710
|
+
if (n.kind === "command" && n.target && !commands.has(n.target)) {
|
|
1711
|
+
orphans.push(`command "${n.target}" (in "${n.label}")`);
|
|
1712
|
+
}
|
|
1713
|
+
if (n.icon && !isKnownCodicon(n.icon)) {
|
|
1714
|
+
badIcons.push(`${n.icon} (in "${n.label}")`);
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
if (tree.icon && !isKnownCodicon(tree.icon)) {
|
|
1718
|
+
badIcons.push(`${tree.icon} (menu icon)`);
|
|
1719
|
+
}
|
|
1720
|
+
if (orphans.length === 0 && badIcons.length === 0) {
|
|
1721
|
+
out.push({ id: `menu.${id}`, level: "ok", message: `menu ${id} refs OK` });
|
|
1722
|
+
} else {
|
|
1723
|
+
if (orphans.length) {
|
|
1724
|
+
const orphanTargets = [];
|
|
1725
|
+
walk(tree.items, (n) => {
|
|
1726
|
+
if (n.kind === "panel" && n.target && !panels.has(n.target)) {
|
|
1727
|
+
orphanTargets.push({ kind: "panel", target: n.target });
|
|
1728
|
+
}
|
|
1729
|
+
if (n.kind === "command" && n.target && !commands.has(n.target)) {
|
|
1730
|
+
orphanTargets.push({ kind: "command", target: n.target });
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
out.push({
|
|
1734
|
+
id: `menu.${id}.refs`,
|
|
1735
|
+
level: "error",
|
|
1736
|
+
message: `menu ${id}: ${orphans.length} orphan ref(s)`,
|
|
1737
|
+
details: orphans,
|
|
1738
|
+
fix: () => {
|
|
1739
|
+
const removed = removeOrphanMenuItems(file, orphanTargets);
|
|
1740
|
+
return `removed ${removed} orphan item(s) from ${path12.relative(root, file)}`;
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
if (badIcons.length) {
|
|
1745
|
+
out.push({
|
|
1746
|
+
id: `menu.${id}.icons`,
|
|
1747
|
+
level: "warn",
|
|
1748
|
+
message: `menu ${id}: ${badIcons.length} unknown codicon(s)`,
|
|
1749
|
+
details: badIcons
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return out;
|
|
1755
|
+
}
|
|
1756
|
+
function checkStatusBars(root) {
|
|
1757
|
+
const dir = path12.join(root, "src", "statusBars");
|
|
1758
|
+
if (!fs12.existsSync(dir))
|
|
1759
|
+
return [];
|
|
1760
|
+
const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".ts"));
|
|
1761
|
+
if (files.length === 0)
|
|
1762
|
+
return [];
|
|
1763
|
+
const commands = new Set(listCommands(root));
|
|
1764
|
+
const out = [];
|
|
1765
|
+
for (const f of files) {
|
|
1766
|
+
const id = f.replace(/\.ts$/, "");
|
|
1767
|
+
const src = fs12.readFileSync(path12.join(dir, f), "utf8");
|
|
1768
|
+
const cmdMatch = /\bcommand\s*:\s*(['"`])([^'"`]+)\1/.exec(src);
|
|
1769
|
+
if (cmdMatch) {
|
|
1770
|
+
const target = cmdMatch[2];
|
|
1771
|
+
if (!target.includes(".") && !commands.has(target)) {
|
|
1772
|
+
out.push({
|
|
1773
|
+
id: `statusBar.${id}`,
|
|
1774
|
+
level: "error",
|
|
1775
|
+
message: `statusBar ${id}: command "${target}" not found in src/commands/`
|
|
1776
|
+
});
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
out.push({ id: `statusBar.${id}`, level: "ok", message: `statusBar ${id} OK` });
|
|
1781
|
+
}
|
|
1782
|
+
return out;
|
|
1783
|
+
}
|
|
1784
|
+
function checkSubpanels(root) {
|
|
1785
|
+
const dir = path12.join(root, "src", "subpanels");
|
|
1786
|
+
if (!fs12.existsSync(dir))
|
|
1787
|
+
return [];
|
|
1788
|
+
const files = fs12.readdirSync(dir).filter((f) => f.endsWith(".ts"));
|
|
1789
|
+
if (files.length === 0)
|
|
1790
|
+
return [];
|
|
1791
|
+
const menus = new Set(listMenus(root));
|
|
1792
|
+
const out = [];
|
|
1793
|
+
for (const f of files) {
|
|
1794
|
+
const id = f.replace(/\.ts$/, "");
|
|
1795
|
+
const src = fs12.readFileSync(path12.join(dir, f), "utf8");
|
|
1796
|
+
const m = /\bmenu\s*:\s*(['"`])([^'"`]+)\1/.exec(src);
|
|
1797
|
+
if (!m) {
|
|
1798
|
+
out.push({ id: `subpanel.${id}`, level: "error", message: `subpanel ${id}: missing \`menu\` field` });
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1801
|
+
if (!menus.has(m[2])) {
|
|
1802
|
+
out.push({
|
|
1803
|
+
id: `subpanel.${id}`,
|
|
1804
|
+
level: "error",
|
|
1805
|
+
message: `subpanel ${id}: references unknown menu "${m[2]}"`
|
|
1806
|
+
});
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
out.push({ id: `subpanel.${id}`, level: "ok", message: `subpanel ${id} → ${m[2]}` });
|
|
1810
|
+
}
|
|
1811
|
+
return out;
|
|
1812
|
+
}
|
|
1813
|
+
function checkContributesSync(root, pkg) {
|
|
1814
|
+
const contributes = pkg?.contributes;
|
|
1815
|
+
if (!contributes) {
|
|
1816
|
+
return { id: "contributes", level: "warn", message: "package.json has no contributes — run `bun run gen`" };
|
|
1817
|
+
}
|
|
1818
|
+
const commands = new Set(listCommands(root));
|
|
1819
|
+
const panels = new Set(listPanels(root));
|
|
1820
|
+
const stale = [];
|
|
1821
|
+
const prefix = pkg?.vsceasy?.commandPrefix ?? pkg.name?.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9]+/g, "") ?? "";
|
|
1822
|
+
for (const c of contributes.commands ?? []) {
|
|
1823
|
+
const id = c.command ?? "";
|
|
1824
|
+
if (!id.startsWith(`${prefix}.`))
|
|
1825
|
+
continue;
|
|
1826
|
+
const suffix = id.slice(prefix.length + 1);
|
|
1827
|
+
if (suffix.startsWith("open")) {
|
|
1828
|
+
const panelId = suffix.slice(4).charAt(0).toLowerCase() + suffix.slice(5);
|
|
1829
|
+
if (!panels.has(panelId))
|
|
1830
|
+
stale.push(id);
|
|
1831
|
+
} else if (!commands.has(suffix)) {
|
|
1832
|
+
stale.push(id);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (stale.length === 0) {
|
|
1836
|
+
return { id: "contributes", level: "ok", message: "contributes in sync with src/" };
|
|
1837
|
+
}
|
|
1838
|
+
return {
|
|
1839
|
+
id: "contributes",
|
|
1840
|
+
level: "warn",
|
|
1841
|
+
message: `contributes: ${stale.length} stale entry(ies) — run \`bun run gen\``,
|
|
1842
|
+
details: stale
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function checkGitignore(root) {
|
|
1846
|
+
const file = path12.join(root, ".gitignore");
|
|
1847
|
+
const required = ["dist", "node_modules"];
|
|
1848
|
+
if (!fs12.existsSync(file)) {
|
|
1849
|
+
return {
|
|
1850
|
+
id: "gitignore",
|
|
1851
|
+
level: "warn",
|
|
1852
|
+
message: ".gitignore missing",
|
|
1853
|
+
fix: () => {
|
|
1854
|
+
fs12.writeFileSync(file, required.join(`
|
|
1855
|
+
`) + `
|
|
1856
|
+
`);
|
|
1857
|
+
return `created .gitignore with ${required.join(", ")}`;
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
const lines = fs12.readFileSync(file, "utf8").split(`
|
|
1862
|
+
`).map((l) => l.trim());
|
|
1863
|
+
const missing = required.filter((p) => !lines.some((l) => l === p || l === `/${p}` || l === `${p}/`));
|
|
1864
|
+
if (missing.length === 0)
|
|
1865
|
+
return { id: "gitignore", level: "ok", message: ".gitignore OK" };
|
|
1866
|
+
return {
|
|
1867
|
+
id: "gitignore",
|
|
1868
|
+
level: "warn",
|
|
1869
|
+
message: `.gitignore missing entries: ${missing.join(", ")}`,
|
|
1870
|
+
fix: () => {
|
|
1871
|
+
const raw = fs12.readFileSync(file, "utf8");
|
|
1872
|
+
const suffix = raw.endsWith(`
|
|
1873
|
+
`) ? "" : `
|
|
1874
|
+
`;
|
|
1875
|
+
fs12.writeFileSync(file, raw + suffix + missing.join(`
|
|
1876
|
+
`) + `
|
|
1877
|
+
`);
|
|
1878
|
+
return `appended to .gitignore: ${missing.join(", ")}`;
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
function checkActivationEvents(pkg) {
|
|
1883
|
+
const ae = pkg?.activationEvents;
|
|
1884
|
+
if (!Array.isArray(ae)) {
|
|
1885
|
+
return {
|
|
1886
|
+
id: "activationEvents",
|
|
1887
|
+
level: "warn",
|
|
1888
|
+
message: "package.json missing activationEvents — should be `[]` (auto-derived from commands)",
|
|
1889
|
+
fix: () => {
|
|
1890
|
+
pkg.activationEvents = [];
|
|
1891
|
+
return "Run `bun run gen` after fixing manually — auto-fix not safe to write package.json here";
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
if (ae.length === 0) {
|
|
1896
|
+
return { id: "activationEvents", level: "ok", message: "activationEvents auto-derived from commands" };
|
|
1897
|
+
}
|
|
1898
|
+
const suspect = ae.filter((e) => typeof e === "string" && /^onCommand:/.test(e));
|
|
1899
|
+
if (suspect.length) {
|
|
1900
|
+
return {
|
|
1901
|
+
id: "activationEvents",
|
|
1902
|
+
level: "warn",
|
|
1903
|
+
message: `activationEvents has ${suspect.length} redundant onCommand: entry(ies) — VS Code now auto-activates contributed commands`,
|
|
1904
|
+
details: suspect.map(String)
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
return { id: "activationEvents", level: "ok", message: `activationEvents ${ae.length} explicit entry(ies)` };
|
|
1908
|
+
}
|
|
1909
|
+
function checkMarketplaceIcon(root, pkg) {
|
|
1910
|
+
const iconRel = pkg?.icon;
|
|
1911
|
+
if (!iconRel) {
|
|
1912
|
+
return {
|
|
1913
|
+
id: "icon",
|
|
1914
|
+
level: "warn",
|
|
1915
|
+
message: "package.json has no `icon` — marketplace listings without an icon look broken"
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
const abs = path12.join(root, iconRel);
|
|
1919
|
+
if (!fs12.existsSync(abs)) {
|
|
1920
|
+
return { id: "icon", level: "error", message: `icon path \`${iconRel}\` not found on disk` };
|
|
1921
|
+
}
|
|
1922
|
+
const buf = fs12.readFileSync(abs);
|
|
1923
|
+
const isPng = buf.length > 24 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71;
|
|
1924
|
+
if (!isPng) {
|
|
1925
|
+
return { id: "icon", level: "warn", message: `icon \`${iconRel}\` is not a PNG — marketplace requires PNG ≥128×128` };
|
|
1926
|
+
}
|
|
1927
|
+
const w = buf.readUInt32BE(16);
|
|
1928
|
+
const h = buf.readUInt32BE(20);
|
|
1929
|
+
if (w < 128 || h < 128) {
|
|
1930
|
+
return {
|
|
1931
|
+
id: "icon",
|
|
1932
|
+
level: "warn",
|
|
1933
|
+
message: `icon is ${w}×${h} — marketplace requires ≥128×128 (use 128×128 square PNG)`
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
if (w !== h) {
|
|
1937
|
+
return { id: "icon", level: "warn", message: `icon is ${w}×${h} (non-square) — use a square PNG` };
|
|
1938
|
+
}
|
|
1939
|
+
return { id: "icon", level: "ok", message: `icon ${iconRel} (${w}×${h})` };
|
|
1940
|
+
}
|
|
1941
|
+
function checkGenScript(root) {
|
|
1942
|
+
const genPath = path12.join(root, "scripts", "gen.ts");
|
|
1943
|
+
if (!fs12.existsSync(genPath)) {
|
|
1944
|
+
return { id: "gen-script", level: "error", message: "scripts/gen.ts missing — run `vsceasy upgrade --apply=true`" };
|
|
1945
|
+
}
|
|
1946
|
+
const src = fs12.readFileSync(genPath, "utf8");
|
|
1947
|
+
const required = [
|
|
1948
|
+
{ name: "treeViews scan", needle: "TREE_VIEWS_DIR" },
|
|
1949
|
+
{ name: "jobs scan", needle: "JOBS_DIR" },
|
|
1950
|
+
{ name: "commandPalette when", needle: "commandPalette" }
|
|
1951
|
+
];
|
|
1952
|
+
const missing = required.filter((r) => !src.includes(r.needle));
|
|
1953
|
+
if (missing.length === 0) {
|
|
1954
|
+
return { id: "gen-script", level: "ok", message: "scripts/gen.ts up to date" };
|
|
1955
|
+
}
|
|
1956
|
+
return {
|
|
1957
|
+
id: "gen-script",
|
|
1958
|
+
level: "warn",
|
|
1959
|
+
message: `scripts/gen.ts is outdated (missing: ${missing.map((m) => m.name).join(", ")}) — run \`vsceasy upgrade --apply=true\``
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function readPkg(root) {
|
|
1963
|
+
const p = path12.join(root, "package.json");
|
|
1964
|
+
if (!fs12.existsSync(p))
|
|
1965
|
+
throw new Error(`package.json not found at ${p}`);
|
|
1966
|
+
return JSON.parse(fs12.readFileSync(p, "utf8"));
|
|
1967
|
+
}
|
|
1968
|
+
function parseInterfaces(src) {
|
|
1969
|
+
const out = {};
|
|
1970
|
+
const re = /export\s+interface\s+(\w+)\s*\{/g;
|
|
1971
|
+
let m;
|
|
1972
|
+
while (m = re.exec(src)) {
|
|
1973
|
+
const name = m[1];
|
|
1974
|
+
const bodyStart = m.index + m[0].length;
|
|
1975
|
+
const bodyEnd = matchBrace(src, bodyStart);
|
|
1976
|
+
if (bodyEnd === -1)
|
|
1977
|
+
continue;
|
|
1978
|
+
const body = src.slice(bodyStart, bodyEnd);
|
|
1979
|
+
const methods = [];
|
|
1980
|
+
const methRe = /(?:^|\n)\s*(\w+)\s*\(/g;
|
|
1981
|
+
let mm;
|
|
1982
|
+
while (mm = methRe.exec(body)) {
|
|
1983
|
+
const key = mm[1];
|
|
1984
|
+
if (!methods.includes(key))
|
|
1985
|
+
methods.push(key);
|
|
1986
|
+
}
|
|
1987
|
+
out[name] = methods;
|
|
1988
|
+
}
|
|
1989
|
+
return out;
|
|
1990
|
+
}
|
|
1991
|
+
function matchBrace(src, start) {
|
|
1992
|
+
let depth = 1;
|
|
1993
|
+
for (let i = start;i < src.length; i++) {
|
|
1994
|
+
const c = src[i];
|
|
1995
|
+
if (c === "{")
|
|
1996
|
+
depth++;
|
|
1997
|
+
else if (c === "}") {
|
|
1998
|
+
depth--;
|
|
1999
|
+
if (depth === 0)
|
|
2000
|
+
return i;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return -1;
|
|
2004
|
+
}
|
|
2005
|
+
function parseRpcHandlers(src) {
|
|
2006
|
+
const re = /\brpc\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/m;
|
|
2007
|
+
const m = re.exec(src);
|
|
2008
|
+
if (!m)
|
|
2009
|
+
return null;
|
|
2010
|
+
const start = m.index + m[0].length;
|
|
2011
|
+
let depth = 1;
|
|
2012
|
+
let end = -1;
|
|
2013
|
+
for (let i = start;i < src.length; i++) {
|
|
2014
|
+
const c = src[i];
|
|
2015
|
+
if (c === "{")
|
|
2016
|
+
depth++;
|
|
2017
|
+
else if (c === "}") {
|
|
2018
|
+
depth--;
|
|
2019
|
+
if (depth === 0) {
|
|
2020
|
+
end = i;
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (end === -1)
|
|
2026
|
+
return [];
|
|
2027
|
+
const body = src.slice(start, end);
|
|
2028
|
+
const handlers = [];
|
|
2029
|
+
const handRe = /(?:^|\n|,)\s*(?:async\s+)?(\w+)\s*\(/g;
|
|
2030
|
+
let mm;
|
|
2031
|
+
while (mm = handRe.exec(body)) {
|
|
2032
|
+
handlers.push(mm[1]);
|
|
2033
|
+
}
|
|
2034
|
+
return handlers;
|
|
2035
|
+
}
|
|
2036
|
+
function walk(items, fn) {
|
|
2037
|
+
for (const n of items) {
|
|
2038
|
+
fn(n);
|
|
2039
|
+
if (n.children)
|
|
2040
|
+
walk(n.children, fn);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
function applyFixes(report) {
|
|
2044
|
+
const applied = [];
|
|
2045
|
+
for (const r of report.results) {
|
|
2046
|
+
if (!r.fix)
|
|
2047
|
+
continue;
|
|
2048
|
+
try {
|
|
2049
|
+
const msg = r.fix();
|
|
2050
|
+
applied.push({ id: r.id, message: msg });
|
|
2051
|
+
} catch (e) {
|
|
2052
|
+
applied.push({ id: r.id, message: `fix failed: ${e?.message ?? e}` });
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return applied;
|
|
2056
|
+
}
|
|
2057
|
+
function addRpcHandlerStubs(panelFile, methods) {
|
|
2058
|
+
let src = fs12.readFileSync(panelFile, "utf8");
|
|
2059
|
+
const stubs = methods.map((m) => ` async ${m}() {
|
|
2060
|
+
// TODO: implement
|
|
2061
|
+
throw new Error('Not implemented');
|
|
2062
|
+
},`);
|
|
2063
|
+
const range = findRpcBodyRange2(src);
|
|
2064
|
+
if (range) {
|
|
2065
|
+
const inner = src.slice(range.start, range.end).replace(/\s+$/, "");
|
|
2066
|
+
const sep = inner ? `
|
|
2067
|
+
` : "";
|
|
2068
|
+
const newInner = `${inner}${sep}${stubs.join(`
|
|
2069
|
+
`)}
|
|
2070
|
+
`;
|
|
2071
|
+
src = src.slice(0, range.start) + newInner + src.slice(range.end);
|
|
2072
|
+
} else {
|
|
2073
|
+
const block = `
|
|
2074
|
+
rpc: (vscode) => ({
|
|
2075
|
+
${stubs.join(`
|
|
2076
|
+
`)}
|
|
2077
|
+
}),`;
|
|
2078
|
+
if (/title\s*:\s*['"][^'"]*['"]\s*,/.test(src)) {
|
|
2079
|
+
src = src.replace(/(title\s*:\s*['"][^'"]*['"]\s*,)/, `$1${block}`);
|
|
2080
|
+
} else {
|
|
2081
|
+
src = src.replace(/\}\s*\)\s*;?\s*$/, `${block}
|
|
2082
|
+
});
|
|
2083
|
+
`);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
fs12.writeFileSync(panelFile, src);
|
|
2087
|
+
}
|
|
2088
|
+
function findRpcBodyRange2(src) {
|
|
2089
|
+
const re = /\brpc\s*:\s*\([^)]*\)\s*=>\s*\(\s*\{/m;
|
|
2090
|
+
const m = re.exec(src);
|
|
2091
|
+
if (!m)
|
|
2092
|
+
return null;
|
|
2093
|
+
const start = m.index + m[0].length;
|
|
2094
|
+
const end = matchBrace(src, start);
|
|
2095
|
+
if (end === -1)
|
|
2096
|
+
return null;
|
|
2097
|
+
return { start, end };
|
|
2098
|
+
}
|
|
2099
|
+
function removeOrphanMenuItems(menuFile, orphans) {
|
|
2100
|
+
let src = fs12.readFileSync(menuFile, "utf8");
|
|
2101
|
+
let removed = 0;
|
|
2102
|
+
for (const o of orphans) {
|
|
2103
|
+
const targetRe = new RegExp(`${o.kind}\\s*:\\s*(['"\`])${escapeRe2(o.target)}\\1`);
|
|
2104
|
+
while (true) {
|
|
2105
|
+
const m = targetRe.exec(src);
|
|
2106
|
+
if (!m)
|
|
2107
|
+
break;
|
|
2108
|
+
const idx = m.index;
|
|
2109
|
+
const openIdx = findEnclosingObjectStart(src, idx);
|
|
2110
|
+
if (openIdx === -1) {
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
const closeIdx = matchBrace(src, openIdx + 1);
|
|
2114
|
+
if (closeIdx === -1)
|
|
2115
|
+
break;
|
|
2116
|
+
let end = closeIdx + 1;
|
|
2117
|
+
while (end < src.length && /[\s,]/.test(src[end]) && src[end] !== `
|
|
2118
|
+
`)
|
|
2119
|
+
end++;
|
|
2120
|
+
if (src[end] === ",")
|
|
2121
|
+
end++;
|
|
2122
|
+
let start = openIdx;
|
|
2123
|
+
while (start > 0 && /[ \t]/.test(src[start - 1]))
|
|
2124
|
+
start--;
|
|
2125
|
+
src = src.slice(0, start) + src.slice(end);
|
|
2126
|
+
removed++;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
fs12.writeFileSync(menuFile, src);
|
|
2130
|
+
return removed;
|
|
2131
|
+
}
|
|
2132
|
+
function escapeRe2(s) {
|
|
2133
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2134
|
+
}
|
|
2135
|
+
function findEnclosingObjectStart(src, from) {
|
|
2136
|
+
let depth = 0;
|
|
2137
|
+
for (let i = from;i >= 0; i--) {
|
|
2138
|
+
const c = src[i];
|
|
2139
|
+
if (c === "}")
|
|
2140
|
+
depth++;
|
|
2141
|
+
else if (c === "{") {
|
|
2142
|
+
if (depth === 0)
|
|
2143
|
+
return i;
|
|
2144
|
+
depth--;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return -1;
|
|
2148
|
+
}
|
|
2149
|
+
var fs12, path12;
|
|
2150
|
+
var init_doctor = __esm(() => {
|
|
2151
|
+
init_edit();
|
|
2152
|
+
init_menuTree();
|
|
2153
|
+
init_codicons();
|
|
2154
|
+
fs12 = __toESM(require("fs"));
|
|
2155
|
+
path12 = __toESM(require("path"));
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
// src/lib/upgrade.ts
|
|
2159
|
+
function upgrade(opts) {
|
|
2160
|
+
const ui = opts.ui ?? "react";
|
|
2161
|
+
const sourceRoot = path13.join(opts.templatesRoot, ui);
|
|
2162
|
+
if (!fs13.existsSync(sourceRoot)) {
|
|
2163
|
+
throw new Error(`Templates UI not found: ${sourceRoot}`);
|
|
2164
|
+
}
|
|
2165
|
+
const changes = [];
|
|
2166
|
+
for (const rel of SYNC_PATHS) {
|
|
2167
|
+
const src = path13.join(sourceRoot, rel);
|
|
2168
|
+
const dest = path13.join(opts.projectRoot, rel);
|
|
2169
|
+
if (!fs13.existsSync(src)) {
|
|
2170
|
+
changes.push({ path: rel, status: "missing-source" });
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
if (!fs13.existsSync(dest)) {
|
|
2174
|
+
if (opts.apply) {
|
|
2175
|
+
fs13.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
2176
|
+
fs13.copyFileSync(src, dest);
|
|
2177
|
+
changes.push({ path: rel, status: "created" });
|
|
2178
|
+
} else {
|
|
2179
|
+
changes.push({ path: rel, status: "would-create" });
|
|
2180
|
+
}
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
const same = filesEqual(src, dest);
|
|
2184
|
+
if (same) {
|
|
2185
|
+
changes.push({ path: rel, status: "in-sync" });
|
|
2186
|
+
} else if (opts.apply) {
|
|
2187
|
+
fs13.copyFileSync(src, dest);
|
|
2188
|
+
changes.push({ path: rel, status: "updated" });
|
|
2189
|
+
} else {
|
|
2190
|
+
changes.push({ path: rel, status: "would-update" });
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
let genRan = false;
|
|
2194
|
+
if (opts.apply && opts.runGen !== false) {
|
|
2195
|
+
if (changes.some((c) => c.status === "updated" || c.status === "created")) {
|
|
2196
|
+
genRan = runGen8(opts.projectRoot);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
return { changes, applied: !!opts.apply, genRan };
|
|
2200
|
+
}
|
|
2201
|
+
function filesEqual(a, b) {
|
|
2202
|
+
const sa = fs13.statSync(a);
|
|
2203
|
+
const sb = fs13.statSync(b);
|
|
2204
|
+
if (sa.size !== sb.size)
|
|
2205
|
+
return false;
|
|
2206
|
+
return fs13.readFileSync(a).equals(fs13.readFileSync(b));
|
|
2207
|
+
}
|
|
2208
|
+
function runGen8(cwd) {
|
|
2209
|
+
const tryRun = (cmd, args) => {
|
|
2210
|
+
const r = import_child_process8.spawnSync(cmd, args, { cwd, stdio: "inherit" });
|
|
2211
|
+
return r.status === 0;
|
|
2212
|
+
};
|
|
2213
|
+
if (which8("bun") && tryRun("bun", ["run", "gen"]))
|
|
2214
|
+
return true;
|
|
2215
|
+
if (which8("npm") && tryRun("npm", ["run", "gen"]))
|
|
2216
|
+
return true;
|
|
2217
|
+
return false;
|
|
2218
|
+
}
|
|
2219
|
+
function which8(cmd) {
|
|
2220
|
+
const r = import_child_process8.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
|
|
2221
|
+
return r.status === 0;
|
|
2222
|
+
}
|
|
2223
|
+
var fs13, path13, import_child_process8, SYNC_PATHS;
|
|
2224
|
+
var init_upgrade = __esm(() => {
|
|
2225
|
+
fs13 = __toESM(require("fs"));
|
|
2226
|
+
path13 = __toESM(require("path"));
|
|
2227
|
+
import_child_process8 = require("child_process");
|
|
2228
|
+
SYNC_PATHS = [
|
|
2229
|
+
"src/shared/vsceasy/define.ts",
|
|
2230
|
+
"src/shared/vsceasy/bootstrap.ts",
|
|
2231
|
+
"src/shared/vsceasy/rpc.ts",
|
|
2232
|
+
"src/shared/vsceasy/client.ts",
|
|
2233
|
+
"src/shared/vsceasy/index.ts",
|
|
2234
|
+
"src/shared/vsceasy/codiconNames.ts",
|
|
2235
|
+
"scripts/gen.ts",
|
|
2236
|
+
"vite.config.ts"
|
|
2237
|
+
];
|
|
2238
|
+
});
|
|
2239
|
+
|
|
2240
|
+
// src/lib/findProject.ts
|
|
2241
|
+
function findProjectRoot(start = process.cwd()) {
|
|
2242
|
+
let dir = path14.resolve(start);
|
|
2243
|
+
const { root } = path14.parse(dir);
|
|
2244
|
+
while (true) {
|
|
2245
|
+
const pkgPath = path14.join(dir, "package.json");
|
|
2246
|
+
if (fs14.existsSync(pkgPath)) {
|
|
2247
|
+
try {
|
|
2248
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
2249
|
+
const genScript = pkg?.scripts?.gen;
|
|
2250
|
+
const hasScan = typeof pkg?.scripts?.["gen:scan"] === "string";
|
|
2251
|
+
if (genScript === "bun scripts/gen.ts" || hasScan)
|
|
2252
|
+
return dir;
|
|
2253
|
+
} catch {}
|
|
2254
|
+
}
|
|
2255
|
+
if (dir === root) {
|
|
2256
|
+
throw new Error(`Not inside a vsceasy project (no package.json with scripts.gen="bun scripts/gen.ts" found above ${start}).`);
|
|
2257
|
+
}
|
|
2258
|
+
dir = path14.dirname(dir);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function findTemplatesRoot(fromFile = __dirname) {
|
|
2262
|
+
const candidates = [
|
|
2263
|
+
path14.resolve(fromFile, "..", "templates"),
|
|
2264
|
+
path14.resolve(fromFile, "..", "..", "templates"),
|
|
2265
|
+
path14.resolve(fromFile, "..", "..", "..", "templates")
|
|
2266
|
+
];
|
|
2267
|
+
for (const c of candidates)
|
|
2268
|
+
if (fs14.existsSync(c))
|
|
2269
|
+
return c;
|
|
2270
|
+
throw new Error(`templates/ directory not found near ${fromFile}`);
|
|
2271
|
+
}
|
|
2272
|
+
var fs14, path14, __dirname = "/home/runner/work/vsceasy/vsceasy/src/lib";
|
|
2273
|
+
var init_findProject = __esm(() => {
|
|
2274
|
+
fs14 = __toESM(require("fs"));
|
|
2275
|
+
path14 = __toESM(require("path"));
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
// src/lib/treeView/add.ts
|
|
2279
|
+
function addTreeView(opts) {
|
|
2280
|
+
const name = assertId("tree view name", normalizeCamel6(opts.name));
|
|
2281
|
+
const menu = assertId("menu id", normalizeCamel6(opts.menu));
|
|
2282
|
+
assertSiblingExists(opts.projectRoot, "menu", menu);
|
|
2283
|
+
const title = opts.title?.trim() || pascal(name);
|
|
2284
|
+
const target = path15.join(opts.projectRoot, "src", "treeViews", `${name}.ts`);
|
|
2285
|
+
assertNoOverwrite(opts.projectRoot, target, "Tree view");
|
|
2286
|
+
const tpl = path15.join(opts.templatesRoot, "_generators", "treeView", "treeView.ts.tpl");
|
|
2287
|
+
const body = substitute(fs15.readFileSync(tpl, "utf8"), { name, Name: pascal(name), title, menu });
|
|
2288
|
+
fs15.mkdirSync(path15.dirname(target), { recursive: true });
|
|
2289
|
+
fs15.writeFileSync(target, body);
|
|
2290
|
+
let genRan = false;
|
|
2291
|
+
if (opts.runGen !== false)
|
|
2292
|
+
genRan = runGen9(opts.projectRoot);
|
|
2293
|
+
return { created: [target], genRan };
|
|
2294
|
+
}
|
|
2295
|
+
function runGen9(cwd) {
|
|
2296
|
+
const tryRun = (cmd, args) => import_child_process9.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
|
|
2297
|
+
if (which9("bun") && tryRun("bun", ["run", "gen"]))
|
|
2298
|
+
return true;
|
|
2299
|
+
if (which9("npm") && tryRun("npm", ["run", "gen"]))
|
|
2300
|
+
return true;
|
|
2301
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
2302
|
+
return false;
|
|
2303
|
+
}
|
|
2304
|
+
function which9(cmd) {
|
|
2305
|
+
return import_child_process9.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
|
|
2306
|
+
}
|
|
2307
|
+
function normalizeCamel6(s) {
|
|
2308
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
2309
|
+
if (!cleaned)
|
|
2310
|
+
return "";
|
|
2311
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
2312
|
+
}
|
|
2313
|
+
function pascal(s) {
|
|
2314
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2315
|
+
}
|
|
2316
|
+
var fs15, path15, import_child_process9;
|
|
2317
|
+
var init_add8 = __esm(() => {
|
|
2318
|
+
init_scaffold();
|
|
2319
|
+
init_validate();
|
|
2320
|
+
fs15 = __toESM(require("fs"));
|
|
2321
|
+
path15 = __toESM(require("path"));
|
|
2322
|
+
import_child_process9 = require("child_process");
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
// src/lib/testSetup/index.ts
|
|
2326
|
+
function setupTests(opts) {
|
|
2327
|
+
const created = [];
|
|
2328
|
+
const vitestCfg = path16.join(opts.projectRoot, "vitest.config.ts");
|
|
2329
|
+
if (!fs16.existsSync(vitestCfg) || opts.force) {
|
|
2330
|
+
fs16.writeFileSync(vitestCfg, fs16.readFileSync(path16.join(opts.templatesRoot, "_generators", "test", "vitest.config.ts.tpl"), "utf8"));
|
|
2331
|
+
created.push(vitestCfg);
|
|
2332
|
+
}
|
|
2333
|
+
const sampleDir = path16.join(opts.projectRoot, "src", "__tests__");
|
|
2334
|
+
fs16.mkdirSync(sampleDir, { recursive: true });
|
|
2335
|
+
const sample = path16.join(sampleDir, "sample.test.ts");
|
|
2336
|
+
if (!fs16.existsSync(sample) || opts.force) {
|
|
2337
|
+
fs16.writeFileSync(sample, fs16.readFileSync(path16.join(opts.templatesRoot, "_generators", "test", "sample.test.ts.tpl"), "utf8"));
|
|
2338
|
+
created.push(sample);
|
|
2339
|
+
}
|
|
2340
|
+
const helpers = path16.join(sampleDir, "_helpers.ts");
|
|
2341
|
+
if (!fs16.existsSync(helpers) || opts.force) {
|
|
2342
|
+
fs16.writeFileSync(helpers, fs16.readFileSync(path16.join(opts.templatesRoot, "_generators", "test", "_helpers.ts.tpl"), "utf8"));
|
|
2343
|
+
created.push(helpers);
|
|
2344
|
+
}
|
|
2345
|
+
const mocksDir = path16.join(sampleDir, "__mocks__");
|
|
2346
|
+
fs16.mkdirSync(mocksDir, { recursive: true });
|
|
2347
|
+
const vscodeStub = path16.join(mocksDir, "vscode.ts");
|
|
2348
|
+
if (!fs16.existsSync(vscodeStub) || opts.force) {
|
|
2349
|
+
fs16.writeFileSync(vscodeStub, fs16.readFileSync(path16.join(opts.templatesRoot, "_generators", "test", "vscode.stub.ts.tpl"), "utf8"));
|
|
2350
|
+
created.push(vscodeStub);
|
|
2351
|
+
}
|
|
2352
|
+
const pkgPath = path16.join(opts.projectRoot, "package.json");
|
|
2353
|
+
const pkg = JSON.parse(fs16.readFileSync(pkgPath, "utf8"));
|
|
2354
|
+
pkg.scripts ??= {};
|
|
2355
|
+
let pkgUpdated = false;
|
|
2356
|
+
if (!pkg.scripts.test || pkg.scripts.test === 'echo "Error: no test specified" && exit 1') {
|
|
2357
|
+
pkg.scripts.test = "vitest run";
|
|
2358
|
+
pkgUpdated = true;
|
|
2359
|
+
}
|
|
2360
|
+
if (!pkg.scripts["test:watch"]) {
|
|
2361
|
+
pkg.scripts["test:watch"] = "vitest";
|
|
2362
|
+
pkgUpdated = true;
|
|
2363
|
+
}
|
|
2364
|
+
pkg.devDependencies ??= {};
|
|
2365
|
+
if (!pkg.devDependencies.vitest) {
|
|
2366
|
+
pkg.devDependencies.vitest = "^2.0.0";
|
|
2367
|
+
pkgUpdated = true;
|
|
2368
|
+
}
|
|
2369
|
+
if (pkgUpdated) {
|
|
2370
|
+
fs16.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
2371
|
+
`);
|
|
2372
|
+
}
|
|
2373
|
+
return { created, pkgUpdated };
|
|
2374
|
+
}
|
|
2375
|
+
var fs16, path16;
|
|
2376
|
+
var init_testSetup = __esm(() => {
|
|
2377
|
+
fs16 = __toESM(require("fs"));
|
|
2378
|
+
path16 = __toESM(require("path"));
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
// src/lib/publish/init.ts
|
|
2382
|
+
function publishInit(opts) {
|
|
2383
|
+
const created = [];
|
|
2384
|
+
const warnings = [];
|
|
2385
|
+
const pkgPath = path17.join(opts.projectRoot, "package.json");
|
|
2386
|
+
if (!fs17.existsSync(pkgPath))
|
|
2387
|
+
throw new Error(`No package.json at ${opts.projectRoot}`);
|
|
2388
|
+
const pkg = JSON.parse(fs17.readFileSync(pkgPath, "utf8"));
|
|
2389
|
+
const vars = {
|
|
2390
|
+
displayName: pkg.displayName ?? pkg.name ?? "My Extension",
|
|
2391
|
+
description: pkg.description ?? ""
|
|
2392
|
+
};
|
|
2393
|
+
const readme = path17.join(opts.projectRoot, "README.md");
|
|
2394
|
+
if (!fs17.existsSync(readme)) {
|
|
2395
|
+
fs17.writeFileSync(readme, substitute(fs17.readFileSync(path17.join(opts.templatesRoot, "_generators", "publish", "README.md.tpl"), "utf8"), vars));
|
|
2396
|
+
created.push(readme);
|
|
2397
|
+
}
|
|
2398
|
+
const changelog = path17.join(opts.projectRoot, "CHANGELOG.md");
|
|
2399
|
+
if (!fs17.existsSync(changelog)) {
|
|
2400
|
+
fs17.writeFileSync(changelog, fs17.readFileSync(path17.join(opts.templatesRoot, "_generators", "publish", "CHANGELOG.md.tpl"), "utf8"));
|
|
2401
|
+
created.push(changelog);
|
|
2402
|
+
}
|
|
2403
|
+
const license = path17.join(opts.projectRoot, "LICENSE");
|
|
2404
|
+
if (!fs17.existsSync(license)) {
|
|
2405
|
+
warnings.push("LICENSE not found — marketplace recommends an explicit license file.");
|
|
2406
|
+
}
|
|
2407
|
+
const iconDir = path17.join(opts.projectRoot, "assets");
|
|
2408
|
+
const iconFile = path17.join(iconDir, "icon.png");
|
|
2409
|
+
if (!fs17.existsSync(iconFile)) {
|
|
2410
|
+
fs17.mkdirSync(iconDir, { recursive: true });
|
|
2411
|
+
fs17.writeFileSync(iconFile + ".placeholder", `Drop a 128×128 PNG named icon.png here.
|
|
2412
|
+
`);
|
|
2413
|
+
warnings.push("No `assets/icon.png` yet — marketplace requires a 128×128 icon.");
|
|
2414
|
+
}
|
|
2415
|
+
let pkgUpdated = false;
|
|
2416
|
+
if (!pkg.publisher || pkg.publisher === "your-publisher") {
|
|
2417
|
+
warnings.push("`publisher` in package.json is missing or placeholder. Set it before publishing.");
|
|
2418
|
+
}
|
|
2419
|
+
if (!pkg.repository) {
|
|
2420
|
+
pkg.repository = { type: "git", url: "" };
|
|
2421
|
+
pkgUpdated = true;
|
|
2422
|
+
warnings.push("Added empty `repository.url` — fill it before publishing.");
|
|
2423
|
+
}
|
|
2424
|
+
if (!pkg.icon && fs17.existsSync(iconFile)) {
|
|
2425
|
+
pkg.icon = "assets/icon.png";
|
|
2426
|
+
pkgUpdated = true;
|
|
2427
|
+
}
|
|
2428
|
+
if (!pkg.categories) {
|
|
2429
|
+
pkg.categories = ["Other"];
|
|
2430
|
+
pkgUpdated = true;
|
|
2431
|
+
}
|
|
2432
|
+
if (pkgUpdated)
|
|
2433
|
+
fs17.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
2434
|
+
`);
|
|
2435
|
+
let dryPackOk = null;
|
|
2436
|
+
if (opts.runDryPack !== false) {
|
|
2437
|
+
const r = import_child_process10.spawnSync("npx", ["--yes", "@vscode/vsce", "ls"], { cwd: opts.projectRoot, stdio: "inherit" });
|
|
2438
|
+
dryPackOk = r.status === 0;
|
|
2439
|
+
if (!dryPackOk)
|
|
2440
|
+
warnings.push("`vsce ls` exited non-zero — inspect output above.");
|
|
2441
|
+
}
|
|
2442
|
+
return { created, pkgUpdated, warnings, dryPackOk };
|
|
2443
|
+
}
|
|
2444
|
+
var fs17, path17, import_child_process10;
|
|
2445
|
+
var init_init = __esm(() => {
|
|
2446
|
+
init_scaffold();
|
|
2447
|
+
fs17 = __toESM(require("fs"));
|
|
2448
|
+
path17 = __toESM(require("path"));
|
|
2449
|
+
import_child_process10 = require("child_process");
|
|
2450
|
+
});
|
|
2451
|
+
|
|
2452
|
+
// src/lib/helper/add.ts
|
|
2453
|
+
function addHelper(opts) {
|
|
2454
|
+
if (!HELPER_KINDS.includes(opts.kind)) {
|
|
2455
|
+
throw new Error(`Unknown helper kind: "${opts.kind}". Available: ${HELPER_KINDS.map((k) => `"${k}"`).join(", ")}.`);
|
|
2456
|
+
}
|
|
2457
|
+
const cfg = readConfig(opts.projectRoot);
|
|
2458
|
+
const pkgPath = path18.join(opts.projectRoot, "package.json");
|
|
2459
|
+
const pkg = fs18.existsSync(pkgPath) ? JSON.parse(fs18.readFileSync(pkgPath, "utf8")) : {};
|
|
2460
|
+
const vars = {
|
|
2461
|
+
commandPrefix: cfg.commandPrefix ?? pkg.vsceasy?.commandPrefix ?? deriveCommandPrefix(pkg.name ?? "ext"),
|
|
2462
|
+
displayName: pkg.displayName ?? pkg.name ?? "My Extension"
|
|
2463
|
+
};
|
|
2464
|
+
const tpl = path18.join(opts.templatesRoot, "_generators", "helper", `${opts.kind}.ts.tpl`);
|
|
2465
|
+
if (!fs18.existsSync(tpl))
|
|
2466
|
+
throw new Error(`Helper template missing: ${tpl}`);
|
|
2467
|
+
const targetDir = path18.join(opts.projectRoot, "src", "helpers");
|
|
2468
|
+
fs18.mkdirSync(targetDir, { recursive: true });
|
|
2469
|
+
const target = path18.join(targetDir, `${opts.kind}.ts`);
|
|
2470
|
+
const created = [];
|
|
2471
|
+
const skipped = [];
|
|
2472
|
+
if (fs18.existsSync(target) && !opts.force) {
|
|
2473
|
+
skipped.push(target);
|
|
2474
|
+
} else {
|
|
2475
|
+
fs18.writeFileSync(target, substitute(fs18.readFileSync(tpl, "utf8"), vars));
|
|
2476
|
+
created.push(target);
|
|
2477
|
+
}
|
|
2478
|
+
return { created, skipped };
|
|
2479
|
+
}
|
|
2480
|
+
function deriveCommandPrefix(name) {
|
|
2481
|
+
return name.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9]+/g, "");
|
|
2482
|
+
}
|
|
2483
|
+
var fs18, path18, HELPER_KINDS;
|
|
2484
|
+
var init_add9 = __esm(() => {
|
|
2485
|
+
init_scaffold();
|
|
2486
|
+
init_config();
|
|
2487
|
+
fs18 = __toESM(require("fs"));
|
|
2488
|
+
path18 = __toESM(require("path"));
|
|
2489
|
+
HELPER_KINDS = ["secrets", "config", "state", "notifications", "cache"];
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
// src/lib/job/add.ts
|
|
2493
|
+
function addJob(opts) {
|
|
2494
|
+
const name = assertId("job name", normalizeCamel7(opts.name));
|
|
2495
|
+
const Pascal = name.charAt(0).toUpperCase() + name.slice(1);
|
|
2496
|
+
const title = opts.title?.trim() || Pascal;
|
|
2497
|
+
const target = path19.join(opts.projectRoot, "src", "jobs", `${name}.ts`);
|
|
2498
|
+
assertNoOverwrite(opts.projectRoot, target, "Job");
|
|
2499
|
+
const tpl = path19.join(opts.templatesRoot, "_generators", "job", "job.ts.tpl");
|
|
2500
|
+
if (!fs19.existsSync(tpl))
|
|
2501
|
+
throw new Error(`Job template missing: ${tpl}`);
|
|
2502
|
+
const vars = {
|
|
2503
|
+
name,
|
|
2504
|
+
Name: Pascal,
|
|
2505
|
+
title,
|
|
2506
|
+
schedule: stringifyTrigger(opts.trigger),
|
|
2507
|
+
minIntervalLine: opts.minIntervalMs ? `
|
|
2508
|
+
minIntervalMs: ${opts.minIntervalMs},` : ""
|
|
2509
|
+
};
|
|
2510
|
+
fs19.mkdirSync(path19.dirname(target), { recursive: true });
|
|
2511
|
+
fs19.writeFileSync(target, substitute(fs19.readFileSync(tpl, "utf8"), vars));
|
|
2512
|
+
let genRan = false;
|
|
2513
|
+
if (opts.runGen !== false)
|
|
2514
|
+
genRan = runGen10(opts.projectRoot);
|
|
2515
|
+
return { created: [target], genRan };
|
|
2516
|
+
}
|
|
2517
|
+
function stringifyTrigger(t) {
|
|
2518
|
+
if ("every" in t)
|
|
2519
|
+
return `{ every: '${escape(t.every)}' }`;
|
|
2520
|
+
if ("dailyAt" in t)
|
|
2521
|
+
return `{ dailyAt: '${escape(t.dailyAt)}' }`;
|
|
2522
|
+
if ("on" in t)
|
|
2523
|
+
return `{ on: '${escape(t.on)}' }`;
|
|
2524
|
+
if ("onFile" in t)
|
|
2525
|
+
return `{ onFile: '${escape(t.onFile)}' }`;
|
|
2526
|
+
throw new Error("Job requires a schedule: --every | --dailyAt | --on | --onFile");
|
|
2527
|
+
}
|
|
2528
|
+
function escape(s) {
|
|
2529
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
2530
|
+
}
|
|
2531
|
+
function runGen10(cwd) {
|
|
2532
|
+
const tryRun = (cmd, args) => import_child_process11.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
|
|
2533
|
+
if (which10("bun") && tryRun("bun", ["run", "gen"]))
|
|
2534
|
+
return true;
|
|
2535
|
+
if (which10("npm") && tryRun("npm", ["run", "gen"]))
|
|
2536
|
+
return true;
|
|
2537
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
function which10(cmd) {
|
|
2541
|
+
return import_child_process11.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
|
|
2542
|
+
}
|
|
2543
|
+
function normalizeCamel7(s) {
|
|
2544
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
2545
|
+
if (!cleaned)
|
|
2546
|
+
return "";
|
|
2547
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
2548
|
+
}
|
|
2549
|
+
var fs19, path19, import_child_process11;
|
|
2550
|
+
var init_add10 = __esm(() => {
|
|
2551
|
+
init_scaffold();
|
|
2552
|
+
init_validate();
|
|
2553
|
+
fs19 = __toESM(require("fs"));
|
|
2554
|
+
path19 = __toESM(require("path"));
|
|
2555
|
+
import_child_process11 = require("child_process");
|
|
2556
|
+
});
|
|
2557
|
+
|
|
2558
|
+
// src/lib/db/init.ts
|
|
2559
|
+
function initDb(opts) {
|
|
2560
|
+
const provider = opts.provider ?? "storage";
|
|
2561
|
+
const targetDir = path20.join(opts.projectRoot, "src", "helpers");
|
|
2562
|
+
fs20.mkdirSync(targetDir, { recursive: true });
|
|
2563
|
+
const target = path20.join(targetDir, "db.ts");
|
|
2564
|
+
const tpl = path20.join(opts.templatesRoot, "_generators", "helper", "db.ts.tpl");
|
|
2565
|
+
if (!fs20.existsSync(tpl))
|
|
2566
|
+
throw new Error(`db template missing: ${tpl}`);
|
|
2567
|
+
const created = [];
|
|
2568
|
+
const skipped = [];
|
|
2569
|
+
if (fs20.existsSync(target) && !opts.force) {
|
|
2570
|
+
skipped.push(target);
|
|
2571
|
+
} else {
|
|
2572
|
+
const body = substitute(fs20.readFileSync(tpl, "utf8"), { provider });
|
|
2573
|
+
fs20.writeFileSync(target, body);
|
|
2574
|
+
created.push(target);
|
|
2575
|
+
}
|
|
2576
|
+
return { created, skipped, path: target, provider };
|
|
2577
|
+
}
|
|
2578
|
+
function dbExists(projectRoot) {
|
|
2579
|
+
return fs20.existsSync(path20.join(projectRoot, "src", "helpers", "db.ts"));
|
|
2580
|
+
}
|
|
2581
|
+
var fs20, path20;
|
|
2582
|
+
var init_init2 = __esm(() => {
|
|
2583
|
+
init_scaffold();
|
|
2584
|
+
fs20 = __toESM(require("fs"));
|
|
2585
|
+
path20 = __toESM(require("path"));
|
|
2586
|
+
});
|
|
2587
|
+
|
|
2588
|
+
// src/lib/db/wire.ts
|
|
2589
|
+
function wireInitDb(projectRoot, importPath = "../helpers/db") {
|
|
2590
|
+
const entry = path21.join(projectRoot, "src", "extension", "extension.ts");
|
|
2591
|
+
if (!fs21.existsSync(entry)) {
|
|
2592
|
+
return { status: "no-entry", path: entry };
|
|
2593
|
+
}
|
|
2594
|
+
let src = fs21.readFileSync(entry, "utf8");
|
|
2595
|
+
if (/from\s+['"]\.\.\/helpers\/db['"]/.test(src) && /onActivate\s*:\s*\[[^\]]*initDb/.test(src)) {
|
|
2596
|
+
return { status: "already-wired", path: entry, message: "initDb already in onActivate hooks." };
|
|
2597
|
+
}
|
|
2598
|
+
if (!/from\s+['"]\.\.\/helpers\/db['"]/.test(src)) {
|
|
2599
|
+
const importLine = `import { initDb } from '${importPath}';
|
|
2600
|
+
`;
|
|
2601
|
+
const lastImport = src.match(/(^import[^\n]*\n)+/m);
|
|
2602
|
+
src = lastImport ? src.slice(0, lastImport.index + lastImport[0].length) + importLine + src.slice(lastImport.index + lastImport[0].length) : importLine + src;
|
|
2603
|
+
} else if (!/\binitDb\b/.test(src)) {
|
|
2604
|
+
src = src.replace(/import\s+\{([^}]*)\}\s+from\s+(['"])\.\.\/helpers\/db\2/, (_m, names, q) => {
|
|
2605
|
+
const set = new Set(String(names).split(",").map((n) => n.trim()).filter(Boolean));
|
|
2606
|
+
set.add("initDb");
|
|
2607
|
+
return `import { ${[...set].join(", ")} } from ${q}../helpers/db${q}`;
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
const callRe = /\bbootstrap\s*\(\s*registry\s*(,\s*\{[\s\S]*?\})?\s*\)/;
|
|
2611
|
+
const m = callRe.exec(src);
|
|
2612
|
+
if (!m) {
|
|
2613
|
+
return { status: "unrecognized", path: entry, message: "Could not locate `bootstrap(registry)` call. Wire manually:\n bootstrap(registry, { onActivate: [initDb] })" };
|
|
2614
|
+
}
|
|
2615
|
+
if (!m[1]) {
|
|
2616
|
+
src = src.replace(callRe, "bootstrap(registry, { onActivate: [initDb] })");
|
|
2617
|
+
} else {
|
|
2618
|
+
const opts = m[1];
|
|
2619
|
+
let nextOpts;
|
|
2620
|
+
if (/onActivate\s*:\s*\[/.test(opts)) {
|
|
2621
|
+
nextOpts = opts.replace(/onActivate\s*:\s*\[([^\]]*)\]/, (_o, inner) => {
|
|
2622
|
+
const items = inner.split(",").map((x) => x.trim()).filter(Boolean);
|
|
2623
|
+
if (items.includes("initDb"))
|
|
2624
|
+
return `onActivate: [${items.join(", ")}]`;
|
|
2625
|
+
return `onActivate: [${[...items, "initDb"].join(", ")}]`;
|
|
2626
|
+
});
|
|
2627
|
+
} else {
|
|
2628
|
+
nextOpts = opts.replace(/\{/, "{ onActivate: [initDb],");
|
|
2629
|
+
}
|
|
2630
|
+
src = src.replace(callRe, `bootstrap(registry${nextOpts})`);
|
|
2631
|
+
}
|
|
2632
|
+
fs21.writeFileSync(entry, src);
|
|
2633
|
+
return { status: "wired", path: entry };
|
|
2634
|
+
}
|
|
2635
|
+
var fs21, path21;
|
|
2636
|
+
var init_wire = __esm(() => {
|
|
2637
|
+
fs21 = __toESM(require("fs"));
|
|
2638
|
+
path21 = __toESM(require("path"));
|
|
2639
|
+
});
|
|
2640
|
+
|
|
2641
|
+
// src/lib/crud/parseModel.ts
|
|
2642
|
+
function parseModelFile(file) {
|
|
2643
|
+
if (!fs22.existsSync(file))
|
|
2644
|
+
throw new Error(`Model file not found: ${file}`);
|
|
2645
|
+
const src = fs22.readFileSync(file, "utf8");
|
|
2646
|
+
const ifaceMatch = /export\s+interface\s+([A-Z][A-Za-z0-9_]*)\s*\{([\s\S]*?)\n\}/.exec(src);
|
|
2647
|
+
if (!ifaceMatch) {
|
|
2648
|
+
throw new Error(`Model "${path22.basename(file)}" does not declare \`export interface\`.`);
|
|
2649
|
+
}
|
|
2650
|
+
const name = ifaceMatch[1];
|
|
2651
|
+
const body = ifaceMatch[2];
|
|
2652
|
+
const fields = parseInterfaceBody(body);
|
|
2653
|
+
if (fields.length === 0) {
|
|
2654
|
+
throw new Error(`Model "${name}" has no fields — add at least one before generating CRUD.`);
|
|
2655
|
+
}
|
|
2656
|
+
const pluralMatch = /export\s+const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*defineEntity\s*<\s*[^>]+>\s*\(\s*(['"`])([^'"`]+)\2\s*,\s*\{([\s\S]*?)\}\s*\)/m.exec(src);
|
|
2657
|
+
if (!pluralMatch) {
|
|
2658
|
+
throw new Error(`Model "${name}" does not call \`defineEntity\`. Was it generated by \`vsceasy model add\`?`);
|
|
2659
|
+
}
|
|
2660
|
+
const plural = pluralMatch[1];
|
|
2661
|
+
const collection = pluralMatch[3];
|
|
2662
|
+
const entityBody = pluralMatch[4];
|
|
2663
|
+
const pk = /\bprimaryKey\s*:\s*(['"`])([^'"`]+)\1/.exec(entityBody)?.[2];
|
|
2664
|
+
if (!pk)
|
|
2665
|
+
throw new Error(`Model "${name}" defineEntity is missing primaryKey.`);
|
|
2666
|
+
const indexes = [];
|
|
2667
|
+
const idxMatch = /\bindexes\s*:\s*\[([^\]]*)\]/.exec(entityBody);
|
|
2668
|
+
if (idxMatch) {
|
|
2669
|
+
const inner = idxMatch[1];
|
|
2670
|
+
const re = /['"`]([^'"`]+)['"`]/g;
|
|
2671
|
+
let m;
|
|
2672
|
+
while (m = re.exec(inner))
|
|
2673
|
+
indexes.push(m[1]);
|
|
2674
|
+
}
|
|
2675
|
+
const id = path22.basename(file).replace(/\.(ts|tsx)$/, "");
|
|
2676
|
+
return { name, id, plural, collection, primaryKey: pk, indexes, fields, path: file };
|
|
2677
|
+
}
|
|
2678
|
+
function parseInterfaceBody(body) {
|
|
2679
|
+
const fields = [];
|
|
2680
|
+
const lines = body.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
|
2681
|
+
for (const raw of lines) {
|
|
2682
|
+
if (raw.startsWith("//") || raw.startsWith("*") || raw.startsWith("/*"))
|
|
2683
|
+
continue;
|
|
2684
|
+
const line = raw.replace(/;$/, "").replace(/,$/, "").trim();
|
|
2685
|
+
if (!line)
|
|
2686
|
+
continue;
|
|
2687
|
+
const colon = line.indexOf(":");
|
|
2688
|
+
if (colon < 0)
|
|
2689
|
+
continue;
|
|
2690
|
+
let name = line.slice(0, colon).trim();
|
|
2691
|
+
const type = line.slice(colon + 1).trim();
|
|
2692
|
+
if (!name || !type)
|
|
2693
|
+
continue;
|
|
2694
|
+
let optional = false;
|
|
2695
|
+
if (name.endsWith("?")) {
|
|
2696
|
+
optional = true;
|
|
2697
|
+
name = name.slice(0, -1);
|
|
2698
|
+
}
|
|
2699
|
+
fields.push({ name, type, optional });
|
|
2700
|
+
}
|
|
2701
|
+
return fields;
|
|
2702
|
+
}
|
|
2703
|
+
function inferInputSpec(type) {
|
|
2704
|
+
const t = type.trim();
|
|
2705
|
+
const cleaned = t.split("|").map((p) => p.trim()).filter((p) => p !== "null" && p !== "undefined");
|
|
2706
|
+
if (cleaned.length > 1 && cleaned.every((p) => /^(['"`]).*\1$/.test(p))) {
|
|
2707
|
+
return { kind: "select", options: cleaned.map((p) => p.slice(1, -1)) };
|
|
2708
|
+
}
|
|
2709
|
+
const single = cleaned[0] ?? t;
|
|
2710
|
+
if (/^number$/.test(single))
|
|
2711
|
+
return { kind: "number" };
|
|
2712
|
+
if (/^boolean$/.test(single))
|
|
2713
|
+
return { kind: "boolean" };
|
|
2714
|
+
if (/^Date$/.test(single))
|
|
2715
|
+
return { kind: "date" };
|
|
2716
|
+
if (/^string$/.test(single))
|
|
2717
|
+
return { kind: "text" };
|
|
2718
|
+
return { kind: "text" };
|
|
2719
|
+
}
|
|
2720
|
+
var fs22, path22;
|
|
2721
|
+
var init_parseModel = __esm(() => {
|
|
2722
|
+
fs22 = __toESM(require("fs"));
|
|
2723
|
+
path22 = __toESM(require("path"));
|
|
2724
|
+
});
|
|
2725
|
+
|
|
2726
|
+
// src/lib/crud/crudConfig.ts
|
|
2727
|
+
function readCrudConfig(projectRoot, modelName) {
|
|
2728
|
+
const file = path23.join(projectRoot, "src", "models", `${modelName}.crud.ts`);
|
|
2729
|
+
if (!fs23.existsSync(file))
|
|
2730
|
+
return {};
|
|
2731
|
+
try {
|
|
2732
|
+
const src = fs23.readFileSync(file, "utf8");
|
|
2733
|
+
return parseConfigSource(src);
|
|
2734
|
+
} catch {
|
|
2735
|
+
return {};
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
function parseConfigSource(src) {
|
|
2739
|
+
const head = /export\s+default\s*\{/.exec(src);
|
|
2740
|
+
if (!head)
|
|
2741
|
+
return {};
|
|
2742
|
+
const open = head.index + head[0].length - 1;
|
|
2743
|
+
let depth = 0;
|
|
2744
|
+
let close = -1;
|
|
2745
|
+
for (let i = open;i < src.length; i++) {
|
|
2746
|
+
const c = src[i];
|
|
2747
|
+
if (c === "{")
|
|
2748
|
+
depth++;
|
|
2749
|
+
else if (c === "}") {
|
|
2750
|
+
depth--;
|
|
2751
|
+
if (depth === 0) {
|
|
2752
|
+
close = i;
|
|
2753
|
+
break;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
if (close < 0)
|
|
2758
|
+
return {};
|
|
2759
|
+
const literal = src.slice(open, close + 1);
|
|
2760
|
+
try {
|
|
2761
|
+
const jsonish = literal.replace(/\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1").replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":').replace(/'/g, '"');
|
|
2762
|
+
return JSON.parse(jsonish);
|
|
2763
|
+
} catch {
|
|
2764
|
+
return {};
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
var fs23, path23;
|
|
2768
|
+
var init_crudConfig = __esm(() => {
|
|
2769
|
+
fs23 = __toESM(require("fs"));
|
|
2770
|
+
path23 = __toESM(require("path"));
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
// src/lib/crud/add.ts
|
|
2774
|
+
function addCrud(opts) {
|
|
2775
|
+
const modelFile = path24.join(opts.projectRoot, "src", "models", `${opts.model}.ts`);
|
|
2776
|
+
const model = parseModelFile(modelFile);
|
|
2777
|
+
const cfg = readCrudConfig(opts.projectRoot, opts.model);
|
|
2778
|
+
const hidden = new Set(cfg.hidden ?? []);
|
|
2779
|
+
const ordered = orderFields(model.fields, cfg.order ?? []);
|
|
2780
|
+
const visible = ordered.filter((f) => !hidden.has(f.name));
|
|
2781
|
+
if (visible.length === 0) {
|
|
2782
|
+
throw new Error(`CRUD for "${model.name}": no visible fields after applying \`hidden\` config.`);
|
|
2783
|
+
}
|
|
2784
|
+
const title = cfg.title ?? model.plural;
|
|
2785
|
+
const listId = `${camelLower(model.plural)}List`;
|
|
2786
|
+
const formId = `${camelLower(model.name)}Form`;
|
|
2787
|
+
const listApiName = `${model.plural}ListApi`;
|
|
2788
|
+
const formApiName = `${model.name}FormApi`;
|
|
2789
|
+
const servicePath = path24.join(opts.projectRoot, "src", "services", `${model.name}Service.ts`);
|
|
2790
|
+
const listPanelPath = path24.join(opts.projectRoot, "src", "panels", `${listId}.ts`);
|
|
2791
|
+
const formPanelPath = path24.join(opts.projectRoot, "src", "panels", `${formId}.ts`);
|
|
2792
|
+
const listWebDir = path24.join(opts.projectRoot, "src", "webview", "panels", listId);
|
|
2793
|
+
const formWebDir = path24.join(opts.projectRoot, "src", "webview", "panels", formId);
|
|
2794
|
+
const apiPath = path24.join(opts.projectRoot, "src", "shared", "api.ts");
|
|
2795
|
+
assertNoOverwrite(opts.projectRoot, servicePath, "Service");
|
|
2796
|
+
assertNoOverwrite(opts.projectRoot, listPanelPath, "List panel");
|
|
2797
|
+
assertNoOverwrite(opts.projectRoot, formPanelPath, "Form panel");
|
|
2798
|
+
assertNoOverwrite(opts.projectRoot, listWebDir, "List webview dir");
|
|
2799
|
+
assertNoOverwrite(opts.projectRoot, formWebDir, "Form webview dir");
|
|
2800
|
+
const pkg = JSON.parse(fs24.readFileSync(path24.join(opts.projectRoot, "package.json"), "utf8"));
|
|
2801
|
+
const prefix = pkg.vsceasy?.commandPrefix ?? pkg.name?.replace(/^@[^/]+\//, "").replace(/[^a-zA-Z0-9]+/g, "") ?? "ext";
|
|
2802
|
+
const baseVars = {
|
|
2803
|
+
Name: model.name,
|
|
2804
|
+
name: camelLower(model.name),
|
|
2805
|
+
Plural: model.plural,
|
|
2806
|
+
plural: camelLower(model.plural),
|
|
2807
|
+
primaryKey: model.primaryKey,
|
|
2808
|
+
title,
|
|
2809
|
+
listId,
|
|
2810
|
+
formId,
|
|
2811
|
+
prefix
|
|
2812
|
+
};
|
|
2813
|
+
const created = [];
|
|
2814
|
+
const modified = [];
|
|
2815
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "service.ts.tpl"), servicePath, baseVars, created);
|
|
2816
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formNav.ts.tpl"), path24.join(opts.projectRoot, "src", "services", `${camelLower(model.name)}FormNav.ts`), baseVars, created);
|
|
2817
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "listPanel.ts.tpl"), listPanelPath, baseVars, created);
|
|
2818
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formPanel.ts.tpl"), formPanelPath, baseVars, created);
|
|
2819
|
+
fs24.mkdirSync(listWebDir, { recursive: true });
|
|
2820
|
+
const listVars = {
|
|
2821
|
+
...baseVars,
|
|
2822
|
+
listHeaderCells: visible.map((f) => ` <th style={{ padding: '6px 8px' }}>${escapeJsx(label(f, cfg))}</th>`).join(`
|
|
2823
|
+
`),
|
|
2824
|
+
listBodyCells: visible.map((f) => ` <td style={{ padding: '6px 8px' }}>{String(r.${f.name} ?? '')}</td>`).join(`
|
|
2825
|
+
`),
|
|
2826
|
+
listColCount: String(visible.length + 1)
|
|
2827
|
+
};
|
|
2828
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "listApp.tsx.tpl"), path24.join(listWebDir, "App.tsx"), listVars, created);
|
|
2829
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "main.tsx.tpl"), path24.join(listWebDir, "main.tsx"), baseVars, created);
|
|
2830
|
+
fs24.mkdirSync(formWebDir, { recursive: true });
|
|
2831
|
+
const formInputs = visible.filter((f) => !cfg.fields?.[f.name]?.hideInForm).map((f) => renderInput(f, cfg.fields?.[f.name])).join(`
|
|
2832
|
+
`);
|
|
2833
|
+
const emptyLit = buildEmptyFormLiteral(visible, cfg);
|
|
2834
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "formApp.tsx.tpl"), path24.join(formWebDir, "App.tsx"), { ...baseVars, formFieldInputs: formInputs, emptyFormLiteral: emptyLit }, created);
|
|
2835
|
+
writeFromTpl(path24.join(opts.templatesRoot, "_generators", "crud", "main.tsx.tpl"), path24.join(formWebDir, "main.tsx"), baseVars, created);
|
|
2836
|
+
appendApi3(apiPath, listApiName, model, created, modified);
|
|
2837
|
+
appendApiForm(apiPath, formApiName, model, created, modified);
|
|
2838
|
+
let menuInfo;
|
|
2839
|
+
if (opts.menu && opts.menu !== "none") {
|
|
2840
|
+
menuInfo = wireMenu(opts, model, cfg, listId, formId);
|
|
2841
|
+
}
|
|
2842
|
+
let genRan = false;
|
|
2843
|
+
if (opts.runGen !== false)
|
|
2844
|
+
genRan = runGen11(opts.projectRoot);
|
|
2845
|
+
return { created, modified, menu: menuInfo, genRan };
|
|
2846
|
+
}
|
|
2847
|
+
function writeFromTpl(tpl, target, vars, created) {
|
|
2848
|
+
if (!fs24.existsSync(tpl))
|
|
2849
|
+
throw new Error(`CRUD template missing: ${tpl}`);
|
|
2850
|
+
fs24.mkdirSync(path24.dirname(target), { recursive: true });
|
|
2851
|
+
fs24.writeFileSync(target, substitute(fs24.readFileSync(tpl, "utf8"), vars));
|
|
2852
|
+
created.push(target);
|
|
2853
|
+
}
|
|
2854
|
+
function orderFields(fields, order) {
|
|
2855
|
+
const byName = new Map(fields.map((f) => [f.name, f]));
|
|
2856
|
+
const out = [];
|
|
2857
|
+
for (const name of order) {
|
|
2858
|
+
const f = byName.get(name);
|
|
2859
|
+
if (f) {
|
|
2860
|
+
out.push(f);
|
|
2861
|
+
byName.delete(name);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
for (const f of byName.values())
|
|
2865
|
+
out.push(f);
|
|
2866
|
+
return out;
|
|
2867
|
+
}
|
|
2868
|
+
function label(field, cfg) {
|
|
2869
|
+
const override = cfg.fields?.[field.name]?.label;
|
|
2870
|
+
if (override)
|
|
2871
|
+
return override;
|
|
2872
|
+
return field.name.charAt(0).toUpperCase() + field.name.slice(1).replace(/([A-Z])/g, " $1");
|
|
2873
|
+
}
|
|
2874
|
+
function escapeJsx(s) {
|
|
2875
|
+
return s.replace(/'/g, "'").replace(/"/g, """);
|
|
2876
|
+
}
|
|
2877
|
+
function escapeAttr(s) {
|
|
2878
|
+
return s.replace(/'/g, "\\'");
|
|
2879
|
+
}
|
|
2880
|
+
function renderInput(field, override) {
|
|
2881
|
+
const spec = override?.input ? { kind: override.input, options: override.options } : inferInputSpec(field.type);
|
|
2882
|
+
const labelText = override?.label ?? field.name.charAt(0).toUpperCase() + field.name.slice(1).replace(/([A-Z])/g, " $1");
|
|
2883
|
+
const placeholder = override?.placeholder ?? "";
|
|
2884
|
+
const placeholderAttr = placeholder ? ` placeholder='${escapeAttr(placeholder)}'` : "";
|
|
2885
|
+
const required = field.optional ? "" : " required";
|
|
2886
|
+
const name = field.name;
|
|
2887
|
+
const wrap = (input) => ` <label style={{ display: 'grid', gap: 4 }}>
|
|
2888
|
+
` + ` <span style={{ opacity: 0.8 }}>${labelText}</span>
|
|
2889
|
+
` + `${input}
|
|
2890
|
+
` + ` </label>`;
|
|
2891
|
+
switch (spec.kind) {
|
|
2892
|
+
case "number":
|
|
2893
|
+
return wrap(` <input type="number"${placeholderAttr}${required} value={form.${name} as any ?? ''} onChange={(e) => onChange('${name}', e.target.value === '' ? undefined : Number(e.target.value))} />`);
|
|
2894
|
+
case "boolean":
|
|
2895
|
+
return wrap(` <input type="checkbox" checked={!!form.${name}} onChange={(e) => onChange('${name}', e.target.checked as any)} />`);
|
|
2896
|
+
case "date":
|
|
2897
|
+
return wrap(` <input type="date"${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
2898
|
+
case "select": {
|
|
2899
|
+
const opts = (spec.options ?? []).map((o) => ` <option value=${JSON.stringify(o)}>${escapeJsx(o)}</option>`).join(`
|
|
2900
|
+
`);
|
|
2901
|
+
return wrap(` <select${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)}>
|
|
2902
|
+
` + ` <option value=""></option>
|
|
2903
|
+
` + `${opts}
|
|
2904
|
+
` + ` </select>`);
|
|
2905
|
+
}
|
|
2906
|
+
case "textarea":
|
|
2907
|
+
return wrap(` <textarea${placeholderAttr}${required} rows={4} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
2908
|
+
case "text":
|
|
2909
|
+
default:
|
|
2910
|
+
return wrap(` <input type="text"${placeholderAttr}${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
function buildEmptyFormLiteral(fields, cfg) {
|
|
2914
|
+
const lines = ["{"];
|
|
2915
|
+
for (const f of fields) {
|
|
2916
|
+
if (cfg.fields?.[f.name]?.hideInForm)
|
|
2917
|
+
continue;
|
|
2918
|
+
const spec = cfg.fields?.[f.name]?.input ? { kind: cfg.fields[f.name].input } : inferInputSpec(f.type);
|
|
2919
|
+
let val = "''";
|
|
2920
|
+
if (spec.kind === "number")
|
|
2921
|
+
val = "0";
|
|
2922
|
+
else if (spec.kind === "boolean")
|
|
2923
|
+
val = "false";
|
|
2924
|
+
lines.push(` ${f.name}: ${val} as any,`);
|
|
2925
|
+
}
|
|
2926
|
+
lines.push("}");
|
|
2927
|
+
return lines.join(`
|
|
2928
|
+
`);
|
|
2929
|
+
}
|
|
2930
|
+
function appendApi3(apiPath, apiName, model, created, modified) {
|
|
2931
|
+
const sig = `
|
|
2932
|
+
export interface ${apiName} {
|
|
2933
|
+
` + ` list(): Promise<${model.name}[]>;
|
|
2934
|
+
` + ` delete(id: ${model.name}['${model.primaryKey}']): Promise<boolean>;
|
|
2935
|
+
` + ` openForm(id?: ${model.name}['${model.primaryKey}']): Promise<void>;
|
|
2936
|
+
` + `}
|
|
2937
|
+
`;
|
|
2938
|
+
ensureImport(apiPath, model.name);
|
|
2939
|
+
appendIfMissing(apiPath, apiName, sig, created, modified);
|
|
2940
|
+
}
|
|
2941
|
+
function appendApiForm(apiPath, apiName, model, created, modified) {
|
|
2942
|
+
const sig = `
|
|
2943
|
+
export interface ${apiName} {
|
|
2944
|
+
` + ` pendingId(): Promise<${model.name}['${model.primaryKey}'] | null>;
|
|
2945
|
+
` + ` get(id: ${model.name}['${model.primaryKey}'] | null): Promise<${model.name} | null>;
|
|
2946
|
+
` + ` save(row: ${model.name}): Promise<${model.name}>;
|
|
2947
|
+
` + ` cancel(): Promise<void>;
|
|
2948
|
+
` + `}
|
|
2949
|
+
`;
|
|
2950
|
+
ensureImport(apiPath, model.name);
|
|
2951
|
+
appendIfMissing(apiPath, apiName, sig, created, modified);
|
|
2952
|
+
}
|
|
2953
|
+
function ensureImport(apiPath, modelName) {
|
|
2954
|
+
if (!fs24.existsSync(apiPath)) {
|
|
2955
|
+
fs24.mkdirSync(path24.dirname(apiPath), { recursive: true });
|
|
2956
|
+
fs24.writeFileSync(apiPath, `import type { ${modelName} } from '../models/${modelName}';
|
|
2957
|
+
`);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
let src = fs24.readFileSync(apiPath, "utf8");
|
|
2961
|
+
if (new RegExp(`from\\s+['"]\\.\\./models/${modelName}['"]`).test(src))
|
|
2962
|
+
return;
|
|
2963
|
+
src = `import type { ${modelName} } from '../models/${modelName}';
|
|
2964
|
+
` + src;
|
|
2965
|
+
fs24.writeFileSync(apiPath, src);
|
|
2966
|
+
}
|
|
2967
|
+
function appendIfMissing(apiPath, apiName, block, created, modified) {
|
|
2968
|
+
if (!fs24.existsSync(apiPath)) {
|
|
2969
|
+
fs24.writeFileSync(apiPath, block.trimStart());
|
|
2970
|
+
created.push(apiPath);
|
|
2971
|
+
return;
|
|
2972
|
+
}
|
|
2973
|
+
const src = fs24.readFileSync(apiPath, "utf8");
|
|
2974
|
+
if (new RegExp(`\\bexport\\s+interface\\s+${apiName}\\b`).test(src))
|
|
2975
|
+
return;
|
|
2976
|
+
const sep = src.endsWith(`
|
|
2977
|
+
`) ? "" : `
|
|
2978
|
+
`;
|
|
2979
|
+
fs24.writeFileSync(apiPath, src + sep + block);
|
|
2980
|
+
if (!modified.includes(apiPath))
|
|
2981
|
+
modified.push(apiPath);
|
|
2982
|
+
}
|
|
2983
|
+
function wireMenu(opts, model, cfg, listId, formId) {
|
|
2984
|
+
const spec = opts.menu;
|
|
2985
|
+
const [kind, value] = spec.split(":");
|
|
2986
|
+
let menuId;
|
|
2987
|
+
let created = false;
|
|
2988
|
+
if (kind === "new") {
|
|
2989
|
+
menuId = value || camelLower(model.plural);
|
|
2990
|
+
addMenu({
|
|
2991
|
+
name: menuId,
|
|
2992
|
+
title: cfg.title ?? model.plural,
|
|
2993
|
+
icon: cfg.icon,
|
|
2994
|
+
projectRoot: opts.projectRoot,
|
|
2995
|
+
templatesRoot: opts.templatesRoot,
|
|
2996
|
+
runGen: false
|
|
2997
|
+
});
|
|
2998
|
+
created = true;
|
|
2999
|
+
} else if (kind === "existing") {
|
|
3000
|
+
if (!value)
|
|
3001
|
+
throw new Error("--menu existing requires `:<menuId>` (e.g. existing:settings).");
|
|
3002
|
+
menuId = value;
|
|
3003
|
+
} else {
|
|
3004
|
+
throw new Error(`Invalid --menu "${spec}". Use: none | existing:<id> | new:<id>`);
|
|
3005
|
+
}
|
|
3006
|
+
editMenu({
|
|
3007
|
+
projectRoot: opts.projectRoot,
|
|
3008
|
+
menuName: menuId,
|
|
3009
|
+
runGen: false,
|
|
3010
|
+
item: { label: cfg.title ?? model.plural, kind: "panel", target: listId, icon: cfg.icon ?? "list-unordered" }
|
|
3011
|
+
});
|
|
3012
|
+
editMenu({
|
|
3013
|
+
projectRoot: opts.projectRoot,
|
|
3014
|
+
menuName: menuId,
|
|
3015
|
+
runGen: false,
|
|
3016
|
+
item: { label: `New ${model.name}`, kind: "panel", target: formId, icon: "add" }
|
|
3017
|
+
});
|
|
3018
|
+
return { id: menuId, created };
|
|
3019
|
+
}
|
|
3020
|
+
function camelLower(s) {
|
|
3021
|
+
return s.charAt(0).toLowerCase() + s.slice(1);
|
|
3022
|
+
}
|
|
3023
|
+
function runGen11(cwd) {
|
|
3024
|
+
const tryRun = (cmd, args) => import_child_process12.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
|
|
3025
|
+
if (which11("bun") && tryRun("bun", ["run", "gen"]))
|
|
3026
|
+
return true;
|
|
3027
|
+
if (which11("npm") && tryRun("npm", ["run", "gen"]))
|
|
3028
|
+
return true;
|
|
3029
|
+
console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
|
|
3030
|
+
return false;
|
|
3031
|
+
}
|
|
3032
|
+
function which11(cmd) {
|
|
3033
|
+
return import_child_process12.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
|
|
3034
|
+
}
|
|
3035
|
+
var fs24, path24, import_child_process12;
|
|
3036
|
+
var init_add11 = __esm(() => {
|
|
3037
|
+
init_scaffold();
|
|
3038
|
+
init_validate();
|
|
3039
|
+
init_parseModel();
|
|
3040
|
+
init_crudConfig();
|
|
3041
|
+
init_add3();
|
|
3042
|
+
init_edit();
|
|
3043
|
+
fs24 = __toESM(require("fs"));
|
|
3044
|
+
path24 = __toESM(require("path"));
|
|
3045
|
+
import_child_process12 = require("child_process");
|
|
3046
|
+
});
|
|
3047
|
+
|
|
3048
|
+
// src/lib/model/add.ts
|
|
3049
|
+
function addModel(opts) {
|
|
3050
|
+
const name = assertId("model name", normalizeCamel8(opts.name));
|
|
3051
|
+
const Name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
3052
|
+
if (!dbExists(opts.projectRoot)) {
|
|
3053
|
+
throw new Error("No database initialized. Run `vsceasy db init` first, then re-run `vsceasy model add`.");
|
|
3054
|
+
}
|
|
3055
|
+
if (opts.fields.length === 0) {
|
|
3056
|
+
throw new Error("Model needs at least one field. Re-run and add fields in the interactive loop.");
|
|
3057
|
+
}
|
|
3058
|
+
const Plural = opts.plural?.trim() || `${Name}s`;
|
|
3059
|
+
const collection = opts.collection?.trim() || Plural.toLowerCase();
|
|
3060
|
+
const explicitPk = opts.fields.filter((f) => f.primaryKey);
|
|
3061
|
+
if (explicitPk.length > 1) {
|
|
3062
|
+
throw new Error(`Model "${Name}": more than one field marked primaryKey: ${explicitPk.map((f) => f.name).join(", ")}.`);
|
|
3063
|
+
}
|
|
3064
|
+
const pkField = explicitPk[0] ?? opts.fields.find((f) => f.name === "id") ?? opts.fields[0];
|
|
3065
|
+
const primaryKey = pkField.name;
|
|
3066
|
+
const indexes = opts.fields.filter((f) => f.indexed && f.name !== primaryKey).map((f) => f.name);
|
|
3067
|
+
const target = path25.join(opts.projectRoot, "src", "models", `${Name}.ts`);
|
|
3068
|
+
assertNoOverwrite(opts.projectRoot, target, "Model");
|
|
3069
|
+
const tpl = path25.join(opts.templatesRoot, "_generators", "model", "model.ts.tpl");
|
|
3070
|
+
const fieldLines = opts.fields.map((f) => ` ${f.name}${f.optional ? "?" : ""}: ${f.type};`).join(`
|
|
3071
|
+
`);
|
|
3072
|
+
const vars = {
|
|
3073
|
+
name,
|
|
3074
|
+
Name,
|
|
3075
|
+
Plural,
|
|
3076
|
+
collection,
|
|
3077
|
+
primaryKey,
|
|
3078
|
+
fieldLines,
|
|
3079
|
+
indexesLine: indexes.length ? `
|
|
3080
|
+
indexes: [${indexes.map((i) => `'${i}'`).join(", ")}],` : ""
|
|
3081
|
+
};
|
|
3082
|
+
fs25.mkdirSync(path25.dirname(target), { recursive: true });
|
|
3083
|
+
fs25.writeFileSync(target, substitute(fs25.readFileSync(tpl, "utf8"), vars));
|
|
3084
|
+
return { created: [target], primaryKey, indexes };
|
|
3085
|
+
}
|
|
3086
|
+
function normalizeCamel8(s) {
|
|
3087
|
+
const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
|
|
3088
|
+
if (!cleaned)
|
|
3089
|
+
return "";
|
|
3090
|
+
return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
|
|
3091
|
+
}
|
|
3092
|
+
var fs25, path25;
|
|
3093
|
+
var init_add12 = __esm(() => {
|
|
3094
|
+
init_scaffold();
|
|
3095
|
+
init_validate();
|
|
3096
|
+
init_init2();
|
|
3097
|
+
fs25 = __toESM(require("fs"));
|
|
3098
|
+
path25 = __toESM(require("path"));
|
|
3099
|
+
});
|
|
3100
|
+
|
|
3101
|
+
// src/index.ts
|
|
3102
|
+
var exports_src = {};
|
|
3103
|
+
__export(exports_src, {
|
|
3104
|
+
writeConfig: () => writeConfig,
|
|
3105
|
+
wireInitDb: () => wireInitDb,
|
|
3106
|
+
upgrade: () => upgrade,
|
|
3107
|
+
substitute: () => substitute,
|
|
3108
|
+
setupTests: () => setupTests,
|
|
3109
|
+
scaffold: () => scaffold,
|
|
3110
|
+
runDoctor: () => runDoctor,
|
|
3111
|
+
readCrudConfig: () => readCrudConfig,
|
|
3112
|
+
readConfig: () => readConfig,
|
|
3113
|
+
publishInit: () => publishInit,
|
|
3114
|
+
parseModelFile: () => parseModelFile,
|
|
3115
|
+
listPanels: () => listPanels,
|
|
3116
|
+
listMenus: () => listMenus,
|
|
3117
|
+
listGroups: () => listGroups,
|
|
3118
|
+
listCommands: () => listCommands,
|
|
3119
|
+
initDb: () => initDb,
|
|
3120
|
+
inferInputSpec: () => inferInputSpec,
|
|
3121
|
+
findTemplatesRoot: () => findTemplatesRoot,
|
|
3122
|
+
findProjectRoot: () => findProjectRoot,
|
|
3123
|
+
editMenu: () => editMenu,
|
|
3124
|
+
dbExists: () => dbExists,
|
|
3125
|
+
configPath: () => configPath,
|
|
3126
|
+
assertSiblingExists: () => assertSiblingExists,
|
|
3127
|
+
assertNoOverwrite: () => assertNoOverwrite,
|
|
3128
|
+
assertId: () => assertId,
|
|
3129
|
+
applyFixes: () => applyFixes,
|
|
3130
|
+
addTreeView: () => addTreeView,
|
|
3131
|
+
addSubpanel: () => addSubpanel,
|
|
3132
|
+
addStatusBar: () => addStatusBar,
|
|
3133
|
+
addRpcMethod: () => addRpcMethod,
|
|
3134
|
+
addPanel: () => addPanel,
|
|
3135
|
+
addModel: () => addModel,
|
|
3136
|
+
addMenu: () => addMenu,
|
|
3137
|
+
addJob: () => addJob,
|
|
3138
|
+
addHelper: () => addHelper,
|
|
3139
|
+
addCrud: () => addCrud,
|
|
3140
|
+
addCommand: () => addCommand,
|
|
3141
|
+
HELPER_KINDS: () => HELPER_KINDS
|
|
3142
|
+
});
|
|
3143
|
+
module.exports = __toCommonJS(exports_src);
|
|
3144
|
+
|
|
3145
|
+
// src/lib/index.ts
|
|
3146
|
+
init_scaffold();
|
|
3147
|
+
init_add2();
|
|
3148
|
+
init_add3();
|
|
3149
|
+
init_add4();
|
|
3150
|
+
init_add5();
|
|
3151
|
+
init_add6();
|
|
3152
|
+
init_add7();
|
|
3153
|
+
init_edit();
|
|
3154
|
+
init_doctor();
|
|
3155
|
+
init_upgrade();
|
|
3156
|
+
init_findProject();
|
|
3157
|
+
init_config();
|
|
3158
|
+
init_add8();
|
|
3159
|
+
init_testSetup();
|
|
3160
|
+
init_validate();
|
|
3161
|
+
init_init();
|
|
3162
|
+
init_add9();
|
|
3163
|
+
init_add10();
|
|
3164
|
+
init_init2();
|
|
3165
|
+
init_wire();
|
|
3166
|
+
init_add11();
|
|
3167
|
+
init_parseModel();
|
|
3168
|
+
init_crudConfig();
|
|
3169
|
+
init_add12();
|