@vsceasy/cli 0.1.4 → 0.1.6

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/dist/bin/cli.js CHANGED
@@ -3825,6 +3825,3796 @@ var init_scaffold = __esm(() => {
3825
3825
  SKIP_NAMES = new Set(["node_modules", "dist", ".DS_Store"]);
3826
3826
  });
3827
3827
 
3828
+ // src/lib/templatesData.ts
3829
+ var TEMPLATES_VERSION = "0.1.6", TEMPLATE_FILES;
3830
+ var init_templatesData = __esm(() => {
3831
+ TEMPLATE_FILES = {
3832
+ "_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
3833
+
3834
+ export default defineCommand({
3835
+ title: '{{title}}',{{categoryLine}}{{keybindingLine}}{{whenLine}}
3836
+ run: (vscode) => {
3837
+ vscode.window.showInformationMessage('{{title}} ran');
3838
+ },
3839
+ });
3840
+ `,
3841
+ "_generators/components/Button.tsx.tpl": `import React from 'react';
3842
+
3843
+ type Variant = 'primary' | 'secondary';
3844
+
3845
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
3846
+ variant?: Variant;
3847
+ }
3848
+
3849
+ /** Theme-aware button using VS Code button tokens. */
3850
+ export function Button({ variant = 'primary', className = '', ...rest }: ButtonProps) {
3851
+ return <button className={\`vx-btn vx-btn--\${variant} \${className}\`.trim()} {...rest} />;
3852
+ }
3853
+ `,
3854
+ "_generators/components/Card.tsx.tpl": `import React from 'react';
3855
+
3856
+ export interface CardProps {
3857
+ title?: string;
3858
+ actions?: React.ReactNode;
3859
+ children: React.ReactNode;
3860
+ }
3861
+
3862
+ /** Bordered surface for grouping content, with an optional title + actions row. */
3863
+ export function Card({ title, actions, children }: CardProps) {
3864
+ return (
3865
+ <section className="vx-card">
3866
+ {(title || actions) && (
3867
+ <header className="vx-card__head">
3868
+ {title ? <h2 className="vx-card__title">{title}</h2> : <span />}
3869
+ {actions ? <div className="vx-card__actions">{actions}</div> : null}
3870
+ </header>
3871
+ )}
3872
+ <div className="vx-card__body">{children}</div>
3873
+ </section>
3874
+ );
3875
+ }
3876
+ `,
3877
+ "_generators/components/Field.tsx.tpl": `import React from 'react';
3878
+
3879
+ export interface FieldProps {
3880
+ label: string;
3881
+ htmlFor?: string;
3882
+ hint?: string;
3883
+ error?: string;
3884
+ children: React.ReactNode;
3885
+ }
3886
+
3887
+ /** Labeled form field wrapper with optional hint / error text. */
3888
+ export function Field({ label, htmlFor, hint, error, children }: FieldProps) {
3889
+ return (
3890
+ <div className="vx-field">
3891
+ <label className="vx-field__label" htmlFor={htmlFor}>{label}</label>
3892
+ {children}
3893
+ {error ? <span className="vx-field__error">{error}</span> : hint ? <span className="vx-field__hint">{hint}</span> : null}
3894
+ </div>
3895
+ );
3896
+ }
3897
+ `,
3898
+ "_generators/components/Input.tsx.tpl": `import React from 'react';
3899
+
3900
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
3901
+
3902
+ /** Theme-aware text input using VS Code input tokens. */
3903
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
3904
+ function Input({ className = '', ...rest }, ref) {
3905
+ return <input ref={ref} className={\`vx-input \${className}\`.trim()} {...rest} />;
3906
+ },
3907
+ );
3908
+ `,
3909
+ "_generators/components/List.tsx.tpl": `import React from 'react';
3910
+
3911
+ export interface ListProps<T> {
3912
+ items: T[];
3913
+ getKey: (item: T, index: number) => string | number;
3914
+ renderItem: (item: T, index: number) => React.ReactNode;
3915
+ onSelect?: (item: T, index: number) => void;
3916
+ empty?: React.ReactNode;
3917
+ }
3918
+
3919
+ /** Selectable, theme-aware list. Rows highlight on hover and on selection. */
3920
+ export function List<T>({ items, getKey, renderItem, onSelect, empty }: ListProps<T>) {
3921
+ if (items.length === 0) {
3922
+ return <div className="vx-list__empty">{empty ?? 'Nothing here yet.'}</div>;
3923
+ }
3924
+ return (
3925
+ <ul className="vx-list" role="list">
3926
+ {items.map((item, i) => (
3927
+ <li
3928
+ key={getKey(item, i)}
3929
+ className={\`vx-list__row\${onSelect ? ' vx-list__row--clickable' : ''}\`}
3930
+ onClick={onSelect ? () => onSelect(item, i) : undefined}
3931
+ >
3932
+ {renderItem(item, i)}
3933
+ </li>
3934
+ ))}
3935
+ </ul>
3936
+ );
3937
+ }
3938
+ `,
3939
+ "_generators/components/components.css.tpl": `.vx-btn {
3940
+ font: inherit;
3941
+ padding: 0.35rem 0.85rem;
3942
+ border-radius: 2px;
3943
+ border: 1px solid transparent;
3944
+ cursor: pointer;
3945
+ }
3946
+ .vx-btn:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; }
3947
+ .vx-btn:disabled { opacity: 0.5; cursor: default; }
3948
+ .vx-btn--primary {
3949
+ color: var(--vscode-button-foreground);
3950
+ background: var(--vscode-button-background);
3951
+ }
3952
+ .vx-btn--primary:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
3953
+ .vx-btn--secondary {
3954
+ color: var(--vscode-button-secondaryForeground, var(--vscode-foreground));
3955
+ background: var(--vscode-button-secondaryBackground, transparent);
3956
+ border-color: var(--vscode-button-border, var(--vscode-input-border, #555));
3957
+ }
3958
+ .vx-btn--secondary:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-list-hoverBackground)); }
3959
+
3960
+ .vx-input {
3961
+ font: inherit;
3962
+ width: 100%;
3963
+ box-sizing: border-box;
3964
+ padding: 0.35rem 0.5rem;
3965
+ border-radius: 2px;
3966
+ color: var(--vscode-input-foreground);
3967
+ background: var(--vscode-input-background);
3968
+ border: 1px solid var(--vscode-input-border, #555);
3969
+ }
3970
+ .vx-input:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; }
3971
+ .vx-input::placeholder { color: var(--vscode-input-placeholderForeground); }
3972
+
3973
+ .vx-field { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.75rem; }
3974
+ .vx-field__label { font-size: 0.85em; color: var(--vscode-foreground); }
3975
+ .vx-field__hint { font-size: 0.8em; color: var(--vscode-descriptionForeground); }
3976
+ .vx-field__error { font-size: 0.8em; color: var(--vscode-errorForeground); }
3977
+
3978
+ .vx-card {
3979
+ border: 1px solid var(--vscode-panel-border, var(--vscode-input-border, #555));
3980
+ border-radius: 4px;
3981
+ margin-bottom: 0.75rem;
3982
+ background: var(--vscode-editorWidget-background, transparent);
3983
+ }
3984
+ .vx-card__head {
3985
+ display: flex;
3986
+ align-items: center;
3987
+ justify-content: space-between;
3988
+ gap: 0.5rem;
3989
+ padding: 0.5rem 0.75rem;
3990
+ border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-input-border, #555));
3991
+ }
3992
+ .vx-card__title { font-size: 1em; font-weight: 600; margin: 0; }
3993
+ .vx-card__actions { display: flex; gap: 0.5rem; }
3994
+ .vx-card__body { padding: 0.75rem; }
3995
+
3996
+ .vx-list { list-style: none; margin: 0; padding: 0; }
3997
+ .vx-list__row {
3998
+ padding: 0.4rem 0.6rem;
3999
+ border-radius: 2px;
4000
+ line-height: 1.5;
4001
+ }
4002
+ .vx-list__row--clickable { cursor: pointer; }
4003
+ .vx-list__row--clickable:hover { background: var(--vscode-list-hoverBackground); }
4004
+ .vx-list__empty { color: var(--vscode-descriptionForeground); padding: 0.6rem; }
4005
+ `,
4006
+ "_generators/components/index.ts.tpl": `export { Button } from './Button';
4007
+ export type { ButtonProps } from './Button';
4008
+ export { Input } from './Input';
4009
+ export type { InputProps } from './Input';
4010
+ export { Field } from './Field';
4011
+ export type { FieldProps } from './Field';
4012
+ export { Card } from './Card';
4013
+ export type { CardProps } from './Card';
4014
+ export { List } from './List';
4015
+ export type { ListProps } from './List';
4016
+ `,
4017
+ "_generators/crud/formApp.tsx.tpl": `import { useCallback, useEffect, useState } from 'react';
4018
+ import { connectWebview } from '../../../shared/vsceasy/client';
4019
+ import type { {{Name}}FormApi } from '../../../shared/api';
4020
+ import type { {{Name}} } from '../../../models/{{Name}}';
4021
+
4022
+ const api = connectWebview<{{Name}}FormApi>();
4023
+
4024
+ type FormState = Partial<{{Name}}>;
4025
+
4026
+ const emptyForm: FormState = {{emptyFormLiteral}};
4027
+
4028
+ export function App() {
4029
+ const [form, setForm] = useState<FormState>(emptyForm);
4030
+ const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
4031
+ const [error, setError] = useState<string | null>(null);
4032
+ const [saving, setSaving] = useState(false);
4033
+
4034
+ const load = useCallback(async () => {
4035
+ // The list stashes the row id before revealing this panel. Pull it (the host
4036
+ // clears it after handing it over) and pre-fill the form for editing.
4037
+ const id = await api.pendingId();
4038
+ if (id == null || id === '') {
4039
+ setForm(emptyForm);
4040
+ setEditingId(null);
4041
+ return;
4042
+ }
4043
+ const row = await api.get(id);
4044
+ if (row) {
4045
+ setForm(row);
4046
+ setEditingId(row.{{primaryKey}});
4047
+ }
4048
+ }, []);
4049
+
4050
+ useEffect(() => {
4051
+ void load();
4052
+ // Webviews retain state when hidden, so re-load whenever the panel is
4053
+ // revealed — the list may have asked to edit a different row.
4054
+ const onFocus = () => { void load(); };
4055
+ const onVisible = () => { if (document.visibilityState === 'visible') void load(); };
4056
+ window.addEventListener('focus', onFocus);
4057
+ document.addEventListener('visibilitychange', onVisible);
4058
+ return () => {
4059
+ window.removeEventListener('focus', onFocus);
4060
+ document.removeEventListener('visibilitychange', onVisible);
4061
+ };
4062
+ }, [load]);
4063
+
4064
+ const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
4065
+ setForm((f) => ({ ...f, [k]: v }));
4066
+ };
4067
+
4068
+ const onSubmit = async (e: React.FormEvent) => {
4069
+ e.preventDefault();
4070
+ setSaving(true);
4071
+ setError(null);
4072
+ try {
4073
+ const wasNew = editingId == null;
4074
+ await api.save(form as {{Name}});
4075
+ // After creating a new row, reset for the next entry. After an edit, keep
4076
+ // the row loaded so further tweaks are possible.
4077
+ if (wasNew) {
4078
+ setForm(emptyForm);
4079
+ setEditingId(null);
4080
+ }
4081
+ } catch (err: any) {
4082
+ setError(String(err?.message ?? err));
4083
+ } finally {
4084
+ setSaving(false);
4085
+ }
4086
+ };
4087
+
4088
+ return (
4089
+ <form onSubmit={onSubmit} style={{ padding: 16, display: 'grid', gap: 12, color: 'var(--vscode-foreground)' }}>
4090
+ <h2 style={{ margin: 0 }}>{editingId ? 'Edit {{title}}' : 'New {{title}}'}</h2>
4091
+ {{formFieldInputs}}
4092
+ {error && <div style={{ color: 'var(--vscode-errorForeground)' }}>{error}</div>}
4093
+ <div style={{ display: 'flex', gap: 8 }}>
4094
+ <button type="submit" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
4095
+ <button type="button" onClick={() => { setForm(emptyForm); setEditingId(null); }}>Reset</button>
4096
+ </div>
4097
+ </form>
4098
+ );
4099
+ }
4100
+ `,
4101
+ "_generators/crud/formNav.ts.tpl": `/**
4102
+ * In-memory hand-off for "open the {{Name}} form to edit row X". The list panel
4103
+ * sets the pending id before revealing the form; the form reads (and clears) it
4104
+ * on mount. Lives in the extension host, shared across the two panel modules.
4105
+ */
4106
+ type {{Name}}Id = unknown;
4107
+
4108
+ let pendingId: {{Name}}Id | null = null;
4109
+
4110
+ export function setPending{{Name}}Id(id: {{Name}}Id | null): void {
4111
+ pendingId = id ?? null;
4112
+ }
4113
+
4114
+ /** Returns the pending id once, then clears it. */
4115
+ export function takePending{{Name}}Id(): {{Name}}Id | null {
4116
+ const v = pendingId;
4117
+ pendingId = null;
4118
+ return v;
4119
+ }
4120
+ `,
4121
+ "_generators/crud/formPanel.ts.tpl": `import { definePanel } from '../shared/vsceasy';
4122
+ import { {{Name}}Service } from '../services/{{Name}}Service';
4123
+ import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
4124
+ import type { {{Name}}FormApi } from '../shared/api';
4125
+ import type { {{Name}} } from '../models/{{Name}}';
4126
+
4127
+ export default definePanel<{{Name}}FormApi>({
4128
+ title: '{{title}}',
4129
+ column: 'beside',
4130
+ command: { title: '{{title}}: New / Edit' },
4131
+ rpc: (vscode) => ({
4132
+ async pendingId() {
4133
+ // Consumed once by the webview on mount to decide edit vs new.
4134
+ return (takePending{{Name}}Id() as {{Name}}['{{primaryKey}}'] | null) ?? null;
4135
+ },
4136
+ async get(id) {
4137
+ if (!id) return null;
4138
+ return {{Name}}Service.get(id as {{Name}}['{{primaryKey}}']);
4139
+ },
4140
+ async save(row) {
4141
+ const saved = await {{Name}}Service.save(row);
4142
+ void vscode.window.showInformationMessage(\`{{title}} saved (\${String(saved.{{primaryKey}})})\`);
4143
+ // Reveal the list so the new/edited row shows. Revealing fires the list
4144
+ // webview's focus/visibility listener, which reloads it.
4145
+ void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
4146
+ return saved;
4147
+ },
4148
+ async cancel() {
4149
+ // No-op — webview closes itself.
4150
+ },
4151
+ }),
4152
+ });
4153
+ `,
4154
+ "_generators/crud/listApp.tsx.tpl": `import { useEffect, useState, useCallback } from 'react';
4155
+ import { connectWebview } from '../../../shared/vsceasy/client';
4156
+ import type { {{Plural}}ListApi } from '../../../shared/api';
4157
+ import type { {{Name}} } from '../../../models/{{Name}}';
4158
+
4159
+ const api = connectWebview<{{Plural}}ListApi>();
4160
+
4161
+ export function App() {
4162
+ const [rows, setRows] = useState<{{Name}}[]>([]);
4163
+ const [loading, setLoading] = useState(true);
4164
+ const [error, setError] = useState<string | null>(null);
4165
+
4166
+ const reload = useCallback(async () => {
4167
+ setLoading(true);
4168
+ try {
4169
+ setRows(await api.list());
4170
+ setError(null);
4171
+ } catch (e: any) {
4172
+ setError(String(e?.message ?? e));
4173
+ } finally {
4174
+ setLoading(false);
4175
+ }
4176
+ }, []);
4177
+
4178
+ useEffect(() => {
4179
+ void reload();
4180
+ // Webviews keep their state when hidden (retainContextWhenHidden), so the
4181
+ // mount effect won't re-run when the panel is revealed again. Reload when the
4182
+ // webview regains focus/visibility so edits made in another panel show up.
4183
+ const onFocus = () => { void reload(); };
4184
+ const onVisible = () => { if (document.visibilityState === 'visible') void reload(); };
4185
+ window.addEventListener('focus', onFocus);
4186
+ document.addEventListener('visibilitychange', onVisible);
4187
+ return () => {
4188
+ window.removeEventListener('focus', onFocus);
4189
+ document.removeEventListener('visibilitychange', onVisible);
4190
+ };
4191
+ }, [reload]);
4192
+
4193
+ const onDelete = async (id: {{Name}}['{{primaryKey}}']) => {
4194
+ // \`confirm()\` is disabled inside VS Code webviews — confirmation happens in
4195
+ // the host (the \`delete\` RPC handler shows a modal). Just call + reload.
4196
+ await api.delete(id);
4197
+ await reload();
4198
+ };
4199
+
4200
+ return (
4201
+ <div style={{ padding: 16, color: 'var(--vscode-foreground)' }}>
4202
+ <header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
4203
+ <h2 style={{ margin: 0 }}>{{title}}</h2>
4204
+ <div style={{ display: 'flex', gap: 8 }}>
4205
+ <button onClick={() => void reload()} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</button>
4206
+ <button onClick={() => api.openForm()}>+ New</button>
4207
+ </div>
4208
+ </header>
4209
+
4210
+ {error && <div style={{ color: 'var(--vscode-errorForeground)', marginBottom: 8 }}>{error}</div>}
4211
+ {loading ? <div>Loading…</div> : (
4212
+ <table style={{ width: '100%', borderCollapse: 'collapse' }}>
4213
+ <thead>
4214
+ <tr style={{ textAlign: 'left', borderBottom: '1px solid var(--vscode-panel-border)' }}>
4215
+ {{listHeaderCells}}
4216
+ <th style={{ padding: '6px 8px' }}></th>
4217
+ </tr>
4218
+ </thead>
4219
+ <tbody>
4220
+ {rows.length === 0 && (
4221
+ <tr><td colSpan={{{listColCount}}} style={{ padding: 16, opacity: 0.6 }}>No rows yet.</td></tr>
4222
+ )}
4223
+ {rows.map((r) => (
4224
+ <tr key={String(r.{{primaryKey}})} style={{ borderBottom: '1px solid var(--vscode-panel-border)' }}>
4225
+ {{listBodyCells}}
4226
+ <td style={{ padding: '6px 8px', whiteSpace: 'nowrap' }}>
4227
+ <button onClick={() => api.openForm(r.{{primaryKey}})}>Edit</button>{' '}
4228
+ <button onClick={() => onDelete(r.{{primaryKey}})}>Delete</button>
4229
+ </td>
4230
+ </tr>
4231
+ ))}
4232
+ </tbody>
4233
+ </table>
4234
+ )}
4235
+ </div>
4236
+ );
4237
+ }
4238
+ `,
4239
+ "_generators/crud/listPanel.ts.tpl": `import { definePanel } from '../shared/vsceasy';
4240
+ import { {{Name}}Service } from '../services/{{Name}}Service';
4241
+ import { setPending{{Name}}Id } from '../services/{{name}}FormNav';
4242
+ import type { {{Plural}}ListApi } from '../shared/api';
4243
+
4244
+ export default definePanel<{{Plural}}ListApi>({
4245
+ title: '{{title}}',
4246
+ column: 'active',
4247
+ command: { title: '{{title}}: List' },
4248
+ rpc: (vscode) => ({
4249
+ async list() {
4250
+ return {{Name}}Service.list();
4251
+ },
4252
+ async delete(id) {
4253
+ // Confirm in the host — browser confirm() is disabled in webviews.
4254
+ const pick = await vscode.window.showWarningMessage(
4255
+ \`Delete {{title}} "\${String(id)}"?\`,
4256
+ { modal: true },
4257
+ 'Delete',
4258
+ );
4259
+ if (pick !== 'Delete') return false;
4260
+ return {{Name}}Service.delete(id);
4261
+ },
4262
+ async openForm(id) {
4263
+ // Stash the id so the form can pre-load it on mount, then reveal the form.
4264
+ setPending{{Name}}Id(id ?? null);
4265
+ await vscode.commands.executeCommand('{{prefix}}.open{{Name}}Form', id ?? null);
4266
+ },
4267
+ }),
4268
+ });
4269
+ `,
4270
+ "_generators/crud/main.tsx.tpl": `import React from 'react';
4271
+ import { createRoot } from 'react-dom/client';
4272
+ import { App } from './App';
4273
+ import '../../styles.css';
4274
+
4275
+ createRoot(document.getElementById('root')!).render(<App />);
4276
+ `,
4277
+ "_generators/crud/service.ts.tpl": `import { {{Plural}}Repo } from '../models/{{Name}}';
4278
+ import type { {{Name}} } from '../models/{{Name}}';
4279
+
4280
+ /**
4281
+ * {{Name}} service — business logic between RPC handlers and the repo.
4282
+ * Put validation, derivations (e.g. timestamps), and cross-entity work here.
4283
+ */
4284
+ export const {{Name}}Service = {
4285
+ async list(): Promise<{{Name}}[]> {
4286
+ return {{Plural}}Repo().findMany({ orderBy: '{{primaryKey}}:desc' });
4287
+ },
4288
+
4289
+ async get(id: {{Name}}['{{primaryKey}}']): Promise<{{Name}} | null> {
4290
+ return {{Plural}}Repo().findById(id);
4291
+ },
4292
+
4293
+ async save(row: {{Name}}): Promise<{{Name}}> {
4294
+ if (!row.{{primaryKey}}) {
4295
+ throw new Error('{{Name}}: {{primaryKey}} is required');
4296
+ }
4297
+ return {{Plural}}Repo().upsert(row);
4298
+ },
4299
+
4300
+ async delete(id: {{Name}}['{{primaryKey}}']): Promise<boolean> {
4301
+ return {{Plural}}Repo().delete(id);
4302
+ },
4303
+ };
4304
+ `,
4305
+ "_generators/helper/cache.ts.tpl": `/**
4306
+ * In-memory TTL + LRU cache. Pair with the ORM for cheap reads:
4307
+ *
4308
+ * const cache = createCache<User>({ ttlMs: 60_000, max: 200 });
4309
+ * const u = await cache.wrap(\`user:\${id}\`, () => orm(User).findById(id));
4310
+ *
4311
+ * Survives only while the extension host is running (cleared on reload).
4312
+ * For persistent caches, write through to the ORM or \`globalState\`.
4313
+ */
4314
+
4315
+ export interface CacheOptions {
4316
+ /** Time-to-live in ms. 0 = never expire. Default: 60000. */
4317
+ ttlMs?: number;
4318
+ /** Max entries. LRU eviction once exceeded. 0 = unlimited. Default: 500. */
4319
+ max?: number;
4320
+ }
4321
+
4322
+ export interface Cache<V> {
4323
+ get(key: string): V | undefined;
4324
+ set(key: string, value: V, ttlMsOverride?: number): void;
4325
+ delete(key: string): boolean;
4326
+ has(key: string): boolean;
4327
+ clear(): void;
4328
+ /** Memoize a loader. Returns cached value if fresh, else runs \`fn\` and stores. */
4329
+ wrap(key: string, fn: () => Promise<V>, ttlMsOverride?: number): Promise<V>;
4330
+ /** Force-refresh: invalidate then wrap. */
4331
+ refresh(key: string, fn: () => Promise<V>, ttlMsOverride?: number): Promise<V>;
4332
+ readonly size: number;
4333
+ }
4334
+
4335
+ interface Entry<V> {
4336
+ value: V;
4337
+ expiresAt: number; // 0 = no expiry
4338
+ }
4339
+
4340
+ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
4341
+ const ttl = opts.ttlMs ?? 60_000;
4342
+ const max = opts.max ?? 500;
4343
+ // Map preserves insertion order — re-insert on access for LRU behavior.
4344
+ const store = new Map<string, Entry<V>>();
4345
+ // De-dupe concurrent loads for the same key.
4346
+ const inflight = new Map<string, Promise<V>>();
4347
+
4348
+ const isFresh = (e: Entry<V>) => e.expiresAt === 0 || e.expiresAt > Date.now();
4349
+
4350
+ const evictIfNeeded = () => {
4351
+ if (max <= 0) return;
4352
+ while (store.size > max) {
4353
+ const oldest = store.keys().next().value;
4354
+ if (oldest === undefined) break;
4355
+ store.delete(oldest);
4356
+ }
4357
+ };
4358
+
4359
+ const cache: Cache<V> = {
4360
+ get(key) {
4361
+ const e = store.get(key);
4362
+ if (!e) return undefined;
4363
+ if (!isFresh(e)) {
4364
+ store.delete(key);
4365
+ return undefined;
4366
+ }
4367
+ // LRU touch
4368
+ store.delete(key);
4369
+ store.set(key, e);
4370
+ return e.value;
4371
+ },
4372
+ set(key, value, ttlMsOverride) {
4373
+ const t = ttlMsOverride ?? ttl;
4374
+ store.delete(key); // re-insert for LRU order
4375
+ store.set(key, { value, expiresAt: t > 0 ? Date.now() + t : 0 });
4376
+ evictIfNeeded();
4377
+ },
4378
+ delete(key) {
4379
+ return store.delete(key);
4380
+ },
4381
+ has(key) {
4382
+ const e = store.get(key);
4383
+ if (!e) return false;
4384
+ if (!isFresh(e)) {
4385
+ store.delete(key);
4386
+ return false;
4387
+ }
4388
+ return true;
4389
+ },
4390
+ clear() {
4391
+ store.clear();
4392
+ inflight.clear();
4393
+ },
4394
+ async wrap(key, fn, ttlMsOverride) {
4395
+ const cached = cache.get(key);
4396
+ if (cached !== undefined) return cached;
4397
+ const pending = inflight.get(key);
4398
+ if (pending) return pending;
4399
+ const p = (async () => {
4400
+ try {
4401
+ const v = await fn();
4402
+ cache.set(key, v, ttlMsOverride);
4403
+ return v;
4404
+ } finally {
4405
+ inflight.delete(key);
4406
+ }
4407
+ })();
4408
+ inflight.set(key, p);
4409
+ return p;
4410
+ },
4411
+ async refresh(key, fn, ttlMsOverride) {
4412
+ cache.delete(key);
4413
+ return cache.wrap(key, fn, ttlMsOverride);
4414
+ },
4415
+ get size() {
4416
+ return store.size;
4417
+ },
4418
+ };
4419
+
4420
+ return cache;
4421
+ }
4422
+ `,
4423
+ "_generators/helper/config.ts.tpl": `import * as vscode from 'vscode';
4424
+
4425
+ /**
4426
+ * Typed wrapper over \`vscode.workspace.getConfiguration('{{commandPrefix}}')\`.
4427
+ * Reads settings declared under \`contributes.configuration\` in package.json.
4428
+ *
4429
+ * Example package.json snippet:
4430
+ * "contributes": {
4431
+ * "configuration": {
4432
+ * "title": "{{displayName}}",
4433
+ * "properties": {
4434
+ * "{{commandPrefix}}.apiUrl": { "type": "string", "default": "" }
4435
+ * }
4436
+ * }
4437
+ * }
4438
+ *
4439
+ * Usage:
4440
+ * const url = config.get<string>('apiUrl');
4441
+ * await config.set('apiUrl', 'https://...');
4442
+ */
4443
+ const SECTION = '{{commandPrefix}}';
4444
+
4445
+ export const config = {
4446
+ get<T>(key: string, fallback?: T): T {
4447
+ const v = vscode.workspace.getConfiguration(SECTION).get<T>(key);
4448
+ return (v === undefined ? (fallback as T) : v) as T;
4449
+ },
4450
+ set(key: string, value: unknown, target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global): Thenable<void> {
4451
+ return vscode.workspace.getConfiguration(SECTION).update(key, value, target);
4452
+ },
4453
+ onChange(listener: (key: string) => void): vscode.Disposable {
4454
+ return vscode.workspace.onDidChangeConfiguration((e) => {
4455
+ if (e.affectsConfiguration(SECTION)) listener(SECTION);
4456
+ });
4457
+ },
4458
+ };
4459
+ `,
4460
+ "_generators/helper/db.ts.tpl": `import * as vscode from 'vscode';
4461
+ import * as fs from 'fs';
4462
+ import * as path from 'path';
4463
+
4464
+ /**
4465
+ * Mini-ORM with pluggable providers. Ships with a filesystem JSON provider that
4466
+ * writes each entity to a single file under the extension's storage dir. Future
4467
+ * providers (sqlite, etc.) implement the same \`Provider\` interface — entity
4468
+ * definitions and call sites don't change.
4469
+ *
4470
+ * Usage:
4471
+ * const User = defineEntity<{ id: string; name: string }>('users', { primaryKey: 'id' });
4472
+ * const orm = createDb(context, { provider: 'storage' });
4473
+ * await orm(User).insert({ id: 'u1', name: 'Jairo' });
4474
+ * const u = await orm(User).findById('u1');
4475
+ */
4476
+
4477
+ // ── Entity definition ────────────────────────────────────────────────────────
4478
+
4479
+ export interface EntityOptions<T> {
4480
+ /** Field used as the unique key. */
4481
+ primaryKey: keyof T & string;
4482
+ /** Optional indexes — speeds up \`findOne({ [k]: v })\` for these fields. */
4483
+ indexes?: (keyof T & string)[];
4484
+ }
4485
+
4486
+ export interface Entity<T> {
4487
+ readonly name: string;
4488
+ readonly primaryKey: keyof T & string;
4489
+ readonly indexes: (keyof T & string)[];
4490
+ /** Phantom type carrier so \`orm(E)\` infers \`T\`. Never read. */
4491
+ readonly __t?: T;
4492
+ }
4493
+
4494
+ export function defineEntity<T extends object>(
4495
+ name: string,
4496
+ opts: EntityOptions<T>,
4497
+ ): Entity<T> {
4498
+ return { name, primaryKey: opts.primaryKey, indexes: opts.indexes ?? [] };
4499
+ }
4500
+
4501
+ // ── Query types ──────────────────────────────────────────────────────────────
4502
+
4503
+ export type Where<T> = Partial<{ [K in keyof T]: T[K] | { in: T[K][] } | { neq: T[K] } }>;
4504
+
4505
+ export interface FindOptions<T> {
4506
+ where?: Where<T>;
4507
+ limit?: number;
4508
+ offset?: number;
4509
+ /** \`'field:asc'\` | \`'field:desc'\`. Default asc when only field given. */
4510
+ orderBy?: \`\${keyof T & string}:\${'asc' | 'desc'}\` | (keyof T & string);
4511
+ }
4512
+
4513
+ export interface Repository<T> {
4514
+ findById(id: T[keyof T]): Promise<T | null>;
4515
+ findOne(where: Where<T>): Promise<T | null>;
4516
+ findMany(opts?: FindOptions<T>): Promise<T[]>;
4517
+ count(opts?: { where?: Where<T> }): Promise<number>;
4518
+ insert(row: T): Promise<T>;
4519
+ upsert(row: T): Promise<T>;
4520
+ update(id: T[keyof T], patch: Partial<T>): Promise<T | null>;
4521
+ delete(id: T[keyof T]): Promise<boolean>;
4522
+ deleteMany(where: Where<T>): Promise<number>;
4523
+ clear(): Promise<void>;
4524
+ }
4525
+
4526
+ // ── Provider interface (future-proof for sqlite/etc.) ────────────────────────
4527
+
4528
+ export interface Provider {
4529
+ load(entity: string): Promise<Record<string, unknown>[]>;
4530
+ save(entity: string, rows: Record<string, unknown>[]): Promise<void>;
4531
+ /** Atomic batch. Implementations may optimize. */
4532
+ transaction(work: (snapshot: Map<string, Record<string, unknown>[]>) => Promise<void> | void): Promise<void>;
4533
+ }
4534
+
4535
+ // ── Storage provider (filesystem JSON) ───────────────────────────────────────
4536
+
4537
+ class StorageProvider implements Provider {
4538
+ private cache = new Map<string, Record<string, unknown>[]>();
4539
+
4540
+ constructor(private readonly root: string) {
4541
+ fs.mkdirSync(root, { recursive: true });
4542
+ }
4543
+
4544
+ private fileFor(entity: string): string {
4545
+ return path.join(this.root, \`\${entity}.json\`);
4546
+ }
4547
+
4548
+ async load(entity: string): Promise<Record<string, unknown>[]> {
4549
+ if (this.cache.has(entity)) return this.cache.get(entity)!;
4550
+ const f = this.fileFor(entity);
4551
+ if (!fs.existsSync(f)) {
4552
+ this.cache.set(entity, []);
4553
+ return [];
4554
+ }
4555
+ try {
4556
+ const rows = JSON.parse(fs.readFileSync(f, 'utf8'));
4557
+ this.cache.set(entity, rows);
4558
+ return rows;
4559
+ } catch {
4560
+ this.cache.set(entity, []);
4561
+ return [];
4562
+ }
4563
+ }
4564
+
4565
+ async save(entity: string, rows: Record<string, unknown>[]): Promise<void> {
4566
+ this.cache.set(entity, rows);
4567
+ const f = this.fileFor(entity);
4568
+ const tmp = \`\${f}.tmp\`;
4569
+ fs.writeFileSync(tmp, JSON.stringify(rows));
4570
+ fs.renameSync(tmp, f); // atomic on same filesystem
4571
+ }
4572
+
4573
+ async transaction(
4574
+ work: (snapshot: Map<string, Record<string, unknown>[]>) => Promise<void> | void,
4575
+ ): Promise<void> {
4576
+ // Snapshot pre-tx state for rollback. Working copy is what \`work\` mutates.
4577
+ const backup = new Map<string, Record<string, unknown>[]>();
4578
+ for (const [k, v] of this.cache) backup.set(k, structuredClone(v));
4579
+ const working = new Map<string, Record<string, unknown>[]>();
4580
+ for (const [k, v] of this.cache) working.set(k, structuredClone(v));
4581
+ try {
4582
+ await work(working);
4583
+ for (const [entity, rows] of working) await this.save(entity, rows);
4584
+ } catch (err) {
4585
+ // Roll back in-memory cache to the pre-tx snapshot.
4586
+ for (const [k, v] of backup) this.cache.set(k, v);
4587
+ throw err;
4588
+ }
4589
+ }
4590
+ }
4591
+
4592
+ // ── Public DB type ───────────────────────────────────────────────────────────
4593
+
4594
+ export interface Db {
4595
+ <T extends object>(entity: Entity<T>): Repository<T>;
4596
+ transaction(work: (tx: Db) => Promise<void> | void): Promise<void>;
4597
+ /** Wipe a single entity. */
4598
+ drop(entity: Entity<unknown>): Promise<void>;
4599
+ /** Underlying provider — escape hatch for advanced use. */
4600
+ readonly provider: Provider;
4601
+ }
4602
+
4603
+ export interface CreateDbOptions {
4604
+ /** \`'storage'\` writes under \`context.storageUri/<subdir>/\`. \`'global'\` uses \`globalStorageUri\`. */
4605
+ provider: 'storage' | 'global';
4606
+ /** Override sub-directory under the chosen storage root. Default: \`db\`. */
4607
+ subdir?: string;
4608
+ }
4609
+
4610
+ export function createDb(ctx: vscode.ExtensionContext, opts: CreateDbOptions): Db {
4611
+ // \`storageUri\` is only defined when a workspace/folder is open. \`globalStorageUri\`
4612
+ // is always available — fall back to it so the extension still activates with no
4613
+ // folder open (e.g. the Extension Development Host on first launch).
4614
+ const baseUri =
4615
+ opts.provider === 'global'
4616
+ ? ctx.globalStorageUri
4617
+ : (ctx.storageUri ?? ctx.globalStorageUri);
4618
+ if (!baseUri) {
4619
+ throw new Error('createDb: no storage URI available from the extension context.');
4620
+ }
4621
+ const root = path.join(baseUri.fsPath, opts.subdir ?? 'db');
4622
+ const provider = new StorageProvider(root);
4623
+ ctx.subscriptions.push({ dispose: () => {} }); // future hook
4624
+ return makeDb(provider);
4625
+ }
4626
+
4627
+ // ── Singleton accessor — \`import { db } from './db'; await db()(Users).insert(...)\` ──
4628
+
4629
+ let _db: Db | undefined;
4630
+
4631
+ /**
4632
+ * Default options used by \`initDb\` when called as a bootstrap hook (one-arg).
4633
+ * Override via the 2-arg form: \`initDb(context, { provider: 'global' })\`.
4634
+ */
4635
+ export const dbOptions: CreateDbOptions = { provider: '{{provider}}' };
4636
+
4637
+ /**
4638
+ * Initialize the shared db. Call once on activate. Idempotent.
4639
+ *
4640
+ * As a bootstrap hook (recommended — \`bootstrap(registry, { onActivate: [initDb] })\`):
4641
+ * initDb(context)
4642
+ *
4643
+ * Direct call with custom options:
4644
+ * initDb(context, { provider: 'global' })
4645
+ */
4646
+ export function initDb(ctx: vscode.ExtensionContext, opts?: CreateDbOptions): Db {
4647
+ if (_db) return _db;
4648
+ _db = createDb(ctx, opts ?? dbOptions);
4649
+ return _db;
4650
+ }
4651
+
4652
+ /** Access the shared db. Throws if \`initDb()\` wasn't called yet. */
4653
+ export function db(): Db {
4654
+ if (!_db) throw new Error('db not initialized — call initDb(context) on activate.');
4655
+ return _db;
4656
+ }
4657
+
4658
+ function makeDb(provider: Provider): Db {
4659
+ const repoFor = <T extends object>(entity: Entity<T>): Repository<T> => {
4660
+ const pk = entity.primaryKey;
4661
+ const load = () => provider.load(entity.name) as Promise<T[]>;
4662
+ const save = (rows: T[]) => provider.save(entity.name, rows as Record<string, unknown>[]);
4663
+
4664
+ return {
4665
+ async findById(id) {
4666
+ const rows = await load();
4667
+ return rows.find((r) => r[pk] === id) ?? null;
4668
+ },
4669
+ async findOne(where) {
4670
+ const rows = await load();
4671
+ return rows.find((r) => match(r, where)) ?? null;
4672
+ },
4673
+ async findMany(opts = {}) {
4674
+ let rows = await load();
4675
+ if (opts.where) rows = rows.filter((r) => match(r, opts.where!));
4676
+ if (opts.orderBy) {
4677
+ const [field, dir] = String(opts.orderBy).split(':');
4678
+ const sign = dir === 'desc' ? -1 : 1;
4679
+ rows = [...rows].sort((a, b) => compare(a[field as keyof T], b[field as keyof T]) * sign);
4680
+ }
4681
+ if (opts.offset) rows = rows.slice(opts.offset);
4682
+ if (opts.limit !== undefined) rows = rows.slice(0, opts.limit);
4683
+ return rows;
4684
+ },
4685
+ async count(opts = {}) {
4686
+ const rows = await load();
4687
+ return opts.where ? rows.filter((r) => match(r, opts.where!)).length : rows.length;
4688
+ },
4689
+ async insert(row) {
4690
+ const rows = await load();
4691
+ if (rows.some((r) => r[pk] === row[pk])) {
4692
+ throw new Error(\`\${entity.name}: duplicate \${pk}=\${String(row[pk])}\`);
4693
+ }
4694
+ rows.push(row);
4695
+ await save(rows);
4696
+ return row;
4697
+ },
4698
+ async upsert(row) {
4699
+ const rows = await load();
4700
+ const i = rows.findIndex((r) => r[pk] === row[pk]);
4701
+ if (i >= 0) rows[i] = row; else rows.push(row);
4702
+ await save(rows);
4703
+ return row;
4704
+ },
4705
+ async update(id, patch) {
4706
+ const rows = await load();
4707
+ const i = rows.findIndex((r) => r[pk] === id);
4708
+ if (i < 0) return null;
4709
+ rows[i] = { ...rows[i], ...patch };
4710
+ await save(rows);
4711
+ return rows[i];
4712
+ },
4713
+ async delete(id) {
4714
+ const rows = await load();
4715
+ const before = rows.length;
4716
+ const next = rows.filter((r) => r[pk] !== id);
4717
+ if (next.length === before) return false;
4718
+ await save(next);
4719
+ return true;
4720
+ },
4721
+ async deleteMany(where) {
4722
+ const rows = await load();
4723
+ const next = rows.filter((r) => !match(r, where));
4724
+ const removed = rows.length - next.length;
4725
+ if (removed > 0) await save(next);
4726
+ return removed;
4727
+ },
4728
+ async clear() {
4729
+ await save([]);
4730
+ },
4731
+ };
4732
+ };
4733
+
4734
+ const db: Db = Object.assign(repoFor as any, {
4735
+ provider,
4736
+ async drop(entity: Entity<unknown>) {
4737
+ await provider.save(entity.name, []);
4738
+ },
4739
+ async transaction(work: (tx: Db) => Promise<void> | void) {
4740
+ await provider.transaction(async (snapshot) => {
4741
+ // Build a tx-scoped Db that reads/writes the snapshot map.
4742
+ const txProvider: Provider = {
4743
+ async load(name) { return snapshot.get(name) ?? []; },
4744
+ async save(name, rows) { snapshot.set(name, rows); },
4745
+ async transaction() { throw new Error('Nested transactions are not supported'); },
4746
+ };
4747
+ await work(makeDb(txProvider));
4748
+ });
4749
+ },
4750
+ });
4751
+
4752
+ return db;
4753
+ }
4754
+
4755
+ // ── matcher ──────────────────────────────────────────────────────────────────
4756
+
4757
+ function match<T extends object>(row: T, where: Where<T>): boolean {
4758
+ for (const key of Object.keys(where) as (keyof T)[]) {
4759
+ const expected = where[key] as unknown;
4760
+ const actual = row[key];
4761
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
4762
+ if ('in' in expected) {
4763
+ if (!(expected as { in: unknown[] }).in.includes(actual)) return false;
4764
+ continue;
4765
+ }
4766
+ if ('neq' in expected) {
4767
+ if (actual === (expected as { neq: unknown }).neq) return false;
4768
+ continue;
4769
+ }
4770
+ }
4771
+ if (actual !== expected) return false;
4772
+ }
4773
+ return true;
4774
+ }
4775
+
4776
+ function compare(a: unknown, b: unknown): number {
4777
+ if (a === b) return 0;
4778
+ if (a === null || a === undefined) return -1;
4779
+ if (b === null || b === undefined) return 1;
4780
+ return a < b ? -1 : 1;
4781
+ }
4782
+ `,
4783
+ "_generators/helper/notifications.ts.tpl": `import * as vscode from 'vscode';
4784
+
4785
+ /**
4786
+ * Concise wrappers over \`vscode.window.show*Message\`. Each accepts an optional
4787
+ * list of action labels and resolves to the selected label (or undefined).
4788
+ *
4789
+ * Usage:
4790
+ * notify.info('Saved');
4791
+ * const pick = await notify.warn('Discard?', 'Discard', 'Keep');
4792
+ * if (pick === 'Discard') ...
4793
+ *
4794
+ * For long-running tasks use \`withProgress\`:
4795
+ * await withProgress('Indexing…', async (report) => {
4796
+ * for (let i = 0; i <= 100; i += 10) {
4797
+ * report({ increment: 10, message: \`\${i}%\` });
4798
+ * await new Promise(r => setTimeout(r, 100));
4799
+ * }
4800
+ * });
4801
+ */
4802
+ export const notify = {
4803
+ info(message: string, ...actions: string[]) {
4804
+ return vscode.window.showInformationMessage(message, ...actions);
4805
+ },
4806
+ warn(message: string, ...actions: string[]) {
4807
+ return vscode.window.showWarningMessage(message, ...actions);
4808
+ },
4809
+ error(message: string, ...actions: string[]) {
4810
+ return vscode.window.showErrorMessage(message, ...actions);
4811
+ },
4812
+ confirm(message: string, yesLabel = 'Yes', noLabel = 'No'): Thenable<boolean> {
4813
+ return vscode.window
4814
+ .showInformationMessage(message, { modal: true }, yesLabel, noLabel)
4815
+ .then((pick) => pick === yesLabel);
4816
+ },
4817
+ };
4818
+
4819
+ export function withProgress<T>(
4820
+ title: string,
4821
+ task: (report: (p: { message?: string; increment?: number }) => void) => Thenable<T> | T,
4822
+ location: vscode.ProgressLocation = vscode.ProgressLocation.Notification,
4823
+ ): Thenable<T> {
4824
+ return vscode.window.withProgress({ location, title, cancellable: false }, (progress) =>
4825
+ Promise.resolve(task((p) => progress.report(p))),
4826
+ );
4827
+ }
4828
+ `,
4829
+ "_generators/helper/secrets.ts.tpl": `import * as vscode from 'vscode';
4830
+
4831
+ /**
4832
+ * Typed wrapper over \`context.secrets\` (SecretStorage backed by OS keychain).
4833
+ * Inject the extension context once on activate (bootstrap does this if you
4834
+ * import this module from your extension entry).
4835
+ *
4836
+ * Usage:
4837
+ * await secrets.set('githubToken', 'ghp_xxx');
4838
+ * const token = await secrets.get('githubToken');
4839
+ */
4840
+ let _ctx: vscode.ExtensionContext | undefined;
4841
+
4842
+ export function initSecrets(ctx: vscode.ExtensionContext) {
4843
+ _ctx = ctx;
4844
+ }
4845
+
4846
+ function ctx(): vscode.ExtensionContext {
4847
+ if (!_ctx) throw new Error('Secrets helper not initialized — call initSecrets(context) on activate.');
4848
+ return _ctx;
4849
+ }
4850
+
4851
+ export const secrets = {
4852
+ get(key: string): Thenable<string | undefined> {
4853
+ return ctx().secrets.get(key);
4854
+ },
4855
+ set(key: string, value: string): Thenable<void> {
4856
+ return ctx().secrets.store(key, value);
4857
+ },
4858
+ delete(key: string): Thenable<void> {
4859
+ return ctx().secrets.delete(key);
4860
+ },
4861
+ onChange(listener: (key: string) => void): vscode.Disposable {
4862
+ return ctx().secrets.onDidChange((e) => listener(e.key));
4863
+ },
4864
+ };
4865
+ `,
4866
+ "_generators/helper/state.ts.tpl": `import * as vscode from 'vscode';
4867
+
4868
+ /**
4869
+ * Typed wrapper over \`context.{workspaceState, globalState}\`.
4870
+ * - workspace: scoped to the current workspace (per-project preferences)
4871
+ * - global: shared across all workspaces (user-wide settings, last-opened file)
4872
+ *
4873
+ * Usage:
4874
+ * await state.workspace.set('lastQuery', 'foo');
4875
+ * const q = state.workspace.get<string>('lastQuery');
4876
+ */
4877
+ let _ctx: vscode.ExtensionContext | undefined;
4878
+
4879
+ export function initState(ctx: vscode.ExtensionContext) {
4880
+ _ctx = ctx;
4881
+ }
4882
+
4883
+ function ctx(): vscode.ExtensionContext {
4884
+ if (!_ctx) throw new Error('State helper not initialized — call initState(context) on activate.');
4885
+ return _ctx;
4886
+ }
4887
+
4888
+ function wrap(memento: () => vscode.Memento) {
4889
+ return {
4890
+ get<T>(key: string, fallback?: T): T | undefined {
4891
+ const v = memento().get<T>(key);
4892
+ return v === undefined ? fallback : v;
4893
+ },
4894
+ set(key: string, value: unknown): Thenable<void> {
4895
+ return memento().update(key, value);
4896
+ },
4897
+ delete(key: string): Thenable<void> {
4898
+ return memento().update(key, undefined);
4899
+ },
4900
+ keys(): readonly string[] {
4901
+ return memento().keys();
4902
+ },
4903
+ };
4904
+ }
4905
+
4906
+ export const state = {
4907
+ workspace: wrap(() => ctx().workspaceState),
4908
+ global: wrap(() => ctx().globalState),
4909
+ };
4910
+ `,
4911
+ "_generators/job/job.ts.tpl": `import { defineJob } from '../shared/vsceasy';
4912
+
4913
+ export default defineJob({
4914
+ title: '{{title}}',
4915
+ schedule: {{schedule}},{{minIntervalLine}}
4916
+ run: async (vscode, ctx) => {
4917
+ // TODO: implement {{name}} work
4918
+ console.log('[{{name}}] tick', new Date().toISOString());
4919
+ },
4920
+ });
4921
+ `,
4922
+ "_generators/menu/menu.ts.tpl": `import { defineMenu } from '../shared/vsceasy';
4923
+
4924
+ export default defineMenu({
4925
+ title: '{{title}}',
4926
+ icon: '{{icon}}',
4927
+ items: [
4928
+ {
4929
+ label: 'Panels',
4930
+ children: [
4931
+ // { label: 'Dashboard', panel: 'dashboard' },
4932
+ ],
4933
+ },
4934
+ {
4935
+ label: 'Actions',
4936
+ children: [
4937
+ // { label: 'Hello', command: 'hello', icon: 'play' },
4938
+ // { label: 'Docs', url: 'https://example.com', icon: 'book' },
4939
+ ],
4940
+ },
4941
+ ],
4942
+ });
4943
+ `,
4944
+ "_generators/model/model.ts.tpl": `import { defineEntity, db } from '../helpers/db';
4945
+
4946
+ export interface {{Name}} {
4947
+ {{fieldLines}}
4948
+ }
4949
+
4950
+ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
4951
+ primaryKey: '{{primaryKey}}',{{indexesLine}}
4952
+ });
4953
+
4954
+ /**
4955
+ * Typed repo accessor. Lazy — assumes \`initDb(context)\` ran on activate.
4956
+ *
4957
+ * import { {{Plural}}Repo } from '../models/{{Name}}';
4958
+ * await {{Plural}}Repo().insert({ ... });
4959
+ */
4960
+ export const {{Plural}}Repo = () => db()({{Plural}});
4961
+ `,
4962
+ "_generators/panel/App.tsx.tpl": `import React from 'react';
4963
+ {{apiBlock}}
4964
+ export function App() {
4965
+ return (
4966
+ <div className="app">
4967
+ <h1>{{title}}</h1>
4968
+ <p>Edit <code>src/webview/panels/{{name}}/App.tsx</code> to start building.</p>
4969
+ </div>
4970
+ );
4971
+ }
4972
+ `,
4973
+ "_generators/panel/main.tsx.tpl": `import React from 'react';
4974
+ import { createRoot } from 'react-dom/client';
4975
+ import { App } from './App';
4976
+ import '../../styles.css';
4977
+
4978
+ createRoot(document.getElementById('root')!).render(<App />);
4979
+ `,
4980
+ "_generators/panel/panel.ts.tpl": `import { definePanel } from '../shared/vsceasy';
4981
+ {{apiImport}}
4982
+ export default definePanel{{apiGeneric}}({
4983
+ title: '{{title}}',{{rpcBlock}}
4984
+ });
4985
+ `,
4986
+ "_generators/panel/templates/dashboard/App.tsx.tpl": `import React, { useEffect, useState } from 'react';
4987
+ import { Button, Card } from '../../components';
4988
+ import '../../components/components.css';
4989
+ {{apiBlock}}
4990
+ interface Stats {
4991
+ total: number;
4992
+ active: number;
4993
+ updatedAt: string;
4994
+ }
4995
+
4996
+ export function App() {
4997
+ const [stats, setStats] = useState<Stats | null>(null);
4998
+ const [loading, setLoading] = useState(true);
4999
+
5000
+ async function refresh() {
5001
+ setLoading(true);
5002
+ try {
5003
+ {{statsCall}}
5004
+ } finally {
5005
+ setLoading(false);
5006
+ }
5007
+ }
5008
+
5009
+ useEffect(() => {
5010
+ void refresh();
5011
+ }, []);
5012
+
5013
+ return (
5014
+ <div className="app">
5015
+ <h1>{{title}}</h1>
5016
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(8rem, 1fr))', gap: '0.75rem' }}>
5017
+ <Card title="Total"><strong style={{ fontSize: '1.5rem' }}>{stats?.total ?? '—'}</strong></Card>
5018
+ <Card title="Active"><strong style={{ fontSize: '1.5rem' }}>{stats?.active ?? '—'}</strong></Card>
5019
+ <Card title="Updated"><span>{stats?.updatedAt ?? '—'}</span></Card>
5020
+ </div>
5021
+ <div style={{ marginTop: '0.75rem' }}>
5022
+ <Button variant="secondary" onClick={refresh} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</Button>
5023
+ </div>
5024
+ </div>
5025
+ );
5026
+ }
5027
+ `,
5028
+ "_generators/panel/templates/form/App.tsx.tpl": `import React, { useState } from 'react';
5029
+ import { Button, Input, Field, Card } from '../../components';
5030
+ import '../../components/components.css';
5031
+ {{apiBlock}}
5032
+ export function App() {
5033
+ const [name, setName] = useState('');
5034
+ const [email, setEmail] = useState('');
5035
+ const [status, setStatus] = useState<string | null>(null);
5036
+ const [busy, setBusy] = useState(false);
5037
+
5038
+ async function onSubmit(e: React.FormEvent) {
5039
+ e.preventDefault();
5040
+ setBusy(true);
5041
+ setStatus(null);
5042
+ try {
5043
+ {{submitCall}}
5044
+ setStatus('Saved.');
5045
+ setName('');
5046
+ setEmail('');
5047
+ } catch (err) {
5048
+ setStatus(err instanceof Error ? err.message : 'Failed.');
5049
+ } finally {
5050
+ setBusy(false);
5051
+ }
5052
+ }
5053
+
5054
+ return (
5055
+ <div className="app">
5056
+ <h1>{{title}}</h1>
5057
+ <Card title="New entry">
5058
+ <form onSubmit={onSubmit}>
5059
+ <Field label="Name" htmlFor="name">
5060
+ <Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Jane Doe" required />
5061
+ </Field>
5062
+ <Field label="Email" htmlFor="email">
5063
+ <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="jane@acme.io" />
5064
+ </Field>
5065
+ <Button type="submit" disabled={busy}>{busy ? 'Saving…' : 'Save'}</Button>
5066
+ {status ? <span style={{ marginLeft: '0.75rem' }}>{status}</span> : null}
5067
+ </form>
5068
+ </Card>
5069
+ </div>
5070
+ );
5071
+ }
5072
+ `,
5073
+ "_generators/panel/templates/list/App.tsx.tpl": `import React, { useEffect, useState } from 'react';
5074
+ import { Button, List, Card } from '../../components';
5075
+ import '../../components/components.css';
5076
+ {{apiBlock}}
5077
+ interface Row {
5078
+ id: string;
5079
+ label: string;
5080
+ }
5081
+
5082
+ export function App() {
5083
+ const [rows, setRows] = useState<Row[]>([]);
5084
+ const [loading, setLoading] = useState(true);
5085
+
5086
+ async function refresh() {
5087
+ setLoading(true);
5088
+ try {
5089
+ {{loadCall}}
5090
+ } finally {
5091
+ setLoading(false);
5092
+ }
5093
+ }
5094
+
5095
+ useEffect(() => {
5096
+ void refresh();
5097
+ }, []);
5098
+
5099
+ return (
5100
+ <div className="app">
5101
+ <h1>{{title}}</h1>
5102
+ <Card title="Items" actions={<Button variant="secondary" onClick={refresh} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</Button>}>
5103
+ <List
5104
+ items={rows}
5105
+ getKey={(r) => r.id}
5106
+ renderItem={(r) => r.label}
5107
+ empty="No items yet."
5108
+ />
5109
+ </Card>
5110
+ </div>
5111
+ );
5112
+ }
5113
+ `,
5114
+ "_generators/publish/CHANGELOG.md.tpl": `# Change Log
5115
+
5116
+ All notable changes follow [Keep a Changelog](https://keepachangelog.com/).
5117
+
5118
+ ## [Unreleased]
5119
+
5120
+ ### Added
5121
+ - Initial release.
5122
+ `,
5123
+ "_generators/publish/README.md.tpl": `# {{displayName}}
5124
+
5125
+ {{description}}
5126
+
5127
+ ## Features
5128
+
5129
+ - (describe what this extension does)
5130
+
5131
+ ## Requirements
5132
+
5133
+ VS Code ≥ 1.80
5134
+
5135
+ ## Extension Settings
5136
+
5137
+ (list your \`contributes.configuration\` entries)
5138
+
5139
+ ## Known Issues
5140
+
5141
+
5142
+
5143
+ ## Release Notes
5144
+
5145
+ See [CHANGELOG.md](./CHANGELOG.md).
5146
+ `,
5147
+ "_generators/statusBar/statusBar.ts.tpl": `import { defineStatusBar } from '../shared/vsceasy';
5148
+
5149
+ export default defineStatusBar({
5150
+ text: '{{text}}',{{iconLine}}{{tooltipLine}}
5151
+ alignment: '{{alignment}}',
5152
+ priority: {{priority}},{{commandLine}}{{panelLine}}
5153
+ });
5154
+ `,
5155
+ "_generators/subpanel/App.tsx.tpl": `import React from 'react';
5156
+ {{apiBlock}}
5157
+ export function App() {
5158
+ return (
5159
+ <div className="sidebar-view">
5160
+ <h2>{{title}}</h2>
5161
+ <p>Edit <code>src/webview/subpanels/{{name}}/App.tsx</code> to start building.</p>
5162
+ </div>
5163
+ );
5164
+ }
5165
+ `,
5166
+ "_generators/subpanel/main.tsx.tpl": `import React from 'react';
5167
+ import { createRoot } from 'react-dom/client';
5168
+ import { App } from './App';
5169
+ import '../../styles.css';
5170
+
5171
+ createRoot(document.getElementById('root')!).render(<App />);
5172
+ `,
5173
+ "_generators/subpanel/subpanel.ts.tpl": `import { defineSubpanel } from '../shared/vsceasy';
5174
+ {{apiImport}}
5175
+ export default defineSubpanel{{apiGeneric}}({
5176
+ title: '{{title}}',
5177
+ menu: '{{menu}}',{{rpcBlock}}
5178
+ });
5179
+ `,
5180
+ "_generators/test/_helpers.ts.tpl": `import { vi } from 'vitest';
5181
+ import { createRpcServer, createRpcClient } from '../shared/vsceasy/rpc';
5182
+ import type { Transport, Handlers, RpcClient } from '../shared/vsceasy/rpc';
5183
+
5184
+ /**
5185
+ * Minimal \`vscode\` namespace mock — covers the surface most extensions touch.
5186
+ * Extend per test by spreading or assigning new spies onto the returned object.
5187
+ *
5188
+ * const vscode = mockVscode();
5189
+ * vscode.window.showInformationMessage('hi');
5190
+ * expect(vscode.window.showInformationMessage).toHaveBeenCalledWith('hi');
5191
+ */
5192
+ export function mockVscode() {
5193
+ const subscriptions: { dispose(): void }[] = [];
5194
+ return {
5195
+ window: {
5196
+ showInformationMessage: vi.fn(),
5197
+ showWarningMessage: vi.fn(),
5198
+ showErrorMessage: vi.fn(),
5199
+ showInputBox: vi.fn(),
5200
+ showQuickPick: vi.fn(),
5201
+ withProgress: vi.fn(async (_opts: any, task: any) => task({ report: vi.fn() })),
5202
+ createStatusBarItem: vi.fn(() => ({ show: vi.fn(), dispose: vi.fn(), text: '' })),
5203
+ createTreeView: vi.fn(() => ({ dispose: vi.fn() })),
5204
+ createWebviewPanel: vi.fn(),
5205
+ activeTextEditor: undefined as any,
5206
+ },
5207
+ workspace: {
5208
+ getConfiguration: vi.fn(() => ({ get: vi.fn(), update: vi.fn(async () => undefined) })),
5209
+ onDidChangeConfiguration: vi.fn(() => ({ dispose: vi.fn() })),
5210
+ findFiles: vi.fn(async () => [] as any[]),
5211
+ workspaceFolders: [] as any[],
5212
+ },
5213
+ commands: {
5214
+ registerCommand: vi.fn((_id: string, _fn: any) => ({ dispose: vi.fn() })),
5215
+ executeCommand: vi.fn(async () => undefined),
5216
+ },
5217
+ env: {
5218
+ openExternal: vi.fn(async () => true),
5219
+ },
5220
+ Uri: { parse: (s: string) => ({ toString: () => s }), file: (p: string) => ({ fsPath: p, toString: () => p }) },
5221
+ ProgressLocation: { Notification: 15, SourceControl: 1, Window: 10 } as const,
5222
+ ConfigurationTarget: { Global: 1, Workspace: 2, WorkspaceFolder: 3 } as const,
5223
+ TreeItemCollapsibleState: { None: 0, Collapsed: 1, Expanded: 2 } as const,
5224
+ StatusBarAlignment: { Left: 1, Right: 2 } as const,
5225
+ EventEmitter: class {
5226
+ private listeners: Array<(v: any) => void> = [];
5227
+ event = (l: (v: any) => void) => {
5228
+ this.listeners.push(l);
5229
+ return { dispose: () => (this.listeners = this.listeners.filter((x) => x !== l)) };
5230
+ };
5231
+ fire(v: any) {
5232
+ this.listeners.forEach((l) => l(v));
5233
+ }
5234
+ dispose() {
5235
+ this.listeners = [];
5236
+ }
5237
+ },
5238
+ Disposable: { from: (...d: any[]) => ({ dispose: () => d.forEach((x) => x.dispose?.()) }) },
5239
+ subscriptions,
5240
+ };
5241
+ }
5242
+
5243
+ export type VscodeMock = ReturnType<typeof mockVscode>;
5244
+
5245
+ /**
5246
+ * Minimal \`ExtensionContext\` mock — backed by in-memory Maps for state/secrets.
5247
+ */
5248
+ export function mockContext() {
5249
+ const wm = new Map<string, unknown>();
5250
+ const gm = new Map<string, unknown>();
5251
+ const sm = new Map<string, string>();
5252
+ const memento = (m: Map<string, unknown>) => ({
5253
+ get: (k: string, d?: unknown) => (m.has(k) ? m.get(k) : d),
5254
+ update: async (k: string, v: unknown) => void (v === undefined ? m.delete(k) : m.set(k, v)),
5255
+ keys: () => Array.from(m.keys()),
5256
+ });
5257
+ return {
5258
+ subscriptions: [] as { dispose(): void }[],
5259
+ workspaceState: memento(wm),
5260
+ globalState: memento(gm),
5261
+ secrets: {
5262
+ get: async (k: string) => sm.get(k),
5263
+ store: async (k: string, v: string) => void sm.set(k, v),
5264
+ delete: async (k: string) => void sm.delete(k),
5265
+ onDidChange: () => ({ dispose: () => {} }),
5266
+ },
5267
+ extensionPath: '/mock',
5268
+ extensionUri: { fsPath: '/mock' },
5269
+ };
5270
+ }
5271
+
5272
+ /**
5273
+ * Build an in-memory RPC pair (server + typed client). No webview involved.
5274
+ * Useful for testing your handlers end-to-end with the same types the UI sees.
5275
+ *
5276
+ * const handlers = { greet: (n: string) => \`hi \${n}\` };
5277
+ * const api = mockRpcPair<typeof handlers>(handlers);
5278
+ * expect(await api.greet('Jairo')).toBe('hi Jairo');
5279
+ */
5280
+ export function mockRpcPair<H extends Handlers>(handlers: H): RpcClient<H> {
5281
+ const aListeners = new Set<(m: any) => void>();
5282
+ const bListeners = new Set<(m: any) => void>();
5283
+ const aToB: Transport = {
5284
+ send: (m) => bListeners.forEach((l) => l(m)),
5285
+ onMessage: (h) => {
5286
+ aListeners.add(h);
5287
+ return () => aListeners.delete(h);
5288
+ },
5289
+ };
5290
+ const bToA: Transport = {
5291
+ send: (m) => aListeners.forEach((l) => l(m)),
5292
+ onMessage: (h) => {
5293
+ bListeners.add(h);
5294
+ return () => bListeners.delete(h);
5295
+ },
5296
+ };
5297
+ createRpcServer<H>(bToA, handlers);
5298
+ return createRpcClient<H>(aToB, { callTimeoutMs: 0 });
5299
+ }
5300
+ `,
5301
+ "_generators/test/sample.test.ts.tpl": `import { describe, it, expect } from 'vitest';
5302
+ import { mockVscode, mockContext, mockRpcPair } from './_helpers';
5303
+
5304
+ describe('vscode mock', () => {
5305
+ it('captures notification calls', () => {
5306
+ const vscode = mockVscode();
5307
+ vscode.window.showInformationMessage('hello');
5308
+ expect(vscode.window.showInformationMessage).toHaveBeenCalledWith('hello');
5309
+ });
5310
+
5311
+ it('persists state via mock context', async () => {
5312
+ const ctx = mockContext();
5313
+ await ctx.workspaceState.update('key', 42);
5314
+ expect(ctx.workspaceState.get('key')).toBe(42);
5315
+ });
5316
+ });
5317
+
5318
+ describe('RPC pair', () => {
5319
+ it('round-trips a typed handler call', async () => {
5320
+ const handlers = {
5321
+ async greet(name: string) {
5322
+ return \`hi \${name}\`;
5323
+ },
5324
+ };
5325
+ const api = mockRpcPair<typeof handlers>(handlers);
5326
+ expect(await api.greet('Jairo')).toBe('hi Jairo');
5327
+ });
5328
+
5329
+ it('propagates handler errors', async () => {
5330
+ const handlers = {
5331
+ async boom() {
5332
+ throw new Error('nope');
5333
+ },
5334
+ };
5335
+ const api = mockRpcPair<typeof handlers>(handlers);
5336
+ await expect(api.boom()).rejects.toThrow('nope');
5337
+ });
5338
+ });
5339
+ `,
5340
+ "_generators/test/vitest.config.ts.tpl": `import { defineConfig } from 'vitest/config';
5341
+ import * as path from 'path';
5342
+
5343
+ export default defineConfig({
5344
+ resolve: {
5345
+ alias: {
5346
+ // \`vscode\` is only available inside the extension host. Tests stub it
5347
+ // with an empty module so importing any file that does \`import * as vscode\`
5348
+ // doesn't crash. Use \`mockVscode()\` from \`_helpers.ts\` for the surface
5349
+ // you actually need to assert against.
5350
+ vscode: path.resolve(__dirname, 'src/__tests__/__mocks__/vscode.ts'),
5351
+ },
5352
+ },
5353
+ test: {
5354
+ include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
5355
+ environment: 'node',
5356
+ globals: false,
5357
+ coverage: {
5358
+ include: ['src/**/*.{ts,tsx}'],
5359
+ exclude: ['src/extension/_registry.ts', 'src/webview/**', 'src/__tests__/**'],
5360
+ },
5361
+ },
5362
+ });
5363
+ `,
5364
+ "_generators/test/vscode.stub.ts.tpl": `/**
5365
+ * Auto-generated \`vscode\` module stub for vitest. Replaces the real \`vscode\`
5366
+ * import inside tests (the real module only exists in the extension host).
5367
+ *
5368
+ * The stub exposes the enums and class shapes the runtime touches at module
5369
+ * load time so files that do \`import * as vscode from 'vscode'\` don't crash
5370
+ * during test collection.
5371
+ *
5372
+ * To assert behaviour against \`vscode.window.X\`, use \`mockVscode()\` from
5373
+ * \`_helpers.ts\` and inject it into the code under test directly.
5374
+ */
5375
+ export const TreeItemCollapsibleState = { None: 0, Collapsed: 1, Expanded: 2 } as const;
5376
+ export const StatusBarAlignment = { Left: 1, Right: 2 } as const;
5377
+ export const ConfigurationTarget = { Global: 1, Workspace: 2, WorkspaceFolder: 3 } as const;
5378
+ export const ProgressLocation = { SourceControl: 1, Window: 10, Notification: 15 } as const;
5379
+ export const ViewColumn = { Active: -1, Beside: -2, One: 1, Two: 2, Three: 3 } as const;
5380
+ export const ThemeIcon = class { constructor(public id: string) {} };
5381
+ export const ThemeColor = class { constructor(public id: string) {} };
5382
+ export const TreeItem = class { constructor(public label: string, public collapsibleState?: number) {} };
5383
+ export const MarkdownString = class {
5384
+ value = '';
5385
+ isTrusted = false;
5386
+ supportHtml = false;
5387
+ constructor(value = '') {
5388
+ this.value = value;
5389
+ }
5390
+ appendMarkdown(v: string) {
5391
+ this.value += v;
5392
+ return this;
5393
+ }
5394
+ };
5395
+ export const Uri = {
5396
+ parse: (s: string) => ({ toString: () => s, fsPath: s }),
5397
+ file: (p: string) => ({ fsPath: p, toString: () => p }),
5398
+ joinPath: (base: { fsPath: string }, ...segs: string[]) => ({
5399
+ fsPath: [base.fsPath, ...segs].join('/'),
5400
+ toString: () => [base.fsPath, ...segs].join('/'),
5401
+ }),
5402
+ };
5403
+ export const EventEmitter = class<T> {
5404
+ private listeners: Array<(v: T) => void> = [];
5405
+ event = (l: (v: T) => void) => {
5406
+ this.listeners.push(l);
5407
+ return { dispose: () => (this.listeners = this.listeners.filter((x) => x !== l)) };
5408
+ };
5409
+ fire(v: T) {
5410
+ this.listeners.forEach((l) => l(v));
5411
+ }
5412
+ dispose() {
5413
+ this.listeners = [];
5414
+ }
5415
+ };
5416
+ export const Disposable = {
5417
+ from: (...d: Array<{ dispose: () => void }>) => ({ dispose: () => d.forEach((x) => x.dispose()) }),
5418
+ };
5419
+
5420
+ const noop = () => {};
5421
+ const noopDisposable = { dispose: noop };
5422
+ const noopAsync = async () => undefined;
5423
+ const noopReg = () => noopDisposable;
5424
+
5425
+ export const window = {
5426
+ showInformationMessage: noopAsync,
5427
+ showWarningMessage: noopAsync,
5428
+ showErrorMessage: noopAsync,
5429
+ showInputBox: noopAsync,
5430
+ showQuickPick: noopAsync,
5431
+ withProgress: async (_o: unknown, task: (p: { report: () => void }) => unknown) =>
5432
+ task({ report: noop }),
5433
+ createStatusBarItem: () => ({ show: noop, hide: noop, dispose: noop, text: '' }),
5434
+ createTreeView: () => ({ dispose: noop }),
5435
+ createWebviewPanel: () => ({
5436
+ webview: { html: '', onDidReceiveMessage: noopReg, postMessage: noop, asWebviewUri: (u: unknown) => u },
5437
+ onDidDispose: noopReg,
5438
+ dispose: noop,
5439
+ }),
5440
+ registerWebviewViewProvider: noopReg,
5441
+ activeTextEditor: undefined as unknown,
5442
+ onDidChangeActiveTextEditor: noopReg,
5443
+ };
5444
+
5445
+ export const workspace = {
5446
+ getConfiguration: () => ({ get: noop, update: noopAsync, has: () => false, inspect: () => undefined }),
5447
+ onDidChangeConfiguration: noopReg,
5448
+ findFiles: async () => [] as unknown[],
5449
+ workspaceFolders: [] as unknown[],
5450
+ asRelativePath: (p: string) => p,
5451
+ };
5452
+
5453
+ export const commands = {
5454
+ registerCommand: noopReg,
5455
+ executeCommand: noopAsync,
5456
+ getCommands: async () => [] as string[],
5457
+ };
5458
+
5459
+ export const env = {
5460
+ openExternal: async () => true,
5461
+ clipboard: { writeText: noopAsync, readText: async () => '' },
5462
+ };
5463
+
5464
+ export const extensions = {
5465
+ getExtension: () => undefined,
5466
+ all: [] as unknown[],
5467
+ };
5468
+
5469
+ export const languages = {
5470
+ registerHoverProvider: noopReg,
5471
+ registerCompletionItemProvider: noopReg,
5472
+ };
5473
+ `,
5474
+ "_generators/treeView/treeView.ts.tpl": `import { defineTreeView, TreeNode } from '../shared/vsceasy';
5475
+
5476
+ export default defineTreeView({
5477
+ title: '{{title}}',
5478
+ menu: '{{menu}}',
5479
+ getChildren: async (parent, vscode, ctx) => {
5480
+ if (!parent) {
5481
+ return [
5482
+ { label: 'Item 1', icon: 'file', tooltip: 'Replace with real data' },
5483
+ { label: 'Group', icon: 'folder', collapsed: 'collapsed', children: [] },
5484
+ ] as TreeNode[];
5485
+ }
5486
+ // Lazy children — return based on parent.id / parent.contextValue.
5487
+ return [];
5488
+ },
5489
+ });
5490
+ `,
5491
+ "react/.gitignore": `node_modules
5492
+ dist
5493
+ *.vsix
5494
+ .DS_Store
5495
+ `,
5496
+ "react/.vscode/launch.json": `{
5497
+ "version": "0.2.0",
5498
+ "configurations": [
5499
+ {
5500
+ "name": "Run Extension",
5501
+ "type": "extensionHost",
5502
+ "request": "launch",
5503
+ "args": ["--extensionDevelopmentPath=\${workspaceFolder}"],
5504
+ "outFiles": ["\${workspaceFolder}/dist/**/*.js"],
5505
+ "sourceMaps": true,
5506
+ "smartStep": true,
5507
+ "preLaunchTask": "vsceasy: build"
5508
+ },
5509
+ {
5510
+ "name": "Run Extension (watch / HMR)",
5511
+ "type": "extensionHost",
5512
+ "request": "launch",
5513
+ "args": ["--extensionDevelopmentPath=\${workspaceFolder}"],
5514
+ "outFiles": ["\${workspaceFolder}/dist/**/*.js"],
5515
+ "sourceMaps": true,
5516
+ "smartStep": true,
5517
+ "preLaunchTask": "vsceasy: dev"
5518
+ },
5519
+ {
5520
+ "name": "Attach to Extension Host",
5521
+ "type": "node",
5522
+ "request": "attach",
5523
+ "port": 9229,
5524
+ "skipFiles": ["<node_internals>/**"],
5525
+ "sourceMaps": true,
5526
+ "outFiles": ["\${workspaceFolder}/dist/**/*.js"]
5527
+ }
5528
+ ]
5529
+ }
5530
+ `,
5531
+ "react/.vscode/tasks.json": `{
5532
+ "version": "2.0.0",
5533
+ "tasks": [
5534
+ {
5535
+ "label": "vsceasy: build",
5536
+ "type": "shell",
5537
+ "command": "bun run build",
5538
+ "presentation": { "reveal": "silent", "panel": "dedicated" },
5539
+ "problemMatcher": []
5540
+ },
5541
+ {
5542
+ "label": "vsceasy: dev",
5543
+ "type": "shell",
5544
+ "command": "bun run dev",
5545
+ "isBackground": true,
5546
+ "presentation": { "reveal": "always", "panel": "dedicated" },
5547
+ "group": { "kind": "build", "isDefault": true },
5548
+ "problemMatcher": {
5549
+ "owner": "esbuild-watch",
5550
+ "pattern": {
5551
+ "regexp": "^\\\\s*✘\\\\s*\\\\[ERROR\\\\]\\\\s+(.*)$",
5552
+ "message": 1
5553
+ },
5554
+ "background": {
5555
+ "activeOnStart": true,
5556
+ "beginsPattern": ".*build started.*|.*\\\\[watch\\\\] build started.*",
5557
+ "endsPattern": ".*built in.*|.*\\\\[watch\\\\] build finished.*"
5558
+ }
5559
+ }
5560
+ }
5561
+ ]
5562
+ }
5563
+ `,
5564
+ "react/.vscodeignore": `.vscode/**
5565
+ src/**
5566
+ scripts/**
5567
+ node_modules/**
5568
+ .gitignore
5569
+ tsconfig.json
5570
+ vite.config.ts
5571
+ *.map
5572
+ `,
5573
+ "react/README.md": `# {{displayName}}
5574
+
5575
+ Generated with [\`vsceasy\`](https://www.npmjs.com/package/vsceasy).
5576
+
5577
+ ## Develop
5578
+
5579
+ Two ways to launch the Extension Development Host:
5580
+
5581
+ ### Option A — one-shot launch (recommended for first run)
5582
+
5583
+ \`\`\`bash
5584
+ bun install
5585
+ bun run launch
5586
+ \`\`\`
5587
+
5588
+ Builds extension + webview once, then opens a new VS Code window with the extension loaded.
5589
+ Then in the dev window: Command Palette → **{{displayName}}: Open Dashboard**.
5590
+
5591
+ ### Option B — watch mode + F5
5592
+
5593
+ \`\`\`bash
5594
+ bun install
5595
+ bun run dev # leave running — watches both extension + webview
5596
+ \`\`\`
5597
+
5598
+ Then in VS Code (this folder open):
5599
+ - Press <kbd>F5</kbd> → picks **Run Extension** launch config → opens Extension Development Host.
5600
+ - Re-press <kbd>Ctrl/Cmd+R</kbd> inside the dev host after code changes to reload.
5601
+
5602
+ > Note: \`bun run dev\` only builds — it does NOT launch VS Code. F5 (or \`bun run launch\`) does.
5603
+
5604
+ ## Commands
5605
+
5606
+ - Command Palette → **{{displayName}}: Hello** → info toast
5607
+ - Command Palette → **{{displayName}}: Open Dashboard** → React webview
5608
+
5609
+ ## Structure
5610
+
5611
+ - \`src/extension/extension.ts\` — entry, registers commands
5612
+ - \`src/extension/panels/DashboardPanel.ts\` — webview panel + RPC handlers
5613
+ - \`src/webview/App.tsx\` — React UI (typed RPC client)
5614
+ - \`src/shared/api.ts\` — RPC contract (types flow to both sides)
5615
+ - \`src/shared/rpc.ts\` — bridge implementation
5616
+
5617
+ ## Package as \`.vsix\`
5618
+
5619
+ \`\`\`bash
5620
+ bun run build
5621
+ bun run package # → {{name}}-0.0.1.vsix
5622
+ \`\`\`
5623
+ `,
5624
+ "react/package.json": `{
5625
+ "name": "{{name}}",
5626
+ "displayName": "{{displayName}}",
5627
+ "description": "{{description}}",
5628
+ "version": "0.0.1",
5629
+ "publisher": "{{publisher}}",
5630
+ "engines": {
5631
+ "vscode": "^1.85.0"
5632
+ },
5633
+ "main": "./dist/extension.js",
5634
+ "contributes": {
5635
+ "commands": [
5636
+ {
5637
+ "command": "{{commandPrefix}}.hello",
5638
+ "title": "{{displayName}}: Hello"
5639
+ },
5640
+ {
5641
+ "command": "{{commandPrefix}}.openDashboard",
5642
+ "title": "{{displayName}}: Open Dashboard"
5643
+ }
5644
+ ]
5645
+ },
5646
+ "activationEvents": [],
5647
+ "scripts": {
5648
+ "gen:scan": "bun scripts/gen.ts",
5649
+ "gen": "bun run gen:scan && bun run build:ext",
5650
+ "dev": "bun run gen:scan && concurrently -k -n ext,ui -c blue,magenta \\"bun run dev:ext\\" \\"bun run dev:ui\\"",
5651
+ "dev:ext": "esbuild src/extension/extension.ts --bundle --platform=node --target=node18 --external:vscode --outfile=dist/extension.js --watch --sourcemap",
5652
+ "dev:ui": "vite build --watch --mode development",
5653
+ "launch": "bun run build && code --extensionDevelopmentPath=$PWD --new-window",
5654
+ "build": "bun run gen:scan && bun run build:ext && bun run build:ui",
5655
+ "build:ext": "esbuild src/extension/extension.ts --bundle --platform=node --target=node18 --external:vscode --outfile=dist/extension.js --sourcemap",
5656
+ "build:ext:prod": "esbuild src/extension/extension.ts --bundle --platform=node --target=node18 --external:vscode --outfile=dist/extension.js --minify",
5657
+ "build:ui": "vite build",
5658
+ "build:prod": "bun run gen:scan && bun run build:ext:prod && bun run build:ui",
5659
+ "package": "bun run build:prod && vsce package --no-dependencies"
5660
+ },
5661
+ "devDependencies": {
5662
+ "@types/node": "^20.0.0",
5663
+ "@types/react": "^18.2.0",
5664
+ "@types/react-dom": "^18.2.0",
5665
+ "@types/vscode": "^1.85.0",
5666
+ "@vitejs/plugin-react": "^4.2.0",
5667
+ "@vscode/vsce": "^2.24.0",
5668
+ "concurrently": "^8.2.0",
5669
+ "esbuild": "^0.20.0",
5670
+ "typescript": "^5.3.0",
5671
+ "vite": "^5.0.0"
5672
+ },
5673
+ "dependencies": {
5674
+ "react": "^18.2.0",
5675
+ "react-dom": "^18.2.0"
5676
+ }
5677
+ }
5678
+ `,
5679
+ "react/scripts/gen.ts": `#!/usr/bin/env bun
5680
+ // Scans src/panels, src/commands, and src/menus; writes src/extension/_registry.ts
5681
+ // and syncs package.json#contributes (commands, viewsContainers, views).
5682
+
5683
+ import * as fs from 'fs';
5684
+ import * as path from 'path';
5685
+
5686
+ const ROOT = process.cwd();
5687
+ const SRC = path.join(ROOT, 'src');
5688
+ const PANELS_DIR = path.join(SRC, 'panels');
5689
+ const COMMANDS_DIR = path.join(SRC, 'commands');
5690
+ const MENUS_DIR = path.join(SRC, 'menus');
5691
+ const STATUS_BARS_DIR = path.join(SRC, 'statusBars');
5692
+ const SUBPANELS_DIR = path.join(SRC, 'subpanels');
5693
+ const TREE_VIEWS_DIR = path.join(SRC, 'treeViews');
5694
+ const JOBS_DIR = path.join(SRC, 'jobs');
5695
+ const OUT = path.join(SRC, 'extension', '_registry.ts');
5696
+ const PKG_PATH = path.join(ROOT, 'package.json');
5697
+
5698
+ interface Discovered {
5699
+ id: string;
5700
+ importPath: string;
5701
+ }
5702
+
5703
+ function scan(dir: string, registryDir: string): Discovered[] {
5704
+ if (!fs.existsSync(dir)) return [];
5705
+ return fs
5706
+ .readdirSync(dir, { withFileTypes: true })
5707
+ .filter((e) => e.isFile() && /\\.(ts|tsx)$/.test(e.name) && !e.name.startsWith('_'))
5708
+ .map((e) => {
5709
+ const id = e.name.replace(/\\.(ts|tsx)$/, '');
5710
+ const abs = path.join(dir, e.name);
5711
+ const rel = path.relative(registryDir, abs).replace(/\\\\/g, '/').replace(/\\.(ts|tsx)$/, '');
5712
+ const importPath = rel.startsWith('.') ? rel : \`./\${rel}\`;
5713
+ return { id, importPath };
5714
+ });
5715
+ }
5716
+
5717
+ function writeRegistry(
5718
+ panels: Discovered[],
5719
+ commands: Discovered[],
5720
+ menus: Discovered[],
5721
+ statusBars: Discovered[],
5722
+ subpanels: Discovered[],
5723
+ treeViews: Discovered[],
5724
+ jobs: Discovered[],
5725
+ prefix: string,
5726
+ ) {
5727
+ const lines: string[] = [
5728
+ '// AUTO-GENERATED — do not edit. Run \`bun run gen\`.',
5729
+ \`import type { Registry } from '../shared/vsceasy';\`,
5730
+ ...panels.map((p, i) => \`import panel\${i} from '\${p.importPath}';\`),
5731
+ ...commands.map((c, i) => \`import command\${i} from '\${c.importPath}';\`),
5732
+ ...menus.map((m, i) => \`import menu\${i} from '\${m.importPath}';\`),
5733
+ ...statusBars.map((s, i) => \`import statusBar\${i} from '\${s.importPath}';\`),
5734
+ ...subpanels.map((w, i) => \`import subpanel\${i} from '\${w.importPath}';\`),
5735
+ ...treeViews.map((t, i) => \`import treeView\${i} from '\${t.importPath}';\`),
5736
+ ...jobs.map((j, i) => \`import job\${i} from '\${j.importPath}';\`),
5737
+ '',
5738
+ 'export const registry: Registry = {',
5739
+ \` prefix: \${JSON.stringify(prefix)},\`,
5740
+ ' panels: {',
5741
+ ...panels.map((p, i) => \` \${JSON.stringify(p.id)}: panel\${i},\`),
5742
+ ' },',
5743
+ ' commands: {',
5744
+ ...commands.map((c, i) => \` \${JSON.stringify(c.id)}: command\${i},\`),
5745
+ ' },',
5746
+ ' menus: {',
5747
+ ...menus.map((m, i) => \` \${JSON.stringify(m.id)}: menu\${i},\`),
5748
+ ' },',
5749
+ ' statusBars: {',
5750
+ ...statusBars.map((s, i) => \` \${JSON.stringify(s.id)}: statusBar\${i},\`),
5751
+ ' },',
5752
+ ' subpanels: {',
5753
+ ...subpanels.map((w, i) => \` \${JSON.stringify(w.id)}: subpanel\${i},\`),
5754
+ ' },',
5755
+ ' treeViews: {',
5756
+ ...treeViews.map((t, i) => \` \${JSON.stringify(t.id)}: treeView\${i},\`),
5757
+ ' },',
5758
+ ' jobs: {',
5759
+ ...jobs.map((j, i) => \` \${JSON.stringify(j.id)}: job\${i},\`),
5760
+ ' },',
5761
+ '};',
5762
+ '',
5763
+ ];
5764
+ fs.mkdirSync(path.dirname(OUT), { recursive: true });
5765
+ fs.writeFileSync(OUT, lines.join('\\n'));
5766
+ }
5767
+
5768
+ function syncPackageJson(
5769
+ panels: Discovered[],
5770
+ commands: Discovered[],
5771
+ menus: Discovered[],
5772
+ subpanels: Discovered[],
5773
+ treeViews: Discovered[],
5774
+ prefix: string,
5775
+ displayName: string,
5776
+ ) {
5777
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
5778
+ const contributes = (pkg.contributes ??= {});
5779
+ const cmds: Array<{ command: string; title: string; category?: string; enablement?: string }> = [];
5780
+ const keybindings: Array<{ command: string; key: string; mac?: string; when?: string }> = [];
5781
+ const palette: Array<{ command: string; when?: string }> = [];
5782
+
5783
+ for (const c of commands) {
5784
+ const def = loadDef(path.join(COMMANDS_DIR, c.id + '.ts')) ?? loadDef(path.join(COMMANDS_DIR, c.id + '.tsx'));
5785
+ const fullId = \`\${prefix}.\${def?.id ?? c.id}\`;
5786
+ cmds.push({
5787
+ command: fullId,
5788
+ title: def?.title ?? c.id,
5789
+ category: def?.category ?? displayName,
5790
+ ...(def?.when ? { enablement: def.when } : {}),
5791
+ });
5792
+ if (def?.when) palette.push({ command: fullId, when: def.when });
5793
+ if (def?.keybindings) {
5794
+ for (const kb of def.keybindings) {
5795
+ keybindings.push({
5796
+ command: fullId,
5797
+ key: kb.key,
5798
+ ...(kb.mac ? { mac: kb.mac } : {}),
5799
+ ...(kb.when ? { when: kb.when } : {}),
5800
+ });
5801
+ }
5802
+ }
5803
+ }
5804
+ for (const p of panels) {
5805
+ const def = loadDef(path.join(PANELS_DIR, p.id + '.ts')) ?? loadDef(path.join(PANELS_DIR, p.id + '.tsx'));
5806
+ if (def?.command === false) continue;
5807
+ const opts = typeof def?.command === 'object' ? def!.command : {};
5808
+ cmds.push({
5809
+ command: \`\${prefix}.open\${capitalize(def?.id ?? p.id)}\`,
5810
+ title: (opts as any).title ?? \`Open \${def?.title ?? p.id}\`,
5811
+ category: (opts as any).category ?? displayName,
5812
+ });
5813
+ }
5814
+
5815
+ contributes.commands = cmds;
5816
+ if (keybindings.length) {
5817
+ contributes.keybindings = keybindings;
5818
+ } else {
5819
+ delete contributes.keybindings;
5820
+ }
5821
+ if (palette.length) {
5822
+ contributes.menus ??= {};
5823
+ contributes.menus.commandPalette = palette;
5824
+ } else if (contributes.menus) {
5825
+ delete contributes.menus.commandPalette;
5826
+ if (Object.keys(contributes.menus).length === 0) delete contributes.menus;
5827
+ }
5828
+
5829
+ // Menus → viewsContainers.activitybar + views.<containerId>
5830
+ const containers: Array<{ id: string; title: string; icon: string }> = [];
5831
+ const views: Record<string, Array<{ id: string; name: string; type?: 'webview' }>> = {};
5832
+
5833
+ // Index subpanels by menu they belong to
5834
+ const wvByMenu: Record<string, Array<{ id: string; name: string }>> = {};
5835
+ for (const w of subpanels) {
5836
+ const def = loadSubpanelDef(path.join(SUBPANELS_DIR, w.id + '.ts'))
5837
+ ?? loadSubpanelDef(path.join(SUBPANELS_DIR, w.id + '.tsx'));
5838
+ if (!def?.menu) continue;
5839
+ const viewId = \`\${prefix}-\${def.menu}-\${def.id ?? w.id}\`;
5840
+ const name = def.title ?? w.id;
5841
+ (wvByMenu[def.menu] ??= []).push({ id: viewId, name });
5842
+ }
5843
+
5844
+ // Index tree views by their menu container
5845
+ const tvByMenu: Record<string, Array<{ id: string; name: string }>> = {};
5846
+ for (const t of treeViews) {
5847
+ const def = loadSubpanelDef(path.join(TREE_VIEWS_DIR, t.id + '.ts'))
5848
+ ?? loadSubpanelDef(path.join(TREE_VIEWS_DIR, t.id + '.tsx'));
5849
+ if (!def?.menu) continue;
5850
+ const viewId = \`\${prefix}-\${def.menu}-\${def.id ?? t.id}\`;
5851
+ const name = def.title ?? t.id;
5852
+ (tvByMenu[def.menu] ??= []).push({ id: viewId, name });
5853
+ }
5854
+
5855
+ for (const m of menus) {
5856
+ const def = loadMenuDef(path.join(MENUS_DIR, m.id + '.ts')) ?? loadMenuDef(path.join(MENUS_DIR, m.id + '.tsx'));
5857
+ // VS Code requires viewsContainer / view ids to match /^[A-Za-z0-9_-]+$/ — no dots.
5858
+ const menuId = def?.id ?? m.id;
5859
+ const containerId = \`\${prefix}-\${menuId}\`;
5860
+ const title = def?.title ?? m.id;
5861
+ const icon = resolveIconForPkg(def?.icon);
5862
+ containers.push({ id: containerId, title, icon });
5863
+ const containerViews: Array<{ id: string; name: string; type?: 'webview' }> = [
5864
+ { id: containerId, name: title }, // primary tree view
5865
+ ];
5866
+ for (const v of wvByMenu[menuId] ?? []) {
5867
+ containerViews.push({ id: v.id, name: v.name, type: 'webview' });
5868
+ }
5869
+ for (const t of tvByMenu[menuId] ?? []) {
5870
+ containerViews.push({ id: t.id, name: t.name });
5871
+ }
5872
+ views[containerId] = containerViews;
5873
+ }
5874
+ if (containers.length) {
5875
+ (contributes.viewsContainers ??= {}).activitybar = containers;
5876
+ contributes.views = views;
5877
+ } else {
5878
+ delete contributes.viewsContainers?.activitybar;
5879
+ if (contributes.viewsContainers && Object.keys(contributes.viewsContainers).length === 0) {
5880
+ delete contributes.viewsContainers;
5881
+ }
5882
+ delete contributes.views;
5883
+ }
5884
+
5885
+ fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + '\\n');
5886
+ }
5887
+
5888
+ function loadDef(file: string): {
5889
+ id?: string;
5890
+ title?: string;
5891
+ category?: string;
5892
+ command?: any;
5893
+ when?: string;
5894
+ keybindings?: Array<{ key: string; mac?: string; when?: string }>;
5895
+ } | null {
5896
+ if (!fs.existsSync(file)) return null;
5897
+ const src = fs.readFileSync(file, 'utf8');
5898
+ const grab = (key: string) => {
5899
+ const m = new RegExp(\`\\\\b\${key}\\\\s*:\\\\s*(['"\\\`])((?:\\\\\\\\.|(?!\\\\1).)*)\\\\1\`).exec(src);
5900
+ return m?.[2];
5901
+ };
5902
+ const command =
5903
+ /\\bcommand\\s*:\\s*false\\b/.test(src) ? false :
5904
+ /\\bcommand\\s*:\\s*true\\b/.test(src) ? true :
5905
+ undefined;
5906
+ return {
5907
+ id: grab('id'),
5908
+ title: grab('title'),
5909
+ category: grab('category'),
5910
+ when: grab('when'),
5911
+ command,
5912
+ keybindings: parseKeybindings(src),
5913
+ };
5914
+ }
5915
+
5916
+ /** Extract keybinding(s) declared on a defineCommand call. Supports string, object, or array shorthand. */
5917
+ function parseKeybindings(src: string): Array<{ key: string; mac?: string; when?: string }> {
5918
+ // 1) Plain string: keybinding: 'ctrl+shift+h'
5919
+ const stringMatch = /\\bkeybinding\\s*:\\s*(['"\`])([^'"\`]+)\\1\\s*,?/.exec(src);
5920
+ if (stringMatch) return [{ key: stringMatch[2] }];
5921
+
5922
+ // 2) Single object: keybinding: { key: '...', mac?: '...', when?: '...' }
5923
+ const objMatch = /\\bkeybinding\\s*:\\s*\\{([^}]+)\\}/.exec(src);
5924
+ if (objMatch) {
5925
+ const obj = parseKbObject(objMatch[1]);
5926
+ return obj ? [obj] : [];
5927
+ }
5928
+
5929
+ // 3) Array shorthand: keybinding: [ 'ctrl+a', { key: 'ctrl+b', mac: 'cmd+b' } ]
5930
+ const arrMatch = /\\bkeybinding\\s*:\\s*\\[([\\s\\S]*?)\\]/.exec(src);
5931
+ if (arrMatch) {
5932
+ const inner = arrMatch[1];
5933
+ const out: Array<{ key: string; mac?: string; when?: string }> = [];
5934
+ const strRe = /(['"\`])([^'"\`]+)\\1/g;
5935
+ const objRe = /\\{([^}]+)\\}/g;
5936
+ let sm: RegExpExecArray | null;
5937
+ while ((sm = strRe.exec(inner))) out.push({ key: sm[2] });
5938
+ let om: RegExpExecArray | null;
5939
+ while ((om = objRe.exec(inner))) {
5940
+ const o = parseKbObject(om[1]);
5941
+ if (o) out.push(o);
5942
+ }
5943
+ return out;
5944
+ }
5945
+ return [];
5946
+ }
5947
+
5948
+ function parseKbObject(body: string): { key: string; mac?: string; when?: string } | null {
5949
+ const grab = (key: string) => {
5950
+ const m = new RegExp(\`\\\\b\${key}\\\\s*:\\\\s*(['"\\\`])((?:\\\\\\\\.|(?!\\\\1).)*)\\\\1\`).exec(body);
5951
+ return m?.[2];
5952
+ };
5953
+ const key = grab('key');
5954
+ if (!key) return null;
5955
+ const mac = grab('mac');
5956
+ const when = grab('when');
5957
+ return { key, ...(mac ? { mac } : {}), ...(when ? { when } : {}) };
5958
+ }
5959
+
5960
+ interface MenuLoaded {
5961
+ id?: string;
5962
+ title?: string;
5963
+ icon?: string | { path?: string; light?: string; dark?: string };
5964
+ }
5965
+
5966
+ function loadMenuDef(file: string): MenuLoaded | null {
5967
+ if (!fs.existsSync(file)) return null;
5968
+ const src = fs.readFileSync(file, 'utf8');
5969
+ const grab = (key: string) => {
5970
+ const m = new RegExp(\`\\\\b\${key}\\\\s*:\\\\s*(['"\\\`])((?:\\\\\\\\.|(?!\\\\1).)*)\\\\1\`).exec(src);
5971
+ return m?.[2];
5972
+ };
5973
+ let icon: MenuLoaded['icon'];
5974
+ const iconString = grab('icon');
5975
+ if (iconString !== undefined) {
5976
+ icon = iconString;
5977
+ } else {
5978
+ // Try object form: icon: { path: '...' } OR { light: '...', dark: '...' }
5979
+ const objMatch = /\\bicon\\s*:\\s*\\{([^}]+)\\}/.exec(src);
5980
+ if (objMatch) {
5981
+ const body = objMatch[1];
5982
+ const p = /\\bpath\\s*:\\s*(['"\\\`])((?:\\\\.|(?!\\1).)*)\\1/.exec(body);
5983
+ const l = /\\blight\\s*:\\s*(['"\\\`])((?:\\\\.|(?!\\1).)*)\\1/.exec(body);
5984
+ const d = /\\bdark\\s*:\\s*(['"\\\`])((?:\\\\.|(?!\\1).)*)\\1/.exec(body);
5985
+ if (p) icon = { path: p[2] };
5986
+ else if (l && d) icon = { light: l[2], dark: d[2] };
5987
+ }
5988
+ }
5989
+ return { id: grab('id'), title: grab('title'), icon };
5990
+ }
5991
+
5992
+ interface SubpanelLoaded {
5993
+ id?: string;
5994
+ title?: string;
5995
+ menu?: string;
5996
+ }
5997
+
5998
+ function loadSubpanelDef(file: string): SubpanelLoaded | null {
5999
+ if (!fs.existsSync(file)) return null;
6000
+ const src = fs.readFileSync(file, 'utf8');
6001
+ const grab = (key: string) => {
6002
+ const m = new RegExp(\`\\\\b\${key}\\\\s*:\\\\s*(['"\\\`])((?:\\\\\\\\.|(?!\\\\1).)*)\\\\1\`).exec(src);
6003
+ return m?.[2];
6004
+ };
6005
+ return { id: grab('id'), title: grab('title'), menu: grab('menu') };
6006
+ }
6007
+
6008
+ function resolveIconForPkg(icon: MenuLoaded['icon']): string {
6009
+ // VS Code's viewsContainers.activitybar.icon must be a string (path to SVG or codicon ref via "$(name)").
6010
+ if (!icon) return '$(symbol-misc)';
6011
+ if (typeof icon === 'string') return \`$(\${icon})\`;
6012
+ if ('path' in icon && icon.path) return icon.path;
6013
+ if ('light' in icon && icon.light) return icon.light;
6014
+ return '$(symbol-misc)';
6015
+ }
6016
+
6017
+ function capitalize(s: string): string {
6018
+ return s.charAt(0).toUpperCase() + s.slice(1);
6019
+ }
6020
+
6021
+ function ensurePanelHtml(panels: Discovered[]) {
6022
+ ensureBundleHtml(path.join(SRC, 'webview', 'panels'), panels);
6023
+ }
6024
+
6025
+ function ensureSubpanelHtml(views: Discovered[]) {
6026
+ ensureBundleHtml(path.join(SRC, 'webview', 'subpanels'), views);
6027
+ }
6028
+
6029
+ function ensureBundleHtml(baseDir: string, entries: Discovered[]) {
6030
+ for (const e of entries) {
6031
+ const dir = path.join(baseDir, e.id);
6032
+ if (!fs.existsSync(dir)) continue;
6033
+ const htmlPath = path.join(dir, 'index.html');
6034
+ if (fs.existsSync(htmlPath)) continue;
6035
+ const mainCandidates = ['main.tsx', 'main.ts', 'index.tsx', 'index.ts'];
6036
+ const main = mainCandidates.find((f) => fs.existsSync(path.join(dir, f))) ?? 'main.tsx';
6037
+ fs.writeFileSync(
6038
+ htmlPath,
6039
+ \`<!DOCTYPE html>
6040
+ <html><head><meta charset="UTF-8" /></head>
6041
+ <body><div id="root"></div><script type="module" src="./\${main}"></script></body>
6042
+ </html>
6043
+ \`,
6044
+ );
6045
+ }
6046
+ }
6047
+
6048
+ function main() {
6049
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
6050
+ const prefix: string =
6051
+ pkg.vsceasy?.commandPrefix ?? pkg.name.replace(/^@[^/]+\\//, '').replace(/[^a-zA-Z0-9]+/g, '');
6052
+ const displayName: string = pkg.displayName ?? pkg.name;
6053
+
6054
+ const registryDir = path.dirname(OUT);
6055
+ const panels = scan(PANELS_DIR, registryDir);
6056
+ const commands = scan(COMMANDS_DIR, registryDir);
6057
+ const menus = scan(MENUS_DIR, registryDir);
6058
+ const statusBars = scan(STATUS_BARS_DIR, registryDir);
6059
+ const subpanels = scan(SUBPANELS_DIR, registryDir);
6060
+ const treeViews = scan(TREE_VIEWS_DIR, registryDir);
6061
+ const jobs = scan(JOBS_DIR, registryDir);
6062
+
6063
+ writeRegistry(panels, commands, menus, statusBars, subpanels, treeViews, jobs, prefix);
6064
+ syncPackageJson(panels, commands, menus, subpanels, treeViews, prefix, displayName);
6065
+ ensurePanelHtml(panels);
6066
+ ensureSubpanelHtml(subpanels);
6067
+
6068
+ console.log(
6069
+ \`✓ vsceasy gen → \${panels.length} panel(s), \${commands.length} command(s), \${menus.length} menu(s), \${statusBars.length} statusBar(s), \${subpanels.length} subpanel(s), \${treeViews.length} treeView(s), \${jobs.length} job(s)\`,
6070
+ );
6071
+ }
6072
+
6073
+ main();
6074
+ `,
6075
+ "react/src/commands/hello.ts": `import { defineCommand } from '../shared/vsceasy';
6076
+
6077
+ export default defineCommand({
6078
+ title: 'Hello',
6079
+ run: (vscode) => vscode.window.showInformationMessage('Hello from {{displayName}}!'),
6080
+ });
6081
+ `,
6082
+ "react/src/extension/extension.ts": `import { bootstrap } from '../shared/vsceasy';
6083
+ import { registry } from './_registry';
6084
+
6085
+ export const activate = bootstrap(registry);
6086
+ export function deactivate() {}
6087
+ `,
6088
+ "react/src/panels/dashboard.ts": `import { definePanel } from '../shared/vsceasy';
6089
+ import type { DashboardApi } from '../shared/api';
6090
+
6091
+ export default definePanel<DashboardApi>({
6092
+ title: '{{displayName}} Dashboard',
6093
+ rpc: (vscode) => ({
6094
+ async getInfo() {
6095
+ return {
6096
+ workspace: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? null,
6097
+ vscodeVersion: vscode.version,
6098
+ };
6099
+ },
6100
+ async showMessage(text) {
6101
+ await vscode.window.showInformationMessage(text);
6102
+ },
6103
+ async listFiles(pattern) {
6104
+ const uris = await vscode.workspace.findFiles(pattern, '**/node_modules/**', 100);
6105
+ return uris.map((u) => vscode.workspace.asRelativePath(u));
6106
+ },
6107
+ }),
6108
+ });
6109
+ `,
6110
+ "react/src/shared/api.ts": `// RPC contracts — one interface per panel. Imported by both extension and webview.
6111
+
6112
+ export interface DashboardApi {
6113
+ getInfo(): Promise<{ workspace: string | null; vscodeVersion: string }>;
6114
+ showMessage(text: string): Promise<void>;
6115
+ listFiles(pattern: string): Promise<string[]>;
6116
+ }
6117
+ `,
6118
+ "react/src/shared/vsceasy/bootstrap.ts": `import * as vscode from 'vscode';
6119
+ import type { PanelDef, CommandDef, MenuDef, MenuItem, StatusBarDef, StatusBarMenuItem, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule } from './define';
6120
+ import { createRpcServer, webviewTransport } from './rpc';
6121
+
6122
+ export interface Registry {
6123
+ panels: Record<string, PanelDef>;
6124
+ commands: Record<string, CommandDef>;
6125
+ menus?: Record<string, MenuDef>;
6126
+ statusBars?: Record<string, StatusBarDef>;
6127
+ subpanels?: Record<string, SubpanelDef>;
6128
+ treeViews?: Record<string, TreeViewDef>;
6129
+ jobs?: Record<string, JobDef>;
6130
+ /** Command prefix from package.json (e.g. "myExt"). */
6131
+ prefix: string;
6132
+ }
6133
+
6134
+ const openPanels = new Map<string, vscode.WebviewPanel>();
6135
+
6136
+ /**
6137
+ * Hook fired with the \`ExtensionContext\`. Use to wire \`initDb(context)\`,
6138
+ * \`initSecrets(context)\`, \`initState(context)\`, etc. Return value ignored;
6139
+ * may be sync or async (awaited in order).
6140
+ *
6141
+ * Signature uses \`...rest: any[]\` so any 1- or 2-arg helper (including ones
6142
+ * that declare a typed second parameter like \`initDb(ctx, opts?)\`) assigns
6143
+ * cleanly. The bootstrap runtime always passes the \`vscode\` namespace as the
6144
+ * second arg — helpers free to ignore it or declare their own type.
6145
+ */
6146
+ export type ActivateHook = (
6147
+ context: vscode.ExtensionContext,
6148
+ ...rest: any[]
6149
+ ) => unknown | Promise<unknown>;
6150
+
6151
+ export interface BootstrapOptions {
6152
+ /**
6153
+ * Hooks that receive the \`ExtensionContext\` on activate, before any panel /
6154
+ * command / job is registered. Use to wire \`initDb(context)\`, \`initSecrets(context)\`,
6155
+ * \`initState(context)\`, telemetry, etc.
6156
+ */
6157
+ onActivate?: ActivateHook[];
6158
+ /** Symmetric: runs in reverse on deactivate. */
6159
+ onDeactivate?: ActivateHook[];
6160
+ }
6161
+
6162
+ export function bootstrap(registry: Registry, options: BootstrapOptions = {}) {
6163
+ return async function activate(context: vscode.ExtensionContext) {
6164
+ for (const hook of options.onActivate ?? []) {
6165
+ await hook(context, vscode);
6166
+ }
6167
+ for (const [id, def] of Object.entries(registry.commands)) {
6168
+ const cmd = \`\${registry.prefix}.\${def.id ?? id}\`;
6169
+ context.subscriptions.push(
6170
+ vscode.commands.registerCommand(cmd, (...args) => def.run(vscode, context, ...args)),
6171
+ );
6172
+ }
6173
+
6174
+ for (const [id, def] of Object.entries(registry.panels)) {
6175
+ if (def.command !== false) {
6176
+ const cmd = \`\${registry.prefix}.open\${capitalize(def.id ?? id)}\`;
6177
+ context.subscriptions.push(
6178
+ vscode.commands.registerCommand(cmd, () => openPanel(context, registry.prefix, id, def)),
6179
+ );
6180
+ }
6181
+ }
6182
+
6183
+ if (registry.menus) {
6184
+ for (const [id, def] of Object.entries(registry.menus)) {
6185
+ registerMenu(context, registry, id, def);
6186
+ }
6187
+ }
6188
+
6189
+ if (registry.statusBars) {
6190
+ for (const [id, def] of Object.entries(registry.statusBars)) {
6191
+ registerStatusBar(context, registry, id, def);
6192
+ }
6193
+ }
6194
+
6195
+ if (registry.subpanels) {
6196
+ for (const [id, def] of Object.entries(registry.subpanels)) {
6197
+ registerSubpanel(context, registry, id, def);
6198
+ }
6199
+ }
6200
+
6201
+ if (registry.treeViews) {
6202
+ for (const [id, def] of Object.entries(registry.treeViews)) {
6203
+ registerTreeView(context, registry, id, def);
6204
+ }
6205
+ }
6206
+
6207
+ if (registry.jobs) {
6208
+ for (const [id, def] of Object.entries(registry.jobs)) {
6209
+ registerJob(context, registry, id, def);
6210
+ }
6211
+ }
6212
+
6213
+ // Register deactivate hooks as disposables — fire in reverse order on shutdown.
6214
+ for (const hook of [...(options.onDeactivate ?? [])].reverse()) {
6215
+ context.subscriptions.push({
6216
+ dispose: () => {
6217
+ void hook(context, vscode);
6218
+ },
6219
+ });
6220
+ }
6221
+ };
6222
+ }
6223
+
6224
+ // --- Jobs (recurring / event-triggered) ---
6225
+
6226
+ function registerJob(
6227
+ context: vscode.ExtensionContext,
6228
+ registry: Registry,
6229
+ id: string,
6230
+ def: JobDef,
6231
+ ) {
6232
+ const jobId = def.id ?? id;
6233
+ const lastRunKey = \`vsceasy.job.\${jobId}.lastRun\`;
6234
+
6235
+ const exec = async (reason: string) => {
6236
+ if (def.minIntervalMs) {
6237
+ const last = (context.globalState.get<number>(lastRunKey) ?? 0);
6238
+ if (Date.now() - last < def.minIntervalMs) return;
6239
+ }
6240
+ try {
6241
+ await def.run(vscode, context);
6242
+ await context.globalState.update(lastRunKey, Date.now());
6243
+ } catch (err) {
6244
+ console.error(\`[vsceasy job:\${jobId}] (\${reason}) failed:\`, err);
6245
+ }
6246
+ };
6247
+
6248
+ const sched = def.schedule;
6249
+ if ('every' in sched) {
6250
+ const ms = parseDuration(sched.every);
6251
+ if (ms <= 0) throw new Error(\`Job "\${jobId}": invalid every=\${sched.every}\`);
6252
+ if (sched.runOnStart !== false) void exec('startup');
6253
+ const handle = setInterval(() => void exec('interval'), ms);
6254
+ context.subscriptions.push({ dispose: () => clearInterval(handle) });
6255
+ return;
6256
+ }
6257
+ if ('dailyAt' in sched) {
6258
+ const [hStr, mStr] = sched.dailyAt.split(':');
6259
+ const h = Number(hStr);
6260
+ const m = Number(mStr ?? '0');
6261
+ if (!Number.isFinite(h) || !Number.isFinite(m)) {
6262
+ throw new Error(\`Job "\${jobId}": invalid dailyAt=\${sched.dailyAt} (expected "HH:MM")\`);
6263
+ }
6264
+ let timer: NodeJS.Timeout | undefined;
6265
+ const scheduleNext = () => {
6266
+ const next = new Date();
6267
+ next.setHours(h, m, 0, 0);
6268
+ if (next.getTime() <= Date.now()) next.setDate(next.getDate() + 1);
6269
+ timer = setTimeout(async () => {
6270
+ await exec('dailyAt');
6271
+ scheduleNext();
6272
+ }, next.getTime() - Date.now());
6273
+ };
6274
+ scheduleNext();
6275
+ context.subscriptions.push({ dispose: () => { if (timer) clearTimeout(timer); } });
6276
+ return;
6277
+ }
6278
+ if ('on' in sched) {
6279
+ let sub: vscode.Disposable;
6280
+ switch (sched.on) {
6281
+ case 'startup':
6282
+ void exec('startup');
6283
+ return;
6284
+ case 'saveDocument':
6285
+ sub = vscode.workspace.onDidSaveTextDocument(() => void exec('saveDocument'));
6286
+ break;
6287
+ case 'openDocument':
6288
+ sub = vscode.workspace.onDidOpenTextDocument(() => void exec('openDocument'));
6289
+ break;
6290
+ case 'changeActiveEditor':
6291
+ sub = vscode.window.onDidChangeActiveTextEditor(() => void exec('changeActiveEditor'));
6292
+ break;
6293
+ case 'changeConfig':
6294
+ sub = vscode.workspace.onDidChangeConfiguration(() => void exec('changeConfig'));
6295
+ break;
6296
+ default:
6297
+ throw new Error(\`Job "\${jobId}": unknown on=\${(sched as { on: string }).on}\`);
6298
+ }
6299
+ context.subscriptions.push(sub);
6300
+ return;
6301
+ }
6302
+ if ('onFile' in sched) {
6303
+ const watcher = vscode.workspace.createFileSystemWatcher(sched.onFile);
6304
+ watcher.onDidChange(() => void exec('onFile:change'));
6305
+ watcher.onDidCreate(() => void exec('onFile:create'));
6306
+ watcher.onDidDelete(() => void exec('onFile:delete'));
6307
+ context.subscriptions.push(watcher);
6308
+ return;
6309
+ }
6310
+ }
6311
+
6312
+ const DURATION_RE = /^(\\d+)\\s*(ms|s|m|h|d)?$/;
6313
+
6314
+ function parseDuration(input: string | number): number {
6315
+ if (typeof input === 'number') return input;
6316
+ const m = DURATION_RE.exec(input.trim());
6317
+ if (!m) return -1;
6318
+ const n = Number(m[1]);
6319
+ switch (m[2] ?? 'ms') {
6320
+ case 'ms': return n;
6321
+ case 's': return n * 1000;
6322
+ case 'm': return n * 60_000;
6323
+ case 'h': return n * 3_600_000;
6324
+ case 'd': return n * 86_400_000;
6325
+ default: return -1;
6326
+ }
6327
+ }
6328
+
6329
+ // --- Tree Views (data-driven) ---
6330
+
6331
+ function registerTreeView(
6332
+ context: vscode.ExtensionContext,
6333
+ registry: Registry,
6334
+ id: string,
6335
+ def: TreeViewDef,
6336
+ ) {
6337
+ const viewId = \`\${registry.prefix}-\${def.menu}-\${def.id ?? id}\`;
6338
+ const provider = new DataTreeProvider(def, context);
6339
+ const view = vscode.window.createTreeView(viewId, {
6340
+ treeDataProvider: provider,
6341
+ showCollapseAll: def.showCollapseAll !== false,
6342
+ });
6343
+ context.subscriptions.push(view);
6344
+
6345
+ const refreshCmd = \`\${registry.prefix}._tree.\${def.id ?? id}.refresh\`;
6346
+ context.subscriptions.push(
6347
+ vscode.commands.registerCommand(refreshCmd, () => provider.refresh()),
6348
+ );
6349
+
6350
+ const dispatchCmd = \`\${registry.prefix}._tree.\${def.id ?? id}.run\`;
6351
+ context.subscriptions.push(
6352
+ vscode.commands.registerCommand(dispatchCmd, async (node: TreeNode) => {
6353
+ if (node.run) return node.run(vscode, context);
6354
+ if (node.panel) {
6355
+ const p = registry.panels[node.panel];
6356
+ if (p) return openPanel(context, registry.prefix, node.panel, p);
6357
+ }
6358
+ if (node.command) {
6359
+ const c = registry.commands[node.command];
6360
+ if (c) return c.run(vscode, context);
6361
+ }
6362
+ }),
6363
+ );
6364
+ provider.setDispatchCommand(dispatchCmd);
6365
+ }
6366
+
6367
+ class DataTreeProvider implements vscode.TreeDataProvider<TreeNode> {
6368
+ private _onDidChange = new vscode.EventEmitter<TreeNode | undefined>();
6369
+ readonly onDidChangeTreeData = this._onDidChange.event;
6370
+ private dispatchCmd = '';
6371
+
6372
+ constructor(private readonly def: TreeViewDef, private readonly context: vscode.ExtensionContext) {}
6373
+
6374
+ setDispatchCommand(cmd: string) {
6375
+ this.dispatchCmd = cmd;
6376
+ }
6377
+
6378
+ refresh() {
6379
+ this._onDidChange.fire(undefined);
6380
+ }
6381
+
6382
+ getTreeItem(node: TreeNode): vscode.TreeItem {
6383
+ const hasChildren = !!node.children?.length;
6384
+ const state = hasChildren || node.children === undefined
6385
+ ? node.collapsed === 'expanded'
6386
+ ? vscode.TreeItemCollapsibleState.Expanded
6387
+ : vscode.TreeItemCollapsibleState.Collapsed
6388
+ : vscode.TreeItemCollapsibleState.None;
6389
+ const item = new vscode.TreeItem(node.label, state);
6390
+ item.id = node.id;
6391
+ item.tooltip = node.tooltip;
6392
+ item.description = node.description;
6393
+ item.contextValue = node.contextValue;
6394
+ item.iconPath = resolveIcon(this.context, node.icon);
6395
+ if (this.dispatchCmd && (node.run || node.panel || node.command)) {
6396
+ item.command = { command: this.dispatchCmd, title: node.label, arguments: [node] };
6397
+ }
6398
+ return item;
6399
+ }
6400
+
6401
+ async getChildren(node?: TreeNode): Promise<TreeNode[]> {
6402
+ if (node?.children) return node.children;
6403
+ return Promise.resolve(this.def.getChildren(node, vscode, this.context));
6404
+ }
6405
+ }
6406
+
6407
+ // --- Webview Views (sidebar inline) ---
6408
+
6409
+ function registerSubpanel(
6410
+ context: vscode.ExtensionContext,
6411
+ registry: Registry,
6412
+ id: string,
6413
+ def: SubpanelDef,
6414
+ ) {
6415
+ // Must match the view id gen.ts writes into package.json#views.<container>.
6416
+ const viewId = \`\${registry.prefix}-\${def.menu}-\${def.id ?? id}\`;
6417
+ const provider: vscode.WebviewViewProvider = {
6418
+ resolveWebviewView(view) {
6419
+ view.webview.options = {
6420
+ enableScripts: true,
6421
+ localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist', 'webview')],
6422
+ };
6423
+ const ui = def.ui ?? \`subpanels/\${def.id ?? id}\`;
6424
+ view.webview.html = renderHtml(view.webview, context, ui, def.title);
6425
+ if (def.rpc) {
6426
+ const handlers = def.rpc(vscode, context);
6427
+ const server = createRpcServer(webviewTransport(view.webview), handlers);
6428
+ view.onDidDispose(() => server.dispose());
6429
+ }
6430
+ },
6431
+ };
6432
+ context.subscriptions.push(
6433
+ vscode.window.registerWebviewViewProvider(viewId, provider, {
6434
+ webviewOptions: { retainContextWhenHidden: def.retainContext ?? true },
6435
+ }),
6436
+ );
6437
+ }
6438
+
6439
+ // --- Status bar items ---
6440
+
6441
+ function registerStatusBar(
6442
+ context: vscode.ExtensionContext,
6443
+ registry: Registry,
6444
+ id: string,
6445
+ def: StatusBarDef,
6446
+ ) {
6447
+ const alignment = def.alignment === 'right'
6448
+ ? vscode.StatusBarAlignment.Right
6449
+ : vscode.StatusBarAlignment.Left;
6450
+ const item = vscode.window.createStatusBarItem(alignment, def.priority ?? 100);
6451
+ item.text = def.icon ? \`$(\${def.icon}) \${def.text}\` : def.text;
6452
+
6453
+ // Tooltip: markdown takes precedence over plain string
6454
+ if (def.tooltipMarkdown) {
6455
+ const md = new vscode.MarkdownString(def.tooltipMarkdown, true);
6456
+ md.supportHtml = true;
6457
+ md.isTrusted = true;
6458
+ item.tooltip = md;
6459
+ } else if (def.tooltip) {
6460
+ item.tooltip = def.tooltip;
6461
+ }
6462
+
6463
+ // Click behaviour priority: menu > panel > command
6464
+ if (def.menu && def.menu.length > 0) {
6465
+ const dispatchCmd = \`\${registry.prefix}._statusBar.\${id}.click\`;
6466
+ context.subscriptions.push(
6467
+ vscode.commands.registerCommand(dispatchCmd, () => openStatusBarMenu(context, registry, def.menu!)),
6468
+ );
6469
+ item.command = dispatchCmd;
6470
+ } else if (def.panel) {
6471
+ const panelDef = registry.panels[def.panel];
6472
+ if (panelDef) {
6473
+ const suffix = capitalize(panelDef.id ?? def.panel);
6474
+ item.command = \`\${registry.prefix}.open\${suffix}\`;
6475
+ } else {
6476
+ console.warn(\`[vsceasy] statusBar "\${id}" references unknown panel "\${def.panel}"\`);
6477
+ }
6478
+ } else if (def.command) {
6479
+ item.command = registry.commands[def.command]
6480
+ ? \`\${registry.prefix}.\${registry.commands[def.command].id ?? def.command}\`
6481
+ : def.command;
6482
+ }
6483
+
6484
+ if (def.backgroundColor) {
6485
+ item.backgroundColor = new vscode.ThemeColor(def.backgroundColor);
6486
+ }
6487
+ item.show();
6488
+ context.subscriptions.push(item);
6489
+ }
6490
+
6491
+ async function openStatusBarMenu(
6492
+ context: vscode.ExtensionContext,
6493
+ registry: Registry,
6494
+ items: StatusBarMenuItem[],
6495
+ ) {
6496
+ type QP = vscode.QuickPickItem & { __item: StatusBarMenuItem };
6497
+ const picks: QP[] = items.map((it) => ({
6498
+ label: it.label,
6499
+ description: it.description,
6500
+ detail: it.detail,
6501
+ __item: it,
6502
+ }));
6503
+ const selected = await vscode.window.showQuickPick(picks, { placeHolder: 'Choose action' });
6504
+ if (!selected) return;
6505
+ const it = selected.__item;
6506
+ if (it.url) {
6507
+ await vscode.env.openExternal(vscode.Uri.parse(it.url));
6508
+ return;
6509
+ }
6510
+ if (it.panel) {
6511
+ const panelDef = registry.panels[it.panel];
6512
+ if (panelDef) {
6513
+ const cmd = \`\${registry.prefix}.open\${capitalize(panelDef.id ?? it.panel)}\`;
6514
+ await vscode.commands.executeCommand(cmd);
6515
+ }
6516
+ return;
6517
+ }
6518
+ if (it.command) {
6519
+ const cmd = registry.commands[it.command]
6520
+ ? \`\${registry.prefix}.\${registry.commands[it.command].id ?? it.command}\`
6521
+ : it.command;
6522
+ await vscode.commands.executeCommand(cmd);
6523
+ }
6524
+ }
6525
+
6526
+ // --- Menus ---
6527
+
6528
+ function registerMenu(
6529
+ context: vscode.ExtensionContext,
6530
+ registry: Registry,
6531
+ id: string,
6532
+ def: MenuDef,
6533
+ ) {
6534
+ // Must match the id gen.ts writes into package.json#viewsContainers/views.
6535
+ // VS Code disallows '.' in view ids, so we use '-' as separator.
6536
+ const viewId = \`\${registry.prefix}-\${def.id ?? id}\`;
6537
+ const provider = new MenuTreeDataProvider(def.items, context);
6538
+ const view = vscode.window.createTreeView(viewId, {
6539
+ treeDataProvider: provider,
6540
+ showCollapseAll: true,
6541
+ });
6542
+ context.subscriptions.push(view);
6543
+
6544
+ // Single dispatch command per menu — passes the item through arguments[0] of contributes.commands.
6545
+ const dispatchCmd = \`\${registry.prefix}._menu.\${def.id ?? id}.run\`;
6546
+ context.subscriptions.push(
6547
+ vscode.commands.registerCommand(dispatchCmd, (item: MenuItem) =>
6548
+ dispatchMenuItem(context, registry, item),
6549
+ ),
6550
+ );
6551
+
6552
+ provider.setDispatchCommand(dispatchCmd);
6553
+ }
6554
+
6555
+ async function dispatchMenuItem(
6556
+ context: vscode.ExtensionContext,
6557
+ registry: Registry,
6558
+ item: MenuItem,
6559
+ ) {
6560
+ if (item.url) {
6561
+ await vscode.env.openExternal(vscode.Uri.parse(item.url));
6562
+ return;
6563
+ }
6564
+ if (item.panel) {
6565
+ const panel = registry.panels[item.panel];
6566
+ if (!panel) {
6567
+ vscode.window.showErrorMessage(\`Menu item references unknown panel: \${item.panel}\`);
6568
+ return;
6569
+ }
6570
+ openPanel(context, registry.prefix, item.panel, panel);
6571
+ return;
6572
+ }
6573
+ if (item.command) {
6574
+ const cmd = registry.commands[item.command];
6575
+ if (!cmd) {
6576
+ vscode.window.showErrorMessage(\`Menu item references unknown command: \${item.command}\`);
6577
+ return;
6578
+ }
6579
+ await cmd.run(vscode, context);
6580
+ return;
6581
+ }
6582
+ if (item.run) {
6583
+ await item.run(vscode, context);
6584
+ return;
6585
+ }
6586
+ }
6587
+
6588
+ class MenuTreeDataProvider implements vscode.TreeDataProvider<MenuItem> {
6589
+ private _onDidChange = new vscode.EventEmitter<MenuItem | undefined>();
6590
+ readonly onDidChangeTreeData = this._onDidChange.event;
6591
+ private dispatchCmd = '';
6592
+
6593
+ constructor(private readonly items: MenuItem[], private readonly context: vscode.ExtensionContext) {}
6594
+
6595
+ setDispatchCommand(cmd: string) {
6596
+ this.dispatchCmd = cmd;
6597
+ this._onDidChange.fire(undefined);
6598
+ }
6599
+
6600
+ getTreeItem(item: MenuItem): vscode.TreeItem {
6601
+ const hasChildren = !!item.children?.length;
6602
+ const collapsibleState = hasChildren
6603
+ ? item.collapsed === 'collapsed'
6604
+ ? vscode.TreeItemCollapsibleState.Collapsed
6605
+ : vscode.TreeItemCollapsibleState.Expanded
6606
+ : vscode.TreeItemCollapsibleState.None;
6607
+ const node = new vscode.TreeItem(item.label, collapsibleState);
6608
+ node.tooltip = item.description ?? item.label;
6609
+ node.description = item.description;
6610
+ node.iconPath = resolveIcon(this.context, item.icon);
6611
+ if (!hasChildren && this.dispatchCmd) {
6612
+ node.command = {
6613
+ command: this.dispatchCmd,
6614
+ title: item.label,
6615
+ arguments: [item],
6616
+ };
6617
+ }
6618
+ return node;
6619
+ }
6620
+
6621
+ getChildren(item?: MenuItem): MenuItem[] {
6622
+ if (!item) return this.items;
6623
+ return item.children ?? [];
6624
+ }
6625
+ }
6626
+
6627
+ function resolveIcon(
6628
+ context: vscode.ExtensionContext,
6629
+ icon: MenuItem['icon'],
6630
+ ): vscode.TreeItem['iconPath'] {
6631
+ if (!icon) return undefined;
6632
+ if (typeof icon === 'string') return new vscode.ThemeIcon(icon);
6633
+ const path = require('path') as typeof import('path');
6634
+ const toUri = (p: string) =>
6635
+ path.isAbsolute(p) ? vscode.Uri.file(p) : vscode.Uri.joinPath(context.extensionUri, p);
6636
+ if ('path' in icon) return toUri(icon.path);
6637
+ return { light: toUri(icon.light), dark: toUri(icon.dark) };
6638
+ }
6639
+
6640
+ function openPanel(context: vscode.ExtensionContext, prefix: string, id: string, def: PanelDef) {
6641
+ const key = \`\${prefix}.\${def.id ?? id}\`;
6642
+ const existing = openPanels.get(key);
6643
+ const column = resolveColumn(def.column);
6644
+ if (existing) {
6645
+ existing.reveal(column);
6646
+ return existing;
6647
+ }
6648
+
6649
+ const panel = vscode.window.createWebviewPanel(
6650
+ key,
6651
+ def.title,
6652
+ column,
6653
+ {
6654
+ enableScripts: true,
6655
+ retainContextWhenHidden: def.retainContext ?? true,
6656
+ localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'dist', 'webview')],
6657
+ },
6658
+ );
6659
+
6660
+ const ui = def.ui ?? \`panels/\${def.id ?? id}\`;
6661
+ panel.webview.html = renderHtml(panel.webview, context, ui, def.title);
6662
+
6663
+ if (def.rpc) {
6664
+ const handlers = def.rpc(vscode, context);
6665
+ const server = createRpcServer(webviewTransport(panel.webview), handlers);
6666
+ panel.onDidDispose(() => server.dispose());
6667
+ }
6668
+
6669
+ openPanels.set(key, panel);
6670
+ panel.onDidDispose(() => openPanels.delete(key));
6671
+ return panel;
6672
+ }
6673
+
6674
+ interface ViteManifestEntry {
6675
+ file: string;
6676
+ css?: string[];
6677
+ assets?: string[];
6678
+ imports?: string[];
6679
+ }
6680
+ type ViteManifest = Record<string, ViteManifestEntry>;
6681
+
6682
+ let cachedManifest: { mtime: number; data: ViteManifest } | null = null;
6683
+
6684
+ function loadManifest(extensionUri: vscode.Uri): ViteManifest | null {
6685
+ const fs = require('fs') as typeof import('fs');
6686
+ const path = require('path') as typeof import('path');
6687
+ // Vite manifest can land at either \`manifest.json\` (new) or \`.vite/manifest.json\` (default).
6688
+ const webviewRoot = vscode.Uri.joinPath(extensionUri, 'dist', 'webview').fsPath;
6689
+ for (const rel of ['manifest.json', '.vite/manifest.json']) {
6690
+ const p = path.join(webviewRoot, rel);
6691
+ if (!fs.existsSync(p)) continue;
6692
+ const mtime = fs.statSync(p).mtimeMs;
6693
+ if (cachedManifest?.mtime === mtime) return cachedManifest.data;
6694
+ cachedManifest = { mtime, data: JSON.parse(fs.readFileSync(p, 'utf8')) };
6695
+ return cachedManifest.data;
6696
+ }
6697
+ return null;
6698
+ }
6699
+
6700
+ function resolveAssets(extensionUri: vscode.Uri, ui: string): { js: string[]; css: string[] } {
6701
+ const manifest = loadManifest(extensionUri);
6702
+ if (!manifest) {
6703
+ // Fallback to convention: <ui>/index.js + <ui>/index.css
6704
+ return { js: [\`\${ui}/index.js\`], css: [\`\${ui}/index.css\`] };
6705
+ }
6706
+ // Manifest keys for HTML entries look like \`<ui>/index.html\`.
6707
+ const key = \`\${ui}/index.html\`;
6708
+ const entry = manifest[key];
6709
+ if (!entry) return { js: [\`\${ui}/index.js\`], css: [] };
6710
+ const js = [entry.file];
6711
+ const css = [...(entry.css ?? [])];
6712
+ // Recursively pull CSS from imported chunks.
6713
+ const seen = new Set<string>();
6714
+ const walk = (imp: string) => {
6715
+ if (seen.has(imp)) return;
6716
+ seen.add(imp);
6717
+ const e = manifest[imp];
6718
+ if (!e) return;
6719
+ if (e.css) css.push(...e.css);
6720
+ e.imports?.forEach(walk);
6721
+ };
6722
+ entry.imports?.forEach(walk);
6723
+ return { js, css };
6724
+ }
6725
+
6726
+ function renderHtml(webview: vscode.Webview, context: vscode.ExtensionContext, ui: string, title: string): string {
6727
+ const root = vscode.Uri.joinPath(context.extensionUri, 'dist', 'webview');
6728
+ const { js, css } = resolveAssets(context.extensionUri, ui);
6729
+ const toUri = (rel: string) =>
6730
+ webview.asWebviewUri(vscode.Uri.joinPath(root, ...rel.split('/'))).toString();
6731
+ const scriptTags = js
6732
+ .map((f) => \`<script type="module" nonce="{{NONCE}}" src="\${toUri(f)}"></script>\`)
6733
+ .join('\\n ');
6734
+ const styleTags = css.map((f) => \`<link rel="stylesheet" href="\${toUri(f)}" />\`).join('\\n ');
6735
+ const nonce = Array.from({ length: 16 }, () => Math.random().toString(36)[2]).join('');
6736
+ const csp = [
6737
+ \`default-src 'none'\`,
6738
+ \`style-src \${webview.cspSource} 'unsafe-inline'\`,
6739
+ \`script-src 'nonce-\${nonce}'\`,
6740
+ \`img-src \${webview.cspSource} https: data:\`,
6741
+ \`font-src \${webview.cspSource}\`,
6742
+ ].join('; ');
6743
+
6744
+ return \`<!DOCTYPE html>
6745
+ <html lang="en">
6746
+ <head>
6747
+ <meta charset="UTF-8" />
6748
+ <meta http-equiv="Content-Security-Policy" content="\${csp}" />
6749
+ \${styleTags}
6750
+ <title>\${escapeHtml(title)}</title>
6751
+ </head>
6752
+ <body><div id="root"></div>
6753
+ \${scriptTags.replace(/\\{\\{NONCE\\}\\}/g, nonce)}
6754
+ </body>
6755
+ </html>\`;
6756
+ }
6757
+
6758
+ function resolveColumn(c: PanelDef['column']): vscode.ViewColumn {
6759
+ switch (c) {
6760
+ case 'beside': return vscode.ViewColumn.Beside;
6761
+ case 'one': return vscode.ViewColumn.One;
6762
+ case 'two': return vscode.ViewColumn.Two;
6763
+ case 'three': return vscode.ViewColumn.Three;
6764
+ default: return vscode.window.activeTextEditor?.viewColumn ?? vscode.ViewColumn.One;
6765
+ }
6766
+ }
6767
+
6768
+ function capitalize(s: string): string {
6769
+ return s.charAt(0).toUpperCase() + s.slice(1);
6770
+ }
6771
+
6772
+ function escapeHtml(s: string): string {
6773
+ return s.replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
6774
+ }
6775
+ `,
6776
+ "react/src/shared/vsceasy/client.ts": `// Webview-safe exports only — does NOT import 'vscode'.
6777
+ export {
6778
+ createRpcClient,
6779
+ vscodeApiTransport,
6780
+ connectWebview,
6781
+ webviewState,
6782
+ } from './rpc';
6783
+ export type { RpcClient, Handlers, Transport, RpcClientOptions, WebviewApi } from './rpc';
6784
+ `,
6785
+ "react/src/shared/vsceasy/codiconNames.ts": `/**
6786
+ * AUTO-GENERATED by scripts/genCodiconTypes.ts — do not edit.
6787
+ *
6788
+ * Curated subset of VS Code codicon names that the CLI's icon picker offers.
6789
+ * Used by \`MenuIcon\` for editor autocomplete. Any string is still accepted at
6790
+ * runtime via the \`(string & {})\` fallback in define.ts.
6791
+ *
6792
+ * Full list: https://microsoft.github.io/vscode-codicons/dist/codicon.html
6793
+ */
6794
+ export type CodiconName =
6795
+ | 'account'
6796
+ | 'add'
6797
+ | 'archive'
6798
+ | 'beaker'
6799
+ | 'bell'
6800
+ | 'bell-dot'
6801
+ | 'book'
6802
+ | 'bookmark'
6803
+ | 'broadcast'
6804
+ | 'browser'
6805
+ | 'bug'
6806
+ | 'calendar'
6807
+ | 'check'
6808
+ | 'clock'
6809
+ | 'close'
6810
+ | 'cloud'
6811
+ | 'cloud-download'
6812
+ | 'cloud-upload'
6813
+ | 'code'
6814
+ | 'comment'
6815
+ | 'comment-discussion'
6816
+ | 'compass'
6817
+ | 'console'
6818
+ | 'dashboard'
6819
+ | 'database'
6820
+ | 'debug'
6821
+ | 'debug-alt'
6822
+ | 'debug-breakpoint'
6823
+ | 'debug-console'
6824
+ | 'debug-continue'
6825
+ | 'debug-disconnect'
6826
+ | 'debug-pause'
6827
+ | 'debug-restart'
6828
+ | 'debug-start'
6829
+ | 'debug-step-into'
6830
+ | 'debug-step-out'
6831
+ | 'debug-step-over'
6832
+ | 'debug-stop'
6833
+ | 'desktop-download'
6834
+ | 'edit'
6835
+ | 'error'
6836
+ | 'export'
6837
+ | 'extensions'
6838
+ | 'eye'
6839
+ | 'eye-closed'
6840
+ | 'file'
6841
+ | 'file-binary'
6842
+ | 'file-code'
6843
+ | 'file-media'
6844
+ | 'file-pdf'
6845
+ | 'file-symlink-directory'
6846
+ | 'file-symlink-file'
6847
+ | 'file-text'
6848
+ | 'file-zip'
6849
+ | 'files'
6850
+ | 'filter'
6851
+ | 'flag'
6852
+ | 'flame'
6853
+ | 'folder'
6854
+ | 'folder-active'
6855
+ | 'folder-library'
6856
+ | 'folder-opened'
6857
+ | 'gear'
6858
+ | 'git-branch'
6859
+ | 'git-commit'
6860
+ | 'git-compare'
6861
+ | 'git-fork'
6862
+ | 'git-merge'
6863
+ | 'git-pull-request'
6864
+ | 'github'
6865
+ | 'github-alt'
6866
+ | 'github-inverted'
6867
+ | 'globe'
6868
+ | 'graph'
6869
+ | 'heart'
6870
+ | 'history'
6871
+ | 'home'
6872
+ | 'image'
6873
+ | 'inbox'
6874
+ | 'info'
6875
+ | 'json'
6876
+ | 'key'
6877
+ | 'layout'
6878
+ | 'layout-panel'
6879
+ | 'layout-sidebar-left'
6880
+ | 'layout-sidebar-right'
6881
+ | 'lightbulb'
6882
+ | 'link'
6883
+ | 'link-external'
6884
+ | 'list-flat'
6885
+ | 'list-ordered'
6886
+ | 'list-selection'
6887
+ | 'list-tree'
6888
+ | 'list-unordered'
6889
+ | 'location'
6890
+ | 'lock'
6891
+ | 'mail'
6892
+ | 'markdown'
6893
+ | 'megaphone'
6894
+ | 'mortar-board'
6895
+ | 'mute'
6896
+ | 'new-file'
6897
+ | 'new-folder'
6898
+ | 'note'
6899
+ | 'notebook'
6900
+ | 'organization'
6901
+ | 'output'
6902
+ | 'package'
6903
+ | 'pencil'
6904
+ | 'person'
6905
+ | 'pin'
6906
+ | 'pinned'
6907
+ | 'play'
6908
+ | 'play-circle'
6909
+ | 'plug'
6910
+ | 'preferences'
6911
+ | 'preview'
6912
+ | 'pulse'
6913
+ | 'question'
6914
+ | 'record'
6915
+ | 'refresh'
6916
+ | 'remove'
6917
+ | 'replace'
6918
+ | 'repo'
6919
+ | 'repo-clone'
6920
+ | 'repo-forked'
6921
+ | 'repo-pull'
6922
+ | 'repo-push'
6923
+ | 'rocket'
6924
+ | 'save'
6925
+ | 'save-all'
6926
+ | 'search'
6927
+ | 'server'
6928
+ | 'server-environment'
6929
+ | 'server-process'
6930
+ | 'settings'
6931
+ | 'settings-gear'
6932
+ | 'shield'
6933
+ | 'source-control'
6934
+ | 'sparkle'
6935
+ | 'split-horizontal'
6936
+ | 'split-vertical'
6937
+ | 'star'
6938
+ | 'star-empty'
6939
+ | 'star-full'
6940
+ | 'stop'
6941
+ | 'stop-circle'
6942
+ | 'symbol-array'
6943
+ | 'symbol-boolean'
6944
+ | 'symbol-class'
6945
+ | 'symbol-color'
6946
+ | 'symbol-constant'
6947
+ | 'symbol-enum'
6948
+ | 'symbol-event'
6949
+ | 'symbol-field'
6950
+ | 'symbol-function'
6951
+ | 'symbol-interface'
6952
+ | 'symbol-key'
6953
+ | 'symbol-method'
6954
+ | 'symbol-misc'
6955
+ | 'symbol-module'
6956
+ | 'symbol-namespace'
6957
+ | 'symbol-numeric'
6958
+ | 'symbol-parameter'
6959
+ | 'symbol-property'
6960
+ | 'symbol-snippet'
6961
+ | 'symbol-string'
6962
+ | 'symbol-variable'
6963
+ | 'sync'
6964
+ | 'tag'
6965
+ | 'terminal'
6966
+ | 'terminal-bash'
6967
+ | 'terminal-cmd'
6968
+ | 'terminal-linux'
6969
+ | 'terminal-powershell'
6970
+ | 'tools'
6971
+ | 'trash'
6972
+ | 'unlock'
6973
+ | 'unmute'
6974
+ | 'verified'
6975
+ | 'video'
6976
+ | 'wand'
6977
+ | 'warning'
6978
+ | 'watch'
6979
+ | 'window'
6980
+ | 'zap';
6981
+ `,
6982
+ "react/src/shared/vsceasy/define.ts": `import type * as vscode from 'vscode';
6983
+ import type { Handlers } from './rpc';
6984
+ import type { CodiconName } from './codiconNames';
6985
+
6986
+ export interface PanelDef<H extends Handlers = Handlers> {
6987
+ /** Stable id. Default: file basename. Used as command suffix and webview key. */
6988
+ id?: string;
6989
+ /** Tab title. */
6990
+ title: string;
6991
+ /** Webview bundle name under dist/webview/<ui>/. Default: same as id. */
6992
+ ui?: string;
6993
+ /** Where to open. Default: 'active'. */
6994
+ column?: 'active' | 'beside' | 'one' | 'two' | 'three';
6995
+ /** Keep DOM alive when hidden. Default: true. */
6996
+ retainContext?: boolean;
6997
+ /** RPC handlers — receives vscode namespace + extension context. */
6998
+ rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => H;
6999
+ /** Optional command palette entry that opens this panel. Default: true. */
7000
+ command?:
7001
+ | boolean
7002
+ | { title?: string; category?: string };
7003
+ }
7004
+
7005
+ export interface CommandDef {
7006
+ /** Stable id. Default: file basename. */
7007
+ id?: string;
7008
+ /** Command palette title. */
7009
+ title: string;
7010
+ /** Optional category prefix (default: extension displayName). */
7011
+ category?: string;
7012
+ /** Handler. Receives vscode + extension context. */
7013
+ run: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, ...args: unknown[]) => unknown | Promise<unknown>;
7014
+ /**
7015
+ * Keyboard shortcut. String shorthand uses the same key on every platform.
7016
+ * Object form supports \`mac\` override and a VS Code \`when\` clause.
7017
+ * Written to package.json#contributes.keybindings by \`bun run gen\`.
7018
+ */
7019
+ keybinding?: string | KeybindingDef | (string | KeybindingDef)[];
7020
+ /**
7021
+ * VS Code \`when\` clause that controls visibility/enablement of this command
7022
+ * in the command palette and auto-generated menu entries. Written to
7023
+ * \`contributes.commands[].enablement\` and used as the default \`when\` on
7024
+ * the palette menu entry by \`bun run gen\`.
7025
+ *
7026
+ * Examples: \`'editorTextFocus'\`, \`'resourceLangId == typescript'\`,
7027
+ * \`'explorerResourceIsFolder && !virtualWorkspace'\`.
7028
+ * Reference: https://code.visualstudio.com/api/references/when-clause-contexts
7029
+ */
7030
+ when?: string;
7031
+ }
7032
+
7033
+ export interface KeybindingDef {
7034
+ /** Default key combo (e.g. 'ctrl+shift+h'). */
7035
+ key: string;
7036
+ /** Override combo on macOS (e.g. 'cmd+shift+h'). */
7037
+ mac?: string;
7038
+ /** VS Code context \`when\` clause (e.g. 'editorTextFocus'). */
7039
+ when?: string;
7040
+ }
7041
+
7042
+ export function definePanel<H extends Handlers = Handlers>(def: PanelDef<H>): PanelDef<H> {
7043
+ return def;
7044
+ }
7045
+
7046
+ export function defineCommand(def: CommandDef): CommandDef {
7047
+ return def;
7048
+ }
7049
+
7050
+ // --- Menus (Activity Bar + Tree View) ---
7051
+
7052
+ export type MenuIcon =
7053
+ | CodiconName // known codicon (autocompletes)
7054
+ | (string & {}) // any codicon name (escape hatch, keeps autocomplete)
7055
+ | { path: string } // single SVG path relative to project root
7056
+ | { light: string; dark: string }; // theme-aware SVG paths
7057
+
7058
+ export type { CodiconName };
7059
+
7060
+ export interface MenuItem {
7061
+ /** Display label. */
7062
+ label: string;
7063
+ /** Optional icon (codicon name or asset path). */
7064
+ icon?: MenuIcon;
7065
+ /** Optional tooltip / hover description. */
7066
+ description?: string;
7067
+ /** Open a panel by id (file basename in src/panels/). */
7068
+ panel?: string;
7069
+ /** Execute a command by id (file basename in src/commands/). */
7070
+ command?: string;
7071
+ /** Open an external URL in the user's browser. */
7072
+ url?: string;
7073
+ /** Run an arbitrary handler (full vscode access). */
7074
+ run?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => unknown | Promise<unknown>;
7075
+ /** Nested items — renders as a collapsible group. */
7076
+ children?: MenuItem[];
7077
+ /** Initial collapsed state for groups. Default: 'expanded'. */
7078
+ collapsed?: 'expanded' | 'collapsed';
7079
+ }
7080
+
7081
+ export interface MenuDef {
7082
+ /** Stable id. Default: file basename. Becomes the view container id. */
7083
+ id?: string;
7084
+ /** Title shown at the top of the sidebar panel and as the activity bar tooltip. */
7085
+ title: string;
7086
+ /** Activity bar icon. Codicon string OR SVG path(s). */
7087
+ icon: MenuIcon;
7088
+ /** Items shown in the tree view. */
7089
+ items: MenuItem[];
7090
+ }
7091
+
7092
+ export function defineMenu(def: MenuDef): MenuDef {
7093
+ return def;
7094
+ }
7095
+
7096
+ // --- Webview Views (inline sidebar sections) ---
7097
+
7098
+ export interface SubpanelDef<H extends Handlers = Handlers> {
7099
+ /** Stable id. Default: file basename. */
7100
+ id?: string;
7101
+ /** Section header shown in the sidebar. */
7102
+ title: string;
7103
+ /** Menu (activity bar container) this view lives in — basename in src/menus/. */
7104
+ menu: string;
7105
+ /** Webview bundle name under dist/webview/<ui>/. Default: same as id. */
7106
+ ui?: string;
7107
+ /** Keep DOM alive when hidden. Default: true. */
7108
+ retainContext?: boolean;
7109
+ /** RPC handlers — receives vscode namespace + extension context. */
7110
+ rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => H;
7111
+ }
7112
+
7113
+ export function defineSubpanel<H extends Handlers = Handlers>(def: SubpanelDef<H>): SubpanelDef<H> {
7114
+ return def;
7115
+ }
7116
+
7117
+ // --- Status Bar items ---
7118
+
7119
+ export interface StatusBarDef {
7120
+ /** Stable id. Default: file basename. */
7121
+ id?: string;
7122
+ /** Display text. May include \`$(codicon)\` syntax. */
7123
+ text: string;
7124
+ /** Tooltip on hover. */
7125
+ tooltip?: string;
7126
+ /** Optional codicon, prepended as \`$(icon) text\` when both present. */
7127
+ icon?: CodiconName | (string & {});
7128
+ /** Bar side. Default: 'left'. */
7129
+ alignment?: 'left' | 'right';
7130
+ /** Higher = leftmost on its side. Default: 100. */
7131
+ priority?: number;
7132
+ /** Command id to run on click (basename in src/commands/, or full vscode command id). */
7133
+ command?: string;
7134
+ /** Open a panel by id (basename in src/panels/). Takes precedence over \`command\` when set. */
7135
+ panel?: string;
7136
+ /** Background color theme key (e.g. 'statusBarItem.warningBackground'). */
7137
+ backgroundColor?: string;
7138
+ /**
7139
+ * Rich markdown tooltip (overrides \`tooltip\`). Supports command links
7140
+ * (\`[text](command:ext.foo)\`), codicons (\`$(rocket)\`), and HTML.
7141
+ * Rendered on hover. Mimics Copilot/GitLens popup style.
7142
+ */
7143
+ tooltipMarkdown?: string;
7144
+ /**
7145
+ * Open a popup menu on click instead of running a single command/panel.
7146
+ * Each item runs its \`command\`, opens its \`panel\`, or opens its \`url\`.
7147
+ */
7148
+ menu?: StatusBarMenuItem[];
7149
+ }
7150
+
7151
+ export interface StatusBarMenuItem {
7152
+ /** Display label. May include \`$(codicon)\`. */
7153
+ label: string;
7154
+ /** Inline secondary text. */
7155
+ description?: string;
7156
+ /** Detail line (smaller, below). */
7157
+ detail?: string;
7158
+ /** Command id (basename in src/commands/) or full vscode command id. */
7159
+ command?: string;
7160
+ /** Panel id (basename in src/panels/). */
7161
+ panel?: string;
7162
+ /** External URL. */
7163
+ url?: string;
7164
+ }
7165
+
7166
+ export function defineStatusBar(def: StatusBarDef): StatusBarDef {
7167
+ return def;
7168
+ }
7169
+
7170
+ // --- Tree Views (data-driven) ---
7171
+
7172
+ export interface TreeNode {
7173
+ /** Display label. */
7174
+ label: string;
7175
+ /** Stable id, defaults to label. Used for reveal/select. */
7176
+ id?: string;
7177
+ /** Optional icon. */
7178
+ icon?: MenuIcon;
7179
+ /** Tooltip on hover. */
7180
+ tooltip?: string;
7181
+ /** Right-aligned description text. */
7182
+ description?: string;
7183
+ /** Context value used by \`view/item/context\` menu entries. */
7184
+ contextValue?: string;
7185
+ /** Initial state when this node has children. Default: 'collapsed'. */
7186
+ collapsed?: 'expanded' | 'collapsed';
7187
+ /** Eagerly provided children. If omitted, getChildren(this) is called lazily. */
7188
+ children?: TreeNode[];
7189
+ /** Click handler — run an arbitrary callback when the node is selected. */
7190
+ run?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => unknown | Promise<unknown>;
7191
+ /** Click → open a panel by id. */
7192
+ panel?: string;
7193
+ /** Click → run a command by id. */
7194
+ command?: string;
7195
+ }
7196
+
7197
+ export interface TreeViewDef {
7198
+ /** Stable id. Default: file basename. */
7199
+ id?: string;
7200
+ /** Sidebar section header. */
7201
+ title: string;
7202
+ /** Activity bar container id (menu basename in src/menus/). */
7203
+ menu: string;
7204
+ /** Show "Collapse All" button. Default: true. */
7205
+ showCollapseAll?: boolean;
7206
+ /** Initial / refreshed nodes. Called on mount and whenever the view is refreshed. */
7207
+ getChildren: (
7208
+ parent: TreeNode | undefined,
7209
+ vscode: typeof import('vscode'),
7210
+ ctx: vscode.ExtensionContext,
7211
+ ) => TreeNode[] | Promise<TreeNode[]>;
7212
+ }
7213
+
7214
+ export function defineTreeView(def: TreeViewDef): TreeViewDef {
7215
+ return def;
7216
+ }
7217
+
7218
+ // --- Jobs (recurring / event-triggered tasks) ---
7219
+
7220
+ export type JobSchedule =
7221
+ /** Run every <interval>. Accepts ms number or duration string: "30s", "5m", "2h", "1d". */
7222
+ | { every: string | number; runOnStart?: boolean }
7223
+ /** Run at HH:MM local time, every day. */
7224
+ | { dailyAt: string }
7225
+ /** Run on a VS Code lifecycle event. */
7226
+ | { on: 'startup' | 'saveDocument' | 'openDocument' | 'changeActiveEditor' | 'changeConfig' }
7227
+ /** Run on filesystem changes matching a glob (relative to workspace). */
7228
+ | { onFile: string };
7229
+
7230
+ export interface JobDef {
7231
+ /** Stable id. Default: file basename. */
7232
+ id?: string;
7233
+ /** Display label (used in logs + opt. status bar). */
7234
+ title: string;
7235
+ /** When to run. */
7236
+ schedule: JobSchedule;
7237
+ /**
7238
+ * Skip if last successful run was less than this many ms ago. Stored in
7239
+ * \`context.globalState\` under \`vsceasy.job.<id>.lastRun\`. Useful for jobs
7240
+ * that should at most run every N hours regardless of how often the
7241
+ * trigger fires.
7242
+ */
7243
+ minIntervalMs?: number;
7244
+ /** The actual work. Errors are caught and logged — they never crash the host. */
7245
+ run: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => unknown | Promise<unknown>;
7246
+ }
7247
+
7248
+ export function defineJob(def: JobDef): JobDef {
7249
+ return def;
7250
+ }
7251
+ `,
7252
+ "react/src/shared/vsceasy/index.ts": `export { definePanel, defineCommand, defineMenu, defineStatusBar, defineSubpanel, defineTreeView, defineJob } from './define';
7253
+ export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName } from './define';
7254
+ export { bootstrap } from './bootstrap';
7255
+ export type { Registry, BootstrapOptions, ActivateHook } from './bootstrap';
7256
+ export {
7257
+ createRpcClient,
7258
+ createRpcServer,
7259
+ webviewTransport,
7260
+ vscodeApiTransport,
7261
+ connectWebview,
7262
+ webviewState,
7263
+ } from './rpc';
7264
+ export type { Transport, RpcClient, Handlers, RpcClientOptions, WebviewApi } from './rpc';
7265
+ `,
7266
+ "react/src/shared/vsceasy/rpc.ts": `// Typed RPC bridge — webview <-> extension.
7267
+ // Used by both sides. Transport-agnostic core + thin adapters.
7268
+
7269
+ export type RpcMessage =
7270
+ | { id: string; kind: 'call'; method: string; args: unknown[] }
7271
+ | { id: string; kind: 'result'; ok: true; value: unknown }
7272
+ | { id: string; kind: 'result'; ok: false; error: { message: string; stack?: string } }
7273
+ | { kind: 'event'; topic: string; payload: unknown };
7274
+
7275
+ export interface Transport {
7276
+ send(msg: RpcMessage): void;
7277
+ onMessage(handler: (msg: RpcMessage) => void): () => void;
7278
+ }
7279
+
7280
+ // --- Server (extension side) ---
7281
+
7282
+ /**
7283
+ * Loose constraint: an interface with method members. Using \`object\` (instead
7284
+ * of a Record with index signature) lets user-declared interfaces satisfy the
7285
+ * constraint without forcing them to declare \`[k: string]: any\`.
7286
+ */
7287
+ export type Handlers = object;
7288
+
7289
+ export function createRpcServer<H extends Handlers>(transport: Transport, handlers: H) {
7290
+ const off = transport.onMessage(async (msg) => {
7291
+ if (msg.kind !== 'call') return;
7292
+ const fn = (handlers as Record<string, (...args: any[]) => any>)[msg.method];
7293
+ if (!fn) {
7294
+ transport.send({
7295
+ id: msg.id,
7296
+ kind: 'result',
7297
+ ok: false,
7298
+ error: { message: \`Unknown RPC method: \${msg.method}\` },
7299
+ });
7300
+ return;
7301
+ }
7302
+ try {
7303
+ const value = await fn(...msg.args);
7304
+ transport.send({ id: msg.id, kind: 'result', ok: true, value });
7305
+ } catch (err: any) {
7306
+ transport.send({
7307
+ id: msg.id,
7308
+ kind: 'result',
7309
+ ok: false,
7310
+ error: { message: String(err?.message ?? err), stack: err?.stack },
7311
+ });
7312
+ }
7313
+ });
7314
+
7315
+ return {
7316
+ emit(topic: string, payload: unknown) {
7317
+ transport.send({ kind: 'event', topic, payload });
7318
+ },
7319
+ dispose: off,
7320
+ };
7321
+ }
7322
+
7323
+ // --- Client (webview side) ---
7324
+
7325
+ export type RpcClient<H extends Handlers> = {
7326
+ [K in keyof H]: H[K] extends (...args: infer A) => infer R
7327
+ ? (...args: A) => Promise<Awaited<R>>
7328
+ : never;
7329
+ } & {
7330
+ on(topic: string, handler: (payload: any) => void): () => void;
7331
+ };
7332
+
7333
+ export interface RpcClientOptions {
7334
+ /**
7335
+ * Max wait (ms) for a call's reply before rejecting with a timeout error.
7336
+ * Prevents hangs when the extension host reloads mid-flight during \`bun run dev\`.
7337
+ * Default: 15000. Set to 0 to disable.
7338
+ */
7339
+ callTimeoutMs?: number;
7340
+ }
7341
+
7342
+ export function createRpcClient<H extends Handlers>(
7343
+ transport: Transport,
7344
+ opts: RpcClientOptions = {},
7345
+ ): RpcClient<H> {
7346
+ const callTimeoutMs = opts.callTimeoutMs ?? 15000;
7347
+ const pending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void; timer?: ReturnType<typeof setTimeout> }>();
7348
+ const listeners = new Map<string, Set<(p: any) => void>>();
7349
+
7350
+ transport.onMessage((msg) => {
7351
+ if (msg.kind === 'result') {
7352
+ const p = pending.get(msg.id);
7353
+ if (!p) return;
7354
+ pending.delete(msg.id);
7355
+ if (p.timer) clearTimeout(p.timer);
7356
+ if (msg.ok) p.resolve(msg.value);
7357
+ else p.reject(Object.assign(new Error(msg.error.message), { stack: msg.error.stack }));
7358
+ } else if (msg.kind === 'event') {
7359
+ listeners.get(msg.topic)?.forEach((l) => l(msg.payload));
7360
+ }
7361
+ });
7362
+
7363
+ let counter = 0;
7364
+ const newId = () => \`r\${++counter}_\${Date.now()}\`;
7365
+
7366
+ const proxy = new Proxy({} as any, {
7367
+ get(_t, prop: string) {
7368
+ if (prop === 'on') {
7369
+ return (topic: string, handler: (p: any) => void) => {
7370
+ let set = listeners.get(topic);
7371
+ if (!set) listeners.set(topic, (set = new Set()));
7372
+ set.add(handler);
7373
+ return () => set!.delete(handler);
7374
+ };
7375
+ }
7376
+ return (...args: unknown[]) =>
7377
+ new Promise((resolve, reject) => {
7378
+ const id = newId();
7379
+ const entry: { resolve: (v: any) => void; reject: (e: any) => void; timer?: ReturnType<typeof setTimeout> } = { resolve, reject };
7380
+ if (callTimeoutMs > 0) {
7381
+ entry.timer = setTimeout(() => {
7382
+ if (pending.delete(id)) {
7383
+ reject(new Error(\`RPC \\\`\${prop}\\\` timed out after \${callTimeoutMs}ms (extension host reloaded?)\`));
7384
+ }
7385
+ }, callTimeoutMs);
7386
+ }
7387
+ pending.set(id, entry);
7388
+ try {
7389
+ transport.send({ id, kind: 'call', method: prop, args });
7390
+ } catch (err) {
7391
+ pending.delete(id);
7392
+ if (entry.timer) clearTimeout(entry.timer);
7393
+ reject(err);
7394
+ }
7395
+ });
7396
+ },
7397
+ });
7398
+
7399
+ return proxy as RpcClient<H>;
7400
+ }
7401
+
7402
+ // --- Transports ---
7403
+
7404
+ export function webviewTransport(webview: { postMessage(m: any): any; onDidReceiveMessage: any }): Transport {
7405
+ return {
7406
+ send: (m) => webview.postMessage(m),
7407
+ onMessage: (h) => {
7408
+ const sub = webview.onDidReceiveMessage((m: RpcMessage) => h(m));
7409
+ return () => sub.dispose();
7410
+ },
7411
+ };
7412
+ }
7413
+
7414
+ export function vscodeApiTransport(vscode: { postMessage(m: any): void }): Transport {
7415
+ return {
7416
+ send: (m) => vscode.postMessage(m),
7417
+ onMessage: (h) => {
7418
+ const listener = (e: MessageEvent) => h(e.data as RpcMessage);
7419
+ window.addEventListener('message', listener);
7420
+ return () => window.removeEventListener('message', listener);
7421
+ },
7422
+ };
7423
+ }
7424
+
7425
+ declare global {
7426
+ function acquireVsCodeApi(): WebviewApi;
7427
+ }
7428
+
7429
+ export interface WebviewApi {
7430
+ postMessage(m: any): void;
7431
+ getState(): unknown;
7432
+ setState<T>(s: T): T;
7433
+ }
7434
+
7435
+ let _cachedVscode: WebviewApi | null = null;
7436
+ function vscodeApi(): WebviewApi {
7437
+ // acquireVsCodeApi() may only be called once per webview lifetime.
7438
+ if (_cachedVscode) return _cachedVscode;
7439
+ return (_cachedVscode = acquireVsCodeApi());
7440
+ }
7441
+
7442
+ /** One-liner for webview: returns a typed RPC client. */
7443
+ export function connectWebview<H extends Handlers>(opts?: RpcClientOptions): RpcClient<H> {
7444
+ return createRpcClient<H>(vscodeApiTransport(vscodeApi()), opts);
7445
+ }
7446
+
7447
+ /**
7448
+ * Typed \`vscode.getState() / setState()\` wrapper for webviews. State survives
7449
+ * panel hide/show, host reloads triggered by \`retainContextWhenHidden\`, and
7450
+ * is the recommended way to persist scroll positions, form data, and selection.
7451
+ *
7452
+ * Usage:
7453
+ * const state = webviewState<{ query: string }>({ query: '' });
7454
+ * state.set({ query: 'foo' });
7455
+ * const { query } = state.get();
7456
+ */
7457
+ export function webviewState<T>(defaults: T): {
7458
+ get(): T;
7459
+ set(next: T | ((prev: T) => T)): T;
7460
+ patch(partial: Partial<T>): T;
7461
+ } {
7462
+ const api = vscodeApi();
7463
+ const init = (): T => ({ ...defaults, ...((api.getState() as T | undefined) ?? {}) });
7464
+ return {
7465
+ get: init,
7466
+ set(next) {
7467
+ const current = init();
7468
+ const value = typeof next === 'function' ? (next as (p: T) => T)(current) : next;
7469
+ api.setState(value);
7470
+ return value;
7471
+ },
7472
+ patch(partial) {
7473
+ const current = init();
7474
+ const value = { ...current, ...partial };
7475
+ api.setState(value);
7476
+ return value;
7477
+ },
7478
+ };
7479
+ }
7480
+ `,
7481
+ "react/src/webview/panels/dashboard/App.tsx": `import React, { useEffect, useState } from 'react';
7482
+ import { connectWebview } from '../../../shared/vsceasy/client';
7483
+ import type { DashboardApi } from '../../../shared/api';
7484
+
7485
+ const api = connectWebview<DashboardApi>();
7486
+
7487
+ export function App() {
7488
+ const [info, setInfo] = useState<{ workspace: string | null; vscodeVersion: string } | null>(null);
7489
+ const [files, setFiles] = useState<string[]>([]);
7490
+ const [pattern, setPattern] = useState('**/*.ts');
7491
+
7492
+ useEffect(() => { api.getInfo().then(setInfo); }, []);
7493
+
7494
+ return (
7495
+ <div className="app">
7496
+ <h1>{{displayName}} Dashboard</h1>
7497
+ {info && (
7498
+ <section>
7499
+ <p><strong>Workspace:</strong> {info.workspace ?? '(none)'}</p>
7500
+ <p><strong>VS Code:</strong> {info.vscodeVersion}</p>
7501
+ </section>
7502
+ )}
7503
+ <section>
7504
+ <input value={pattern} onChange={(e) => setPattern(e.target.value)} />
7505
+ <button onClick={async () => setFiles(await api.listFiles(pattern))}>Find files</button>
7506
+ <button onClick={() => api.showMessage('Hello from the webview!')}>Toast</button>
7507
+ <ul>{files.map((f) => <li key={f}>{f}</li>)}</ul>
7508
+ </section>
7509
+ </div>
7510
+ );
7511
+ }
7512
+ `,
7513
+ "react/src/webview/panels/dashboard/main.tsx": `import React from 'react';
7514
+ import { createRoot } from 'react-dom/client';
7515
+ import { App } from './App';
7516
+ import '../../styles.css';
7517
+
7518
+ createRoot(document.getElementById('root')!).render(<App />);
7519
+ `,
7520
+ "react/src/webview/styles.css": `:root { color-scheme: light dark; }
7521
+
7522
+ body {
7523
+ margin: 0;
7524
+ padding: 1rem;
7525
+ font-family: var(--vscode-font-family);
7526
+ font-size: var(--vscode-font-size);
7527
+ color: var(--vscode-foreground);
7528
+ background: var(--vscode-editor-background);
7529
+ }
7530
+
7531
+ .app h1 { font-size: 1.25rem; margin: 0 0 1rem; }
7532
+
7533
+ button, input {
7534
+ font: inherit;
7535
+ color: var(--vscode-button-foreground, inherit);
7536
+ background: var(--vscode-button-background, transparent);
7537
+ border: 1px solid var(--vscode-button-border, var(--vscode-input-border, #555));
7538
+ padding: 0.35rem 0.75rem;
7539
+ border-radius: 2px;
7540
+ margin-right: 0.5rem;
7541
+ }
7542
+
7543
+ input {
7544
+ background: var(--vscode-input-background);
7545
+ color: var(--vscode-input-foreground);
7546
+ min-width: 16rem;
7547
+ }
7548
+
7549
+ button:hover { background: var(--vscode-button-hoverBackground); }
7550
+
7551
+ ul { padding-left: 1.25rem; }
7552
+ li { line-height: 1.6; }
7553
+ `,
7554
+ "react/tsconfig.json": `{
7555
+ "compilerOptions": {
7556
+ "target": "ES2022",
7557
+ "module": "ESNext",
7558
+ "moduleResolution": "Bundler",
7559
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7560
+ "jsx": "react-jsx",
7561
+ "strict": true,
7562
+ "esModuleInterop": true,
7563
+ "skipLibCheck": true,
7564
+ "forceConsistentCasingInFileNames": true,
7565
+ "resolveJsonModule": true,
7566
+ "isolatedModules": true,
7567
+ "noEmit": true
7568
+ },
7569
+ "include": ["src"]
7570
+ }
7571
+ `,
7572
+ "react/vite.config.ts": `import { defineConfig } from 'vite';
7573
+ import react from '@vitejs/plugin-react';
7574
+ import * as fs from 'fs';
7575
+ import * as path from 'path';
7576
+
7577
+ const WEBVIEW_ROOT = path.resolve(__dirname, 'src/webview');
7578
+
7579
+ function discoverHtmlEntries(subdir: string): Record<string, string> {
7580
+ const dir = path.join(WEBVIEW_ROOT, subdir);
7581
+ if (!fs.existsSync(dir)) return {};
7582
+ const out: Record<string, string> = {};
7583
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
7584
+ if (!entry.isDirectory()) continue;
7585
+ const html = path.join(dir, entry.name, 'index.html');
7586
+ // Key includes subdir so panel \`dashboard\` and subpanel \`dashboard\` never collide.
7587
+ if (fs.existsSync(html)) out[\`\${subdir}/\${entry.name}\`] = html;
7588
+ }
7589
+ return out;
7590
+ }
7591
+
7592
+ const entries = {
7593
+ ...discoverHtmlEntries('panels'),
7594
+ ...discoverHtmlEntries('subpanels'),
7595
+ };
7596
+
7597
+ export default defineConfig({
7598
+ plugins: [react()],
7599
+ root: WEBVIEW_ROOT,
7600
+ build: {
7601
+ outDir: path.resolve(__dirname, 'dist/webview'),
7602
+ emptyOutDir: true,
7603
+ manifest: 'manifest.json',
7604
+ rollupOptions: {
7605
+ input: entries,
7606
+ output: {
7607
+ entryFileNames: '[name]/index.js',
7608
+ chunkFileNames: 'chunks/[name]-[hash].js',
7609
+ assetFileNames: 'assets/[name]-[hash].[ext]',
7610
+ },
7611
+ },
7612
+ },
7613
+ });
7614
+ `
7615
+ };
7616
+ });
7617
+
3828
7618
  // src/lib/findProject.ts
3829
7619
  function findProjectRoot(start = process.cwd()) {
3830
7620
  let dir = path3.resolve(start);
@@ -3847,87 +7637,61 @@ function findProjectRoot(start = process.cwd()) {
3847
7637
  }
3848
7638
  }
3849
7639
  function findTemplatesRoot(fromFile = process.argv[1] ?? __dirname) {
7640
+ const onDisk = findTemplatesOnDisk(fromFile);
7641
+ if (onDisk)
7642
+ return onDisk;
7643
+ return materializeEmbeddedTemplates();
7644
+ }
7645
+ function findTemplatesOnDisk(fromFile) {
3850
7646
  let dir = path3.dirname(path3.resolve(fromFile));
3851
7647
  const { root } = path3.parse(dir);
3852
- const tried = [];
3853
7648
  while (true) {
3854
7649
  const candidate = path3.join(dir, "templates");
3855
- tried.push(candidate);
3856
- if (fs3.existsSync(candidate))
3857
- return candidate;
7650
+ if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
7651
+ try {
7652
+ if (fs3.readdirSync(candidate).length > 0)
7653
+ return candidate;
7654
+ } catch {}
7655
+ }
3858
7656
  if (dir === root)
3859
- break;
7657
+ return;
3860
7658
  dir = path3.dirname(dir);
3861
7659
  }
3862
- throw new Error(`templates/ directory not found. Looked in:
3863
- ${tried.join(`
3864
- `)}`);
3865
7660
  }
