@vsceasy/cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/cli.js CHANGED
@@ -3826,7 +3826,7 @@ var init_scaffold = __esm(() => {
3826
3826
  });
3827
3827
 
3828
3828
  // src/lib/templatesData.ts
3829
- var TEMPLATES_VERSION = "0.1.6", TEMPLATE_FILES;
3829
+ var TEMPLATES_VERSION = "0.1.8", 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 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.
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
- setForm(emptyForm);
4040
- setEditingId(null);
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-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(); };
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 () => {
@@ -4523,6 +4538,35 @@ export interface Repository<T> {
4523
4538
  clear(): Promise<void>;
4524
4539
  }
4525
4540
 
4541
+ // ── Change notifications (reactivity) ────────────────────────────────────────
4542
+
4543
+ /**
4544
+ * Per-entity change listeners. Any mutation (insert/upsert/update/delete/...) on
4545
+ * an entity fires its listeners with the entity name. This is the host-side hook
4546
+ * the \`watch()\` helper builds on to push updates to webviews.
4547
+ */
4548
+ const entityListeners = new Map<string, Set<(entity: string) => void>>();
4549
+
4550
+ function emitChange(entity: string): void {
4551
+ entityListeners.get(entity)?.forEach((cb) => {
4552
+ try { cb(entity); } catch { /* a bad listener must not break a write */ }
4553
+ });
4554
+ }
4555
+
4556
+ /**
4557
+ * Subscribe to changes on an entity. Returns an unsubscribe function.
4558
+ *
4559
+ * watchEntity(Todos, () => server.emit('todos:changed'));
4560
+ *
4561
+ * Prefer the higher-level \`watch()\` helper, which wires this to an RPC emit.
4562
+ */
4563
+ export function watchEntity<T>(entity: Entity<T>, cb: (entity: string) => void): () => void {
4564
+ let set = entityListeners.get(entity.name);
4565
+ if (!set) entityListeners.set(entity.name, (set = new Set()));
4566
+ set.add(cb);
4567
+ return () => { set!.delete(cb); };
4568
+ }
4569
+
4526
4570
  // ── Provider interface (future-proof for sqlite/etc.) ────────────────────────
4527
4571
 
4528
4572
  export interface Provider {
@@ -4693,6 +4737,7 @@ function makeDb(provider: Provider): Db {
4693
4737
  }
4694
4738
  rows.push(row);
4695
4739
  await save(rows);
4740
+ emitChange(entity.name);
4696
4741
  return row;
4697
4742
  },
4698
4743
  async upsert(row) {
@@ -4700,6 +4745,7 @@ function makeDb(provider: Provider): Db {
4700
4745
  const i = rows.findIndex((r) => r[pk] === row[pk]);
4701
4746
  if (i >= 0) rows[i] = row; else rows.push(row);
4702
4747
  await save(rows);
4748
+ emitChange(entity.name);
4703
4749
  return row;
4704
4750
  },
4705
4751
  async update(id, patch) {
@@ -4708,6 +4754,7 @@ function makeDb(provider: Provider): Db {
4708
4754
  if (i < 0) return null;
4709
4755
  rows[i] = { ...rows[i], ...patch };
4710
4756
  await save(rows);
4757
+ emitChange(entity.name);
4711
4758
  return rows[i];
4712
4759
  },
4713
4760
  async delete(id) {
@@ -4716,17 +4763,19 @@ function makeDb(provider: Provider): Db {
4716
4763
  const next = rows.filter((r) => r[pk] !== id);
4717
4764
  if (next.length === before) return false;
4718
4765
  await save(next);
4766
+ emitChange(entity.name);
4719
4767
  return true;
4720
4768
  },
4721
4769
  async deleteMany(where) {
4722
4770
  const rows = await load();
4723
4771
  const next = rows.filter((r) => !match(r, where));
4724
4772
  const removed = rows.length - next.length;
4725
- if (removed > 0) await save(next);
4773
+ if (removed > 0) { await save(next); emitChange(entity.name); }
4726
4774
  return removed;
4727
4775
  },
4728
4776
  async clear() {
4729
4777
  await save([]);
4778
+ emitChange(entity.name);
4730
4779
  },
4731
4780
  };
4732
4781
  };
@@ -5151,6 +5200,30 @@ export default defineStatusBar({
5151
5200
  alignment: '{{alignment}}',
5152
5201
  priority: {{priority}},{{commandLine}}{{panelLine}}
5153
5202
  });
5203
+ `,
5204
+ "_generators/store/store.ts.tpl": `import { defineStore } from '../shared/vsceasy';
5205
+
5206
+ /**
5207
+ * Reactive store: a single observable value. Mutate it with \`.set()\` / \`.update()\`
5208
+ * and anything subscribed reacts.
5209
+ *
5210
+ * import { {{name}} } from '../stores/{{name}}';
5211
+ * {{name}}.set({{example}});
5212
+ * {{name}}.update((v) => v);
5213
+ *
5214
+ * To push changes to a webview, watch it on the host and emit over RPC:
5215
+ *
5216
+ * // inside a panel/subpanel rpc():
5217
+ * import { watch } from '../shared/vsceasy';
5218
+ * import { {{name}} } from '../stores/{{name}}';
5219
+ * watch({{name}}, () => server.emit('{{name}}:changed', {{name}}.get()));
5220
+ *
5221
+ * Then listen in the webview:
5222
+ *
5223
+ * import { listen } from '../shared/vsceasy/client';
5224
+ * listen(api, '{{name}}:changed', (v) => render(v));
5225
+ */
5226
+ export const {{name}} = defineStore<{{type}}>({{initial}});
5154
5227
  `,
5155
5228
  "_generators/subpanel/App.tsx.tpl": `import React from 'react';
5156
5229
  {{apiBlock}}
@@ -6347,6 +6420,13 @@ function registerTreeView(
6347
6420
  vscode.commands.registerCommand(refreshCmd, () => provider.refresh()),
6348
6421
  );
6349
6422
 
6423
+ // Keep the tree live: let the def subscribe to a data source and re-run
6424
+ // getChildren on change. The unsubscribe is disposed on deactivate.
6425
+ if (def.watch) {
6426
+ const off = def.watch(() => provider.refresh(), vscode, context);
6427
+ if (off) context.subscriptions.push({ dispose: off });
6428
+ }
6429
+
6350
6430
  const dispatchCmd = \`\${registry.prefix}._tree.\${def.id ?? id}.run\`;
6351
6431
  context.subscriptions.push(
6352
6432
  vscode.commands.registerCommand(dispatchCmd, async (node: TreeNode) => {
@@ -6423,9 +6503,11 @@ function registerSubpanel(
6423
6503
  const ui = def.ui ?? \`subpanels/\${def.id ?? id}\`;
6424
6504
  view.webview.html = renderHtml(view.webview, context, ui, def.title);
6425
6505
  if (def.rpc) {
6426
- const handlers = def.rpc(vscode, context);
6427
- const server = createRpcServer(webviewTransport(view.webview), handlers);
6428
- view.onDidDispose(() => server.dispose());
6506
+ let server: ReturnType<typeof createRpcServer> | undefined;
6507
+ const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
6508
+ const handlers = def.rpc(vscode, context, emit);
6509
+ server = createRpcServer(webviewTransport(view.webview), handlers);
6510
+ view.onDidDispose(() => server!.dispose());
6429
6511
  }
6430
6512
  },
6431
6513
  };
@@ -6661,9 +6743,13 @@ function openPanel(context: vscode.ExtensionContext, prefix: string, id: string,
6661
6743
  panel.webview.html = renderHtml(panel.webview, context, ui, def.title);
6662
6744
 
6663
6745
  if (def.rpc) {
6664
- const handlers = def.rpc(vscode, context);
6665
- const server = createRpcServer(webviewTransport(panel.webview), handlers);
6666
- panel.onDidDispose(() => server.dispose());
6746
+ // \`emit\` is wired lazily: the rpc factory may call watch()/watchEntity()
6747
+ // immediately, but the server (and thus the real emit) only exists after.
6748
+ let server: ReturnType<typeof createRpcServer> | undefined;
6749
+ const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
6750
+ const handlers = def.rpc(vscode, context, emit);
6751
+ server = createRpcServer(webviewTransport(panel.webview), handlers);
6752
+ panel.onDidDispose(() => server!.dispose());
6667
6753
  }
6668
6754
 
6669
6755
  openPanels.set(key, panel);
@@ -6781,6 +6867,33 @@ export {
6781
6867
  webviewState,
6782
6868
  } from './rpc';
6783
6869
  export type { RpcClient, Handlers, Transport, RpcClientOptions, WebviewApi } from './rpc';
6870
+
6871
+ export { defineStore } from './store';
6872
+ export type { Store } from './store';
6873
+
6874
+ import type { RpcClient, Handlers } from './rpc';
6875
+
6876
+ /**
6877
+ * Listen for a change pushed from the host over the RPC event channel, and run a
6878
+ * callback when it arrives. This is the webview side of the reactivity model —
6879
+ * the host emits with \`server.emit(topic, …)\` (usually wired via \`watch()\` /
6880
+ * \`watchEntity()\`), and your visual element reacts here. Returns an unsubscribe
6881
+ * function; call it on unmount.
6882
+ *
6883
+ * const api = connectWebview<TodoStatsViewApi>();
6884
+ * // re-read + re-render whenever todos change on the host:
6885
+ * const off = listen(api, 'todos:changed', () => refresh());
6886
+ *
6887
+ * It's a thin, named wrapper over \`api.on(topic, handler)\` so the place you
6888
+ * listen reads clearly in the UI code. The payload (if any) is passed through.
6889
+ */
6890
+ export function listen<H extends Handlers>(
6891
+ api: RpcClient<H>,
6892
+ topic: string,
6893
+ handler: (payload?: unknown) => void,
6894
+ ): () => void {
6895
+ return api.on(topic, handler);
6896
+ }
6784
6897
  `,
6785
6898
  "react/src/shared/vsceasy/codiconNames.ts": `/**
6786
6899
  * AUTO-GENERATED by scripts/genCodiconTypes.ts — do not edit.
@@ -6983,6 +7096,18 @@ export type CodiconName =
6983
7096
  import type { Handlers } from './rpc';
6984
7097
  import type { CodiconName } from './codiconNames';
6985
7098
 
7099
+ /**
7100
+ * Push a change to this webview over the RPC event channel. The webview reacts
7101
+ * with \`listen(api, topic, …)\`. Wire it to a data source with \`watch()\` /
7102
+ * \`watchEntity()\` inside \`rpc()\`:
7103
+ *
7104
+ * rpc: (vscode, ctx, emit) => {
7105
+ * watchEntity(Todos, () => emit('todos:changed'));
7106
+ * return { stats: () => … };
7107
+ * }
7108
+ */
7109
+ export type RpcEmit = (topic: string, payload?: unknown) => void;
7110
+
6986
7111
  export interface PanelDef<H extends Handlers = Handlers> {
6987
7112
  /** Stable id. Default: file basename. Used as command suffix and webview key. */
6988
7113
  id?: string;
@@ -6994,8 +7119,11 @@ export interface PanelDef<H extends Handlers = Handlers> {
6994
7119
  column?: 'active' | 'beside' | 'one' | 'two' | 'three';
6995
7120
  /** Keep DOM alive when hidden. Default: true. */
6996
7121
  retainContext?: boolean;
6997
- /** RPC handlers — receives vscode namespace + extension context. */
6998
- rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => H;
7122
+ /**
7123
+ * RPC handlers — receives the vscode namespace, the extension context, and
7124
+ * \`emit\` for pushing change events to this webview (see {@link RpcEmit}).
7125
+ */
7126
+ rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
6999
7127
  /** Optional command palette entry that opens this panel. Default: true. */
7000
7128
  command?:
7001
7129
  | boolean
@@ -7106,8 +7234,11 @@ export interface SubpanelDef<H extends Handlers = Handlers> {
7106
7234
  ui?: string;
7107
7235
  /** Keep DOM alive when hidden. Default: true. */
7108
7236
  retainContext?: boolean;
7109
- /** RPC handlers — receives vscode namespace + extension context. */
7110
- rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext) => H;
7237
+ /**
7238
+ * RPC handlers — receives the vscode namespace, the extension context, and
7239
+ * \`emit\` for pushing change events to this webview (see {@link RpcEmit}).
7240
+ */
7241
+ rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
7111
7242
  }
7112
7243
 
7113
7244
  export function defineSubpanel<H extends Handlers = Handlers>(def: SubpanelDef<H>): SubpanelDef<H> {
@@ -7209,6 +7340,18 @@ export interface TreeViewDef {
7209
7340
  vscode: typeof import('vscode'),
7210
7341
  ctx: vscode.ExtensionContext,
7211
7342
  ) => TreeNode[] | Promise<TreeNode[]>;
7343
+ /**
7344
+ * Keep the tree live. Receives \`refresh\` — call it (directly or as a callback)
7345
+ * to re-run \`getChildren\`. Subscribe to a data source here so the tree updates
7346
+ * itself; return an unsubscribe to clean up.
7347
+ *
7348
+ * watch: (refresh) => watchEntity(Todos, refresh),
7349
+ */
7350
+ watch?: (
7351
+ refresh: () => void,
7352
+ vscode: typeof import('vscode'),
7353
+ ctx: vscode.ExtensionContext,
7354
+ ) => (() => void) | void;
7212
7355
  }
7213
7356
 
7214
7357
  export function defineTreeView(def: TreeViewDef): TreeViewDef {
@@ -7250,7 +7393,7 @@ export function defineJob(def: JobDef): JobDef {
7250
7393
  }
7251
7394
  `,
7252
7395
  "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';
7396
+ export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName, RpcEmit } from './define';
7254
7397
  export { bootstrap } from './bootstrap';
7255
7398
  export type { Registry, BootstrapOptions, ActivateHook } from './bootstrap';
7256
7399
  export {
@@ -7262,6 +7405,9 @@ export {
7262
7405
  webviewState,
7263
7406
  } from './rpc';
7264
7407
  export type { Transport, RpcClient, Handlers, RpcClientOptions, WebviewApi } from './rpc';
7408
+ export { defineStore, watch } from './store';
7409
+ export type { Store, Watchable } from './store';
7410
+ export { listen } from './client';
7265
7411
  `,
7266
7412
  "react/src/shared/vsceasy/rpc.ts": `// Typed RPC bridge — webview <-> extension.
7267
7413
  // Used by both sides. Transport-agnostic core + thin adapters.
@@ -7477,6 +7623,80 @@ export function webviewState<T>(defaults: T): {
7477
7623
  },
7478
7624
  };
7479
7625
  }
