@vsceasy/cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +3877 -48
- package/dist/index.js +3840 -11
- package/dist/lib/findProject.d.ts +13 -2
- package/dist/lib/templatesData.d.ts +2 -0
- package/dist/lib/wizard/run.d.ts +1 -1
- package/package.json +4 -2
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.5", 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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);
|
|
@@ -3846,20 +7636,59 @@ function findProjectRoot(start = process.cwd()) {
|
|
|
3846
7636
|
dir = path3.dirname(dir);
|
|
3847
7637
|
}
|
|
3848
7638
|
}
|
|
3849
|
-
function findTemplatesRoot(fromFile = __dirname) {
|
|
3850
|
-
const
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
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) {
|
|
7646
|
+
let dir = path3.dirname(path3.resolve(fromFile));
|
|
7647
|
+
const { root } = path3.parse(dir);
|
|
7648
|
+
while (true) {
|
|
7649
|
+
const candidate = path3.join(dir, "templates");
|
|
7650
|
+
if (fs3.existsSync(candidate) && fs3.statSync(candidate).isDirectory()) {
|
|
7651
|
+
try {
|
|
7652
|
+
if (fs3.readdirSync(candidate).length > 0)
|
|
7653
|
+
return candidate;
|
|
7654
|
+
} catch {}
|
|
7655
|
+
}
|
|
7656
|
+
if (dir === root)
|
|
7657
|
+
return;
|
|
7658
|
+
dir = path3.dirname(dir);
|
|
7659
|
+
}
|
|
3859
7660
|
}
|
|
3860
|
-
|
|
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;
|
|
3861
7687
|
var init_findProject = __esm(() => {
|
|
7688
|
+
init_templatesData();
|
|
7689
|
+
crypto = __toESM(require("crypto"));
|
|
3862
7690
|
fs3 = __toESM(require("fs"));
|
|
7691
|
+
os = __toESM(require("os"));
|
|
3863
7692
|
path3 = __toESM(require("path"));
|
|
3864
7693
|
});
|
|
3865
7694
|
|
|
@@ -3867,7 +7696,7 @@ var init_findProject = __esm(() => {
|
|
|
3867
7696
|
function toTitle(s) {
|
|
3868
7697
|
return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
3869
7698
|
}
|
|
3870
|
-
var import_cli_maker, path4,
|
|
7699
|
+
var import_cli_maker, path4, createCommand, create_default;
|
|
3871
7700
|
var init_create = __esm(() => {
|
|
3872
7701
|
init_scaffold();
|
|
3873
7702
|
init_findProject();
|
|
@@ -3900,7 +7729,7 @@ var init_create = __esm(() => {
|
|
|
3900
7729
|
ui,
|
|
3901
7730
|
preset,
|
|
3902
7731
|
targetDir,
|
|
3903
|
-
templatesRoot: findTemplatesRoot(
|
|
7732
|
+
templatesRoot: findTemplatesRoot()
|
|
3904
7733
|
});
|
|
3905
7734
|
const rel = path4.relative(process.cwd(), targetDir) || ".";
|
|
3906
7735
|
console.log(`
|
|
@@ -4821,7 +8650,7 @@ function parseFieldLine(raw) {
|
|
|
4821
8650
|
|
|
4822
8651
|
// src/lib/wizard/run.ts
|
|
4823
8652
|
async function runWizard(opts = {}) {
|
|
4824
|
-
const templatesRoot = opts.templatesRoot ?? findTemplatesRoot(
|
|
8653
|
+
const templatesRoot = opts.templatesRoot ?? findTemplatesRoot();
|
|
4825
8654
|
const cwd = opts.cwd ?? process.cwd();
|
|
4826
8655
|
let projectRoot = null;
|
|
4827
8656
|
try {
|
|
@@ -5059,7 +8888,7 @@ function hintGen(genRan) {
|
|
|
5059
8888
|
function toTitle2(s) {
|
|
5060
8889
|
return s.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
|
|
5061
8890
|
}
|
|
5062
|
-
var path13,
|
|
8891
|
+
var path13, GREEN2, DIM2, BOLD2, RST2, CYAN2, YELLOW2;
|
|
5063
8892
|
var init_run = __esm(() => {
|
|
5064
8893
|
init_interactive();
|
|
5065
8894
|
init_findProject();
|
|
@@ -5075,7 +8904,7 @@ var init_run = __esm(() => {
|
|
|
5075
8904
|
});
|
|
5076
8905
|
|
|
5077
8906
|
// src/commands/wizard.ts
|
|
5078
|
-
var
|
|
8907
|
+
var wizardCommand2, wizard_default;
|
|
5079
8908
|
var init_wizard = __esm(() => {
|
|
5080
8909
|
init_run();
|
|
5081
8910
|
init_findProject();
|
|
@@ -5085,7 +8914,7 @@ var init_wizard = __esm(() => {
|
|
|
5085
8914
|
params: [],
|
|
5086
8915
|
action: async () => {
|
|
5087
8916
|
try {
|
|
5088
|
-
await runWizard({ templatesRoot: findTemplatesRoot(
|
|
8917
|
+
await runWizard({ templatesRoot: findTemplatesRoot() });
|
|
5089
8918
|
} catch (err) {
|
|
5090
8919
|
console.error(`
|
|
5091
8920
|
✗ ${err.message}
|
|
@@ -6231,7 +10060,7 @@ function labelFor(status) {
|
|
|
6231
10060
|
return "TEMPLATE MISSING";
|
|
6232
10061
|
}
|
|
6233
10062
|
}
|
|
6234
|
-
var import_cli_maker3,
|
|
10063
|
+
var import_cli_maker3, ICONS2, COLORS2, upgradeCommand, upgrade_default;
|
|
6235
10064
|
var init_upgrade2 = __esm(() => {
|
|
6236
10065
|
init_upgrade();
|
|
6237
10066
|
init_findProject();
|
|
@@ -6280,7 +10109,7 @@ var init_upgrade2 = __esm(() => {
|
|
|
6280
10109
|
action: async (args) => {
|
|
6281
10110
|
try {
|
|
6282
10111
|
const projectRoot = findProjectRoot();
|
|
6283
|
-
const templatesRoot = findTemplatesRoot(
|
|
10112
|
+
const templatesRoot = findTemplatesRoot();
|
|
6284
10113
|
const apply = args.apply === true || args.apply === "true";
|
|
6285
10114
|
const result = upgrade({
|
|
6286
10115
|
projectRoot,
|
|
@@ -6337,7 +10166,7 @@ ${COLORS2.bold}vsceasy upgrade${COLORS2.reset} ${COLORS2.dim}— ${projectRoot}$
|
|
|
6337
10166
|
});
|
|
6338
10167
|
|
|
6339
10168
|
// src/commands/panel/add.ts
|
|
6340
|
-
var import_cli_maker4, path16,
|
|
10169
|
+
var import_cli_maker4, path16, addPanelCommand, add_default;
|
|
6341
10170
|
var init_add6 = __esm(() => {
|
|
6342
10171
|
init_add2();
|
|
6343
10172
|
init_findProject();
|
|
@@ -6367,7 +10196,7 @@ var init_add6 = __esm(() => {
|
|
|
6367
10196
|
action: async (args) => {
|
|
6368
10197
|
try {
|
|
6369
10198
|
const projectRoot = findProjectRoot();
|
|
6370
|
-
const templatesRoot = findTemplatesRoot(
|
|
10199
|
+
const templatesRoot = findTemplatesRoot();
|
|
6371
10200
|
const result = addPanel({
|
|
6372
10201
|
name: args.name,
|
|
6373
10202
|
title: args.title,
|
|
@@ -6464,7 +10293,7 @@ function defaultTitle(name) {
|
|
|
6464
10293
|
return "";
|
|
6465
10294
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
6466
10295
|
}
|
|
6467
|
-
var import_cli_maker5, path18,
|
|
10296
|
+
var import_cli_maker5, path18, addMenuCommand, add_default2;
|
|
6468
10297
|
var init_add8 = __esm(() => {
|
|
6469
10298
|
init_add7();
|
|
6470
10299
|
init_findProject();
|
|
@@ -6493,7 +10322,7 @@ var init_add8 = __esm(() => {
|
|
|
6493
10322
|
action: async (args) => {
|
|
6494
10323
|
try {
|
|
6495
10324
|
const projectRoot = findProjectRoot();
|
|
6496
|
-
const templatesRoot = findTemplatesRoot(
|
|
10325
|
+
const templatesRoot = findTemplatesRoot();
|
|
6497
10326
|
const name = String(args.name).trim();
|
|
6498
10327
|
if (!name)
|
|
6499
10328
|
throw new Error("Menu name is required");
|
|
@@ -6702,7 +10531,7 @@ function pascal(s) {
|
|
|
6702
10531
|
return "";
|
|
6703
10532
|
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
6704
10533
|
}
|
|
6705
|
-
var import_cli_maker7, path20, fs16,
|
|
10534
|
+
var import_cli_maker7, path20, fs16, NONE_SENTINEL = "(none)", ROOT_SENTINEL2 = "(root)", WHEN_HELP, addCommandCommand, add_default3;
|
|
6706
10535
|
var init_add9 = __esm(() => {
|
|
6707
10536
|
init_add3();
|
|
6708
10537
|
init_findProject();
|
|
@@ -6795,7 +10624,7 @@ var init_add9 = __esm(() => {
|
|
|
6795
10624
|
action: async (args) => {
|
|
6796
10625
|
try {
|
|
6797
10626
|
const projectRoot = findProjectRoot();
|
|
6798
|
-
const templatesRoot = findTemplatesRoot(
|
|
10627
|
+
const templatesRoot = findTemplatesRoot();
|
|
6799
10628
|
const name = String(args.name).trim();
|
|
6800
10629
|
if (!name)
|
|
6801
10630
|
throw new Error("Command name is required");
|
|
@@ -7202,7 +11031,7 @@ var init_add12 = __esm(() => {
|
|
|
7202
11031
|
});
|
|
7203
11032
|
|
|
7204
11033
|
// src/commands/statusBar/add.ts
|
|
7205
|
-
var import_cli_maker9, path24,
|
|
11034
|
+
var import_cli_maker9, path24, addStatusBarCommand, add_default5;
|
|
7206
11035
|
var init_add13 = __esm(() => {
|
|
7207
11036
|
init_add12();
|
|
7208
11037
|
init_findProject();
|
|
@@ -7340,7 +11169,7 @@ var init_add13 = __esm(() => {
|
|
|
7340
11169
|
action: async (args) => {
|
|
7341
11170
|
try {
|
|
7342
11171
|
const projectRoot = findProjectRoot();
|
|
7343
|
-
const templatesRoot = findTemplatesRoot(
|
|
11172
|
+
const templatesRoot = findTemplatesRoot();
|
|
7344
11173
|
const isNewCmd = args.bindTo === "create new command";
|
|
7345
11174
|
const isPanel = args.bindTo === "panel";
|
|
7346
11175
|
const isMenu = args.bindTo === "menu";
|
|
@@ -7511,7 +11340,7 @@ function pascal2(s) {
|
|
|
7511
11340
|
return "";
|
|
7512
11341
|
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
7513
11342
|
}
|
|
7514
|
-
var import_cli_maker10, path26,
|
|
11343
|
+
var import_cli_maker10, path26, addSubpanelCommand, add_default6;
|
|
7515
11344
|
var init_add15 = __esm(() => {
|
|
7516
11345
|
init_add14();
|
|
7517
11346
|
init_findProject();
|
|
@@ -7554,7 +11383,7 @@ var init_add15 = __esm(() => {
|
|
|
7554
11383
|
action: async (args) => {
|
|
7555
11384
|
try {
|
|
7556
11385
|
const projectRoot = findProjectRoot();
|
|
7557
|
-
const templatesRoot = findTemplatesRoot(
|
|
11386
|
+
const templatesRoot = findTemplatesRoot();
|
|
7558
11387
|
const result = addSubpanel({
|
|
7559
11388
|
name: String(args.name),
|
|
7560
11389
|
title: args.title ? String(args.title) : undefined,
|
|
@@ -7637,7 +11466,7 @@ var init_add16 = __esm(() => {
|
|
|
7637
11466
|
});
|
|
7638
11467
|
|
|
7639
11468
|
// src/commands/treeView/add.ts
|
|
7640
|
-
var import_cli_maker11, path28,
|
|
11469
|
+
var import_cli_maker11, path28, addTreeViewCommand, add_default7;
|
|
7641
11470
|
var init_add17 = __esm(() => {
|
|
7642
11471
|
init_add16();
|
|
7643
11472
|
init_edit();
|
|
@@ -7667,7 +11496,7 @@ var init_add17 = __esm(() => {
|
|
|
7667
11496
|
action: async (args) => {
|
|
7668
11497
|
try {
|
|
7669
11498
|
const projectRoot = findProjectRoot();
|
|
7670
|
-
const templatesRoot = findTemplatesRoot(
|
|
11499
|
+
const templatesRoot = findTemplatesRoot();
|
|
7671
11500
|
const result = addTreeView({
|
|
7672
11501
|
name: String(args.name).trim(),
|
|
7673
11502
|
menu: String(args.menu).trim(),
|
|
@@ -7752,7 +11581,7 @@ var init_testSetup = __esm(() => {
|
|
|
7752
11581
|
});
|
|
7753
11582
|
|
|
7754
11583
|
// src/commands/test/setup.ts
|
|
7755
|
-
var import_cli_maker12, path30,
|
|
11584
|
+
var import_cli_maker12, path30, testSetupCommand, setup_default;
|
|
7756
11585
|
var init_setup = __esm(() => {
|
|
7757
11586
|
init_testSetup();
|
|
7758
11587
|
init_findProject();
|
|
@@ -7767,7 +11596,7 @@ var init_setup = __esm(() => {
|
|
|
7767
11596
|
action: async (args) => {
|
|
7768
11597
|
try {
|
|
7769
11598
|
const projectRoot = findProjectRoot();
|
|
7770
|
-
const templatesRoot = findTemplatesRoot(
|
|
11599
|
+
const templatesRoot = findTemplatesRoot();
|
|
7771
11600
|
const result = setupTests({ projectRoot, templatesRoot, force: !!args.force });
|
|
7772
11601
|
const rel = (p) => path30.relative(projectRoot, p);
|
|
7773
11602
|
console.log(`
|
|
@@ -7861,7 +11690,7 @@ var init_init2 = __esm(() => {
|
|
|
7861
11690
|
});
|
|
7862
11691
|
|
|
7863
11692
|
// src/commands/publish/init.ts
|
|
7864
|
-
var import_cli_maker13, path32,
|
|
11693
|
+
var import_cli_maker13, path32, publishInitCommand, init_default;
|
|
7865
11694
|
var init_init3 = __esm(() => {
|
|
7866
11695
|
init_init2();
|
|
7867
11696
|
init_findProject();
|
|
@@ -7876,7 +11705,7 @@ var init_init3 = __esm(() => {
|
|
|
7876
11705
|
action: async (args) => {
|
|
7877
11706
|
try {
|
|
7878
11707
|
const projectRoot = findProjectRoot();
|
|
7879
|
-
const templatesRoot = findTemplatesRoot(
|
|
11708
|
+
const templatesRoot = findTemplatesRoot();
|
|
7880
11709
|
const result = publishInit({
|
|
7881
11710
|
projectRoot,
|
|
7882
11711
|
templatesRoot,
|
|
@@ -7912,7 +11741,7 @@ var init_init3 = __esm(() => {
|
|
|
7912
11741
|
function capitalize(s) {
|
|
7913
11742
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
7914
11743
|
}
|
|
7915
|
-
var import_cli_maker14, path33,
|
|
11744
|
+
var import_cli_maker14, path33, addHelperCommand, add_default8;
|
|
7916
11745
|
var init_add18 = __esm(() => {
|
|
7917
11746
|
init_add4();
|
|
7918
11747
|
init_findProject();
|
|
@@ -7939,7 +11768,7 @@ var init_add18 = __esm(() => {
|
|
|
7939
11768
|
action: async (args) => {
|
|
7940
11769
|
try {
|
|
7941
11770
|
const projectRoot = findProjectRoot();
|
|
7942
|
-
const templatesRoot = findTemplatesRoot(
|
|
11771
|
+
const templatesRoot = findTemplatesRoot();
|
|
7943
11772
|
const result = addHelper({
|
|
7944
11773
|
kind: String(args.kind),
|
|
7945
11774
|
force: !!args.force,
|
|
@@ -8072,7 +11901,7 @@ function resolveTrigger(args) {
|
|
|
8072
11901
|
return { on: String(args.on).trim() };
|
|
8073
11902
|
return { onFile: String(args.onFile).trim() };
|
|
8074
11903
|
}
|
|
8075
|
-
var import_cli_maker15, path35,
|
|
11904
|
+
var import_cli_maker15, path35, JOB_HELP, ON_EVENTS, addJobCommand, add_default9;
|
|
8076
11905
|
var init_add20 = __esm(() => {
|
|
8077
11906
|
init_add19();
|
|
8078
11907
|
init_findProject();
|
|
@@ -8111,7 +11940,7 @@ var init_add20 = __esm(() => {
|
|
|
8111
11940
|
action: async (args) => {
|
|
8112
11941
|
try {
|
|
8113
11942
|
const projectRoot = findProjectRoot();
|
|
8114
|
-
const templatesRoot = findTemplatesRoot(
|
|
11943
|
+
const templatesRoot = findTemplatesRoot();
|
|
8115
11944
|
const trigger = resolveTrigger(args);
|
|
8116
11945
|
const result = addJob({
|
|
8117
11946
|
name: String(args.name).trim(),
|
|
@@ -8195,7 +12024,7 @@ var init_wire = __esm(() => {
|
|
|
8195
12024
|
});
|
|
8196
12025
|
|
|
8197
12026
|
// src/commands/db/init.ts
|
|
8198
|
-
var import_cli_maker16, path37,
|
|
12027
|
+
var import_cli_maker16, path37, dbInitCommand, init_default2;
|
|
8199
12028
|
var init_init4 = __esm(() => {
|
|
8200
12029
|
init_init();
|
|
8201
12030
|
init_wire();
|
|
@@ -8224,7 +12053,7 @@ var init_init4 = __esm(() => {
|
|
|
8224
12053
|
action: async (args) => {
|
|
8225
12054
|
try {
|
|
8226
12055
|
const projectRoot = findProjectRoot();
|
|
8227
|
-
const templatesRoot = findTemplatesRoot(
|
|
12056
|
+
const templatesRoot = findTemplatesRoot();
|
|
8228
12057
|
const result = initDb({
|
|
8229
12058
|
projectRoot,
|
|
8230
12059
|
templatesRoot,
|
|
@@ -8298,7 +12127,7 @@ function pascal4(s) {
|
|
|
8298
12127
|
function plural(s) {
|
|
8299
12128
|
return `${pascal4(s)}s`;
|
|
8300
12129
|
}
|
|
8301
|
-
var import_cli_maker17, path38,
|
|
12130
|
+
var import_cli_maker17, path38, FIELD_HELP, addModelCommand, add_default10;
|
|
8302
12131
|
var init_add21 = __esm(() => {
|
|
8303
12132
|
init_add5();
|
|
8304
12133
|
init_init();
|
|
@@ -8347,7 +12176,7 @@ var init_add21 = __esm(() => {
|
|
|
8347
12176
|
action: async (args) => {
|
|
8348
12177
|
try {
|
|
8349
12178
|
const projectRoot = findProjectRoot();
|
|
8350
|
-
const templatesRoot = findTemplatesRoot(
|
|
12179
|
+
const templatesRoot = findTemplatesRoot();
|
|
8351
12180
|
if (!dbExists(projectRoot)) {
|
|
8352
12181
|
throw new Error("No `src/helpers/db.ts` found. Run `vsceasy db init` first.");
|
|
8353
12182
|
}
|
|
@@ -8798,7 +12627,7 @@ var init_add22 = __esm(() => {
|
|
|
8798
12627
|
});
|
|
8799
12628
|
|
|
8800
12629
|
// src/commands/crud/add.ts
|
|
8801
|
-
var import_cli_maker18, path42, fs28,
|
|
12630
|
+
var import_cli_maker18, path42, fs28, NONE_SENTINEL2 = "(no menu)", NEW_SENTINEL = "(create new menu)", addCrudCommand, add_default11;
|
|
8802
12631
|
var init_add23 = __esm(() => {
|
|
8803
12632
|
init_add22();
|
|
8804
12633
|
init_edit();
|
|
@@ -8849,7 +12678,7 @@ var init_add23 = __esm(() => {
|
|
|
8849
12678
|
action: async (args) => {
|
|
8850
12679
|
try {
|
|
8851
12680
|
const projectRoot = findProjectRoot();
|
|
8852
|
-
const templatesRoot = findTemplatesRoot(
|
|
12681
|
+
const templatesRoot = findTemplatesRoot();
|
|
8853
12682
|
let menuSpec;
|
|
8854
12683
|
const choice = String(args.menu ?? NONE_SENTINEL2);
|
|
8855
12684
|
if (choice === NONE_SENTINEL2) {
|
|
@@ -8894,7 +12723,7 @@ var init_add23 = __esm(() => {
|
|
|
8894
12723
|
});
|
|
8895
12724
|
|
|
8896
12725
|
// src/commands/components/add.ts
|
|
8897
|
-
var import_cli_maker19, path43,
|
|
12726
|
+
var import_cli_maker19, path43, addComponentsCommand, add_default12;
|
|
8898
12727
|
var init_add24 = __esm(() => {
|
|
8899
12728
|
init_add();
|
|
8900
12729
|
init_findProject();
|
|
@@ -8914,7 +12743,7 @@ var init_add24 = __esm(() => {
|
|
|
8914
12743
|
action: async (args) => {
|
|
8915
12744
|
try {
|
|
8916
12745
|
const projectRoot = findProjectRoot();
|
|
8917
|
-
const templatesRoot = findTemplatesRoot(
|
|
12746
|
+
const templatesRoot = findTemplatesRoot();
|
|
8918
12747
|
const result = addComponents({ projectRoot, templatesRoot, force: !!args.force });
|
|
8919
12748
|
const rel = (p) => path43.relative(projectRoot, p);
|
|
8920
12749
|
console.log(`
|
|
@@ -9006,7 +12835,7 @@ var init_cli = __esm(() => {
|
|
|
9006
12835
|
import_cli_maker20 = __toESM(require_dist(), 1);
|
|
9007
12836
|
cli = new import_cli_maker20.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
|
|
9008
12837
|
interactive: true,
|
|
9009
|
-
version: "0.1.
|
|
12838
|
+
version: "0.1.5",
|
|
9010
12839
|
introAnimation: {
|
|
9011
12840
|
enabled: true,
|
|
9012
12841
|
preset: "retro-space",
|