@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.
- package/CHANGELOG.md +45 -0
- package/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +9044 -0
- package/dist/cli.d.ts +3 -0
- package/dist/commands/command/add.d.ts +3 -0
- package/dist/commands/components/add.d.ts +3 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/crud/add.d.ts +3 -0
- package/dist/commands/db/init.d.ts +3 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/groups.d.ts +16 -0
- package/dist/commands/helper/add.d.ts +3 -0
- package/dist/commands/job/add.d.ts +3 -0
- package/dist/commands/menu/add.d.ts +3 -0
- package/dist/commands/menu/edit.d.ts +3 -0
- package/dist/commands/model/add.d.ts +3 -0
- package/dist/commands/panel/add.d.ts +3 -0
- package/dist/commands/publish/init.d.ts +3 -0
- package/dist/commands/rpc/add.d.ts +3 -0
- package/dist/commands/statusBar/add.d.ts +3 -0
- package/dist/commands/subpanel/add.d.ts +3 -0
- package/dist/commands/test/setup.d.ts +3 -0
- package/dist/commands/treeView/add.d.ts +3 -0
- package/dist/commands/upgrade.d.ts +3 -0
- package/dist/commands/wizard.d.ts +3 -0
- package/dist/data/codicons.d.ts +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3169 -0
- package/dist/lib/command/add.d.ts +31 -0
- package/dist/lib/components/add.d.ts +20 -0
- package/dist/lib/config.d.ts +10 -0
- package/dist/lib/crud/add.d.ts +19 -0
- package/dist/lib/crud/crudConfig.d.ts +37 -0
- package/dist/lib/crud/parseModel.d.ts +33 -0
- package/dist/lib/db/init.d.ts +16 -0
- package/dist/lib/db/wire.d.ts +10 -0
- package/dist/lib/doctor.d.ts +30 -0
- package/dist/lib/findProject.d.ts +10 -0
- package/dist/lib/helper/add.d.ts +14 -0
- package/dist/lib/iconPicker.d.ts +7 -0
- package/dist/lib/index.d.ts +46 -0
- package/dist/lib/interactive.d.ts +30 -0
- package/dist/lib/job/add.d.ts +24 -0
- package/dist/lib/menu/add.d.ts +13 -0
- package/dist/lib/menu/edit.d.ts +39 -0
- package/dist/lib/menuTree.d.ts +33 -0
- package/dist/lib/model/add.d.ts +27 -0
- package/dist/lib/model/parseFields.d.ts +14 -0
- package/dist/lib/panel/add.d.ts +29 -0
- package/dist/lib/publish/init.d.ts +12 -0
- package/dist/lib/rpc/add.d.ts +22 -0
- package/dist/lib/scaffold.d.ts +13 -0
- package/dist/lib/statusBar/add.d.ts +33 -0
- package/dist/lib/subpanel/add.d.ts +20 -0
- package/dist/lib/testSetup/index.d.ts +10 -0
- package/dist/lib/treeView/add.d.ts +13 -0
- package/dist/lib/upgrade.d.ts +22 -0
- package/dist/lib/validate.d.ts +14 -0
- package/dist/lib/wizard/run.d.ts +13 -0
- package/package.json +67 -0
- package/templates/_generators/command/command.ts.tpl +8 -0
- package/templates/_generators/components/Button.tsx.tpl +12 -0
- package/templates/_generators/components/Card.tsx.tpl +22 -0
- package/templates/_generators/components/Field.tsx.tpl +20 -0
- package/templates/_generators/components/Input.tsx.tpl +10 -0
- package/templates/_generators/components/List.tsx.tpl +29 -0
- package/templates/_generators/components/components.css.tpl +66 -0
- package/templates/_generators/components/index.ts.tpl +10 -0
- package/templates/_generators/crud/formApp.tsx.tpl +83 -0
- package/templates/_generators/crud/formNav.ts.tpl +19 -0
- package/templates/_generators/crud/formPanel.ts.tpl +32 -0
- package/templates/_generators/crud/listApp.tsx.tpl +84 -0
- package/templates/_generators/crud/listPanel.ts.tpl +30 -0
- package/templates/_generators/crud/main.tsx.tpl +6 -0
- package/templates/_generators/crud/service.ts.tpl +27 -0
- package/templates/_generators/helper/cache.ts.tpl +117 -0
- package/templates/_generators/helper/config.ts.tpl +36 -0
- package/templates/_generators/helper/db.ts.tpl +322 -0
- package/templates/_generators/helper/notifications.ts.tpl +45 -0
- package/templates/_generators/helper/secrets.ts.tpl +36 -0
- package/templates/_generators/helper/state.ts.tpl +44 -0
- package/templates/_generators/job/job.ts.tpl +10 -0
- package/templates/_generators/menu/menu.ts.tpl +21 -0
- package/templates/_generators/model/model.ts.tpl +17 -0
- package/templates/_generators/panel/App.tsx.tpl +10 -0
- package/templates/_generators/panel/main.tsx.tpl +6 -0
- package/templates/_generators/panel/panel.ts.tpl +5 -0
- package/templates/_generators/panel/templates/dashboard/App.tsx.tpl +41 -0
- package/templates/_generators/panel/templates/form/App.tsx.tpl +44 -0
- package/templates/_generators/panel/templates/list/App.tsx.tpl +40 -0
- package/templates/_generators/publish/CHANGELOG.md.tpl +8 -0
- package/templates/_generators/publish/README.md.tpl +23 -0
- package/templates/_generators/statusBar/statusBar.ts.tpl +7 -0
- package/templates/_generators/subpanel/App.tsx.tpl +10 -0
- package/templates/_generators/subpanel/main.tsx.tpl +6 -0
- package/templates/_generators/subpanel/subpanel.ts.tpl +6 -0
- package/templates/_generators/test/_helpers.ts.tpl +120 -0
- package/templates/_generators/test/sample.test.ts.tpl +38 -0
- package/templates/_generators/test/vitest.config.ts.tpl +23 -0
- package/templates/_generators/test/vscode.stub.ts.tpl +109 -0
- package/templates/_generators/treeView/treeView.ts.tpl +16 -0
- package/templates/react/.vscode/launch.json +34 -0
- package/templates/react/.vscode/tasks.json +32 -0
- package/templates/react/.vscodeignore +8 -0
- package/templates/react/README.md +50 -0
- package/templates/react/package.json +54 -0
- package/templates/react/scripts/gen.ts +395 -0
- package/templates/react/src/commands/hello.ts +6 -0
- package/templates/react/src/extension/extension.ts +5 -0
- package/templates/react/src/panels/dashboard.ts +21 -0
- package/templates/react/src/shared/api.ts +7 -0
- package/templates/react/src/shared/vsceasy/bootstrap.ts +657 -0
- package/templates/react/src/shared/vsceasy/client.ts +8 -0
- package/templates/react/src/shared/vsceasy/codiconNames.ts +196 -0
- package/templates/react/src/shared/vsceasy/define.ts +269 -0
- package/templates/react/src/shared/vsceasy/index.ts +13 -0
- package/templates/react/src/shared/vsceasy/rpc.ts +214 -0
- package/templates/react/src/webview/panels/dashboard/App.tsx +31 -0
- package/templates/react/src/webview/panels/dashboard/main.tsx +6 -0
- package/templates/react/src/webview/styles.css +33 -0
- package/templates/react/tsconfig.json +17 -0
- package/templates/react/vite.config.ts +42 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type UpgradeStatus = 'in-sync' | 'would-create' | 'would-update' | 'created' | 'updated' | 'missing-source';
|
|
2
|
+
export interface FileChange {
|
|
3
|
+
path: string;
|
|
4
|
+
status: UpgradeStatus;
|
|
5
|
+
}
|
|
6
|
+
export interface UpgradeOptions {
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
/** Bundled templates root (output of findTemplatesRoot). */
|
|
9
|
+
templatesRoot: string;
|
|
10
|
+
/** UI variant subfolder. Default 'react'. */
|
|
11
|
+
ui?: string;
|
|
12
|
+
/** Apply changes. Default false (dry-run). */
|
|
13
|
+
apply?: boolean;
|
|
14
|
+
/** Run `bun run gen` after apply. Default true when apply, ignored on dry-run. */
|
|
15
|
+
runGen?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface UpgradeResult {
|
|
18
|
+
changes: FileChange[];
|
|
19
|
+
applied: boolean;
|
|
20
|
+
genRan: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function upgrade(opts: UpgradeOptions): UpgradeResult;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a generator identifier — camelCase, starts with a letter.
|
|
3
|
+
* Throws with a clear message naming the field that failed.
|
|
4
|
+
*/
|
|
5
|
+
export declare function assertId(field: string, value: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Throw a friendly error if `target` already exists. Quotes the project-relative path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function assertNoOverwrite(projectRoot: string, target: string, kind: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Throw if a referenced sibling resource (panel, menu, command) is missing on disk.
|
|
12
|
+
* Error message lists the existing siblings so the user can pick one without re-running.
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSiblingExists(projectRoot: string, kind: 'panel' | 'menu' | 'command' | 'subpanel', id: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive wizard. Detects whether the cwd is inside a vsceasy project:
|
|
3
|
+
* outside → guides `create`; inside → menus the common generators and delegates
|
|
4
|
+
* to the same lib functions the CLI commands use. Anything not wired here is
|
|
5
|
+
* surfaced as the exact `vsceasy …` command to run.
|
|
6
|
+
*
|
|
7
|
+
* Designed for `findTemplatesRoot(__dirname)` to resolve the bundled templates
|
|
8
|
+
* whether running from src/ or dist/.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runWizard(opts?: {
|
|
11
|
+
templatesRoot?: string;
|
|
12
|
+
cwd?: string;
|
|
13
|
+
}): Promise<void>;
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vsceasy/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "bun test ./src/tests",
|
|
8
|
+
"gen:types": "bun scripts/genCodiconTypes.ts",
|
|
9
|
+
"sync:runtime": "bun scripts/syncRuntime.ts",
|
|
10
|
+
"build:runtime": "cd packages/vsceasy-runtime && bun install && bunx tsc",
|
|
11
|
+
"build": "bun run gen:types && bun run sync:runtime && bun run build:runtime && tsc --emitDeclarationOnly && bun build ./src/index.ts ./src/bin/cli.ts --target=node --outdir ./dist --format cjs",
|
|
12
|
+
"build:test": "bun build ./src/tests/*.ts --target=node --outdir dist/tests --format cjs",
|
|
13
|
+
"prepublishOnly": "bun run build",
|
|
14
|
+
"start": "bun run src/cli.ts",
|
|
15
|
+
"lint": "eslint 'src/**/*.ts'",
|
|
16
|
+
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
|
17
|
+
"format": "prettier --write 'src/**/*.{ts,tsx,json,md}' '*.md'",
|
|
18
|
+
"format:check": "prettier --check 'src/**/*.{ts,tsx,json,md}' '*.md'",
|
|
19
|
+
"release:patch": "npm version patch -m 'chore(release): %s'",
|
|
20
|
+
"release:minor": "npm version minor -m 'chore(release): %s'",
|
|
21
|
+
"release:major": "npm version major -m 'chore(release): %s'"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"vscode",
|
|
25
|
+
"vscode-extension",
|
|
26
|
+
"framework",
|
|
27
|
+
"scaffold",
|
|
28
|
+
"cli",
|
|
29
|
+
"react",
|
|
30
|
+
"webview",
|
|
31
|
+
"rpc"
|
|
32
|
+
],
|
|
33
|
+
"author": "Jairo <jairofg12@gmail.com>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"type": "commonjs",
|
|
36
|
+
"bin": {
|
|
37
|
+
"vsceasy": "dist/bin/cli.js"
|
|
38
|
+
},
|
|
39
|
+
"types": "dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist/**/*",
|
|
42
|
+
"templates/**/*",
|
|
43
|
+
"LICENSE",
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md",
|
|
46
|
+
"!src/tests/**/*"
|
|
47
|
+
],
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=24.11.1"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^25.9.1",
|
|
56
|
+
"@types/vscode": "^1.43.2",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
58
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
59
|
+
"eslint": "^9.0.0",
|
|
60
|
+
"eslint-config-prettier": "^9.1.0",
|
|
61
|
+
"prettier": "^3.3.0",
|
|
62
|
+
"typescript": "^6.0.3"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@ideascol/cli-maker": "^2.2.2"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type Variant = 'primary' | 'secondary';
|
|
4
|
+
|
|
5
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
6
|
+
variant?: Variant;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Theme-aware button using VS Code button tokens. */
|
|
10
|
+
export function Button({ variant = 'primary', className = '', ...rest }: ButtonProps) {
|
|
11
|
+
return <button className={`vx-btn vx-btn--${variant} ${className}`.trim()} {...rest} />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface CardProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
actions?: React.ReactNode;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Bordered surface for grouping content, with an optional title + actions row. */
|
|
10
|
+
export function Card({ title, actions, children }: CardProps) {
|
|
11
|
+
return (
|
|
12
|
+
<section className="vx-card">
|
|
13
|
+
{(title || actions) && (
|
|
14
|
+
<header className="vx-card__head">
|
|
15
|
+
{title ? <h2 className="vx-card__title">{title}</h2> : <span />}
|
|
16
|
+
{actions ? <div className="vx-card__actions">{actions}</div> : null}
|
|
17
|
+
</header>
|
|
18
|
+
)}
|
|
19
|
+
<div className="vx-card__body">{children}</div>
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface FieldProps {
|
|
4
|
+
label: string;
|
|
5
|
+
htmlFor?: string;
|
|
6
|
+
hint?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Labeled form field wrapper with optional hint / error text. */
|
|
12
|
+
export function Field({ label, htmlFor, hint, error, children }: FieldProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className="vx-field">
|
|
15
|
+
<label className="vx-field__label" htmlFor={htmlFor}>{label}</label>
|
|
16
|
+
{children}
|
|
17
|
+
{error ? <span className="vx-field__error">{error}</span> : hint ? <span className="vx-field__hint">{hint}</span> : null}
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
4
|
+
|
|
5
|
+
/** Theme-aware text input using VS Code input tokens. */
|
|
6
|
+
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
7
|
+
function Input({ className = '', ...rest }, ref) {
|
|
8
|
+
return <input ref={ref} className={`vx-input ${className}`.trim()} {...rest} />;
|
|
9
|
+
},
|
|
10
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ListProps<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
getKey: (item: T, index: number) => string | number;
|
|
6
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
7
|
+
onSelect?: (item: T, index: number) => void;
|
|
8
|
+
empty?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Selectable, theme-aware list. Rows highlight on hover and on selection. */
|
|
12
|
+
export function List<T>({ items, getKey, renderItem, onSelect, empty }: ListProps<T>) {
|
|
13
|
+
if (items.length === 0) {
|
|
14
|
+
return <div className="vx-list__empty">{empty ?? 'Nothing here yet.'}</div>;
|
|
15
|
+
}
|
|
16
|
+
return (
|
|
17
|
+
<ul className="vx-list" role="list">
|
|
18
|
+
{items.map((item, i) => (
|
|
19
|
+
<li
|
|
20
|
+
key={getKey(item, i)}
|
|
21
|
+
className={`vx-list__row${onSelect ? ' vx-list__row--clickable' : ''}`}
|
|
22
|
+
onClick={onSelect ? () => onSelect(item, i) : undefined}
|
|
23
|
+
>
|
|
24
|
+
{renderItem(item, i)}
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
.vx-btn {
|
|
2
|
+
font: inherit;
|
|
3
|
+
padding: 0.35rem 0.85rem;
|
|
4
|
+
border-radius: 2px;
|
|
5
|
+
border: 1px solid transparent;
|
|
6
|
+
cursor: pointer;
|
|
7
|
+
}
|
|
8
|
+
.vx-btn:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: 2px; }
|
|
9
|
+
.vx-btn:disabled { opacity: 0.5; cursor: default; }
|
|
10
|
+
.vx-btn--primary {
|
|
11
|
+
color: var(--vscode-button-foreground);
|
|
12
|
+
background: var(--vscode-button-background);
|
|
13
|
+
}
|
|
14
|
+
.vx-btn--primary:hover:not(:disabled) { background: var(--vscode-button-hoverBackground); }
|
|
15
|
+
.vx-btn--secondary {
|
|
16
|
+
color: var(--vscode-button-secondaryForeground, var(--vscode-foreground));
|
|
17
|
+
background: var(--vscode-button-secondaryBackground, transparent);
|
|
18
|
+
border-color: var(--vscode-button-border, var(--vscode-input-border, #555));
|
|
19
|
+
}
|
|
20
|
+
.vx-btn--secondary:hover:not(:disabled) { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-list-hoverBackground)); }
|
|
21
|
+
|
|
22
|
+
.vx-input {
|
|
23
|
+
font: inherit;
|
|
24
|
+
width: 100%;
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
padding: 0.35rem 0.5rem;
|
|
27
|
+
border-radius: 2px;
|
|
28
|
+
color: var(--vscode-input-foreground);
|
|
29
|
+
background: var(--vscode-input-background);
|
|
30
|
+
border: 1px solid var(--vscode-input-border, #555);
|
|
31
|
+
}
|
|
32
|
+
.vx-input:focus-visible { outline: 1px solid var(--vscode-focusBorder); outline-offset: -1px; }
|
|
33
|
+
.vx-input::placeholder { color: var(--vscode-input-placeholderForeground); }
|
|
34
|
+
|
|
35
|
+
.vx-field { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.75rem; }
|
|
36
|
+
.vx-field__label { font-size: 0.85em; color: var(--vscode-foreground); }
|
|
37
|
+
.vx-field__hint { font-size: 0.8em; color: var(--vscode-descriptionForeground); }
|
|
38
|
+
.vx-field__error { font-size: 0.8em; color: var(--vscode-errorForeground); }
|
|
39
|
+
|
|
40
|
+
.vx-card {
|
|
41
|
+
border: 1px solid var(--vscode-panel-border, var(--vscode-input-border, #555));
|
|
42
|
+
border-radius: 4px;
|
|
43
|
+
margin-bottom: 0.75rem;
|
|
44
|
+
background: var(--vscode-editorWidget-background, transparent);
|
|
45
|
+
}
|
|
46
|
+
.vx-card__head {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: space-between;
|
|
50
|
+
gap: 0.5rem;
|
|
51
|
+
padding: 0.5rem 0.75rem;
|
|
52
|
+
border-bottom: 1px solid var(--vscode-panel-border, var(--vscode-input-border, #555));
|
|
53
|
+
}
|
|
54
|
+
.vx-card__title { font-size: 1em; font-weight: 600; margin: 0; }
|
|
55
|
+
.vx-card__actions { display: flex; gap: 0.5rem; }
|
|
56
|
+
.vx-card__body { padding: 0.75rem; }
|
|
57
|
+
|
|
58
|
+
.vx-list { list-style: none; margin: 0; padding: 0; }
|
|
59
|
+
.vx-list__row {
|
|
60
|
+
padding: 0.4rem 0.6rem;
|
|
61
|
+
border-radius: 2px;
|
|
62
|
+
line-height: 1.5;
|
|
63
|
+
}
|
|
64
|
+
.vx-list__row--clickable { cursor: pointer; }
|
|
65
|
+
.vx-list__row--clickable:hover { background: var(--vscode-list-hoverBackground); }
|
|
66
|
+
.vx-list__empty { color: var(--vscode-descriptionForeground); padding: 0.6rem; }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Button } from './Button';
|
|
2
|
+
export type { ButtonProps } from './Button';
|
|
3
|
+
export { Input } from './Input';
|
|
4
|
+
export type { InputProps } from './Input';
|
|
5
|
+
export { Field } from './Field';
|
|
6
|
+
export type { FieldProps } from './Field';
|
|
7
|
+
export { Card } from './Card';
|
|
8
|
+
export type { CardProps } from './Card';
|
|
9
|
+
export { List } from './List';
|
|
10
|
+
export type { ListProps } from './List';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { connectWebview } from '../../../shared/vsceasy/client';
|
|
3
|
+
import type { {{Name}}FormApi } from '../../../shared/api';
|
|
4
|
+
import type { {{Name}} } from '../../../models/{{Name}}';
|
|
5
|
+
|
|
6
|
+
const api = connectWebview<{{Name}}FormApi>();
|
|
7
|
+
|
|
8
|
+
type FormState = Partial<{{Name}}>;
|
|
9
|
+
|
|
10
|
+
const emptyForm: FormState = {{emptyFormLiteral}};
|
|
11
|
+
|
|
12
|
+
export function App() {
|
|
13
|
+
const [form, setForm] = useState<FormState>(emptyForm);
|
|
14
|
+
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
const [saving, setSaving] = useState(false);
|
|
17
|
+
|
|
18
|
+
const load = useCallback(async () => {
|
|
19
|
+
// The list stashes the row id before revealing this panel. Pull it (the host
|
|
20
|
+
// clears it after handing it over) and pre-fill the form for editing.
|
|
21
|
+
const id = await api.pendingId();
|
|
22
|
+
if (id == null || id === '') {
|
|
23
|
+
setForm(emptyForm);
|
|
24
|
+
setEditingId(null);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const row = await api.get(id);
|
|
28
|
+
if (row) {
|
|
29
|
+
setForm(row);
|
|
30
|
+
setEditingId(row.{{primaryKey}});
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
void load();
|
|
36
|
+
// Webviews retain state when hidden, so re-load whenever the panel is
|
|
37
|
+
// revealed — the list may have asked to edit a different row.
|
|
38
|
+
const onFocus = () => { void load(); };
|
|
39
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void load(); };
|
|
40
|
+
window.addEventListener('focus', onFocus);
|
|
41
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
42
|
+
return () => {
|
|
43
|
+
window.removeEventListener('focus', onFocus);
|
|
44
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
45
|
+
};
|
|
46
|
+
}, [load]);
|
|
47
|
+
|
|
48
|
+
const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
|
|
49
|
+
setForm((f) => ({ ...f, [k]: v }));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const onSubmit = async (e: React.FormEvent) => {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
setSaving(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
try {
|
|
57
|
+
const wasNew = editingId == null;
|
|
58
|
+
await api.save(form as {{Name}});
|
|
59
|
+
// After creating a new row, reset for the next entry. After an edit, keep
|
|
60
|
+
// the row loaded so further tweaks are possible.
|
|
61
|
+
if (wasNew) {
|
|
62
|
+
setForm(emptyForm);
|
|
63
|
+
setEditingId(null);
|
|
64
|
+
}
|
|
65
|
+
} catch (err: any) {
|
|
66
|
+
setError(String(err?.message ?? err));
|
|
67
|
+
} finally {
|
|
68
|
+
setSaving(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<form onSubmit={onSubmit} style={{ padding: 16, display: 'grid', gap: 12, color: 'var(--vscode-foreground)' }}>
|
|
74
|
+
<h2 style={{ margin: 0 }}>{editingId ? 'Edit {{title}}' : 'New {{title}}'}</h2>
|
|
75
|
+
{{formFieldInputs}}
|
|
76
|
+
{error && <div style={{ color: 'var(--vscode-errorForeground)' }}>{error}</div>}
|
|
77
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
78
|
+
<button type="submit" disabled={saving}>{saving ? 'Saving…' : 'Save'}</button>
|
|
79
|
+
<button type="button" onClick={() => { setForm(emptyForm); setEditingId(null); }}>Reset</button>
|
|
80
|
+
</div>
|
|
81
|
+
</form>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory hand-off for "open the {{Name}} form to edit row X". The list panel
|
|
3
|
+
* sets the pending id before revealing the form; the form reads (and clears) it
|
|
4
|
+
* on mount. Lives in the extension host, shared across the two panel modules.
|
|
5
|
+
*/
|
|
6
|
+
type {{Name}}Id = unknown;
|
|
7
|
+
|
|
8
|
+
let pendingId: {{Name}}Id | null = null;
|
|
9
|
+
|
|
10
|
+
export function setPending{{Name}}Id(id: {{Name}}Id | null): void {
|
|
11
|
+
pendingId = id ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns the pending id once, then clears it. */
|
|
15
|
+
export function takePending{{Name}}Id(): {{Name}}Id | null {
|
|
16
|
+
const v = pendingId;
|
|
17
|
+
pendingId = null;
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { definePanel } from '../shared/vsceasy';
|
|
2
|
+
import { {{Name}}Service } from '../services/{{Name}}Service';
|
|
3
|
+
import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
|
|
4
|
+
import type { {{Name}}FormApi } from '../shared/api';
|
|
5
|
+
import type { {{Name}} } from '../models/{{Name}}';
|
|
6
|
+
|
|
7
|
+
export default definePanel<{{Name}}FormApi>({
|
|
8
|
+
title: '{{title}}',
|
|
9
|
+
column: 'beside',
|
|
10
|
+
command: { title: '{{title}}: New / Edit' },
|
|
11
|
+
rpc: (vscode) => ({
|
|
12
|
+
async pendingId() {
|
|
13
|
+
// Consumed once by the webview on mount to decide edit vs new.
|
|
14
|
+
return (takePending{{Name}}Id() as {{Name}}['{{primaryKey}}'] | null) ?? null;
|
|
15
|
+
},
|
|
16
|
+
async get(id) {
|
|
17
|
+
if (!id) return null;
|
|
18
|
+
return {{Name}}Service.get(id as {{Name}}['{{primaryKey}}']);
|
|
19
|
+
},
|
|
20
|
+
async save(row) {
|
|
21
|
+
const saved = await {{Name}}Service.save(row);
|
|
22
|
+
void vscode.window.showInformationMessage(`{{title}} saved (${String(saved.{{primaryKey}})})`);
|
|
23
|
+
// Reveal the list so the new/edited row shows. Revealing fires the list
|
|
24
|
+
// webview's focus/visibility listener, which reloads it.
|
|
25
|
+
void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
|
|
26
|
+
return saved;
|
|
27
|
+
},
|
|
28
|
+
async cancel() {
|
|
29
|
+
// No-op — webview closes itself.
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
2
|
+
import { connectWebview } from '../../../shared/vsceasy/client';
|
|
3
|
+
import type { {{Plural}}ListApi } from '../../../shared/api';
|
|
4
|
+
import type { {{Name}} } from '../../../models/{{Name}}';
|
|
5
|
+
|
|
6
|
+
const api = connectWebview<{{Plural}}ListApi>();
|
|
7
|
+
|
|
8
|
+
export function App() {
|
|
9
|
+
const [rows, setRows] = useState<{{Name}}[]>([]);
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [error, setError] = useState<string | null>(null);
|
|
12
|
+
|
|
13
|
+
const reload = useCallback(async () => {
|
|
14
|
+
setLoading(true);
|
|
15
|
+
try {
|
|
16
|
+
setRows(await api.list());
|
|
17
|
+
setError(null);
|
|
18
|
+
} catch (e: any) {
|
|
19
|
+
setError(String(e?.message ?? e));
|
|
20
|
+
} finally {
|
|
21
|
+
setLoading(false);
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
void reload();
|
|
27
|
+
// Webviews keep their state when hidden (retainContextWhenHidden), so the
|
|
28
|
+
// mount effect won't re-run when the panel is revealed again. Reload when the
|
|
29
|
+
// webview regains focus/visibility so edits made in another panel show up.
|
|
30
|
+
const onFocus = () => { void reload(); };
|
|
31
|
+
const onVisible = () => { if (document.visibilityState === 'visible') void reload(); };
|
|
32
|
+
window.addEventListener('focus', onFocus);
|
|
33
|
+
document.addEventListener('visibilitychange', onVisible);
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('focus', onFocus);
|
|
36
|
+
document.removeEventListener('visibilitychange', onVisible);
|
|
37
|
+
};
|
|
38
|
+
}, [reload]);
|
|
39
|
+
|
|
40
|
+
const onDelete = async (id: {{Name}}['{{primaryKey}}']) => {
|
|
41
|
+
// `confirm()` is disabled inside VS Code webviews — confirmation happens in
|
|
42
|
+
// the host (the `delete` RPC handler shows a modal). Just call + reload.
|
|
43
|
+
await api.delete(id);
|
|
44
|
+
await reload();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ padding: 16, color: 'var(--vscode-foreground)' }}>
|
|
49
|
+
<header style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
|
50
|
+
<h2 style={{ margin: 0 }}>{{title}}</h2>
|
|
51
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
52
|
+
<button onClick={() => void reload()} disabled={loading}>{loading ? 'Loading…' : 'Refresh'}</button>
|
|
53
|
+
<button onClick={() => api.openForm()}>+ New</button>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
|
|
57
|
+
{error && <div style={{ color: 'var(--vscode-errorForeground)', marginBottom: 8 }}>{error}</div>}
|
|
58
|
+
{loading ? <div>Loading…</div> : (
|
|
59
|
+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
60
|
+
<thead>
|
|
61
|
+
<tr style={{ textAlign: 'left', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
|
62
|
+
{{listHeaderCells}}
|
|
63
|
+
<th style={{ padding: '6px 8px' }}></th>
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
{rows.length === 0 && (
|
|
68
|
+
<tr><td colSpan={{{listColCount}}} style={{ padding: 16, opacity: 0.6 }}>No rows yet.</td></tr>
|
|
69
|
+
)}
|
|
70
|
+
{rows.map((r) => (
|
|
71
|
+
<tr key={String(r.{{primaryKey}})} style={{ borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
|
72
|
+
{{listBodyCells}}
|
|
73
|
+
<td style={{ padding: '6px 8px', whiteSpace: 'nowrap' }}>
|
|
74
|
+
<button onClick={() => api.openForm(r.{{primaryKey}})}>Edit</button>{' '}
|
|
75
|
+
<button onClick={() => onDelete(r.{{primaryKey}})}>Delete</button>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
))}
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { definePanel } from '../shared/vsceasy';
|
|
2
|
+
import { {{Name}}Service } from '../services/{{Name}}Service';
|
|
3
|
+
import { setPending{{Name}}Id } from '../services/{{name}}FormNav';
|
|
4
|
+
import type { {{Plural}}ListApi } from '../shared/api';
|
|
5
|
+
|
|
6
|
+
export default definePanel<{{Plural}}ListApi>({
|
|
7
|
+
title: '{{title}}',
|
|
8
|
+
column: 'active',
|
|
9
|
+
command: { title: '{{title}}: List' },
|
|
10
|
+
rpc: (vscode) => ({
|
|
11
|
+
async list() {
|
|
12
|
+
return {{Name}}Service.list();
|
|
13
|
+
},
|
|
14
|
+
async delete(id) {
|
|
15
|
+
// Confirm in the host — browser confirm() is disabled in webviews.
|
|
16
|
+
const pick = await vscode.window.showWarningMessage(
|
|
17
|
+
`Delete {{title}} "${String(id)}"?`,
|
|
18
|
+
{ modal: true },
|
|
19
|
+
'Delete',
|
|
20
|
+
);
|
|
21
|
+
if (pick !== 'Delete') return false;
|
|
22
|
+
return {{Name}}Service.delete(id);
|
|
23
|
+
},
|
|
24
|
+
async openForm(id) {
|
|
25
|
+
// Stash the id so the form can pre-load it on mount, then reveal the form.
|
|
26
|
+
setPending{{Name}}Id(id ?? null);
|
|
27
|
+
await vscode.commands.executeCommand('{{prefix}}.open{{Name}}Form', id ?? null);
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { {{Plural}}Repo } from '../models/{{Name}}';
|
|
2
|
+
import type { {{Name}} } from '../models/{{Name}}';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* {{Name}} service — business logic between RPC handlers and the repo.
|
|
6
|
+
* Put validation, derivations (e.g. timestamps), and cross-entity work here.
|
|
7
|
+
*/
|
|
8
|
+
export const {{Name}}Service = {
|
|
9
|
+
async list(): Promise<{{Name}}[]> {
|
|
10
|
+
return {{Plural}}Repo().findMany({ orderBy: '{{primaryKey}}:desc' });
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
async get(id: {{Name}}['{{primaryKey}}']): Promise<{{Name}} | null> {
|
|
14
|
+
return {{Plural}}Repo().findById(id);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
async save(row: {{Name}}): Promise<{{Name}}> {
|
|
18
|
+
if (!row.{{primaryKey}}) {
|
|
19
|
+
throw new Error('{{Name}}: {{primaryKey}} is required');
|
|
20
|
+
}
|
|
21
|
+
return {{Plural}}Repo().upsert(row);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async delete(id: {{Name}}['{{primaryKey}}']): Promise<boolean> {
|
|
25
|
+
return {{Plural}}Repo().delete(id);
|
|
26
|
+
},
|
|
27
|
+
};
|