7626
+ `,
7627
+ "react/src/shared/vsceasy/store.ts": `/**
7628
+ * Reactive stores — a tiny, framework-agnostic observable value.
7629
+ *
7630
+ * A store holds one value and notifies subscribers when it changes. It's the
7631
+ * non-ORM half of the reactivity model: use it for arbitrary state (a counter,
7632
+ * a flag, a selection) that a visual element should track.
7633
+ *
7634
+ * const counter = defineStore(0);
7635
+ * counter.subscribe((v) => console.log('now', v));
7636
+ * counter.set(1); // logs: now 1
7637
+ * counter.update((n) => n + 1);
7638
+ *
7639
+ * Pair it with \`watch()\` on the host to push changes to a webview, and
7640
+ * \`listen()\` on the webview to react — see those helpers below and in client.ts.
7641
+ */
7642
+ export interface Store<T> {
7643
+ /** Read the current value. */
7644
+ get(): T;
7645
+ /** Replace the value and notify subscribers (no-op if \`Object.is\`-equal). */
7646
+ set(next: T): void;
7647
+ /** Derive the next value from the current one, then \`set\` it. */
7648
+ update(fn: (current: T) => T): void;
7649
+ /** Subscribe to changes. Returns an unsubscribe function. */
7650
+ subscribe(cb: (value: T) => void): () => void;
7651
+ }
7652
+
7653
+ export function defineStore<T>(initial: T): Store<T> {
7654
+ let value = initial;
7655
+ const subs = new Set<(value: T) => void>();
7656
+
7657
+ return {
7658
+ get: () => value,
7659
+ set(next) {
7660
+ if (Object.is(value, next)) return;
7661
+ value = next;
7662
+ subs.forEach((cb) => {
7663
+ try { cb(value); } catch { /* a bad subscriber must not break a set */ }
7664
+ });
7665
+ },
7666
+ update(fn) {
7667
+ this.set(fn(value));
7668
+ },
7669
+ subscribe(cb) {
7670
+ subs.add(cb);
7671
+ return () => { subs.delete(cb); };
7672
+ },
7673
+ };
7674
+ }
7675
+
7676
+ /**
7677
+ * Anything that can be watched: it exposes a \`subscribe(cb)\` returning an
7678
+ * unsubscribe function. Stores satisfy this directly. (ORM entities are watched
7679
+ * with \`watchEntity\` from your generated \`db.ts\`, which has the same shape.)
7680
+ */
7681
+ export interface Watchable {
7682
+ subscribe(cb: (...args: any[]) => void): () => void;
7683
+ }
7684
+
7685
+ /**
7686
+ * Bridge a watchable source to a side-effect — typically an RPC emit that pushes
7687
+ * the change to a subscribed webview. Runs \`effect\` on every change. Returns an
7688
+ * unsubscribe function; register it on the panel's \`ctx.subscriptions\` (wrapped
7689
+ * in \`{ dispose }\`) so it's cleaned up when the extension deactivates.
7690
+ *
7691
+ * // host side, in a panel's rpc():
7692
+ * watch(badgeCount, () => server.emit('badge:changed'));
7693
+ *
7694
+ * For ORM entities, use \`watchEntity(Todos, () => server.emit('todos:changed'))\`
7695
+ * from your generated db.ts — same idea, same return.
7696
+ */
7697
+ export function watch(source: Watchable, effect: () => void): () => void {
7698
+ return source.subscribe(() => effect());
7699
+ }
7480
7700
  `,
7481
7701
  "react/src/webview/panels/dashboard/App.tsx": `import React, { useEffect, useState } from 'react';
