toolcraft-openapi 0.0.92 → 0.0.94
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/README.md +38 -38
- package/dist/composition.json +1 -1
- package/node_modules/@poe-code/frontmatter/README.md +2 -2
- package/node_modules/@poe-code/frontmatter/package.json +8 -2
- package/node_modules/auth-store/README.md +15 -0
- package/node_modules/auth-store/package.json +3 -2
- package/node_modules/toolcraft-design/README.md +8 -4
- package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
- package/node_modules/toolcraft-design/package.json +3 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# toolcraft-openapi
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Scaffold for OpenAPI-driven toolcraft clients.
|
|
3
|
+
Generate Toolcraft command clients from OpenAPI documents and share the same
|
|
4
|
+
surface across CLI, MCP, and SDK usage.
|
|
6
5
|
|
|
7
6
|
## Usage
|
|
8
7
|
|
|
@@ -18,20 +17,21 @@ const auth = bearerTokenAuth({
|
|
|
18
17
|
|
|
19
18
|
## Generator CLI
|
|
20
19
|
|
|
21
|
-
`toolcraft-openapi-generate` reads an OpenAPI document from disk or a URL
|
|
22
|
-
command files.
|
|
20
|
+
`toolcraft-openapi-generate` reads an OpenAPI document from disk or a URL and
|
|
21
|
+
writes generated command files.
|
|
23
22
|
|
|
24
|
-
- `--input <path-or-url>`
|
|
25
|
-
- `--output <dir>`
|
|
26
|
-
- `--check`
|
|
27
|
-
- `--diff`
|
|
23
|
+
- `--input <path-or-url>` - OpenAPI document to read. Defaults to `openapi.json`.
|
|
24
|
+
- `--output <dir>` - directory for generated files. Defaults to `src/generated`.
|
|
25
|
+
- `--check` - exits non-zero when generated files would change.
|
|
26
|
+
- `--diff` - prints generated file changes without writing them.
|
|
28
27
|
|
|
29
|
-
When `toolcraft.yml` is present next to the input file, the generator validates
|
|
30
|
-
diagnostics, and uses it to shape generated command names for mapped
|
|
28
|
+
When `toolcraft.yml` is present next to the input file, the generator validates
|
|
29
|
+
it, prints diagnostics, and uses it to shape generated command names for mapped
|
|
30
|
+
resources.
|
|
31
31
|
|
|
32
|
-
The default generated integration point is `src/generated/client.ts`.
|
|
33
|
-
|
|
34
|
-
command surface:
|
|
32
|
+
The default generated integration point is `src/generated/client.ts`.
|
|
33
|
+
Application code provides deployment-specific configuration and lets the
|
|
34
|
+
generated client own the OpenAPI-derived command surface:
|
|
35
35
|
|
|
36
36
|
```ts
|
|
37
37
|
import { defineGeneratedClient } from "./generated/client.js";
|
|
@@ -47,7 +47,7 @@ export const client = defineGeneratedClient({
|
|
|
47
47
|
});
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Generated lower-level group and operation exports remain available
|
|
50
|
+
Generated lower-level group and operation exports remain available when callers
|
|
51
51
|
want a curated command surface with `defineClient()`.
|
|
52
52
|
|
|
53
53
|
### CI drift check
|
|
@@ -73,11 +73,11 @@ toolcraft-openapi-generate --check
|
|
|
73
73
|
- `CommandContributor`
|
|
74
74
|
- `AuthProvider`
|
|
75
75
|
|
|
76
|
-
## Environment
|
|
76
|
+
## Environment Variables
|
|
77
77
|
|
|
78
|
-
- `<envVar passed to bearerTokenAuth>`
|
|
79
|
-
- `AUTH_BACKEND`
|
|
80
|
-
- `TOOLCRAFT_OPENAPI_ENV`
|
|
78
|
+
- `<envVar passed to bearerTokenAuth>` - bearer token override; wins over stored credentials.
|
|
79
|
+
- `AUTH_BACKEND` - forwarded to `auth-store` to select `file` or `keychain` storage.
|
|
80
|
+
- `TOOLCRAFT_OPENAPI_ENV` - selects a configured OpenAPI environment when using
|
|
81
81
|
`resolveOpenApiBaseUrl`.
|
|
82
82
|
|
|
83
83
|
## Configuration
|
|
@@ -110,15 +110,15 @@ unspecified_endpoints:
|
|
|
110
110
|
|
|
111
111
|
Supported keys:
|
|
112
112
|
|
|
113
|
-
- `edition`
|
|
114
|
-
- `environments`
|
|
115
|
-
- `client_settings.idempotency_header`
|
|
116
|
-
- `client_settings.auth.*.env`
|
|
117
|
-
- `pagination`
|
|
113
|
+
- `edition` - required when `toolcraft.yml` exists. Currently `2026-05-16`.
|
|
114
|
+
- `environments` - named base URLs for `resolveOpenApiBaseUrl`.
|
|
115
|
+
- `client_settings.idempotency_header` - header used for generated idempotency keys.
|
|
116
|
+
- `client_settings.auth.*.env` - declarative auth environment metadata.
|
|
117
|
+
- `pagination` - named request/response field mappings. Schemes are validated today; iterator
|
|
118
118
|
generation is not implemented yet.
|
|
119
|
-
- `resources`
|
|
120
|
-
- `readme.examples`
|
|
121
|
-
- `unspecified_endpoints`
|
|
119
|
+
- `resources` - resource/method mapping. Mapped methods shape generated names and file paths.
|
|
120
|
+
- `readme.examples` - examples emitted into generated command help and MCP descriptions.
|
|
121
|
+
- `unspecified_endpoints` - endpoints intentionally omitted from `resources`.
|
|
122
122
|
|
|
123
123
|
Mapped idempotent methods with `client_settings.idempotency_header` get an optional
|
|
124
124
|
`idempotencyKey` param across CLI, MCP, and SDK surfaces. Mapped methods also get a CLI/SDK
|
|
@@ -127,20 +127,20 @@ Mapped idempotent methods with `client_settings.idempotency_header` get an optio
|
|
|
127
127
|
|
|
128
128
|
Diagnostics use stable codes:
|
|
129
129
|
|
|
130
|
-
- `TOOLCRAFT_OPENAPI_001`
|
|
131
|
-
- `TOOLCRAFT_OPENAPI_002`
|
|
132
|
-
- `TOOLCRAFT_OPENAPI_003`
|
|
133
|
-
- `TOOLCRAFT_OPENAPI_004`
|
|
134
|
-
- `TOOLCRAFT_OPENAPI_005`
|
|
135
|
-
- `TOOLCRAFT_OPENAPI_006`
|
|
136
|
-
- `TOOLCRAFT_OPENAPI_007`
|
|
130
|
+
- `TOOLCRAFT_OPENAPI_001` - endpoint is not mapped or listed in `unspecified_endpoints`.
|
|
131
|
+
- `TOOLCRAFT_OPENAPI_002` - duplicate configured method path.
|
|
132
|
+
- `TOOLCRAFT_OPENAPI_003` - unknown pagination scheme.
|
|
133
|
+
- `TOOLCRAFT_OPENAPI_004` - reserved for spec drift.
|
|
134
|
+
- `TOOLCRAFT_OPENAPI_005` - reserved method name.
|
|
135
|
+
- `TOOLCRAFT_OPENAPI_006` - missing or unsupported edition.
|
|
136
|
+
- `TOOLCRAFT_OPENAPI_007` - invalid config shape.
|
|
137
137
|
|
|
138
138
|
### `bearerTokenAuth(opts)`
|
|
139
139
|
|
|
140
|
-
- `serviceName: string`
|
|
141
|
-
- `envVar: string`
|
|
142
|
-
- `whoamiPath?: string`
|
|
143
|
-
- `commandPrefix?: string`
|
|
140
|
+
- `serviceName: string` - auth-store service name and file-store key.
|
|
141
|
+
- `envVar: string` - environment variable checked before stored credentials.
|
|
142
|
+
- `whoamiPath?: string` - optional authenticated endpoint used by `login` and `status`.
|
|
143
|
+
- `commandPrefix?: string` - CLI auth group name. Defaults to `auth`.
|
|
144
144
|
|
|
145
145
|
## Auth commands
|
|
146
146
|
|
package/dist/composition.json
CHANGED
|
@@ -26,10 +26,10 @@ Duplicate mapping keys use YAML's last-wins behavior by default, matching the
|
|
|
26
26
|
legacy frontmatter readers. Callers that need strict YAML mappings can pass
|
|
27
27
|
`{ uniqueKeys: true }` to `parseFrontmatter` or `parseFrontmatterDocument`.
|
|
28
28
|
|
|
29
|
-
## Environment
|
|
29
|
+
## Environment Variables
|
|
30
30
|
|
|
31
31
|
This package reads no environment variables.
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Configuration Options
|
|
34
34
|
|
|
35
35
|
This package has no configuration options.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poe-code/frontmatter",
|
|
3
3
|
"version": "0.0.1",
|
|
4
|
-
"license": "MIT",
|
|
5
4
|
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -18,8 +18,14 @@
|
|
|
18
18
|
"test:unit": "cd ../.. && vitest run packages/frontmatter/src"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"LICENSE"
|
|
22
23
|
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/poe-platform/poe-code.git",
|
|
27
|
+
"directory": "packages/frontmatter"
|
|
28
|
+
},
|
|
23
29
|
"dependencies": {
|
|
24
30
|
"yaml": "^2.8.2"
|
|
25
31
|
}
|
|
@@ -45,3 +45,18 @@ await store.delete();
|
|
|
45
45
|
|
|
46
46
|
- Uses the `security` CLI (`add-generic-password`, `find-generic-password`, `delete-generic-password`)
|
|
47
47
|
- Configurable service and account names
|
|
48
|
+
|
|
49
|
+
## Configuration Options
|
|
50
|
+
|
|
51
|
+
`createSecretStore()` accepts:
|
|
52
|
+
|
|
53
|
+
- `backendEnvVar`: optional env var name that selects `file` or `keychain`.
|
|
54
|
+
- `fileStore.salt`: application-specific encryption salt.
|
|
55
|
+
- `fileStore.defaultDirectory`: default credential directory.
|
|
56
|
+
- `fileStore.defaultFileName`: encrypted file name.
|
|
57
|
+
- `keychainStore.service`: macOS Keychain service name.
|
|
58
|
+
- `keychainStore.account`: macOS Keychain account name.
|
|
59
|
+
|
|
60
|
+
## Environment Variables
|
|
61
|
+
|
|
62
|
+
This package reads only the caller-selected `backendEnvVar`. It does not define a fixed public env var.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auth-store",
|
|
3
3
|
"version": "0.0.1",
|
|
4
|
-
"license": "MIT",
|
|
5
4
|
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"build": "node ../../scripts/guard-package-dist.mjs && tsc"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"LICENSE"
|
|
20
21
|
],
|
|
21
22
|
"repository": {
|
|
22
23
|
"type": "git",
|
|
@@ -118,16 +118,16 @@ Raw HTML in Markdown is escaped by default. Pass `{ allowRawHtml: true }` only w
|
|
|
118
118
|
|
|
119
119
|
Fenced code block syntax highlighting is opt-in:
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
````ts
|
|
122
122
|
import { renderMarkdown, renderMarkdownHtml } from "toolcraft-design";
|
|
123
123
|
|
|
124
|
-
const html = renderMarkdownHtml(
|
|
124
|
+
const html = renderMarkdownHtml('```ts\nconst value = "hello";\n```', {
|
|
125
125
|
syntaxHighlight: true
|
|
126
126
|
});
|
|
127
|
-
const terminal = renderMarkdown(
|
|
127
|
+
const terminal = renderMarkdown('```ts\nconst value = "hello";\n```', {
|
|
128
128
|
syntaxHighlight: true
|
|
129
129
|
});
|
|
130
|
-
|
|
130
|
+
````
|
|
131
131
|
|
|
132
132
|
HTML highlighting emits escaped code text with neutral Toolcraft-owned `<span>` wrappers. The renderer does not ship or inject CSS, so consumers control the appearance:
|
|
133
133
|
|
|
@@ -340,3 +340,7 @@ nvm exec 18.18 node packages/toolcraft-design/scripts/check-node18.mjs
|
|
|
340
340
|
- `NO_COLOR`: disables color when `FORCE_COLOR` does not enable it.
|
|
341
341
|
- `TERM`: participates in color support detection; an unset or `dumb` terminal disables color unless forced.
|
|
342
342
|
- `POE_NO_SPINNER`: uses non-animated spinner behavior when set to `1`.
|
|
343
|
+
|
|
344
|
+
## Configuration Options
|
|
345
|
+
|
|
346
|
+
Use `configureTheme({ brand?, label? })` for process-local brand settings, `withOutputFormat(format, fn)` for scoped output-format overrides, and component-specific options such as `RenderTableOptions`, `RenderMarkdownOptions`, prompt options, dashboard options, and explorer config. Environment variables above are process defaults, not config files.
|
|
@@ -118,19 +118,18 @@ export function createTerminalDriver(opts) {
|
|
|
118
118
|
if (destroyed) {
|
|
119
119
|
return () => { };
|
|
120
120
|
}
|
|
121
|
-
const listener = (
|
|
122
|
-
const event
|
|
123
|
-
if (event !== undefined) {
|
|
121
|
+
const listener = (chunk) => {
|
|
122
|
+
for (const event of parseKeypressChunk(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))) {
|
|
124
123
|
handler(event);
|
|
125
124
|
}
|
|
126
125
|
};
|
|
127
126
|
keypressListeners.add(listener);
|
|
128
|
-
stdin.on("
|
|
127
|
+
stdin.on("data", listener);
|
|
129
128
|
return () => {
|
|
130
129
|
if (!keypressListeners.delete(listener)) {
|
|
131
130
|
return;
|
|
132
131
|
}
|
|
133
|
-
stdin.off("
|
|
132
|
+
stdin.off("data", listener);
|
|
134
133
|
};
|
|
135
134
|
}
|
|
136
135
|
function destroy() {
|
|
@@ -138,7 +137,7 @@ export function createTerminalDriver(opts) {
|
|
|
138
137
|
return;
|
|
139
138
|
}
|
|
140
139
|
for (const listener of keypressListeners) {
|
|
141
|
-
stdin.off("
|
|
140
|
+
stdin.off("data", listener);
|
|
142
141
|
}
|
|
143
142
|
keypressListeners.clear();
|
|
144
143
|
for (const listener of resizeListeners) {
|
|
@@ -173,6 +172,9 @@ export function parseKeypress(data) {
|
|
|
173
172
|
if (data.length === 0) {
|
|
174
173
|
return undefined;
|
|
175
174
|
}
|
|
175
|
+
if (data.length === 1 && data[0] === 0x1b) {
|
|
176
|
+
return { name: "escape", ctrl: false, meta: false, shift: false };
|
|
177
|
+
}
|
|
176
178
|
const stream = new PassThrough();
|
|
177
179
|
const stdin = stream;
|
|
178
180
|
let event;
|
|
@@ -184,6 +186,46 @@ export function parseKeypress(data) {
|
|
|
184
186
|
stream.destroy();
|
|
185
187
|
return event;
|
|
186
188
|
}
|
|
189
|
+
function parseKeypressChunk(data) {
|
|
190
|
+
const events = [];
|
|
191
|
+
const input = data.toString("utf8");
|
|
192
|
+
let index = 0;
|
|
193
|
+
while (index < input.length) {
|
|
194
|
+
const sequence = nextKeySequence(input, index);
|
|
195
|
+
if (sequence.length === 0) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const event = parseKeypress(Buffer.from(sequence));
|
|
199
|
+
if (event !== undefined) {
|
|
200
|
+
events.push(event);
|
|
201
|
+
}
|
|
202
|
+
index += sequence.length;
|
|
203
|
+
}
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
function nextKeySequence(input, index) {
|
|
207
|
+
const character = input[index];
|
|
208
|
+
if (character === undefined) {
|
|
209
|
+
return "";
|
|
210
|
+
}
|
|
211
|
+
if (character !== "\u001b") {
|
|
212
|
+
return character;
|
|
213
|
+
}
|
|
214
|
+
const next = input[index + 1];
|
|
215
|
+
if (next === undefined) {
|
|
216
|
+
return character;
|
|
217
|
+
}
|
|
218
|
+
if (next !== "[") {
|
|
219
|
+
return input.slice(index, index + 2);
|
|
220
|
+
}
|
|
221
|
+
for (let cursor = index + 2; cursor < input.length; cursor += 1) {
|
|
222
|
+
const code = input.charCodeAt(cursor);
|
|
223
|
+
if ((code >= 0x40 && code <= 0x7e) || input[cursor] === "~") {
|
|
224
|
+
return input.slice(index, cursor + 1);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return input.slice(index);
|
|
228
|
+
}
|
|
187
229
|
function toKeypressEvent(str, key) {
|
|
188
230
|
const controlCharacter = controlCharacterToKeypress(key?.sequence);
|
|
189
231
|
if (controlCharacter !== undefined) {
|
|
@@ -7,6 +7,10 @@ type ExplorerKeypressEvent = Extract<ExplorerEvent, {
|
|
|
7
7
|
export type ActionRuntimeHandles = {
|
|
8
8
|
refresh: () => Promise<void>;
|
|
9
9
|
suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
10
|
+
openModal: (content: {
|
|
11
|
+
title: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}) => void;
|
|
10
14
|
toast: (msg: string, tone?: Tone) => void;
|
|
11
15
|
confirm: (prompt: string) => Promise<boolean>;
|
|
12
16
|
exit: (after?: () => void | Promise<void>) => void;
|
|
@@ -20,6 +20,7 @@ export function buildActionContext(state, _action, source, runtimeHandles, rowsO
|
|
|
20
20
|
filter: state.filter,
|
|
21
21
|
refresh: runtimeHandles.refresh,
|
|
22
22
|
suspendAnd: runtimeHandles.suspendAnd,
|
|
23
|
+
openModal: runtimeHandles.openModal,
|
|
23
24
|
toast: runtimeHandles.toast,
|
|
24
25
|
confirm: runtimeHandles.confirm,
|
|
25
26
|
exit: runtimeHandles.exit
|
|
@@ -5,6 +5,7 @@ const NO_EFFECTS = [];
|
|
|
5
5
|
const DEFAULT_ACTION_HANDLES = {
|
|
6
6
|
refresh: async () => undefined,
|
|
7
7
|
suspendAnd: async (fn) => fn(),
|
|
8
|
+
openModal: () => undefined,
|
|
8
9
|
toast: () => undefined,
|
|
9
10
|
confirm: async () => false,
|
|
10
11
|
exit: () => undefined
|
|
@@ -31,6 +32,13 @@ export function step(state, event, runtimeHandles = DEFAULT_ACTION_HANDLES) {
|
|
|
31
32
|
return expireToast(state);
|
|
32
33
|
case "suspendResumed":
|
|
33
34
|
return suspendResumed(state, event.emit, runtimeHandles);
|
|
35
|
+
case "modalOpened":
|
|
36
|
+
return setModal(state, {
|
|
37
|
+
kind: "content",
|
|
38
|
+
title: event.title,
|
|
39
|
+
content: event.content,
|
|
40
|
+
scroll: 0
|
|
41
|
+
});
|
|
34
42
|
case "modalDismissed":
|
|
35
43
|
return modalDismissed(state, event.result, runtimeHandles);
|
|
36
44
|
}
|
|
@@ -164,6 +172,26 @@ function stepModalKey(state, key, target, runtimeHandles) {
|
|
|
164
172
|
}
|
|
165
173
|
return paletteInput(state, key);
|
|
166
174
|
}
|
|
175
|
+
if (state.modal?.kind === "content") {
|
|
176
|
+
if (target?.type === "builtin") {
|
|
177
|
+
switch (target.id) {
|
|
178
|
+
case "escape":
|
|
179
|
+
case "confirm":
|
|
180
|
+
return modalDismissed(state, false, runtimeHandles);
|
|
181
|
+
case "cursorDown":
|
|
182
|
+
case "detailScrollDown":
|
|
183
|
+
return scrollContentModal(state, 1);
|
|
184
|
+
case "cursorUp":
|
|
185
|
+
case "detailScrollUp":
|
|
186
|
+
return scrollContentModal(state, -1);
|
|
187
|
+
case "pageDown":
|
|
188
|
+
return scrollContentModal(state, modalBodyHeight(state));
|
|
189
|
+
case "pageUp":
|
|
190
|
+
return scrollContentModal(state, -modalBodyHeight(state));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return mark(state, 0);
|
|
194
|
+
}
|
|
167
195
|
return mark(state, 0);
|
|
168
196
|
}
|
|
169
197
|
function resize(state, cols, rows) {
|
|
@@ -287,7 +315,7 @@ function suspendResumed(state, emit, runtimeHandles) {
|
|
|
287
315
|
}
|
|
288
316
|
function modalDismissed(state, result, runtimeHandles) {
|
|
289
317
|
const modal = state.modal;
|
|
290
|
-
const closed = { ...state, modal: null, dirty:
|
|
318
|
+
const closed = { ...state, modal: null, dirty: REGION_ALL };
|
|
291
319
|
if (modal?.kind === "confirm") {
|
|
292
320
|
modal.resolver(result === true);
|
|
293
321
|
}
|
|
@@ -466,9 +494,6 @@ function selectionChanged(state, selected) {
|
|
|
466
494
|
return { state: next, effects: NO_EFFECTS };
|
|
467
495
|
}
|
|
468
496
|
function detailScroll(state, delta) {
|
|
469
|
-
if (state.focused !== "detail") {
|
|
470
|
-
return mark(state, 0);
|
|
471
|
-
}
|
|
472
497
|
const scroll = clamp(state.detail.scroll + delta, 0, maxDetailScroll(state));
|
|
473
498
|
if (scroll === state.detail.scroll) {
|
|
474
499
|
return mark(state, 0);
|
|
@@ -478,6 +503,29 @@ function detailScroll(state, delta) {
|
|
|
478
503
|
effects: NO_EFFECTS
|
|
479
504
|
};
|
|
480
505
|
}
|
|
506
|
+
function scrollContentModal(state, delta) {
|
|
507
|
+
if (state.modal?.kind !== "content") {
|
|
508
|
+
return mark(state, 0);
|
|
509
|
+
}
|
|
510
|
+
const scroll = clamp(state.modal.scroll + delta, 0, maxContentModalScroll(state));
|
|
511
|
+
if (scroll === state.modal.scroll) {
|
|
512
|
+
return mark(state, 0);
|
|
513
|
+
}
|
|
514
|
+
return setModal(state, { ...state.modal, scroll });
|
|
515
|
+
}
|
|
516
|
+
function maxContentModalScroll(state) {
|
|
517
|
+
if (state.modal?.kind !== "content") {
|
|
518
|
+
return 0;
|
|
519
|
+
}
|
|
520
|
+
return Math.max(0, state.modal.content.split("\n").length - modalBodyHeight(state));
|
|
521
|
+
}
|
|
522
|
+
function modalBodyHeight(state) {
|
|
523
|
+
const screenHeight = normalizeSize(state.size.rows);
|
|
524
|
+
if (screenHeight <= 2) {
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
return Math.max(0, screenHeight - 4);
|
|
528
|
+
}
|
|
481
529
|
function clampDetailScroll(state) {
|
|
482
530
|
const scroll = clamp(state.detail.scroll, 0, maxDetailScroll(state));
|
|
483
531
|
if (scroll === state.detail.scroll) {
|
|
@@ -28,28 +28,34 @@ export function renderList(state, screen, layout) {
|
|
|
28
28
|
cache.clear();
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
const lines = buildDisplayLines(state);
|
|
32
|
+
const start = visibleStart(lines, bodyRect.height);
|
|
32
33
|
let y = 0;
|
|
33
|
-
for (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const row = state.rows[rowIndex];
|
|
38
|
-
if (!row) {
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
if (row.group && row.group !== lastGroup && y < bodyRect.height) {
|
|
42
|
-
const hash = `group:${row.group}`;
|
|
34
|
+
for (let lineIndex = start; lineIndex < lines.length && y < bodyRect.height; lineIndex += 1) {
|
|
35
|
+
const line = lines[lineIndex];
|
|
36
|
+
if (line.kind === "group") {
|
|
37
|
+
const hash = `group:${line.label}`;
|
|
43
38
|
if (cache.get(y) !== hash) {
|
|
44
|
-
writeLine(screen, bodyRect, y, formatGroupHeader(
|
|
39
|
+
writeLine(screen, bodyRect, y, formatGroupHeader(line.label, bodyRect), styles.muted);
|
|
45
40
|
cache.set(y, hash);
|
|
46
41
|
}
|
|
47
42
|
y += 1;
|
|
48
|
-
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (line.kind === "subtitle") {
|
|
46
|
+
const subtitleHash = `subtitle:${line.row.id}:${line.text}`;
|
|
47
|
+
if (cache.get(y) !== subtitleHash) {
|
|
48
|
+
writeLine(screen, bodyRect, y, ` ${line.text}`, styles.muted);
|
|
49
|
+
cache.set(y, subtitleHash);
|
|
50
|
+
}
|
|
51
|
+
y += 1;
|
|
52
|
+
continue;
|
|
49
53
|
}
|
|
54
|
+
const rowIndex = line.rowIndex;
|
|
50
55
|
if (y >= bodyRect.height) {
|
|
51
56
|
break;
|
|
52
57
|
}
|
|
58
|
+
const row = line.row;
|
|
53
59
|
const selected = state.multiSelect && state.selected.has(row.id);
|
|
54
60
|
const cursor = rowIndex === state.filtered[state.cursor];
|
|
55
61
|
const positions = state.matchPositions.get(rowIndex) ?? [];
|
|
@@ -64,15 +70,36 @@ export function renderList(state, screen, layout) {
|
|
|
64
70
|
cache.set(y, hash);
|
|
65
71
|
}
|
|
66
72
|
y += 1;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function buildDisplayLines(state) {
|
|
76
|
+
const lines = [];
|
|
77
|
+
let lastGroup;
|
|
78
|
+
for (const rowIndex of state.filtered) {
|
|
79
|
+
const row = state.rows[rowIndex];
|
|
80
|
+
if (!row) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (row.group && row.group !== lastGroup) {
|
|
84
|
+
lines.push({ kind: "group", label: row.group });
|
|
85
|
+
lastGroup = row.group;
|
|
74
86
|
}
|
|
87
|
+
lines.push({ kind: "row", rowIndex, row, cursor: rowIndex === state.filtered[state.cursor] });
|
|
88
|
+
if (row.subtitle) {
|
|
89
|
+
lines.push({ kind: "subtitle", row, text: row.subtitle });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return lines;
|
|
93
|
+
}
|
|
94
|
+
function visibleStart(lines, height) {
|
|
95
|
+
if (height <= 0) {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
const cursorLine = lines.findIndex((line) => line.kind === "row" && line.cursor);
|
|
99
|
+
if (cursorLine < 0) {
|
|
100
|
+
return 0;
|
|
75
101
|
}
|
|
102
|
+
return Math.max(0, Math.min(cursorLine, cursorLine - height + 1));
|
|
76
103
|
}
|
|
77
104
|
function formatGroupHeader(label, rect) {
|
|
78
105
|
const text = ` ${label} `;
|
|
@@ -4,7 +4,7 @@ export function renderModal(state, screen) {
|
|
|
4
4
|
if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
|
|
5
5
|
return;
|
|
6
6
|
}
|
|
7
|
-
const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width *
|
|
7
|
+
const width = Math.min(screen.width - 2, Math.max(34, Math.floor(screen.width * modalWidthRatio(state))));
|
|
8
8
|
const height = Math.min(screen.height - 2, modalHeight(state));
|
|
9
9
|
const x = Math.max(0, Math.floor((screen.width - width) / 2));
|
|
10
10
|
const y = Math.max(0, Math.floor((screen.height - height) / 2));
|
|
@@ -39,6 +39,9 @@ function modalLines(state) {
|
|
|
39
39
|
...paletteLines(state)
|
|
40
40
|
];
|
|
41
41
|
}
|
|
42
|
+
if (state.modal?.kind === "content") {
|
|
43
|
+
return state.modal.content.split("\n").slice(state.modal.scroll);
|
|
44
|
+
}
|
|
42
45
|
return [];
|
|
43
46
|
}
|
|
44
47
|
function title(state) {
|
|
@@ -48,11 +51,20 @@ function title(state) {
|
|
|
48
51
|
if (state.modal?.kind === "confirm") {
|
|
49
52
|
return "Confirm";
|
|
50
53
|
}
|
|
54
|
+
if (state.modal?.kind === "content") {
|
|
55
|
+
return state.modal.title;
|
|
56
|
+
}
|
|
51
57
|
return "Command Palette";
|
|
52
58
|
}
|
|
53
59
|
function modalHeight(state) {
|
|
60
|
+
if (state.modal?.kind === "content") {
|
|
61
|
+
return Math.max(5, state.modal.content.split("\n").length + 2);
|
|
62
|
+
}
|
|
54
63
|
return Math.max(5, modalLines(state).length + 2);
|
|
55
64
|
}
|
|
65
|
+
function modalWidthRatio(state) {
|
|
66
|
+
return state.modal?.kind === "content" ? 0.86 : 0.62;
|
|
67
|
+
}
|
|
56
68
|
function drawBox(screen, x, y, width, height, boxTitle, style) {
|
|
57
69
|
screen.clearRect({ x, y, width, height });
|
|
58
70
|
const innerWidth = Math.max(0, width - 2);
|
|
@@ -6,7 +6,7 @@ import { renderExplorer } from "./index.js";
|
|
|
6
6
|
export function renderStateSnapshot(state) {
|
|
7
7
|
const screen = new ScreenBuffer(state.size.cols, state.size.rows);
|
|
8
8
|
renderExplorer(state, screen);
|
|
9
|
-
return dumpScreen(screen);
|
|
9
|
+
return dumpScreen(screen).split("\n").map((line) => line.trimEnd()).join("\n");
|
|
10
10
|
}
|
|
11
11
|
export function dumpScreen(screen) {
|
|
12
12
|
const lines = [];
|
|
@@ -45,6 +45,9 @@ class ExplorerRuntime {
|
|
|
45
45
|
await this.refreshRowsFromSource();
|
|
46
46
|
},
|
|
47
47
|
suspendAnd: async (fn) => this.suspendAnd(fn),
|
|
48
|
+
openModal: (content) => {
|
|
49
|
+
this.dispatch({ type: "modalOpened", title: content.title, content: content.content });
|
|
50
|
+
},
|
|
48
51
|
toast: (msg, tone) => {
|
|
49
52
|
this.showToast(msg, tone);
|
|
50
53
|
},
|
|
@@ -48,6 +48,10 @@ export interface ActionContext<R> {
|
|
|
48
48
|
filter: string;
|
|
49
49
|
refresh: () => Promise<void>;
|
|
50
50
|
suspendAnd: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
51
|
+
openModal: (content: {
|
|
52
|
+
title: string;
|
|
53
|
+
content: string;
|
|
54
|
+
}) => void;
|
|
51
55
|
toast: (msg: string, tone?: Tone) => void;
|
|
52
56
|
confirm: (prompt: string) => Promise<boolean>;
|
|
53
57
|
exit: (after?: () => void | Promise<void>) => void;
|
|
@@ -114,6 +118,11 @@ export interface ExplorerState {
|
|
|
114
118
|
kind: "palette";
|
|
115
119
|
query: string;
|
|
116
120
|
cursor: number;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "content";
|
|
123
|
+
title: string;
|
|
124
|
+
content: string;
|
|
125
|
+
scroll: number;
|
|
117
126
|
};
|
|
118
127
|
toast: {
|
|
119
128
|
message: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-design",
|
|
3
3
|
"version": "0.0.2",
|
|
4
|
-
"license": "MIT",
|
|
5
4
|
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"generate:design-docs:all": "tsx scripts/generate-docs.ts all"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"LICENSE"
|
|
22
23
|
],
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"fast-string-width": "^3.0.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft-openapi",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.94",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"toolcraft-openapi-generate": "dist/bin/generate.js"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"toolcraft": "0.0.
|
|
33
|
+
"toolcraft": "0.0.94",
|
|
34
34
|
"auth-store": "^0.0.1",
|
|
35
35
|
"fast-string-width": "^3.0.2",
|
|
36
36
|
"fast-wrap-ansi": "^0.2.0",
|