3866
- var fs3, path3, __dirname = "/home/runner/work/vsceasy/vsceasy/src/lib";
7661
+ function materializeEmbeddedTemplates() {
7662
+ if (materializedRoot)
7663
+ return materializedRoot;
7664
+ const keys = Object.keys(TEMPLATE_FILES);
7665
+ if (keys.length === 0) {
7666
+ throw new Error("No templates available: on-disk templates/ not found and no embedded " + "templates were bundled. This is a packaging bug — rebuild with " + "`bun run build` (runs scripts/embedTemplates.ts).");
7667
+ }
7668
+ const hash = crypto.createHash("sha1").update(`${TEMPLATES_VERSION}:${keys.length}:${keys.join("|")}`).digest("hex").slice(0, 12);
7669
+ const dest = path3.join(os.tmpdir(), `vsceasy-templates-${TEMPLATES_VERSION}-${hash}`);
7670
+ const sentinel = path3.join(dest, ".complete");
7671
+ if (fs3.existsSync(sentinel)) {
7672
+ materializedRoot = dest;
7673
+ return dest;
7674
+ }
7675
+ fs3.rmSync(dest, { recursive: true, force: true });
7676
+ fs3.mkdirSync(dest, { recursive: true });
7677
+ for (const rel of keys) {
7678
+ const abs = path3.join(dest, rel);
7679
+ fs3.mkdirSync(path3.dirname(abs), { recursive: true });
7680
+ fs3.writeFileSync(abs, TEMPLATE_FILES[rel]);
7681
+ }
7682
+ fs3.writeFileSync(sentinel, "");
7683
+ materializedRoot = dest;
7684
+ return dest;
7685
+ }
7686
+ var crypto, fs3, os, path3, __dirname = "/home/runner/work/vsceasy/vsceasy/src/lib", materializedRoot;
3867
7687
  var init_findProject = __esm(() => {
7688
+ init_templatesData();
7689
+ crypto = __toESM(require("crypto"));
3868
7690
  fs3 = __toESM(require("fs"));
7691
+ os = __toESM(require("os"));
3869
7692
  path3 = __toESM(require("path"));
3870
7693
  });
