@vsceasy/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +474 -0
  4. package/dist/bin/cli.d.ts +1 -0
  5. package/dist/bin/cli.js +9044 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/commands/command/add.d.ts +3 -0
  8. package/dist/commands/components/add.d.ts +3 -0
  9. package/dist/commands/create.d.ts +3 -0
  10. package/dist/commands/crud/add.d.ts +3 -0
  11. package/dist/commands/db/init.d.ts +3 -0
  12. package/dist/commands/doctor.d.ts +3 -0
  13. package/dist/commands/groups.d.ts +16 -0
  14. package/dist/commands/helper/add.d.ts +3 -0
  15. package/dist/commands/job/add.d.ts +3 -0
  16. package/dist/commands/menu/add.d.ts +3 -0
  17. package/dist/commands/menu/edit.d.ts +3 -0
  18. package/dist/commands/model/add.d.ts +3 -0
  19. package/dist/commands/panel/add.d.ts +3 -0
  20. package/dist/commands/publish/init.d.ts +3 -0
  21. package/dist/commands/rpc/add.d.ts +3 -0
  22. package/dist/commands/statusBar/add.d.ts +3 -0
  23. package/dist/commands/subpanel/add.d.ts +3 -0
  24. package/dist/commands/test/setup.d.ts +3 -0
  25. package/dist/commands/treeView/add.d.ts +3 -0
  26. package/dist/commands/upgrade.d.ts +3 -0
  27. package/dist/commands/wizard.d.ts +3 -0
  28. package/dist/data/codicons.d.ts +9 -0
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +3169 -0
  31. package/dist/lib/command/add.d.ts +31 -0
  32. package/dist/lib/components/add.d.ts +20 -0
  33. package/dist/lib/config.d.ts +10 -0
  34. package/dist/lib/crud/add.d.ts +19 -0
  35. package/dist/lib/crud/crudConfig.d.ts +37 -0
  36. package/dist/lib/crud/parseModel.d.ts +33 -0
  37. package/dist/lib/db/init.d.ts +16 -0
  38. package/dist/lib/db/wire.d.ts +10 -0
  39. package/dist/lib/doctor.d.ts +30 -0
  40. package/dist/lib/findProject.d.ts +10 -0
  41. package/dist/lib/helper/add.d.ts +14 -0
  42. package/dist/lib/iconPicker.d.ts +7 -0
  43. package/dist/lib/index.d.ts +46 -0
  44. package/dist/lib/interactive.d.ts +30 -0
  45. package/dist/lib/job/add.d.ts +24 -0
  46. package/dist/lib/menu/add.d.ts +13 -0
  47. package/dist/lib/menu/edit.d.ts +39 -0
  48. package/dist/lib/menuTree.d.ts +33 -0
  49. package/dist/lib/model/add.d.ts +27 -0
  50. package/dist/lib/model/parseFields.d.ts +14 -0
  51. package/dist/lib/panel/add.d.ts +29 -0
  52. package/dist/lib/publish/init.d.ts +12 -0
  53. package/dist/lib/rpc/add.d.ts +22 -0
  54. package/dist/lib/scaffold.d.ts +13 -0
  55. package/dist/lib/statusBar/add.d.ts +33 -0
  56. package/dist/lib/subpanel/add.d.ts +20 -0
  57. package/dist/lib/testSetup/index.d.ts +10 -0
  58. package/dist/lib/treeView/add.d.ts +13 -0
  59. package/dist/lib/upgrade.d.ts +22 -0
  60. package/dist/lib/validate.d.ts +14 -0
  61. package/dist/lib/wizard/run.d.ts +13 -0
  62. package/package.json +67 -0
  63. package/templates/_generators/command/command.ts.tpl +8 -0
  64. package/templates/_generators/components/Button.tsx.tpl +12 -0
  65. package/templates/_generators/components/Card.tsx.tpl +22 -0
  66. package/templates/_generators/components/Field.tsx.tpl +20 -0
  67. package/templates/_generators/components/Input.tsx.tpl +10 -0
  68. package/templates/_generators/components/List.tsx.tpl +29 -0
  69. package/templates/_generators/components/components.css.tpl +66 -0
  70. package/templates/_generators/components/index.ts.tpl +10 -0
  71. package/templates/_generators/crud/formApp.tsx.tpl +83 -0
  72. package/templates/_generators/crud/formNav.ts.tpl +19 -0
  73. package/templates/_generators/crud/formPanel.ts.tpl +32 -0
  74. package/templates/_generators/crud/listApp.tsx.tpl +84 -0
  75. package/templates/_generators/crud/listPanel.ts.tpl +30 -0
  76. package/templates/_generators/crud/main.tsx.tpl +6 -0
  77. package/templates/_generators/crud/service.ts.tpl +27 -0
  78. package/templates/_generators/helper/cache.ts.tpl +117 -0
  79. package/templates/_generators/helper/config.ts.tpl +36 -0
  80. package/templates/_generators/helper/db.ts.tpl +322 -0
  81. package/templates/_generators/helper/notifications.ts.tpl +45 -0
  82. package/templates/_generators/helper/secrets.ts.tpl +36 -0
  83. package/templates/_generators/helper/state.ts.tpl +44 -0
  84. package/templates/_generators/job/job.ts.tpl +10 -0
  85. package/templates/_generators/menu/menu.ts.tpl +21 -0
  86. package/templates/_generators/model/model.ts.tpl +17 -0
  87. package/templates/_generators/panel/App.tsx.tpl +10 -0
  88. package/templates/_generators/panel/main.tsx.tpl +6 -0
  89. package/templates/_generators/panel/panel.ts.tpl +5 -0
  90. package/templates/_generators/panel/templates/dashboard/App.tsx.tpl +41 -0
  91. package/templates/_generators/panel/templates/form/App.tsx.tpl +44 -0
  92. package/templates/_generators/panel/templates/list/App.tsx.tpl +40 -0
  93. package/templates/_generators/publish/CHANGELOG.md.tpl +8 -0
  94. package/templates/_generators/publish/README.md.tpl +23 -0
  95. package/templates/_generators/statusBar/statusBar.ts.tpl +7 -0
  96. package/templates/_generators/subpanel/App.tsx.tpl +10 -0
  97. package/templates/_generators/subpanel/main.tsx.tpl +6 -0
  98. package/templates/_generators/subpanel/subpanel.ts.tpl +6 -0
  99. package/templates/_generators/test/_helpers.ts.tpl +120 -0
  100. package/templates/_generators/test/sample.test.ts.tpl +38 -0
  101. package/templates/_generators/test/vitest.config.ts.tpl +23 -0
  102. package/templates/_generators/test/vscode.stub.ts.tpl +109 -0
  103. package/templates/_generators/treeView/treeView.ts.tpl +16 -0
  104. package/templates/react/.vscode/launch.json +34 -0
  105. package/templates/react/.vscode/tasks.json +32 -0
  106. package/templates/react/.vscodeignore +8 -0
  107. package/templates/react/README.md +50 -0
  108. package/templates/react/package.json +54 -0
  109. package/templates/react/scripts/gen.ts +395 -0
  110. package/templates/react/src/commands/hello.ts +6 -0
  111. package/templates/react/src/extension/extension.ts +5 -0
  112. package/templates/react/src/panels/dashboard.ts +21 -0
  113. package/templates/react/src/shared/api.ts +7 -0
  114. package/templates/react/src/shared/vsceasy/bootstrap.ts +657 -0
  115. package/templates/react/src/shared/vsceasy/client.ts +8 -0
  116. package/templates/react/src/shared/vsceasy/codiconNames.ts +196 -0
  117. package/templates/react/src/shared/vsceasy/define.ts +269 -0
  118. package/templates/react/src/shared/vsceasy/index.ts +13 -0
  119. package/templates/react/src/shared/vsceasy/rpc.ts +214 -0
  120. package/templates/react/src/webview/panels/dashboard/App.tsx +31 -0
  121. package/templates/react/src/webview/panels/dashboard/main.tsx +6 -0
  122. package/templates/react/src/webview/styles.css +33 -0
  123. package/templates/react/tsconfig.json +17 -0
  124. package/templates/react/vite.config.ts +42 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * In-memory TTL + LRU cache. Pair with the ORM for cheap reads:
3
+ *
4
+ * const cache = createCache<User>({ ttlMs: 60_000, max: 200 });
5
+ * const u = await cache.wrap(`user:${id}`, () => orm(User).findById(id));
6
+ *
7
+ * Survives only while the extension host is running (cleared on reload).
8
+ * For persistent caches, write through to the ORM or `globalState`.
9
+ */
10
+
11
+ export interface CacheOptions {
12
+ /** Time-to-live in ms. 0 = never expire. Default: 60000. */
13
+ ttlMs?: number;
14
+ /** Max entries. LRU eviction once exceeded. 0 = unlimited. Default: 500. */
15
+ max?: number;
16
+ }
17
+
18
+ export interface Cache<V> {
19
+ get(key: string): V | undefined;
20
+ set(key: string, value: V, ttlMsOverride?: number): void;
21
+ delete(key: string): boolean;
22
+ has(key: string): boolean;
23
+ clear(): void;
24
+ /** Memoize a loader. Returns cached value if fresh, else runs `fn` and stores. */
25
+ wrap(key: string, fn: () => Promise<V>, ttlMsOverride?: number): Promise<V>;
26
+ /** Force-refresh: invalidate then wrap. */
27
+ refresh(key: string, fn: () => Promise<V>, ttlMsOverride?: number): Promise<V>;
28
+ readonly size: number;
29
+ }
30
+
31
+ interface Entry<V> {
32
+ value: V;
33
+ expiresAt: number; // 0 = no expiry
34
+ }
35
+
36
+ export function createCache<V = unknown>(opts: CacheOptions = {}): Cache<V> {
37
+ const ttl = opts.ttlMs ?? 60_000;
38
+ const max = opts.max ?? 500;
39
+ // Map preserves insertion order — re-insert on access for LRU behavior.
40
+ const store = new Map<string, Entry<V>>();
41
+ // De-dupe concurrent loads for the same key.
42
+ const inflight = new Map<string, Promise<V>>();
43
+
44
+ const isFresh = (e: Entry<V>) => e.expiresAt === 0 || e.expiresAt > Date.now();
45
+
46
+ const evictIfNeeded = () => {
47
+ if (max <= 0) return;
48
+ while (store.size > max) {
49
+ const oldest = store.keys().next().value;
50
+ if (oldest === undefined) break;
51
+ store.delete(oldest);
52
+ }
53
+ };
54
+
55
+ const cache: Cache<V> = {
56
+ get(key) {
57
+ const e = store.get(key);
58
+ if (!e) return undefined;
59
+ if (!isFresh(e)) {
60
+ store.delete(key);
61
+ return undefined;
62
+ }
63
+ // LRU touch
64
+ store.delete(key);
65
+ store.set(key, e);
66
+ return e.value;
67
+ },
68
+ set(key, value, ttlMsOverride) {
69
+ const t = ttlMsOverride ?? ttl;
70
+ store.delete(key); // re-insert for LRU order
71
+ store.set(key, { value, expiresAt: t > 0 ? Date.now() + t : 0 });
72
+ evictIfNeeded();
73
+ },
74
+ delete(key) {
75
+ return store.delete(key);
76
+ },
77
+ has(key) {
78
+ const e = store.get(key);
79
+ if (!e) return false;
80
+ if (!isFresh(e)) {
81
+ store.delete(key);
82
+ return false;
83
+ }
84
+ return true;
85
+ },
86
+ clear() {
87
+ store.clear();
88
+ inflight.clear();
89
+ },
90
+ async wrap(key, fn, ttlMsOverride) {
91
+ const cached = cache.get(key);
92
+ if (cached !== undefined) return cached;
93
+ const pending = inflight.get(key);
94
+ if (pending) return pending;
95
+ const p = (async () => {
96
+ try {
97
+ const v = await fn();
98
+ cache.set(key, v, ttlMsOverride);
99
+ return v;
100
+ } finally {
101
+ inflight.delete(key);
102
+ }
103
+ })();
104
+ inflight.set(key, p);
105
+ return p;
106
+ },
107
+ async refresh(key, fn, ttlMsOverride) {
108
+ cache.delete(key);
109
+ return cache.wrap(key, fn, ttlMsOverride);
110
+ },
111
+ get size() {
112
+ return store.size;
113
+ },
114
+ };
115
+
116
+ return cache;
117
+ }
@@ -0,0 +1,36 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ /**
4
+ * Typed wrapper over `vscode.workspace.getConfiguration('{{commandPrefix}}')`.
5
+ * Reads settings declared under `contributes.configuration` in package.json.
6
+ *
7
+ * Example package.json snippet:
8
+ * "contributes": {
9
+ * "configuration": {
10
+ * "title": "{{displayName}}",
11
+ * "properties": {
12
+ * "{{commandPrefix}}.apiUrl": { "type": "string", "default": "" }
13
+ * }
14
+ * }
15
+ * }
16
+ *
17
+ * Usage:
18
+ * const url = config.get<string>('apiUrl');
19
+ * await config.set('apiUrl', 'https://...');
20
+ */
21
+ const SECTION = '{{commandPrefix}}';
22
+
23
+ export const config = {
24
+ get<T>(key: string, fallback?: T): T {
25
+ const v = vscode.workspace.getConfiguration(SECTION).get<T>(key);
26
+ return (v === undefined ? (fallback as T) : v) as T;
27
+ },
28
+ set(key: string, value: unknown, target: vscode.ConfigurationTarget = vscode.ConfigurationTarget.Global): Thenable<void> {
29
+ return vscode.workspace.getConfiguration(SECTION).update(key, value, target);
30
+ },
31
+ onChange(listener: (key: string) => void): vscode.Disposable {
32
+ return vscode.workspace.onDidChangeConfiguration((e) => {
33
+ if (e.affectsConfiguration(SECTION)) listener(SECTION);
34
+ });
35
+ },
36
+ };
@@ -0,0 +1,322 @@
1
+ import * as vscode from 'vscode';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * Mini-ORM with pluggable providers. Ships with a filesystem JSON provider that
7
+ * writes each entity to a single file under the extension's storage dir. Future
8
+ * providers (sqlite, etc.) implement the same `Provider` interface — entity
9
+ * definitions and call sites don't change.
10
+ *
11
+ * Usage:
12
+ * const User = defineEntity<{ id: string; name: string }>('users', { primaryKey: 'id' });
13
+ * const orm = createDb(context, { provider: 'storage' });
14
+ * await orm(User).insert({ id: 'u1', name: 'Jairo' });
15
+ * const u = await orm(User).findById('u1');
16
+ */
17
+
18
+ // ── Entity definition ────────────────────────────────────────────────────────
19
+
20
+ export interface EntityOptions<T> {
21
+ /** Field used as the unique key. */
22
+ primaryKey: keyof T & string;
23
+ /** Optional indexes — speeds up `findOne({ [k]: v })` for these fields. */
24
+ indexes?: (keyof T & string)[];
25
+ }
26
+
27
+ export interface Entity<T> {
28
+ readonly name: string;
29
+ readonly primaryKey: keyof T & string;
30
+ readonly indexes: (keyof T & string)[];
31
+ /** Phantom type carrier so `orm(E)` infers `T`. Never read. */
32
+ readonly __t?: T;
33
+ }
34
+
35
+ export function defineEntity<T extends object>(
36
+ name: string,
37
+ opts: EntityOptions<T>,
38
+ ): Entity<T> {
39
+ return { name, primaryKey: opts.primaryKey, indexes: opts.indexes ?? [] };
40
+ }
41
+
42
+ // ── Query types ──────────────────────────────────────────────────────────────
43
+
44
+ export type Where<T> = Partial<{ [K in keyof T]: T[K] | { in: T[K][] } | { neq: T[K] } }>;
45
+
46
+ export interface FindOptions<T> {
47
+ where?: Where<T>;
48
+ limit?: number;
49
+ offset?: number;
50
+ /** `'field:asc'` | `'field:desc'`. Default asc when only field given. */
51
+ orderBy?: `${keyof T & string}:${'asc' | 'desc'}` | (keyof T & string);
52
+ }
53
+
54
+ export interface Repository<T> {
55
+ findById(id: T[keyof T]): Promise<T | null>;
56
+ findOne(where: Where<T>): Promise<T | null>;
57
+ findMany(opts?: FindOptions<T>): Promise<T[]>;
58
+ count(opts?: { where?: Where<T> }): Promise<number>;
59
+ insert(row: T): Promise<T>;
60
+ upsert(row: T): Promise<T>;
61
+ update(id: T[keyof T], patch: Partial<T>): Promise<T | null>;
62
+ delete(id: T[keyof T]): Promise<boolean>;
63
+ deleteMany(where: Where<T>): Promise<number>;
64
+ clear(): Promise<void>;
65
+ }
66
+
67
+ // ── Provider interface (future-proof for sqlite/etc.) ────────────────────────
68
+
69
+ export interface Provider {
70
+ load(entity: string): Promise<Record<string, unknown>[]>;
71
+ save(entity: string, rows: Record<string, unknown>[]): Promise<void>;
72
+ /** Atomic batch. Implementations may optimize. */
73
+ transaction(work: (snapshot: Map<string, Record<string, unknown>[]>) => Promise<void> | void): Promise<void>;
74
+ }
75
+
76
+ // ── Storage provider (filesystem JSON) ───────────────────────────────────────
77
+
78
+ class StorageProvider implements Provider {
79
+ private cache = new Map<string, Record<string, unknown>[]>();
80
+
81
+ constructor(private readonly root: string) {
82
+ fs.mkdirSync(root, { recursive: true });
83
+ }
84
+
85
+ private fileFor(entity: string): string {
86
+ return path.join(this.root, `${entity}.json`);
87
+ }
88
+
89
+ async load(entity: string): Promise<Record<string, unknown>[]> {
90
+ if (this.cache.has(entity)) return this.cache.get(entity)!;
91
+ const f = this.fileFor(entity);
92
+ if (!fs.existsSync(f)) {
93
+ this.cache.set(entity, []);
94
+ return [];
95
+ }
96
+ try {
97
+ const rows = JSON.parse(fs.readFileSync(f, 'utf8'));
98
+ this.cache.set(entity, rows);
99
+ return rows;
100
+ } catch {
101
+ this.cache.set(entity, []);
102
+ return [];
103
+ }
104
+ }
105
+
106
+ async save(entity: string, rows: Record<string, unknown>[]): Promise<void> {
107
+ this.cache.set(entity, rows);
108
+ const f = this.fileFor(entity);
109
+ const tmp = `${f}.tmp`;
110
+ fs.writeFileSync(tmp, JSON.stringify(rows));
111
+ fs.renameSync(tmp, f); // atomic on same filesystem
112
+ }
113
+
114
+ async transaction(
115
+ work: (snapshot: Map<string, Record<string, unknown>[]>) => Promise<void> | void,
116
+ ): Promise<void> {
117
+ // Snapshot pre-tx state for rollback. Working copy is what `work` mutates.
118
+ const backup = new Map<string, Record<string, unknown>[]>();
119
+ for (const [k, v] of this.cache) backup.set(k, structuredClone(v));
120
+ const working = new Map<string, Record<string, unknown>[]>();
121
+ for (const [k, v] of this.cache) working.set(k, structuredClone(v));
122
+ try {
123
+ await work(working);
124
+ for (const [entity, rows] of working) await this.save(entity, rows);
125
+ } catch (err) {
126
+ // Roll back in-memory cache to the pre-tx snapshot.
127
+ for (const [k, v] of backup) this.cache.set(k, v);
128
+ throw err;
129
+ }
130
+ }
131
+ }
132
+
133
+ // ── Public DB type ───────────────────────────────────────────────────────────
134
+
135
+ export interface Db {
136
+ <T extends object>(entity: Entity<T>): Repository<T>;
137
+ transaction(work: (tx: Db) => Promise<void> | void): Promise<void>;
138
+ /** Wipe a single entity. */
139
+ drop(entity: Entity<unknown>): Promise<void>;
140
+ /** Underlying provider — escape hatch for advanced use. */
141
+ readonly provider: Provider;
142
+ }
143
+
144
+ export interface CreateDbOptions {
145
+ /** `'storage'` writes under `context.storageUri/<subdir>/`. `'global'` uses `globalStorageUri`. */
146
+ provider: 'storage' | 'global';
147
+ /** Override sub-directory under the chosen storage root. Default: `db`. */
148
+ subdir?: string;
149
+ }
150
+
151
+ export function createDb(ctx: vscode.ExtensionContext, opts: CreateDbOptions): Db {
152
+ // `storageUri` is only defined when a workspace/folder is open. `globalStorageUri`
153
+ // is always available — fall back to it so the extension still activates with no
154
+ // folder open (e.g. the Extension Development Host on first launch).
155
+ const baseUri =
156
+ opts.provider === 'global'
157
+ ? ctx.globalStorageUri
158
+ : (ctx.storageUri ?? ctx.globalStorageUri);
159
+ if (!baseUri) {
160
+ throw new Error('createDb: no storage URI available from the extension context.');
161
+ }
162
+ const root = path.join(baseUri.fsPath, opts.subdir ?? 'db');
163
+ const provider = new StorageProvider(root);
164
+ ctx.subscriptions.push({ dispose: () => {} }); // future hook
165
+ return makeDb(provider);
166
+ }
167
+
168
+ // ── Singleton accessor — `import { db } from './db'; await db()(Users).insert(...)` ──
169
+
170
+ let _db: Db | undefined;
171
+
172
+ /**
173
+ * Default options used by `initDb` when called as a bootstrap hook (one-arg).
174
+ * Override via the 2-arg form: `initDb(context, { provider: 'global' })`.
175
+ */
176
+ export const dbOptions: CreateDbOptions = { provider: '{{provider}}' };
177
+
178
+ /**
179
+ * Initialize the shared db. Call once on activate. Idempotent.
180
+ *
181
+ * As a bootstrap hook (recommended — `bootstrap(registry, { onActivate: [initDb] })`):
182
+ * initDb(context)
183
+ *
184
+ * Direct call with custom options:
185
+ * initDb(context, { provider: 'global' })
186
+ */
187
+ export function initDb(ctx: vscode.ExtensionContext, opts?: CreateDbOptions): Db {
188
+ if (_db) return _db;
189
+ _db = createDb(ctx, opts ?? dbOptions);
190
+ return _db;
191
+ }
192
+
193
+ /** Access the shared db. Throws if `initDb()` wasn't called yet. */
194
+ export function db(): Db {
195
+ if (!_db) throw new Error('db not initialized — call initDb(context) on activate.');
196
+ return _db;
197
+ }
198
+
199
+ function makeDb(provider: Provider): Db {
200
+ const repoFor = <T extends object>(entity: Entity<T>): Repository<T> => {
201
+ const pk = entity.primaryKey;
202
+ const load = () => provider.load(entity.name) as Promise<T[]>;
203
+ const save = (rows: T[]) => provider.save(entity.name, rows as Record<string, unknown>[]);
204
+
205
+ return {
206
+ async findById(id) {
207
+ const rows = await load();
208
+ return rows.find((r) => r[pk] === id) ?? null;
209
+ },
210
+ async findOne(where) {
211
+ const rows = await load();
212
+ return rows.find((r) => match(r, where)) ?? null;
213
+ },
214
+ async findMany(opts = {}) {
215
+ let rows = await load();
216
+ if (opts.where) rows = rows.filter((r) => match(r, opts.where!));
217
+ if (opts.orderBy) {
218
+ const [field, dir] = String(opts.orderBy).split(':');
219
+ const sign = dir === 'desc' ? -1 : 1;
220
+ rows = [...rows].sort((a, b) => compare(a[field as keyof T], b[field as keyof T]) * sign);
221
+ }
222
+ if (opts.offset) rows = rows.slice(opts.offset);
223
+ if (opts.limit !== undefined) rows = rows.slice(0, opts.limit);
224
+ return rows;
225
+ },
226
+ async count(opts = {}) {
227
+ const rows = await load();
228
+ return opts.where ? rows.filter((r) => match(r, opts.where!)).length : rows.length;
229
+ },
230
+ async insert(row) {
231
+ const rows = await load();
232
+ if (rows.some((r) => r[pk] === row[pk])) {
233
+ throw new Error(`${entity.name}: duplicate ${pk}=${String(row[pk])}`);
234
+ }
235
+ rows.push(row);
236
+ await save(rows);
237
+ return row;
238
+ },
239
+ async upsert(row) {
240
+ const rows = await load();
241
+ const i = rows.findIndex((r) => r[pk] === row[pk]);
242
+ if (i >= 0) rows[i] = row; else rows.push(row);
243
+ await save(rows);
244
+ return row;
245
+ },
246
+ async update(id, patch) {
247
+ const rows = await load();
248
+ const i = rows.findIndex((r) => r[pk] === id);
249
+ if (i < 0) return null;
250
+ rows[i] = { ...rows[i], ...patch };
251
+ await save(rows);
252
+ return rows[i];
253
+ },
254
+ async delete(id) {
255
+ const rows = await load();
256
+ const before = rows.length;
257
+ const next = rows.filter((r) => r[pk] !== id);
258
+ if (next.length === before) return false;
259
+ await save(next);
260
+ return true;
261
+ },
262
+ async deleteMany(where) {
263
+ const rows = await load();
264
+ const next = rows.filter((r) => !match(r, where));
265
+ const removed = rows.length - next.length;
266
+ if (removed > 0) await save(next);
267
+ return removed;
268
+ },
269
+ async clear() {
270
+ await save([]);
271
+ },
272
+ };
273
+ };
274
+
275
+ const db: Db = Object.assign(repoFor as any, {
276
+ provider,
277
+ async drop(entity: Entity<unknown>) {
278
+ await provider.save(entity.name, []);
279
+ },
280
+ async transaction(work: (tx: Db) => Promise<void> | void) {
281
+ await provider.transaction(async (snapshot) => {
282
+ // Build a tx-scoped Db that reads/writes the snapshot map.
283
+ const txProvider: Provider = {
284
+ async load(name) { return snapshot.get(name) ?? []; },
285
+ async save(name, rows) { snapshot.set(name, rows); },
286
+ async transaction() { throw new Error('Nested transactions are not supported'); },
287
+ };
288
+ await work(makeDb(txProvider));
289
+ });
290
+ },
291
+ });
292
+
293
+ return db;
294
+ }
295
+
296
+ // ── matcher ──────────────────────────────────────────────────────────────────
297
+
298
+ function match<T extends object>(row: T, where: Where<T>): boolean {
299
+ for (const key of Object.keys(where) as (keyof T)[]) {
300
+ const expected = where[key] as unknown;
301
+ const actual = row[key];
302
+ if (expected && typeof expected === 'object' && !Array.isArray(expected)) {
303
+ if ('in' in expected) {
304
+ if (!(expected as { in: unknown[] }).in.includes(actual)) return false;
305
+ continue;
306
+ }
307
+ if ('neq' in expected) {
308
+ if (actual === (expected as { neq: unknown }).neq) return false;
309
+ continue;
310
+ }
311
+ }
312
+ if (actual !== expected) return false;
313
+ }
314
+ return true;
315
+ }
316
+
317
+ function compare(a: unknown, b: unknown): number {
318
+ if (a === b) return 0;
319
+ if (a === null || a === undefined) return -1;
320
+ if (b === null || b === undefined) return 1;
321
+ return a < b ? -1 : 1;
322
+ }
@@ -0,0 +1,45 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ /**
4
+ * Concise wrappers over `vscode.window.show*Message`. Each accepts an optional
5
+ * list of action labels and resolves to the selected label (or undefined).
6
+ *
7
+ * Usage:
8
+ * notify.info('Saved');
9
+ * const pick = await notify.warn('Discard?', 'Discard', 'Keep');
10
+ * if (pick === 'Discard') ...
11
+ *
12
+ * For long-running tasks use `withProgress`:
13
+ * await withProgress('Indexing…', async (report) => {
14
+ * for (let i = 0; i <= 100; i += 10) {
15
+ * report({ increment: 10, message: `${i}%` });
16
+ * await new Promise(r => setTimeout(r, 100));
17
+ * }
18
+ * });
19
+ */
20
+ export const notify = {
21
+ info(message: string, ...actions: string[]) {
22
+ return vscode.window.showInformationMessage(message, ...actions);
23
+ },
24
+ warn(message: string, ...actions: string[]) {
25
+ return vscode.window.showWarningMessage(message, ...actions);
26
+ },
27
+ error(message: string, ...actions: string[]) {
28
+ return vscode.window.showErrorMessage(message, ...actions);
29
+ },
30
+ confirm(message: string, yesLabel = 'Yes', noLabel = 'No'): Thenable<boolean> {
31
+ return vscode.window
32
+ .showInformationMessage(message, { modal: true }, yesLabel, noLabel)
33
+ .then((pick) => pick === yesLabel);
34
+ },
35
+ };
36
+
37
+ export function withProgress<T>(
38
+ title: string,
39
+ task: (report: (p: { message?: string; increment?: number }) => void) => Thenable<T> | T,
40
+ location: vscode.ProgressLocation = vscode.ProgressLocation.Notification,
41
+ ): Thenable<T> {
42
+ return vscode.window.withProgress({ location, title, cancellable: false }, (progress) =>
43
+ Promise.resolve(task((p) => progress.report(p))),
44
+ );
45
+ }
@@ -0,0 +1,36 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ /**
4
+ * Typed wrapper over `context.secrets` (SecretStorage backed by OS keychain).
5
+ * Inject the extension context once on activate (bootstrap does this if you
6
+ * import this module from your extension entry).
7
+ *
8
+ * Usage:
9
+ * await secrets.set('githubToken', 'ghp_xxx');
10
+ * const token = await secrets.get('githubToken');
11
+ */
12
+ let _ctx: vscode.ExtensionContext | undefined;
13
+
14
+ export function initSecrets(ctx: vscode.ExtensionContext) {
15
+ _ctx = ctx;
16
+ }
17
+
18
+ function ctx(): vscode.ExtensionContext {
19
+ if (!_ctx) throw new Error('Secrets helper not initialized — call initSecrets(context) on activate.');
20
+ return _ctx;
21
+ }
22
+
23
+ export const secrets = {
24
+ get(key: string): Thenable<string | undefined> {
25
+ return ctx().secrets.get(key);
26
+ },
27
+ set(key: string, value: string): Thenable<void> {
28
+ return ctx().secrets.store(key, value);
29
+ },
30
+ delete(key: string): Thenable<void> {
31
+ return ctx().secrets.delete(key);
32
+ },
33
+ onChange(listener: (key: string) => void): vscode.Disposable {
34
+ return ctx().secrets.onDidChange((e) => listener(e.key));
35
+ },
36
+ };
@@ -0,0 +1,44 @@
1
+ import * as vscode from 'vscode';
2
+
3
+ /**
4
+ * Typed wrapper over `context.{workspaceState, globalState}`.
5
+ * - workspace: scoped to the current workspace (per-project preferences)
6
+ * - global: shared across all workspaces (user-wide settings, last-opened file)
7
+ *
8
+ * Usage:
9
+ * await state.workspace.set('lastQuery', 'foo');
10
+ * const q = state.workspace.get<string>('lastQuery');
11
+ */
12
+ let _ctx: vscode.ExtensionContext | undefined;
13
+
14
+ export function initState(ctx: vscode.ExtensionContext) {
15
+ _ctx = ctx;
16
+ }
17
+
18
+ function ctx(): vscode.ExtensionContext {
19
+ if (!_ctx) throw new Error('State helper not initialized — call initState(context) on activate.');
20
+ return _ctx;
21
+ }
22
+
23
+ function wrap(memento: () => vscode.Memento) {
24
+ return {
25
+ get<T>(key: string, fallback?: T): T | undefined {
26
+ const v = memento().get<T>(key);
27
+ return v === undefined ? fallback : v;
28
+ },
29
+ set(key: string, value: unknown): Thenable<void> {
30
+ return memento().update(key, value);
31
+ },
32
+ delete(key: string): Thenable<void> {
33
+ return memento().update(key, undefined);
34
+ },
35
+ keys(): readonly string[] {
36
+ return memento().keys();
37
+ },
38
+ };
39
+ }
40
+
41
+ export const state = {
42
+ workspace: wrap(() => ctx().workspaceState),
43
+ global: wrap(() => ctx().globalState),
44
+ };
@@ -0,0 +1,10 @@
1
+ import { defineJob } from '../shared/vsceasy';
2
+
3
+ export default defineJob({
4
+ title: '{{title}}',
5
+ schedule: {{schedule}},{{minIntervalLine}}
6
+ run: async (vscode, ctx) => {
7
+ // TODO: implement {{name}} work
8
+ console.log('[{{name}}] tick', new Date().toISOString());
9
+ },
10
+ });
@@ -0,0 +1,21 @@
1
+ import { defineMenu } from '../shared/vsceasy';
2
+
3
+ export default defineMenu({
4
+ title: '{{title}}',
5
+ icon: '{{icon}}',
6
+ items: [
7
+ {
8
+ label: 'Panels',
9
+ children: [
10
+ // { label: 'Dashboard', panel: 'dashboard' },
11
+ ],
12
+ },
13
+ {
14
+ label: 'Actions',
15
+ children: [
16
+ // { label: 'Hello', command: 'hello', icon: 'play' },
17
+ // { label: 'Docs', url: 'https://example.com', icon: 'book' },
18
+ ],
19
+ },
20
+ ],
21
+ });
@@ -0,0 +1,17 @@
1
+ import { defineEntity, db } from '../helpers/db';
2
+
3
+ export interface {{Name}} {
4
+ {{fieldLines}}
5
+ }
6
+
7
+ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
8
+ primaryKey: '{{primaryKey}}',{{indexesLine}}
9
+ });
10
+
11
+ /**
12
+ * Typed repo accessor. Lazy — assumes `initDb(context)` ran on activate.
13
+ *
14
+ * import { {{Plural}}Repo } from '../models/{{Name}}';
15
+ * await {{Plural}}Repo().insert({ ... });
16
+ */
17
+ export const {{Plural}}Repo = () => db()({{Plural}});
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ {{apiBlock}}
3
+ export function App() {
4
+ return (
5
+ <div className="app">
6
+ <h1>{{title}}</h1>
7
+ <p>Edit <code>src/webview/panels/{{name}}/App.tsx</code> to start building.</p>
8
+ </div>
9
+ );
10
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import '../../styles.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(<App />);
@@ -0,0 +1,5 @@
1
+ import { definePanel } from '../shared/vsceasy';
2
+ {{apiImport}}
3
+ export default definePanel{{apiGeneric}}({
4
+ title: '{{title}}',{{rpcBlock}}
5
+ });