7482
7702
  import { connectWebview } from '../../../shared/vsceasy/client';
@@ -12542,7 +12762,7 @@ function renderInput(field, override) {
12542
12762
  case "boolean":
12543
12763
  return wrap(` <input type="checkbox" checked={!!form.${name}} onChange={(e) => onChange('${name}', e.target.checked as any)} />`);
12544
12764
  case "date":
12545
- return wrap(` <input type="date"${required} value={(form.${name} as any) ?? ''} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
12765
+ return wrap(` <input type="date"${required} value={toDateInput(form.${name})} onChange={(e) => onChange('${name}', e.target.value as any)} />`);
12546
12766
  case "select": {
12547
12767
  const opts = (spec.options ?? []).map((o) => ` <option value=${JSON.stringify(o)}>${escapeJsx(o)}</option>`).join(`
12548
12768
  `);
@@ -12747,14 +12967,16 @@ var init_add23 = __esm(() => {
12747
12967
  const projectRoot = findProjectRoot();
12748
12968
  const templatesRoot = findTemplatesRoot();
12749
12969
  let menuSpec;
12750
- const choice = String(args.menu ?? NONE_SENTINEL2);
12751
- if (choice === NONE_SENTINEL2) {
12970
+ const choice = String(args.menu ?? NONE_SENTINEL2).trim();
12971
+ if (choice === NONE_SENTINEL2 || choice === "none") {
12752
12972
  menuSpec = "none";
12753
12973
  } else if (choice === NEW_SENTINEL) {
12754
12974
  const id = args.newMenuId ? String(args.newMenuId).trim() : await import_cli_maker18.prompt(" new menu id: ");
12755
12975
  if (!id)
12756
12976
  throw new Error("New menu id required.");
12757
12977
  menuSpec = `new:${id}`;
12978
+ } else if (choice.startsWith("new:") || choice.startsWith("existing:")) {
12979
+ menuSpec = choice;
12758
12980
  } else {
12759
12981
  menuSpec = `existing:${choice}`;
12760
12982
  }
@@ -12836,6 +13058,122 @@ var init_add24 = __esm(() => {
12836
13058
  add_default12 = addComponentsCommand;
12837
13059
  });
12838
13060
 
13061
+ // src/lib/store/add.ts
13062
+ function addStore(opts) {
13063
+ const name = assertId("store name", normalizeCamel9(opts.name));
13064
+ const type = opts.type ?? "number";
13065
+ const initial = opts.initial?.trim() || DEFAULT_INITIAL[type];
13066
+ const storeTs = path44.join(opts.projectRoot, "src", "stores", `${name}.ts`);
13067
+ assertNoOverwrite(opts.projectRoot, storeTs, "Store");
13068
+ const tplPath = path44.join(opts.templatesRoot, "_generators", "store", "store.ts.tpl");
13069
+ const body = substitute(fs29.readFileSync(tplPath, "utf8"), {
13070
+ name,
13071
+ type: TS_TYPE[type],
13072
+ initial,
13073
+ example: EXAMPLE[type]
13074
+ });
13075
+ fs29.mkdirSync(path44.dirname(storeTs), { recursive: true });
13076
+ fs29.writeFileSync(storeTs, body);
13077
+ return { created: [storeTs] };
13078
+ }
13079
+ function normalizeCamel9(s) {
13080
+ const cleaned = s.trim().replace(/[^a-zA-Z0-9]+(.)/g, (_m, c) => c.toUpperCase()).replace(/[^a-zA-Z0-9]/g, "");
13081
+ if (!cleaned)
13082
+ return "";
13083
+ return cleaned.charAt(0).toLowerCase() + cleaned.slice(1);
13084
+ }
13085
+ var fs29, path44, DEFAULT_INITIAL, TS_TYPE, EXAMPLE;
13086
+ var init_add25 = __esm(() => {
13087
+ init_scaffold();
13088
+ init_validate();
13089
+ fs29 = __toESM(require("fs"));
13090
+ path44 = __toESM(require("path"));
13091
+ DEFAULT_INITIAL = {
13092
+ number: "0",
13093
+ string: "''",
13094
+ boolean: "false",
13095
+ json: "null"
13096
+ };
13097
+ TS_TYPE = {
13098
+ number: "number",
13099
+ string: "string",
13100
+ boolean: "boolean",
13101
+ json: "any"
13102
+ };
13103
+ EXAMPLE = {
13104
+ number: "1",
13105
+ string: "'hello'",
13106
+ boolean: "true",
13107
+ json: "{ open: true }"
13108
+ };
13109
+ });
13110
+
13111
+ // src/commands/store/add.ts
13112
+ var import_cli_maker20, path45, addStoreCommand, add_default13;
13113
+ var init_add26 = __esm(() => {
13114
+ init_add25();
13115
+ init_findProject();
13116
+ import_cli_maker20 = __toESM(require_dist(), 1);
13117
+ path45 = __toESM(require("path"));
13118
+ addStoreCommand = {
13119
+ name: "add",
13120
+ description: "Add a reactive store (an observable value) under src/stores/. Watch it on the host and listen in a webview to keep UI in sync.",
13121
+ params: [
13122
+ { name: "name", description: "Store name (camelCase, e.g. badgeCount)", required: true, type: import_cli_maker20.ParamType.Text },
13123
+ {
13124
+ name: "type",
13125
+ description: "Value type",
13126
+ required: false,
13127
+ type: import_cli_maker20.ParamType.List,
13128
+ options: ["number", "string", "boolean", "json"]
13129
+ },
13130
+ {
13131
+ name: "initial",
13132
+ description: "Initial value expression (default per type: 0 / '' / false / null)",
13133
+ required: false,
13134
+ type: import_cli_maker20.ParamType.Text
13135
+ }
13136
+ ],
13137
+ action: async (args) => {
13138
+ try {
13139
+ const projectRoot = findProjectRoot();
13140
+ const templatesRoot = findTemplatesRoot();
13141
+ const result = addStore({
13142
+ name: String(args.name).trim(),
13143
+ type: args.type ? String(args.type) : undefined,
13144
+ initial: args.initial ? String(args.initial) : undefined,
13145
+ projectRoot,
13146
+ templatesRoot
13147
+ });
13148
+ const rel = (p) => path45.relative(projectRoot, p);
13149
+ const name = String(args.name).trim();
13150
+ console.log(`
13151
+ ✓ Store "${name}" created.
13152
+ `);
13153
+ for (const f of result.created)
13154
+ console.log(` + ${rel(f)}`);
13155
+ console.log(`
13156
+ Use it:
13157
+ import { ${name} } from '../stores/${name}';
13158
+ ${name}.set(...); ${name}.update((v) => v);
13159
+ `);
13160
+ console.log(` Push changes to a webview — host side, in a panel rpc():
13161
+ watch(${name}, () => server.emit('${name}:changed', ${name}.get()));
13162
+ `);
13163
+ console.log(` React in the webview:
13164
+ listen(api, '${name}:changed', (v) => render(v));
13165
+ `);
13166
+ } catch (err) {
13167
+ console.error(`
13168
+ ✗ ${err.message}
13169
+ `);
13170
+ process.exitCode = 1;
13171
+ }
13172
+ }
13173
+ };
13174
+ add_default13 = addStoreCommand;
13175
+ });
13176
+
12839
13177
  // src/commands/groups.ts
12840
13178
  function group(name, description, subcommands) {
12841
13179
  return {
@@ -12852,7 +13190,7 @@ Use one of: ${subcommands.map((s) => s.name).join(", ")}
12852
13190
  }
12853
13191
  };
12854
13192
  }
12855
- var PanelGroup, MenuGroup, CommandGroup, RpcGroup, StatusBarGroup, SubpanelGroup, TreeViewGroup, TestGroup, PublishGroup, HelperGroup, JobGroup, DbGroup, ModelGroup, CrudGroup, ComponentsGroup;
13193
+ var PanelGroup, MenuGroup, CommandGroup, RpcGroup, StatusBarGroup, SubpanelGroup, TreeViewGroup, TestGroup, PublishGroup, HelperGroup, JobGroup, DbGroup, ModelGroup, CrudGroup, ComponentsGroup, StoreGroup;
12856
13194
  var init_groups = __esm(() => {
12857
13195
  init_add6();
12858
13196
  init_add8();
@@ -12870,6 +13208,7 @@ var init_groups = __esm(() => {
12870
13208
  init_add21();
12871
13209
  init_add23();
12872
13210
  init_add24();
13211
+ init_add26();
12873
13212
  PanelGroup = group("panel", "Manage panels (webview + RPC bridge)", [add_default]);
12874
13213
  MenuGroup = group("menu", "Manage sidebar menus (activity bar tree views)", [add_default2, edit_default]);
12875
13214
  CommandGroup = group("command", "Manage palette commands", [add_default3]);
@@ -12885,6 +13224,7 @@ var init_groups = __esm(() => {
12885
13224
  ModelGroup = group("model", "Manage typed models (entities + repos) under src/models/", [add_default10]);
12886
13225
  CrudGroup = group("crud", "Scaffold full CRUD UI (service + list panel + form panel + RPC) for a model", [add_default11]);
12887
13226
  ComponentsGroup = group("components", "Generate a theme-aware React component library for webviews", [add_default12]);
13227
+ StoreGroup = group("store", "Manage reactive stores (observable values) under src/stores/", [add_default13]);
12888
13228
  });
12889
13229
 
12890
13230
  // src/cli.ts
@@ -12892,22 +13232,31 @@ var exports_cli = {};
12892
13232
  __export(exports_cli, {
12893
13233
  cli: () => cli
12894
13234
  });
12895
- var import_cli_maker20, cli;
13235
+ var import_cli_maker21, cli;
12896
13236
  var init_cli = __esm(() => {
12897
13237
  init_create();
12898
13238
  init_wizard();
12899
13239
  init_doctor2();
12900
13240
  init_upgrade2();
12901
13241
  init_groups();
12902
- import_cli_maker20 = __toESM(require_dist(), 1);
12903
- cli = new import_cli_maker20.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
13242
+ import_cli_maker21 = __toESM(require_dist(), 1);
13243
+ cli = new import_cli_maker21.CLI("vsceasy", "Build VS Code extensions fast — React UI + typed RPC bridge + zero-config build.", {
12904
13244
  interactive: true,
12905
- version: "0.1.6",
13245
+ version: "0.1.8",
12906
13246
  introAnimation: {
12907
13247
  enabled: true,
12908
13248
  preset: "retro-space",
12909
13249
  title: "vsceasy",
12910
- subtitle: "VS Code Extension Framework"
13250
+ subtitle: "VS Code Extension Framework",
13251
+ asciiArt: [
13252
+ ' .-""""-.',
13253
+ " / o o \\",
13254
+ " | .. |",
13255
+ " \\ '--' /",
13256
+ " /`-.__.-`\\",
13257
+ " _/ /|/||\\|\\ \\_",
13258
+ " `--`-`-``-`-`--`"
13259
+ ]
12911
13260
  },
12912
13261
  defaultCommands: {
12913
13262
  rotatePassphrase: false,
@@ -12931,6 +13280,7 @@ var init_cli = __esm(() => {
12931
13280
  cli.command(ModelGroup);
12932
13281
  cli.command(CrudGroup);
12933
13282
  cli.command(ComponentsGroup);
13283
+ cli.command(StoreGroup);
12934
13284
  cli.command(doctor_default);
12935
13285
  cli.command(upgrade_default);
12936
13286
  cli.parse(process.argv);
@@ -14,3 +14,4 @@ export declare const DbGroup: Command;
14
14
  export declare const ModelGroup: Command;
15
15
  export declare const CrudGroup: Command;
16
16
  export declare const ComponentsGroup: Command;
17
+ export declare const StoreGroup: Command;
@@ -0,0 +1,3 @@
1
+ import { Command } from '@ideascol/cli-maker';
2
+ declare const addStoreCommand: Command;
3
+ export default addStoreCommand;