3871
7694
 
3872
- // src/commands/create.ts
3873
- function toTitle(s) {
3874
- return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
3875
- }
3876
- var import_cli_maker, path4, createCommand, create_default;
3877
- var init_create = __esm(() => {
3878
- init_scaffold();
3879
- init_findProject();
3880
- import_cli_maker = __toESM(require_dist(), 1);
3881
- path4 = __toESM(require("path"));
3882
- createCommand = {
3883
- name: "create",
3884
- description: "Scaffold a new VS Code extension project",
3885
- params: [
3886
- { name: "name", description: "Extension package name (e.g. my-extension or @scope/my-ext)", required: true, type: import_cli_maker.ParamType.Text },
3887
- { name: "displayName", description: "Human-readable extension name", required: false, type: import_cli_maker.ParamType.Text },
3888
- { name: "description", description: "Short description", required: false, type: import_cli_maker.ParamType.Text },
3889
- { name: "publisher", description: "VS Code publisher id", required: false, type: import_cli_maker.ParamType.Text },
3890
- { name: "ui", description: "UI framework", required: false, type: import_cli_maker.ParamType.List, options: ["react"] },
3891
- { name: "preset", description: "Project preset (minimal = empty extension, full = panel + RPC sample)", required: false, type: import_cli_maker.ParamType.List, options: ["minimal", "full"] },
3892
- { name: "dir", description: "Target directory (defaults to ./<name>)", required: false, type: import_cli_maker.ParamType.Text }
3893
- ],
3894
- action: async (args) => {
3895
- const name = args.name;
3896
- const simpleName = name.replace(/^@[^/]+\//, "");
3897
- const ui = args.ui ?? "react";
3898
- const preset = args.preset ?? "full";
3899
- const targetDir = path4.resolve(process.cwd(), args.dir ?? simpleName);
3900
- try {
3901
- await scaffold({
3902
- name,
3903
- displayName: args.displayName ?? toTitle(simpleName),
3904
- description: args.description ?? `${simpleName} VS Code extension`,
3905
- publisher: args.publisher ?? "your-publisher",
3906
- ui,
3907
- preset,
3908
- targetDir,
3909
- templatesRoot: findTemplatesRoot()
3910
- });
3911
- const rel = path4.relative(process.cwd(), targetDir) || ".";
3912
- console.log(`
3913
- ✓ Created ${name} at ${rel}
3914
- `);
3915
- console.log("Next steps:");
3916
- console.log(` cd ${rel}`);
3917
- console.log(" bun install");
3918
- console.log(" bun run launch # builds + opens Extension Development Host");
3919
- console.log(" # or `bun run dev` + F5 inside VS Code for watch mode\n");
3920
- } catch (err) {
3921
- console.error(`
3922
- ✗ Failed to scaffold: ${err.message}
3923
- `);
3924
- process.exitCode = 1;
3925
- }
3926
- }
3927
- };
3928
- create_default = createCommand;
3929
- });
3930
-
3931
7695
  // src/lib/interactive.ts
3932
7696
  function rl() {
3933
7697
  return readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -3935,11 +7699,11 @@ function rl() {
3935
7699
  async function askText(question, defaultValue) {
3936
7700
  const r = rl();
3937
7701
  const hint = defaultValue ? ` ${DIM}[${defaultValue}]${RST}` : "";
3938
- return new Promise((resolve3) => {
7702
+ return new Promise((resolve2) => {
3939
7703
  r.question(`${CYAN}?${RST} ${question}${hint}: `, (ans) => {
3940
7704
  r.close();
3941
7705
  const v = (ans ?? "").trim();
3942
- resolve3(v.length ? v : defaultValue ?? "");
7706
+ resolve2(v.length ? v : defaultValue ?? "");
3943
7707
  });
3944
7708
  });
3945
7709
  }
@@ -3980,7 +7744,7 @@ async function selectArrow(question, options, opts) {
3980
7744
  if (index >= v.length)
3981
7745
  index = Math.max(0, v.length - 1);
3982
7746
  };
3983
- return new Promise((resolve3, reject) => {
7747
+ return new Promise((resolve2, reject) => {
3984
7748
  const stdin = process.stdin;
3985
7749
  const stdout = process.stdout;
3986
7750
  let lastLines = 0;
@@ -4057,7 +7821,7 @@ async function selectArrow(question, options, opts) {
4057
7821
  cleanup();
4058
7822
  stdout.write(`${CYAN}?${RST} ${BOLD}${question}${RST} ${GREEN}${v[index].label}${RST}
4059
7823
  `);
4060
- resolve3(v[index].value);
7824
+ resolve2(v[index].value);
4061
7825
  return;
4062
7826
  }
4063
7827
  if (data === "" || data === "\b") {
@@ -4126,6 +7890,132 @@ var init_interactive = __esm(() => {
4126
7890
  style = { DIM, BOLD, CYAN, GREEN, YELLOW, INV, RST };
4127
7891
  });
4128
7892
 
7893
+ // src/commands/create.ts
7894
+ function toTitle(s) {
7895
+ return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
7896
+ }
7897
+ function toBool(v) {
7898
+ if (v === undefined || v === null || v === "")
7899
+ return;
7900
+ if (typeof v === "boolean")
7901
+ return v;
7902
+ const s = String(v).trim().toLowerCase();
7903
+ if (["true", "1", "yes", "y"].includes(s))
7904
+ return true;
7905
+ if (["false", "0", "no", "n"].includes(s))
7906
+ return false;
7907
+ return;
7908
+ }
7909
+ function which(cmd) {
7910
+ const r = import_child_process.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
7911
+ return r.status === 0;
7912
+ }
7913
+ function initGit(cwd) {
7914
+ const r = import_child_process.spawnSync("git", ["init"], { cwd, stdio: "inherit" });
7915
+ if (r.status === 0) {
7916
+ console.log("✓ Initialized git repository");
7917
+ return true;
7918
+ }
7919
+ console.warn("! Could not initialize git repository");
7920
+ return false;
7921
+ }
7922
+ function runInstall(pm, cwd) {
7923
+ console.log(`
7924
+ Installing dependencies with ${pm}...
7925
+ `);
7926
+ const r = import_child_process.spawnSync(pm, ["install"], { cwd, stdio: "inherit" });
7927
+ if (r.status === 0) {
7928
+ console.log(`
7929
+ ✓ Dependencies installed`);
7930
+ return true;
7931
+ }
7932
+ console.warn(`
7933
+ ! ${pm} install failed — run it manually`);
7934
+ return false;
7935
+ }
7936
+ var import_cli_maker, path4, import_child_process, createCommand, create_default;
7937
+ var init_create = __esm(() => {
7938
+ init_scaffold();
7939
+ init_findProject();
7940
+ init_interactive();
7941
+ import_cli_maker = __toESM(require_dist(), 1);
7942
+ path4 = __toESM(require("path"));
7943
+ import_child_process = require("child_process");
7944
+ createCommand = {
7945
+ name: "create",
7946
+ description: "Scaffold a new VS Code extension project",
7947
+ params: [
7948
+ { name: "name", description: "Extension package name (e.g. my-extension or @scope/my-ext)", required: true, type: import_cli_maker.ParamType.Text },
7949
+ { name: "displayName", description: "Human-readable extension name", required: false, type: import_cli_maker.ParamType.Text },
7950
+ { name: "description", description: "Short description", required: false, type: import_cli_maker.ParamType.Text },
7951
+ { name: "publisher", description: "VS Code publisher id", required: false, type: import_cli_maker.ParamType.Text },
7952
+ { name: "ui", description: "UI framework", required: false, type: import_cli_maker.ParamType.List, options: ["react"] },
7953
+ { name: "preset", description: "Project preset (minimal = empty extension, full = panel + RPC sample)", required: false, type: import_cli_maker.ParamType.List, options: ["minimal", "full"] },
7954
+ { name: "dir", description: "Target directory (defaults to ./<name>)", required: false, type: import_cli_maker.ParamType.Text },
7955
+ { name: "git", description: "Initialize a git repository (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean },
7956
+ { name: "install", description: "Install dependencies after scaffolding (skips the prompt)", required: false, type: import_cli_maker.ParamType.Boolean }
7957
+ ],
7958
+ action: async (args) => {
7959
+ const name = args.name;
7960
+ const simpleName = name.replace(/^@[^/]+\//, "");
7961
+ const ui = args.ui ?? "react";
7962
+ const preset = args.preset ?? "full";
7963
+ const targetDir = path4.resolve(process.cwd(), args.dir ?? simpleName);
7964
+ try {
7965
+ await scaffold({
7966
+ name,
7967
+ displayName: args.displayName ?? toTitle(simpleName),
7968
+ description: args.description ?? `${simpleName} VS Code extension`,
7969
+ publisher: args.publisher ?? "your-publisher",
7970
+ ui,
7971
+ preset,
7972
+ targetDir,
7973
+ templatesRoot: findTemplatesRoot()
7974
+ });
7975
+ const rel = path4.relative(process.cwd(), targetDir) || ".";
7976
+ console.log(`
7977
+ ✓ Created ${name} at ${rel}
7978
+ `);
7979
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
7980
+ const gitFlag = toBool(args.git);
7981
+ const installFlag = toBool(args.install);
7982
+ const wantGit = gitFlag ?? (interactive ? await confirm("Initialize a git repository?", true) : false);
7983
+ if (wantGit) {
7984
+ if (which("git"))
7985
+ initGit(targetDir);
7986
+ else
7987
+ console.warn("! git not found — skipping repository init");
7988
+ }
7989
+ let pm = null;
7990
+ const wantInstall = installFlag ?? (interactive ? await confirm("Install dependencies?", true) : false);
7991
+ let installed = false;
7992
+ if (wantInstall) {
7993
+ pm = which("bun") ? "bun" : which("npm") ? "npm" : null;
7994
+ if (pm)
7995
+ installed = runInstall(pm, targetDir);
7996
+ else
7997
+ console.warn("! No package manager (bun/npm) found — skipping install");
7998
+ }
7999
+ const run = pm ?? "bun";
8000
+ console.log(`
8001
+ Next steps:`);
8002
+ console.log(` cd ${rel}`);
8003
+ if (!installed)
8004
+ console.log(` ${run} install`);
8005
+ console.log(` ${run} run launch # builds + opens Extension Development Host`);
8006
+ console.log(` # or \`${run} run dev\` + F5 inside VS Code for watch mode
8007
+ `);
8008
+ } catch (err) {
8009
+ console.error(`
8010
+ ✗ Failed to scaffold: ${err.message}
8011
+ `);
8012
+ process.exitCode = 1;
8013
+ }
8014
+ }
8015
+ };
8016
+ create_default = createCommand;
8017
+ });
8018
+
4129
8019
  // src/lib/validate.ts
4130
8020
  function assertId(field, value) {
4131
8021
  const trimmed = (value ?? "").trim();
@@ -4293,18 +8183,18 @@ function appendApi(apiPath, apiName, body, created, modified, skipped) {
4293
8183
  }
4294
8184
  function runGen(cwd) {
4295
8185
  const tryRun = (cmd, args) => {
4296
- const r = import_child_process.spawnSync(cmd, args, { cwd, stdio: "inherit" });
8186
+ const r = import_child_process2.spawnSync(cmd, args, { cwd, stdio: "inherit" });
4297
8187
  return r.status === 0;
4298
8188
  };
4299
- if (which("bun") && tryRun("bun", ["run", "gen"]))
8189
+ if (which2("bun") && tryRun("bun", ["run", "gen"]))
4300
8190
  return true;
4301
- if (which("npm") && tryRun("npm", ["run", "gen"]))
8191
+ if (which2("npm") && tryRun("npm", ["run", "gen"]))
4302
8192
  return true;
4303
8193
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` (or `npm run gen`) manually to wire up the new panel.\n');
4304
8194
  return false;
4305
8195
  }
4306
- function which(cmd) {
4307
- const r = import_child_process.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
8196
+ function which2(cmd) {
8197
+ const r = import_child_process2.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
4308
8198
  return r.status === 0;
4309
8199
  }
4310
8200
  function normalizeCamel(s) {
@@ -4313,14 +8203,14 @@ function normalizeCamel(s) {
4313
8203
  return "";
4314
8204
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
4315
8205
  }
4316
- var fs6, path7, import_child_process, PANEL_TEMPLATES, API_BODY, TEMPLATE_VARS;
8206
+ var fs6, path7, import_child_process2, PANEL_TEMPLATES, API_BODY, TEMPLATE_VARS;
4317
8207
  var init_add2 = __esm(() => {
4318
8208
  init_scaffold();
4319
8209
  init_validate();
4320
8210
  init_add();
4321
8211
  fs6 = __toESM(require("fs"));
4322
8212
  path7 = __toESM(require("path"));
4323
- import_child_process = require("child_process");
8213
+ import_child_process2 = require("child_process");
4324
8214
  PANEL_TEMPLATES = ["blank", "form", "list", "dashboard"];
4325
8215
  API_BODY = {
4326
8216
  blank: "",
@@ -4387,18 +8277,18 @@ function editMenu(opts) {
4387
8277
  }
4388
8278
  function runGen2(cwd) {
4389
8279
  const tryRun = (cmd, args) => {
4390
- const r = import_child_process2.spawnSync(cmd, args, { cwd, stdio: "inherit" });
8280
+ const r = import_child_process3.spawnSync(cmd, args, { cwd, stdio: "inherit" });
4391
8281
  return r.status === 0;
4392
8282
  };
4393
- if (which2("bun") && tryRun("bun", ["run", "gen"]))
8283
+ if (which3("bun") && tryRun("bun", ["run", "gen"]))
4394
8284
  return true;
4395
- if (which2("npm") && tryRun("npm", ["run", "gen"]))
8285
+ if (which3("npm") && tryRun("npm", ["run", "gen"]))
4396
8286
  return true;
4397
8287
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
4398
8288
  return false;
4399
8289
  }
4400
- function which2(cmd) {
4401
- const r = import_child_process2.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
8290
+ function which3(cmd) {
8291
+ const r = import_child_process3.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
4402
8292
  return r.status === 0;
4403
8293
  }
4404
8294
  function insertItem(src, item) {
@@ -4570,11 +8460,11 @@ function findMatching(src, openIdx) {
4570
8460
  }
4571
8461
  return -1;
4572
8462
  }
4573
- var fs7, path8, import_child_process2;
8463
+ var fs7, path8, import_child_process3;
4574
8464
  var init_edit = __esm(() => {
4575
8465
  fs7 = __toESM(require("fs"));
4576
8466
  path8 = __toESM(require("path"));
4577
- import_child_process2 = require("child_process");
8467
+ import_child_process3 = require("child_process");
4578
8468
  });
4579
8469
 
4580
8470
  // src/lib/command/add.ts
@@ -4630,18 +8520,18 @@ function addCommand(opts) {
4630
8520
  }
4631
8521
  function runGen3(cwd) {
4632
8522
  const tryRun = (cmd, args) => {
4633
- const r = import_child_process3.spawnSync(cmd, args, { cwd, stdio: "inherit" });
8523
+ const r = import_child_process4.spawnSync(cmd, args, { cwd, stdio: "inherit" });
4634
8524
  return r.status === 0;
4635
8525
  };
4636
- if (which3("bun") && tryRun("bun", ["run", "gen"]))
8526
+ if (which4("bun") && tryRun("bun", ["run", "gen"]))
4637
8527
  return true;
4638
- if (which3("npm") && tryRun("npm", ["run", "gen"]))
8528
+ if (which4("npm") && tryRun("npm", ["run", "gen"]))
4639
8529
  return true;
4640
8530
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
4641
8531
  return false;
4642
8532
  }
4643
- function which3(cmd) {
4644
- const r = import_child_process3.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
8533
+ function which4(cmd) {
8534
+ const r = import_child_process4.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
4645
8535
  return r.status === 0;
4646
8536
  }
4647
8537
  function escapeQuotes(s) {
@@ -4653,7 +8543,7 @@ function normalizeCamel2(s) {
4653
8543
  return "";
4654
8544
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
4655
8545
  }
4656
- var fs8, path9, import_child_process3;
8546
+ var fs8, path9, import_child_process4;
4657
8547
  var init_add3 = __esm(() => {
4658
8548
  init_scaffold();
4659
8549
  init_edit();
@@ -4661,7 +8551,7 @@ var init_add3 = __esm(() => {
4661
8551
  init_config();
4662
8552
  fs8 = __toESM(require("fs"));
4663
8553
  path9 = __toESM(require("path"));
4664
- import_child_process3 = require("child_process");
8554
+ import_child_process4 = require("child_process");
4665
8555
  });
4666
8556
 
4667
8557
  // src/lib/helper/add.ts
@@ -6190,24 +10080,24 @@ function filesEqual(a, b) {
6190
10080
  }
6191
10081
  function runGen4(cwd) {
6192
10082
  const tryRun = (cmd, args) => {
6193
- const r = import_child_process4.spawnSync(cmd, args, { cwd, stdio: "inherit" });
10083
+ const r = import_child_process5.spawnSync(cmd, args, { cwd, stdio: "inherit" });
6194
10084
  return r.status === 0;
6195
10085
  };
6196
- if (which4("bun") && tryRun("bun", ["run", "gen"]))
10086
+ if (which5("bun") && tryRun("bun", ["run", "gen"]))
6197
10087
  return true;
6198
- if (which4("npm") && tryRun("npm", ["run", "gen"]))
10088
+ if (which5("npm") && tryRun("npm", ["run", "gen"]))
6199
10089
  return true;
6200
10090
  return false;
6201
10091
  }
6202
- function which4(cmd) {
6203
- const r = import_child_process4.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
10092
+ function which5(cmd) {
10093
+ const r = import_child_process5.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
6204
10094
  return r.status === 0;
6205
10095
  }
6206
- var fs13, path15, import_child_process4, SYNC_PATHS;
10096
+ var fs13, path15, import_child_process5, SYNC_PATHS;
6207
10097
  var init_upgrade = __esm(() => {
6208
10098
  fs13 = __toESM(require("fs"));
6209
10099
  path15 = __toESM(require("path"));
6210
- import_child_process4 = require("child_process");
10100
+ import_child_process5 = require("child_process");
6211
10101
  SYNC_PATHS = [
6212
10102
  "src/shared/vsceasy/define.ts",
6213
10103
  "src/shared/vsceasy/bootstrap.ts",
@@ -6434,18 +10324,18 @@ function addMenu(opts) {
6434
10324
  }
6435
10325
  function runGen5(cwd) {
6436
10326
  const tryRun = (cmd, args) => {
6437
- const r = import_child_process5.spawnSync(cmd, args, { cwd, stdio: "inherit" });
10327
+ const r = import_child_process6.spawnSync(cmd, args, { cwd, stdio: "inherit" });
6438
10328
  return r.status === 0;
6439
10329
  };
6440
- if (which5("bun") && tryRun("bun", ["run", "gen"]))
10330
+ if (which6("bun") && tryRun("bun", ["run", "gen"]))
6441
10331
  return true;
6442
- if (which5("npm") && tryRun("npm", ["run", "gen"]))
10332
+ if (which6("npm") && tryRun("npm", ["run", "gen"]))
6443
10333
  return true;
6444
10334
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
6445
10335
  return false;
6446
10336
  }
6447
- function which5(cmd) {
6448
- const r = import_child_process5.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
10337
+ function which6(cmd) {
10338
+ const r = import_child_process6.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
6449
10339
  return r.status === 0;
6450
10340
  }
6451
10341
  function normalizeCamel4(s) {
@@ -6454,14 +10344,14 @@ function normalizeCamel4(s) {
6454
10344
  return "";
6455
10345
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
6456
10346
  }
6457
- var fs14, path17, import_child_process5;
10347
+ var fs14, path17, import_child_process6;
6458
10348
  var init_add7 = __esm(() => {
6459
10349
  init_scaffold();
6460
10350
  init_validate();
6461
10351
  init_config();
6462
10352
  fs14 = __toESM(require("fs"));
6463
10353
  path17 = __toESM(require("path"));
6464
- import_child_process5 = require("child_process");
10354
+ import_child_process6 = require("child_process");
6465
10355
  });
6466
10356
 
6467
10357
  // src/commands/menu/add.ts
@@ -7005,25 +10895,25 @@ function buildSnippet(method, argNames) {
7005
10895
  }
7006
10896
  function runGen6(cwd) {
7007
10897
  const tryRun = (cmd, args) => {
7008
- const r = import_child_process6.spawnSync(cmd, args, { cwd, stdio: "inherit" });
10898
+ const r = import_child_process7.spawnSync(cmd, args, { cwd, stdio: "inherit" });
7009
10899
  return r.status === 0;
7010
10900
  };
7011
- if (which6("bun") && tryRun("bun", ["run", "gen"]))
10901
+ if (which7("bun") && tryRun("bun", ["run", "gen"]))
7012
10902
  return true;
7013
- if (which6("npm") && tryRun("npm", ["run", "gen"]))
10903
+ if (which7("npm") && tryRun("npm", ["run", "gen"]))
7014
10904
  return true;
7015
10905
  return false;
7016
10906
  }
7017
- function which6(cmd) {
7018
- const r = import_child_process6.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
10907
+ function which7(cmd) {
10908
+ const r = import_child_process7.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
7019
10909
  return r.status === 0;
7020
10910
  }
7021
- var fs17, path21, import_child_process6;
10911
+ var fs17, path21, import_child_process7;
7022
10912
  var init_add10 = __esm(() => {
7023
10913
  init_validate();
7024
10914
  fs17 = __toESM(require("fs"));
7025
10915
  path21 = __toESM(require("path"));
7026
- import_child_process6 = require("child_process");
10916
+ import_child_process7 = require("child_process");
7027
10917
  });
7028
10918
 
7029
10919
  // src/commands/rpc/add.ts
@@ -7178,17 +11068,17 @@ function asBacktickString(s) {
7178
11068
  }
7179
11069
  function runGen7(cwd) {
7180
11070
  const tryRun = (cmd, args) => {
7181
- const r = import_child_process7.spawnSync(cmd, args, { cwd, stdio: "inherit" });
11071
+ const r = import_child_process8.spawnSync(cmd, args, { cwd, stdio: "inherit" });
7182
11072
  return r.status === 0;
7183
11073
  };
7184
- if (which7("bun") && tryRun("bun", ["run", "gen"]))
11074
+ if (which8("bun") && tryRun("bun", ["run", "gen"]))
7185
11075
  return true;
7186
- if (which7("npm") && tryRun("npm", ["run", "gen"]))
11076
+ if (which8("npm") && tryRun("npm", ["run", "gen"]))
7187
11077
  return true;
7188
11078
  return false;
7189
11079
  }
7190
- function which7(cmd) {
7191
- const r = import_child_process7.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
11080
+ function which8(cmd) {
11081
+ const r = import_child_process8.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
7192
11082
  return r.status === 0;
7193
11083
  }
7194
11084
  function normalizeCamel5(s) {
@@ -7197,14 +11087,14 @@ function normalizeCamel5(s) {
7197
11087
  return "";
7198
11088
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
7199
11089
  }
7200
- var fs18, path23, import_child_process7;
11090
+ var fs18, path23, import_child_process8;
7201
11091
  var init_add12 = __esm(() => {
7202
11092
  init_scaffold();
7203
11093
  init_add3();
7204
11094
  init_validate();
7205
11095
  fs18 = __toESM(require("fs"));
7206
11096
  path23 = __toESM(require("path"));
7207
- import_child_process7 = require("child_process");
11097
+ import_child_process8 = require("child_process");
7208
11098
  });
7209
11099
 
7210
11100
  // src/commands/statusBar/add.ts
@@ -7481,18 +11371,18 @@ function appendApi2(apiPath, apiName, created, modified, skipped) {
7481
11371
  }
7482
11372
  function runGen8(cwd) {
7483
11373
  const tryRun = (cmd, args) => {
7484
- const r = import_child_process8.spawnSync(cmd, args, { cwd, stdio: "inherit" });
11374
+ const r = import_child_process9.spawnSync(cmd, args, { cwd, stdio: "inherit" });
7485
11375
  return r.status === 0;
7486
11376
  };
7487
- if (which8("bun") && tryRun("bun", ["run", "gen"]))
11377
+ if (which9("bun") && tryRun("bun", ["run", "gen"]))
7488
11378
  return true;
7489
- if (which8("npm") && tryRun("npm", ["run", "gen"]))
11379
+ if (which9("npm") && tryRun("npm", ["run", "gen"]))
7490
11380
  return true;
7491
11381
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
7492
11382
  return false;
7493
11383
  }
7494
- function which8(cmd) {
7495
- const r = import_child_process8.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
11384
+ function which9(cmd) {
11385
+ const r = import_child_process9.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" });
7496
11386
  return r.status === 0;
7497
11387
  }
7498
11388
  function normalizeCamel6(s) {
@@ -7501,13 +11391,13 @@ function normalizeCamel6(s) {
7501
11391
  return "";
7502
11392
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
7503
11393
  }
7504
- var fs19, path25, import_child_process8;
11394
+ var fs19, path25, import_child_process9;
7505
11395
  var init_add14 = __esm(() => {
7506
11396
  init_scaffold();
7507
11397
  init_validate();
7508
11398
  fs19 = __toESM(require("fs"));
7509
11399
  path25 = __toESM(require("path"));
7510
- import_child_process8 = require("child_process");
11400
+ import_child_process9 = require("child_process");
7511
11401
  });
7512
11402
 
7513
11403
  // src/commands/subpanel/add.ts
@@ -7613,16 +11503,16 @@ function addTreeView(opts) {
7613
11503
  return { created: [target], genRan };
7614
11504
  }
7615
11505
  function runGen9(cwd) {
7616
- const tryRun = (cmd, args) => import_child_process9.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
7617
- if (which9("bun") && tryRun("bun", ["run", "gen"]))
11506
+ const tryRun = (cmd, args) => import_child_process10.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
11507
+ if (which10("bun") && tryRun("bun", ["run", "gen"]))
7618
11508
  return true;
7619
- if (which9("npm") && tryRun("npm", ["run", "gen"]))
11509
+ if (which10("npm") && tryRun("npm", ["run", "gen"]))
7620
11510
  return true;
7621
11511
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
7622
11512
  return false;
7623
11513
  }
7624
- function which9(cmd) {
7625
- return import_child_process9.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
11514
+ function which10(cmd) {
11515
+ return import_child_process10.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
7626
11516
  }
7627
11517
  function normalizeCamel7(s) {
7628
11518
  const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
@@ -7633,13 +11523,13 @@ function normalizeCamel7(s) {
7633
11523
  function pascal3(s) {
7634
11524
  return s.charAt(0).toUpperCase() + s.slice(1);
7635
11525
  }
7636
- var fs20, path27, import_child_process9;
11526
+ var fs20, path27, import_child_process10;
7637
11527
  var init_add16 = __esm(() => {
7638
11528
  init_scaffold();
7639
11529
  init_validate();
7640
11530
  fs20 = __toESM(require("fs"));
7641
11531
  path27 = __toESM(require("path"));
7642
- import_child_process9 = require("child_process");
11532
+ import_child_process10 = require("child_process");
7643
11533
  });
7644
11534
 
7645
11535
  // src/commands/treeView/add.ts
@@ -7851,19 +11741,19 @@ function publishInit(opts) {
7851
11741
  `);
7852
11742
  let dryPackOk = null;
7853
11743
  if (opts.runDryPack !== false) {
7854
- const r = import_child_process10.spawnSync("npx", ["--yes", "@vscode/vsce", "ls"], { cwd: opts.projectRoot, stdio: "inherit" });
11744
+ const r = import_child_process11.spawnSync("npx", ["--yes", "@vscode/vsce", "ls"], { cwd: opts.projectRoot, stdio: "inherit" });
7855
11745
  dryPackOk = r.status === 0;
7856
11746
  if (!dryPackOk)
7857
11747
  warnings.push("`vsce ls` exited non-zero — inspect output above.");
7858
11748
  }
7859
11749
  return { created, pkgUpdated, warnings, dryPackOk };
7860
11750
  }
7861
- var fs22, path31, import_child_process10;
11751
+ var fs22, path31, import_child_process11;
7862
11752
  var init_init2 = __esm(() => {
7863
11753
  init_scaffold();
7864
11754
  fs22 = __toESM(require("fs"));
7865
11755
  path31 = __toESM(require("path"));
7866
- import_child_process10 = require("child_process");
11756
+ import_child_process11 = require("child_process");
7867
11757
  });
7868
11758
 
7869
11759
  // src/commands/publish/init.ts
@@ -8027,16 +11917,16 @@ function escape(s) {
8027
11917
  return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
8028
11918
  }
8029
11919
  function runGen10(cwd) {
8030
- const tryRun = (cmd, args) => import_child_process11.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
8031
- if (which10("bun") && tryRun("bun", ["run", "gen"]))
11920
+ const tryRun = (cmd, args) => import_child_process12.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
11921
+ if (which11("bun") && tryRun("bun", ["run", "gen"]))
8032
11922
  return true;
8033
- if (which10("npm") && tryRun("npm", ["run", "gen"]))
11923
+ if (which11("npm") && tryRun("npm", ["run", "gen"]))
8034
11924
  return true;
8035
11925
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
8036
11926
  return false;
8037
11927
  }
8038
- function which10(cmd) {
8039
- return import_child_process11.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
11928
+ function which11(cmd) {
11929
+ return import_child_process12.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
8040
11930
  }
8041
11931
  function normalizeCamel8(s) {
8042
11932
  const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
@@ -8044,13 +11934,13 @@ function normalizeCamel8(s) {
8044
11934
  return "";
8045
11935
  return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
8046
11936
  }
8047
- var fs23, path34, import_child_process11;
11937
+ var fs23, path34, import_child_process12;
8048
11938
  var init_add19 = __esm(() => {
8049
11939
  init_scaffold();
8050
11940
  init_validate();
8051
11941
  fs23 = __toESM(require("fs"));
8052
11942
  path34 = __toESM(require("path"));
8053
- import_child_process11 = require("child_process");
11943
+ import_child_process12 = require("child_process");
8054
11944
  });
8055
11945
 
8056
11946
  // src/commands/job/add.ts
@@ -8779,18 +12669,18 @@ function camelLower(s) {
8779
12669
  return s.charAt(0).toLowerCase() + s.slice(1);
8780
12670
  }
8781
12671
  function runGen11(cwd) {
8782
- const tryRun = (cmd, args) => import_child_process12.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
8783
- if (which11("bun") && tryRun("bun", ["run", "gen"]))
12672
+ const tryRun = (cmd, args) => import_child_process13.spawnSync(cmd, args, { cwd, stdio: "inherit" }).status === 0;
12673
+ if (which12("bun") && tryRun("bun", ["run", "gen"]))
8784
12674
  return true;
8785
- if (which11("npm") && tryRun("npm", ["run", "gen"]))
12675
+ if (which12("npm") && tryRun("npm", ["run", "gen"]))
8786
12676
  return true;
8787
12677
  console.warn('\n! Could not run "gen" automatically. Run `bun run gen` manually.\n');
8788
12678
  return false;
8789
12679
  }
8790
- function which11(cmd) {
8791
- return import_child_process12.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
12680
+ function which12(cmd) {
12681
+ return import_child_process13.spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore" }).status === 0;
8792
12682
  }
8793
- var fs27, path41, import_child_process12;
12683
+ var fs27, path41, import_child_process13;
8794
12684
  var init_add22 = __esm(() => {
8795
12685
  init_scaffold();
8796
12686
  init_validate();
@@ -8800,7 +12690,7 @@ var init_add22 = __esm(() => {
8800
12690
  init_edit();
8801
12691
  fs27 = __toESM(require("fs"));
8802
12692
  path41 = __toESM(require("path"));
8803
- import_child_process12 = require("child_process");
12693
+ import_child_process13 = require("child_process");
8804
12694
  });
8805
12695
 
8806
12696
  // src/commands/crud/add.ts
@@ -9012,7 +12902,7 @@ var init_cli = __esm(() => {
9012
12902
  import_cli_maker20 = __toESM(require_dist(), 1);
9013
12903
  cli = new import_cli_maker20.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
9014
12904
  interactive: true,
9015
- version: "0.1.4",
12905
+ version: "0.1.6",
9016
12906
  introAnimation: {
9017
12907
  enabled: true,
9018
12908
  preset: "retro-space",