@vsceasy/cli 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +6 -2
- package/dist/bin/cli.js +42 -16
- package/dist/index.js +27 -12
- package/dist/lib/templatesData.d.ts +1 -1
- package/package.json +1 -1
- package/templates/_generators/crud/formApp.tsx.tpl +25 -10
package/CHANGELOG.md
CHANGED
|
@@ -41,6 +41,11 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
|
|
|
41
41
|
- Bumped to **0.1.0** — first signed/tagged release.
|
|
42
42
|
- Dropped Svelte/Vue/Vanilla mentions from docs and CLI options. Only React UI is supported in this release.
|
|
43
43
|
|
|
44
|
+
### Fixed
|
|
45
|
+
- **`crud add --menu` flag parsing** — passing a raw policy string (`--menu none`, `--menu new:<id>`, `--menu existing:<id>`) non-interactively was mis-mapped to `existing:<literal>`, so `--menu none` failed with `Menu not found: src/menus/none.ts`. The flag forms now work the same as the interactive choices (which was already the documented behavior).
|
|
46
|
+
- **Generated CRUD form wiped on reveal** — the scaffolded form panel re-ran its loader on every focus/visibility change. With no row pending it reset to an empty "New" form, discarding whatever the user was typing (e.g. ticking a boolean then losing focus). The loader now only resets on the initial mount; later reveals adopt a newly-requested edit row but otherwise leave the in-progress form untouched. Re-run `crud add` (or pull the change into existing `*/App.tsx` form panels) to pick up the fix.
|
|
47
|
+
- **Generated CRUD date fields didn't prefill when editing** — `<input type="date">` only accepts a `yyyy-MM-dd` value, but stored dates are ISO strings (or `Date` objects), so editing a row showed the date field blank. The generated form now normalizes through a `toDateInput()` helper. Re-run `crud add` (or add the helper to existing form panels) to pick up the fix.
|
|
48
|
+
|
|
44
49
|
## [0.0.1] — 2026-05-21
|
|
45
50
|
|
|
46
51
|
- Initial MVP. React template, typed RPC bridge, file-based registry, CLI generators (`panel`, `command`, `menu`, `rpc`, `statusBar`, `subpanel`), `doctor`, `upgrade`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo-mark.svg" alt="vsceasy octopus mascot" width="96" height="96" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">vsceasy</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">Build VS Code extensions fast. React UI + typed RPC bridge between extension and webview + zero-config build.</p>
|
|
4
8
|
|
|
5
9
|
> Status: v0.1 — React UI. Typed RPC bridge + file-based registry + scaffolding for panels, commands, menus, tree views, subpanels, status bars.
|
|
6
10
|
|
package/dist/bin/cli.js
CHANGED
|
@@ -3826,7 +3826,7 @@ var init_scaffold = __esm(() => {
|
|
|
3826
3826
|
});
|
|
3827
3827
|
|
|
3828
3828
|
// src/lib/templatesData.ts
|
|
3829
|
-
var TEMPLATES_VERSION = "0.1.
|
|
3829
|
+
var TEMPLATES_VERSION = "0.1.7", TEMPLATE_FILES;
|
|
3830
3830
|
var init_templatesData = __esm(() => {
|
|
3831
3831
|
TEMPLATE_FILES = {
|
|
3832
3832
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
@@ -4025,21 +4025,35 @@ type FormState = Partial<{{Name}}>;
|
|
|
4025
4025
|
|
|
4026
4026
|
const emptyForm: FormState = {{emptyFormLiteral}};
|
|
4027
4027
|
|
|
4028
|
+
// \`<input type="date">\` only accepts a \`yyyy-MM-dd\` value. Stored dates may be
|
|
4029
|
+
// ISO strings or Date objects, so normalize before binding to the input.
|
|
4030
|
+
function toDateInput(v: unknown): string {
|
|
4031
|
+
if (v == null || v === '') return '';
|
|
4032
|
+
const d = v instanceof Date ? v : new Date(v as string);
|
|
4033
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4028
4036
|
export function App() {
|
|
4029
4037
|
const [form, setForm] = useState<FormState>(emptyForm);
|
|
4030
4038
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
4031
4039
|
const [error, setError] = useState<string | null>(null);
|
|
4032
4040
|
const [saving, setSaving] = useState(false);
|
|
4033
4041
|
|
|
4034
|
-
const load = useCallback(async () => {
|
|
4035
|
-
// The list stashes
|
|
4036
|
-
// clears it after handing it over)
|
|
4042
|
+
const load = useCallback(async (initial: boolean) => {
|
|
4043
|
+
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
4044
|
+
// clears it after handing it over).
|
|
4037
4045
|
const id = await api.pendingId();
|
|
4038
4046
|
if (id == null || id === '') {
|
|
4039
|
-
|
|
4040
|
-
|
|
4047
|
+
// No row was requested. On the first mount, start with an empty "new" form.
|
|
4048
|
+
// On later reveals (focus/visibility), DON'T reset — that would wipe a form
|
|
4049
|
+
// the user is busy filling in. Just leave the current state as-is.
|
|
4050
|
+
if (initial) {
|
|
4051
|
+
setForm(emptyForm);
|
|
4052
|
+
setEditingId(null);
|
|
4053
|
+
}
|
|
4041
4054
|
return;
|
|
4042
4055
|
}
|
|
4056
|
+
// The list asked to edit a specific row — load it, replacing the current form.
|
|
4043
4057
|
const row = await api.get(id);
|
|
4044
4058
|
if (row) {
|
|
4045
4059
|
setForm(row);
|
|
@@ -4048,11 +4062,12 @@ export function App() {
|
|
|
4048
4062
|
}, []);
|
|
4049
4063
|
|
|
4050
4064
|
useEffect(() => {
|
|
4051
|
-
void load();
|
|
4052
|
-
// Webviews retain state when hidden, so re-
|
|
4053
|
-
//
|
|
4054
|
-
|
|
4055
|
-
const
|
|
4065
|
+
void load(true);
|
|
4066
|
+
// Webviews retain state when hidden, so re-check on reveal: the list may have
|
|
4067
|
+
// asked to edit a different row. When nothing is pending, \`load\` leaves the
|
|
4068
|
+
// in-progress form untouched (see above).
|
|
4069
|
+
const onFocus = () => { void load(false); };
|
|
4070
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(false); };
|
|
4056
4071
|
window.addEventListener('focus', onFocus);
|
|
4057
4072
|
document.addEventListener('visibilitychange', onVisible);
|
|
4058
4073
|
return () => {
|
|
@@ -12542,7 +12557,7 @@ function renderInput(field, override) {
|
|
|
12542
12557
|
case "boolean":
|
|
12543
12558
|
return wrap(` <input type="checkbox" checked={!!form.${name}} onChange={(e) => onChange('${name}', e.target.checked as any)} />`);
|
|
12544
12559
|
case "date":
|
|
12545
|
-
return wrap(` <input type="date"${required} value={(form.${name}
|
|
12560
|
+
return wrap(` <input type="date"${required} value={toDateInput(form.${name})} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
12546
12561
|
case "select": {
|
|
12547
12562
|
const opts = (spec.options ?? []).map((o) => ` <option value=${JSON.stringify(o)}>${escapeJsx(o)}</option>`).join(`
|
|
12548
12563
|
`);
|
|
@@ -12747,14 +12762,16 @@ var init_add23 = __esm(() => {
|
|
|
12747
12762
|
const projectRoot = findProjectRoot();
|
|
12748
12763
|
const templatesRoot = findTemplatesRoot();
|
|
12749
12764
|
let menuSpec;
|
|
12750
|
-
const choice = String(args.menu ?? NONE_SENTINEL2);
|
|
12751
|
-
if (choice === NONE_SENTINEL2) {
|
|
12765
|
+
const choice = String(args.menu ?? NONE_SENTINEL2).trim();
|
|
12766
|
+
if (choice === NONE_SENTINEL2 || choice === "none") {
|
|
12752
12767
|
menuSpec = "none";
|
|
12753
12768
|
} else if (choice === NEW_SENTINEL) {
|
|
12754
12769
|
const id = args.newMenuId ? String(args.newMenuId).trim() : await import_cli_maker18.prompt(" new menu id: ");
|
|
12755
12770
|
if (!id)
|
|
12756
12771
|
throw new Error("New menu id required.");
|
|
12757
12772
|
menuSpec = `new:${id}`;
|
|
12773
|
+
} else if (choice.startsWith("new:") || choice.startsWith("existing:")) {
|
|
12774
|
+
menuSpec = choice;
|
|
12758
12775
|
} else {
|
|
12759
12776
|
menuSpec = `existing:${choice}`;
|
|
12760
12777
|
}
|
|
@@ -12902,12 +12919,21 @@ var init_cli = __esm(() => {
|
|
|
12902
12919
|
import_cli_maker20 = __toESM(require_dist(), 1);
|
|
12903
12920
|
cli = new import_cli_maker20.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
|
|
12904
12921
|
interactive: true,
|
|
12905
|
-
version: "0.1.
|
|
12922
|
+
version: "0.1.7",
|
|
12906
12923
|
introAnimation: {
|
|
12907
12924
|
enabled: true,
|
|
12908
12925
|
preset: "retro-space",
|
|
12909
12926
|
title: "vsceasy",
|
|
12910
|
-
subtitle: "VS Code Extension Framework"
|
|
12927
|
+
subtitle: "VS Code Extension Framework",
|
|
12928
|
+
asciiArt: [
|
|
12929
|
+
' .-""""-.',
|
|
12930
|
+
" / o o \\",
|
|
12931
|
+
" | .. |",
|
|
12932
|
+
" \\ '--' /",
|
|
12933
|
+
" /`-.__.-`\\",
|
|
12934
|
+
" _/ /|/||\\|\\ \\_",
|
|
12935
|
+
" `--`-`-``-`-`--`"
|
|
12936
|
+
]
|
|
12911
12937
|
},
|
|
12912
12938
|
defaultCommands: {
|
|
12913
12939
|
rotatePassphrase: false,
|
package/dist/index.js
CHANGED
|
@@ -2238,7 +2238,7 @@ var init_upgrade = __esm(() => {
|
|
|
2238
2238
|
});
|
|
2239
2239
|
|
|
2240
2240
|
// src/lib/templatesData.ts
|
|
2241
|
-
var TEMPLATES_VERSION = "0.1.
|
|
2241
|
+
var TEMPLATES_VERSION = "0.1.7", TEMPLATE_FILES;
|
|
2242
2242
|
var init_templatesData = __esm(() => {
|
|
2243
2243
|
TEMPLATE_FILES = {
|
|
2244
2244
|
"_generators/command/command.ts.tpl": `import { defineCommand } from '../shared/vsceasy';
|
|
@@ -2437,21 +2437,35 @@ type FormState = Partial<{{Name}}>;
|
|
|
2437
2437
|
|
|
2438
2438
|
const emptyForm: FormState = {{emptyFormLiteral}};
|
|
2439
2439
|
|
|
2440
|
+
// \`<input type="date">\` only accepts a \`yyyy-MM-dd\` value. Stored dates may be
|
|
2441
|
+
// ISO strings or Date objects, so normalize before binding to the input.
|
|
2442
|
+
function toDateInput(v: unknown): string {
|
|
2443
|
+
if (v == null || v === '') return '';
|
|
2444
|
+
const d = v instanceof Date ? v : new Date(v as string);
|
|
2445
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2440
2448
|
export function App() {
|
|
2441
2449
|
const [form, setForm] = useState<FormState>(emptyForm);
|
|
2442
2450
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
2443
2451
|
const [error, setError] = useState<string | null>(null);
|
|
2444
2452
|
const [saving, setSaving] = useState(false);
|
|
2445
2453
|
|
|
2446
|
-
const load = useCallback(async () => {
|
|
2447
|
-
// The list stashes
|
|
2448
|
-
// clears it after handing it over)
|
|
2454
|
+
const load = useCallback(async (initial: boolean) => {
|
|
2455
|
+
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
2456
|
+
// clears it after handing it over).
|
|
2449
2457
|
const id = await api.pendingId();
|
|
2450
2458
|
if (id == null || id === '') {
|
|
2451
|
-
|
|
2452
|
-
|
|
2459
|
+
// No row was requested. On the first mount, start with an empty "new" form.
|
|
2460
|
+
// On later reveals (focus/visibility), DON'T reset — that would wipe a form
|
|
2461
|
+
// the user is busy filling in. Just leave the current state as-is.
|
|
2462
|
+
if (initial) {
|
|
2463
|
+
setForm(emptyForm);
|
|
2464
|
+
setEditingId(null);
|
|
2465
|
+
}
|
|
2453
2466
|
return;
|
|
2454
2467
|
}
|
|
2468
|
+
// The list asked to edit a specific row — load it, replacing the current form.
|
|
2455
2469
|
const row = await api.get(id);
|
|
2456
2470
|
if (row) {
|
|
2457
2471
|
setForm(row);
|
|
@@ -2460,11 +2474,12 @@ export function App() {
|
|
|
2460
2474
|
}, []);
|
|
2461
2475
|
|
|
2462
2476
|
useEffect(() => {
|
|
2463
|
-
void load();
|
|
2464
|
-
// Webviews retain state when hidden, so re-
|
|
2465
|
-
//
|
|
2466
|
-
|
|
2467
|
-
const
|
|
2477
|
+
void load(true);
|
|
2478
|
+
// Webviews retain state when hidden, so re-check on reveal: the list may have
|
|
2479
|
+
// asked to edit a different row. When nothing is pending, \`load\` leaves the
|
|
2480
|
+
// in-progress form untouched (see above).
|
|
2481
|
+
const onFocus = () => { void load(false); };
|
|
2482
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(false); };
|
|
2468
2483
|
window.addEventListener('focus', onFocus);
|
|
2469
2484
|
document.addEventListener('visibilitychange', onVisible);
|
|
2470
2485
|
return () => {
|
|
@@ -6723,7 +6738,7 @@ function renderInput(field, override) {
|
|
|
6723
6738
|
case "boolean":
|
|
6724
6739
|
return wrap(` <input type="checkbox" checked={!!form.${name}} onChange={(e) => onChange('${name}', e.target.checked as any)} />`);
|
|
6725
6740
|
case "date":
|
|
6726
|
-
return wrap(` <input type="date"${required} value={(form.${name}
|
|
6741
|
+
return wrap(` <input type="date"${required} value={toDateInput(form.${name})} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
|
|
6727
6742
|
case "select": {
|
|
6728
6743
|
const opts = (spec.options ?? []).map((o) => ` <option value=${JSON.stringify(o)}>${escapeJsx(o)}</option>`).join(`
|
|
6729
6744
|
`);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const TEMPLATES_VERSION = "0.1.
|
|
1
|
+
export declare const TEMPLATES_VERSION = "0.1.7";
|
|
2
2
|
export declare const TEMPLATE_FILES: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsceasy/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Build VS Code extensions fast — React UI + typed RPC bridge between extension and webview + file-based routing for panels, commands, menus, tree views, and subpanels.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,21 +9,35 @@ type FormState = Partial<{{Name}}>;
|
|
|
9
9
|
|
|
10
10
|
const emptyForm: FormState = {{emptyFormLiteral}};
|
|
11
11
|
|
|
12
|
+
// `<input type="date">` only accepts a `yyyy-MM-dd` value. Stored dates may be
|
|
13
|
+
// ISO strings or Date objects, so normalize before binding to the input.
|
|
14
|
+
function toDateInput(v: unknown): string {
|
|
15
|
+
if (v == null || v === '') return '';
|
|
16
|
+
const d = v instanceof Date ? v : new Date(v as string);
|
|
17
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString().slice(0, 10);
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export function App() {
|
|
13
21
|
const [form, setForm] = useState<FormState>(emptyForm);
|
|
14
22
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
15
23
|
const [error, setError] = useState<string | null>(null);
|
|
16
24
|
const [saving, setSaving] = useState(false);
|
|
17
25
|
|
|
18
|
-
const load = useCallback(async () => {
|
|
19
|
-
// The list stashes
|
|
20
|
-
// clears it after handing it over)
|
|
26
|
+
const load = useCallback(async (initial: boolean) => {
|
|
27
|
+
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
28
|
+
// clears it after handing it over).
|
|
21
29
|
const id = await api.pendingId();
|
|
22
30
|
if (id == null || id === '') {
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// No row was requested. On the first mount, start with an empty "new" form.
|
|
32
|
+
// On later reveals (focus/visibility), DON'T reset — that would wipe a form
|
|
33
|
+
// the user is busy filling in. Just leave the current state as-is.
|
|
34
|
+
if (initial) {
|
|
35
|
+
setForm(emptyForm);
|
|
36
|
+
setEditingId(null);
|
|
37
|
+
}
|
|
25
38
|
return;
|
|
26
39
|
}
|
|
40
|
+
// The list asked to edit a specific row — load it, replacing the current form.
|
|
27
41
|
const row = await api.get(id);
|
|
28
42
|
if (row) {
|
|
29
43
|
setForm(row);
|
|
@@ -32,11 +46,12 @@ export function App() {
|
|
|
32
46
|
}, []);
|
|
33
47
|
|
|
34
48
|
useEffect(() => {
|
|
35
|
-
void load();
|
|
36
|
-
// Webviews retain state when hidden, so re-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const
|
|
49
|
+
void load(true);
|
|
50
|
+
// Webviews retain state when hidden, so re-check on reveal: the list may have
|
|
51
|
+
// asked to edit a different row. When nothing is pending, `load` leaves the
|
|
52
|
+
// in-progress form untouched (see above).
|
|
53
|
+
const onFocus = () => { void load(false); };
|
|
54
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(false); };
|
|
40
55
|
window.addEventListener('focus', onFocus);
|
|
41
56
|
document.addEventListener('visibilitychange', onVisible);
|
|
42
57
|
return () => {
|