@nbt-dev/components 0.1.0 → 0.1.2

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.
@@ -7,5 +7,7 @@ export { useLiveBulkRegistry } from "./use-cartridge-info";
7
7
  export type { BulkEntity, BulkRegistry, LiveRegistryState, } from "./use-cartridge-info";
8
8
  export { useGsheetsApi } from "./use-gsheets";
9
9
  export type { GsheetSyncConfig, GsheetSaveInput } from "./use-gsheets";
10
+ export { useWorkflowsApi, isWfTerminal, WF_TERMINAL } from "./use-workflows";
11
+ export type { WfStatus, WfListItem, WfExecution, WfEvent, WfDetail, WfStats, WfVersion, WfExecReply, } from "./use-workflows";
10
12
  export { getDevToolsToken, setDevToolsToken, clearDevToolsToken, authHeaders, fetchWhoAmI, devToolsSignIn, devToolsSignUp, devToolsSignOut, wsAuthProtocols, fetchDevtoolsSettings, saveDevtoolsSettings, } from "./auth";
11
13
  export type { WhoAmI } from "./auth";
@@ -0,0 +1,68 @@
1
+ export declare const WF_TERMINAL: readonly ["COMPLETED", "FAILED", "CANCELLED", "CONTINUED"];
2
+ export type WfStatus = "RUNNING" | "SUSPENDED" | "COMPLETED" | "FAILED" | "CANCELLED" | "CONTINUED";
3
+ export type WfListItem = {
4
+ id: string;
5
+ status: WfStatus;
6
+ workflowName: string;
7
+ createdAt: number;
8
+ updatedAt: number;
9
+ lane: string;
10
+ deployVersion: number;
11
+ };
12
+ export type WfExecution = {
13
+ id: string;
14
+ status: WfStatus;
15
+ workflowName: string;
16
+ args?: string;
17
+ result?: string;
18
+ error?: string;
19
+ cursor: number;
20
+ lane?: string;
21
+ deployVersion: number;
22
+ createdAt: number;
23
+ updatedAt: number;
24
+ };
25
+ export type WfEvent = {
26
+ id: string;
27
+ seq: number;
28
+ kind: string;
29
+ capability?: string;
30
+ target?: string;
31
+ op?: string;
32
+ payload?: string;
33
+ };
34
+ export type WfDetail = {
35
+ execution: WfExecution;
36
+ events: WfEvent[];
37
+ };
38
+ export type WfStats = {
39
+ writes: number;
40
+ reads: number;
41
+ emails: number;
42
+ fetches: number;
43
+ emits: number;
44
+ retries: number;
45
+ async_inflight: number;
46
+ async_peak: number;
47
+ };
48
+ export type WfVersion = {
49
+ version: number;
50
+ refcount: number;
51
+ current: boolean;
52
+ };
53
+ export type WfExecReply = {
54
+ id: string;
55
+ outcome: string;
56
+ status: string;
57
+ result?: string;
58
+ error?: string;
59
+ };
60
+ export declare function isWfTerminal(status: string): boolean;
61
+ export declare function useWorkflowsApi(): {
62
+ listExecutions: () => Promise<WfListItem[]>;
63
+ getExecution: (id: string) => Promise<WfDetail>;
64
+ getStats: () => Promise<WfStats>;
65
+ getVersions: () => Promise<WfVersion[]>;
66
+ cancel: (id: string) => Promise<WfExecReply>;
67
+ advance: (id: string) => Promise<WfExecReply>;
68
+ };
package/dist/index.js CHANGED
@@ -162,6 +162,83 @@ function useGsheetsApi() {
162
162
  );
163
163
  return useMemo(() => ({ list, save, syncNow, remove }), [list, save, syncNow, remove]);
164
164
  }
165
+
166
+ // src/core/use-workflows.ts
167
+ import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
168
+ var WF_TERMINAL = ["COMPLETED", "FAILED", "CANCELLED", "CONTINUED"];
169
+ async function jsonOrThrow2(r) {
170
+ const body = await r.json().catch(() => ({}));
171
+ if (!r.ok) throw new Error(body?.error ?? `HTTP ${r.status}`);
172
+ return body;
173
+ }
174
+ function isWfTerminal(status) {
175
+ return WF_TERMINAL.includes(status);
176
+ }
177
+ function useWorkflowsApi() {
178
+ const { apiBaseUrl } = useDevToolsConfig();
179
+ const listExecutions = useCallback2(
180
+ async () => jsonOrThrow2(
181
+ await fetch(`${apiBaseUrl}/_console/wf`, {
182
+ credentials: "include",
183
+ headers: authHeaders()
184
+ })
185
+ ),
186
+ [apiBaseUrl]
187
+ );
188
+ const getExecution = useCallback2(
189
+ async (id) => jsonOrThrow2(
190
+ await fetch(`${apiBaseUrl}/_console/wf/${encodeURIComponent(id)}`, {
191
+ credentials: "include",
192
+ headers: authHeaders()
193
+ })
194
+ ),
195
+ [apiBaseUrl]
196
+ );
197
+ const getStats = useCallback2(
198
+ async () => jsonOrThrow2(
199
+ await fetch(`${apiBaseUrl}/_console/wf/stats`, {
200
+ credentials: "include",
201
+ headers: authHeaders()
202
+ })
203
+ ),
204
+ [apiBaseUrl]
205
+ );
206
+ const getVersions = useCallback2(
207
+ async () => jsonOrThrow2(
208
+ await fetch(`${apiBaseUrl}/_console/wf/versions`, {
209
+ credentials: "include",
210
+ headers: authHeaders()
211
+ })
212
+ ),
213
+ [apiBaseUrl]
214
+ );
215
+ const cancel = useCallback2(
216
+ async (id) => jsonOrThrow2(
217
+ await fetch(`${apiBaseUrl}/_console/wf/cancel`, {
218
+ method: "POST",
219
+ credentials: "include",
220
+ headers: authHeaders({ "content-type": "application/json" }),
221
+ body: JSON.stringify({ id })
222
+ })
223
+ ),
224
+ [apiBaseUrl]
225
+ );
226
+ const advance = useCallback2(
227
+ async (id) => jsonOrThrow2(
228
+ await fetch(`${apiBaseUrl}/_console/wf/advance`, {
229
+ method: "POST",
230
+ credentials: "include",
231
+ headers: authHeaders({ "content-type": "application/json" }),
232
+ body: JSON.stringify({ id })
233
+ })
234
+ ),
235
+ [apiBaseUrl]
236
+ );
237
+ return useMemo2(
238
+ () => ({ listExecutions, getExecution, getStats, getVersions, cancel, advance }),
239
+ [listExecutions, getExecution, getStats, getVersions, cancel, advance]
240
+ );
241
+ }
165
242
  export {
166
243
  BulkStreamProvider,
167
244
  data_table_default as DataTable,
@@ -172,6 +249,7 @@ export {
172
249
  NbtEditor,
173
250
  NbtLspClient,
174
251
  value_popover_default as ValuePopover,
252
+ WF_TERMINAL,
175
253
  authHeaders,
176
254
  buildEntityGraphModel,
177
255
  cartsFromContracts,
@@ -185,6 +263,7 @@ export {
185
263
  fetchWhoAmI,
186
264
  filterEntityGraphModel,
187
265
  getDevToolsToken,
266
+ isWfTerminal,
188
267
  lspExtensions,
189
268
  nbtLanguage,
190
269
  nbtLanguageSupport,
@@ -196,6 +275,7 @@ export {
196
275
  useDevToolsConfig,
197
276
  useGsheetsApi,
198
277
  useLiveBulkRegistry,
278
+ useWorkflowsApi,
199
279
  wsAuthProtocols,
200
280
  wsBaseFrom
201
281
  };
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/core/use-cartridge-info.ts", "../src/core/use-gsheets.ts"],
4
- "sourcesContent": ["import { useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Live cartridge/entity registry for the Data tab. Instead of the build-time\n// `BULK_REGISTRY` (every cart in the repo, generated by `nbt generate`), we\n// read the daemon's `/_console/contracts` \u2014 which only lists *running*\n// cartridges and carries each entity's `searchFields`. Bulk WS routes are\n// derived live (`/_ws/bulk/<cart>/<entity-lower>`) and fed straight into\n// useBulkStream. So the tab reflects what is actually installed right now.\n\nexport type BulkEntity = {\n name: string;\n route: string;\n searchFields: readonly string[];\n};\nexport type BulkRegistry = Record<string, BulkEntity[]>;\n\ntype Contract = {\n cartridge?: string;\n core?: boolean;\n installed?: boolean;\n owns?: Record<string, { searchFields?: string[] }>;\n};\n\nfunction buildRegistry(\n contracts: Contract[],\n): { reg: BulkRegistry; core: Set<string>; installed: Set<string> } {\n const reg: BulkRegistry = {};\n const core = new Set<string>();\n const installed = new Set<string>();\n for (const c of contracts) {\n const cart = c.cartridge;\n if (!cart || !c.owns) continue;\n const entities: BulkEntity[] = [];\n for (const [name, ent] of Object.entries(c.owns)) {\n const sf = Array.isArray(ent?.searchFields) ? ent.searchFields : [];\n entities.push({\n name,\n route: `/_ws/bulk/${cart}/${name.toLowerCase()}`,\n searchFields: sf,\n });\n }\n if (entities.length > 0) {\n reg[cart] = entities;\n if (c.core) core.add(cart);\n if (c.installed) installed.add(cart);\n }\n }\n return { reg, core, installed };\n}\n\nexport type LiveRegistryState = {\n registry: BulkRegistry;\n carts: string[];\n // Cartridge slugs flagged `core` by the daemon. The Data tab keeps `auth`\n // visible but tucks the rest of these behind a \"more cartridges\" menu so\n // deployed (user) cartridges are what's shown by default.\n coreCarts: Set<string>;\n // Cartridge slugs the user (or boot) explicitly installed/activated. The Data\n // tab shows these (plus `auth`) by default; bundled-but-not-installed carts\n // stay behind the \"more cartridges\" menu until the user opens them.\n installedCarts: Set<string>;\n loading: boolean;\n error: string | null;\n};\n\nexport function useLiveBulkRegistry(): LiveRegistryState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [registry, setRegistry] = useState<BulkRegistry>({});\n const [coreCarts, setCoreCarts] = useState<Set<string>>(new Set());\n const [installedCarts, setInstalledCarts] = useState<Set<string>>(new Set());\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const r = await fetch(`${apiBaseUrl}/_console/contracts`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n const data = (await r.json()) as Contract[];\n if (!Array.isArray(data)) throw new Error(\"malformed contracts response\");\n if (cancelled) return;\n const { reg, core, installed } = buildRegistry(data);\n setRegistry(reg);\n setCoreCarts(core);\n setInstalledCarts(installed);\n } catch (e) {\n if (cancelled || (e as { name?: string }).name === \"AbortError\") return;\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n ac.abort();\n };\n }, [apiBaseUrl]);\n\n const carts = Object.keys(registry).sort();\n return { registry, carts, coreCarts, installedCarts, loading, error };\n}\n", "// Fetchers + hook for the Google Sheets cartridge-sync admin endpoints\n// (/_console/integrations/gsheets/*). Admin-gated server-side \u2014 the requests\n// carry the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\nexport type GsheetSyncConfig = {\n cartridge: string;\n spreadsheetId: string;\n saEmail: string;\n hasServiceAccount: boolean;\n intervalSeconds: number;\n enabled: boolean;\n lastSyncAt: number; // ms epoch, 0 = never\n status: string; // \"ok\" | \"error\" | \"\"\n lastError: string;\n};\n\nexport type GsheetSaveInput = {\n cartridge: string;\n spreadsheetUrl: string;\n serviceAccountJson?: string; // omit to keep the stored key\n intervalSeconds: number;\n enabled: boolean;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function useGsheetsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const list = useCallback(\n async (): Promise<GsheetSyncConfig[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const save = useCallback(\n async (input: GsheetSaveInput): Promise<GsheetSyncConfig> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify(input),\n }),\n ),\n [apiBaseUrl],\n );\n\n const syncNow = useCallback(\n async (cartridge: string): Promise<{ ok: boolean; error: string }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/sync`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ cartridge }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const remove = useCallback(\n async (cartridge: string): Promise<{ ok: boolean }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/${encodeURIComponent(cartridge)}`, {\n method: \"DELETE\",\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(() => ({ list, save, syncNow, remove }), [list, save, syncNow, remove]);\n}\n"],
5
- "mappings": "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",
6
- "names": []
3
+ "sources": ["../src/core/use-cartridge-info.ts", "../src/core/use-gsheets.ts", "../src/core/use-workflows.ts"],
4
+ "sourcesContent": ["import { useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Live cartridge/entity registry for the Data tab. Instead of the build-time\n// `BULK_REGISTRY` (every cart in the repo, generated by `nbt generate`), we\n// read the daemon's `/_console/contracts` \u2014 which only lists *running*\n// cartridges and carries each entity's `searchFields`. Bulk WS routes are\n// derived live (`/_ws/bulk/<cart>/<entity-lower>`) and fed straight into\n// useBulkStream. So the tab reflects what is actually installed right now.\n\nexport type BulkEntity = {\n name: string;\n route: string;\n searchFields: readonly string[];\n};\nexport type BulkRegistry = Record<string, BulkEntity[]>;\n\ntype Contract = {\n cartridge?: string;\n core?: boolean;\n installed?: boolean;\n owns?: Record<string, { searchFields?: string[] }>;\n};\n\nfunction buildRegistry(\n contracts: Contract[],\n): { reg: BulkRegistry; core: Set<string>; installed: Set<string> } {\n const reg: BulkRegistry = {};\n const core = new Set<string>();\n const installed = new Set<string>();\n for (const c of contracts) {\n const cart = c.cartridge;\n if (!cart || !c.owns) continue;\n const entities: BulkEntity[] = [];\n for (const [name, ent] of Object.entries(c.owns)) {\n const sf = Array.isArray(ent?.searchFields) ? ent.searchFields : [];\n entities.push({\n name,\n route: `/_ws/bulk/${cart}/${name.toLowerCase()}`,\n searchFields: sf,\n });\n }\n if (entities.length > 0) {\n reg[cart] = entities;\n if (c.core) core.add(cart);\n if (c.installed) installed.add(cart);\n }\n }\n return { reg, core, installed };\n}\n\nexport type LiveRegistryState = {\n registry: BulkRegistry;\n carts: string[];\n // Cartridge slugs flagged `core` by the daemon. The Data tab keeps `auth`\n // visible but tucks the rest of these behind a \"more cartridges\" menu so\n // deployed (user) cartridges are what's shown by default.\n coreCarts: Set<string>;\n // Cartridge slugs the user (or boot) explicitly installed/activated. The Data\n // tab shows these (plus `auth`) by default; bundled-but-not-installed carts\n // stay behind the \"more cartridges\" menu until the user opens them.\n installedCarts: Set<string>;\n loading: boolean;\n error: string | null;\n};\n\nexport function useLiveBulkRegistry(): LiveRegistryState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [registry, setRegistry] = useState<BulkRegistry>({});\n const [coreCarts, setCoreCarts] = useState<Set<string>>(new Set());\n const [installedCarts, setInstalledCarts] = useState<Set<string>>(new Set());\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const r = await fetch(`${apiBaseUrl}/_console/contracts`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n const data = (await r.json()) as Contract[];\n if (!Array.isArray(data)) throw new Error(\"malformed contracts response\");\n if (cancelled) return;\n const { reg, core, installed } = buildRegistry(data);\n setRegistry(reg);\n setCoreCarts(core);\n setInstalledCarts(installed);\n } catch (e) {\n if (cancelled || (e as { name?: string }).name === \"AbortError\") return;\n setError(e instanceof Error ? e.message : String(e));\n } finally {\n if (!cancelled) setLoading(false);\n }\n })();\n return () => {\n cancelled = true;\n ac.abort();\n };\n }, [apiBaseUrl]);\n\n const carts = Object.keys(registry).sort();\n return { registry, carts, coreCarts, installedCarts, loading, error };\n}\n", "// Fetchers + hook for the Google Sheets cartridge-sync admin endpoints\n// (/_console/integrations/gsheets/*). Admin-gated server-side \u2014 the requests\n// carry the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\nexport type GsheetSyncConfig = {\n cartridge: string;\n spreadsheetId: string;\n saEmail: string;\n hasServiceAccount: boolean;\n intervalSeconds: number;\n enabled: boolean;\n lastSyncAt: number; // ms epoch, 0 = never\n status: string; // \"ok\" | \"error\" | \"\"\n lastError: string;\n};\n\nexport type GsheetSaveInput = {\n cartridge: string;\n spreadsheetUrl: string;\n serviceAccountJson?: string; // omit to keep the stored key\n intervalSeconds: number;\n enabled: boolean;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function useGsheetsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const list = useCallback(\n async (): Promise<GsheetSyncConfig[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const save = useCallback(\n async (input: GsheetSaveInput): Promise<GsheetSyncConfig> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify(input),\n }),\n ),\n [apiBaseUrl],\n );\n\n const syncNow = useCallback(\n async (cartridge: string): Promise<{ ok: boolean; error: string }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/sync`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ cartridge }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const remove = useCallback(\n async (cartridge: string): Promise<{ ok: boolean }> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/integrations/gsheets/${encodeURIComponent(cartridge)}`, {\n method: \"DELETE\",\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(() => ({ list, save, syncNow, remove }), [list, save, syncNow, remove]);\n}\n", "// Fetchers + hook for the workflow-engine introspection endpoints\n// (/_console/wf*). The read routes are ungated like /_console/metrics; the two\n// mutating ones (cancel/advance) are admin-gated server-side, so every request\n// carries the devtools Bearer token (authHeaders) like the contracts probe.\n\nimport { useCallback, useMemo } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Terminal statuses a workflow can settle into \u2014 used to decide whether to keep\n// polling an open instance and whether Cancel is offered.\nexport const WF_TERMINAL = [\"COMPLETED\", \"FAILED\", \"CANCELLED\", \"CONTINUED\"] as const;\n\nexport type WfStatus =\n | \"RUNNING\"\n | \"SUSPENDED\"\n | \"COMPLETED\"\n | \"FAILED\"\n | \"CANCELLED\"\n | \"CONTINUED\";\n\n// Row from GET /_console/wf \u2014 the enriched list (createdAt/updatedAt are unix ms).\nexport type WfListItem = {\n id: string;\n status: WfStatus;\n workflowName: string;\n createdAt: number;\n updatedAt: number;\n lane: string;\n deployVersion: number;\n};\n\n// The execution record from GET /_console/wf/:id (raw entity field names).\nexport type WfExecution = {\n id: string;\n status: WfStatus;\n workflowName: string;\n args?: string;\n result?: string;\n error?: string;\n cursor: number;\n lane?: string;\n deployVersion: number;\n createdAt: number;\n updatedAt: number;\n};\n\n// One journal entry. kind \u2208 HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |\n// RESUMED | RETRY | COMPLETED | FAILED | CANCELLED | CONTINUED.\nexport type WfEvent = {\n id: string;\n seq: number;\n kind: string;\n capability?: string;\n target?: string;\n op?: string;\n payload?: string;\n};\n\nexport type WfDetail = { execution: WfExecution; events: WfEvent[] };\n\nexport type WfStats = {\n writes: number;\n reads: number;\n emails: number;\n fetches: number;\n emits: number;\n retries: number;\n async_inflight: number;\n async_peak: number;\n};\n\nexport type WfVersion = { version: number; refcount: number; current: boolean };\n\n// The exec-reply shape returned by run/signal/advance/cancel.\nexport type WfExecReply = {\n id: string;\n outcome: string;\n status: string;\n result?: string;\n error?: string;\n};\n\nasync function jsonOrThrow(r: Response) {\n const body = await r.json().catch(() => ({}));\n if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);\n return body;\n}\n\nexport function isWfTerminal(status: string): boolean {\n return (WF_TERMINAL as readonly string[]).includes(status);\n}\n\nexport function useWorkflowsApi() {\n const { apiBaseUrl } = useDevToolsConfig();\n\n const listExecutions = useCallback(\n async (): Promise<WfListItem[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getExecution = useCallback(\n async (id: string): Promise<WfDetail> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/${encodeURIComponent(id)}`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getStats = useCallback(\n async (): Promise<WfStats> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/stats`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const getVersions = useCallback(\n async (): Promise<WfVersion[]> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/versions`, {\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ),\n [apiBaseUrl],\n );\n\n const cancel = useCallback(\n async (id: string): Promise<WfExecReply> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/cancel`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ id }),\n }),\n ),\n [apiBaseUrl],\n );\n\n const advance = useCallback(\n async (id: string): Promise<WfExecReply> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/advance`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ id }),\n }),\n ),\n [apiBaseUrl],\n );\n\n return useMemo(\n () => ({ listExecutions, getExecution, getStats, getVersions, cancel, advance }),\n [listExecutions, getExecution, getStats, getVersions, cancel, advance],\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["useCallback", "useMemo", "jsonOrThrow", "useCallback", "useMemo"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbt-dev/components",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Reusable React building blocks for NBT-console apps: the CodeMirror NBT editor (+LSP), the entity graph, and the live data table.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/core/index.ts CHANGED
@@ -23,6 +23,17 @@ export type {
23
23
  } from "./use-cartridge-info";
24
24
  export { useGsheetsApi } from "./use-gsheets";
25
25
  export type { GsheetSyncConfig, GsheetSaveInput } from "./use-gsheets";
26
+ export { useWorkflowsApi, isWfTerminal, WF_TERMINAL } from "./use-workflows";
27
+ export type {
28
+ WfStatus,
29
+ WfListItem,
30
+ WfExecution,
31
+ WfEvent,
32
+ WfDetail,
33
+ WfStats,
34
+ WfVersion,
35
+ WfExecReply,
36
+ } from "./use-workflows";
26
37
  export {
27
38
  getDevToolsToken,
28
39
  setDevToolsToken,
@@ -0,0 +1,171 @@
1
+ // Fetchers + hook for the workflow-engine introspection endpoints
2
+ // (/_console/wf*). The read routes are ungated like /_console/metrics; the two
3
+ // mutating ones (cancel/advance) are admin-gated server-side, so every request
4
+ // carries the devtools Bearer token (authHeaders) like the contracts probe.
5
+
6
+ import { useCallback, useMemo } from "react";
7
+ import { useDevToolsConfig } from "./config";
8
+ import { authHeaders } from "./auth";
9
+
10
+ // Terminal statuses a workflow can settle into — used to decide whether to keep
11
+ // polling an open instance and whether Cancel is offered.
12
+ export const WF_TERMINAL = ["COMPLETED", "FAILED", "CANCELLED", "CONTINUED"] as const;
13
+
14
+ export type WfStatus =
15
+ | "RUNNING"
16
+ | "SUSPENDED"
17
+ | "COMPLETED"
18
+ | "FAILED"
19
+ | "CANCELLED"
20
+ | "CONTINUED";
21
+
22
+ // Row from GET /_console/wf — the enriched list (createdAt/updatedAt are unix ms).
23
+ export type WfListItem = {
24
+ id: string;
25
+ status: WfStatus;
26
+ workflowName: string;
27
+ createdAt: number;
28
+ updatedAt: number;
29
+ lane: string;
30
+ deployVersion: number;
31
+ };
32
+
33
+ // The execution record from GET /_console/wf/:id (raw entity field names).
34
+ export type WfExecution = {
35
+ id: string;
36
+ status: WfStatus;
37
+ workflowName: string;
38
+ args?: string;
39
+ result?: string;
40
+ error?: string;
41
+ cursor: number;
42
+ lane?: string;
43
+ deployVersion: number;
44
+ createdAt: number;
45
+ updatedAt: number;
46
+ };
47
+
48
+ // One journal entry. kind ∈ HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |
49
+ // RESUMED | RETRY | COMPLETED | FAILED | CANCELLED | CONTINUED.
50
+ export type WfEvent = {
51
+ id: string;
52
+ seq: number;
53
+ kind: string;
54
+ capability?: string;
55
+ target?: string;
56
+ op?: string;
57
+ payload?: string;
58
+ };
59
+
60
+ export type WfDetail = { execution: WfExecution; events: WfEvent[] };
61
+
62
+ export type WfStats = {
63
+ writes: number;
64
+ reads: number;
65
+ emails: number;
66
+ fetches: number;
67
+ emits: number;
68
+ retries: number;
69
+ async_inflight: number;
70
+ async_peak: number;
71
+ };
72
+
73
+ export type WfVersion = { version: number; refcount: number; current: boolean };
74
+
75
+ // The exec-reply shape returned by run/signal/advance/cancel.
76
+ export type WfExecReply = {
77
+ id: string;
78
+ outcome: string;
79
+ status: string;
80
+ result?: string;
81
+ error?: string;
82
+ };
83
+
84
+ async function jsonOrThrow(r: Response) {
85
+ const body = await r.json().catch(() => ({}));
86
+ if (!r.ok) throw new Error((body as { error?: string })?.error ?? `HTTP ${r.status}`);
87
+ return body;
88
+ }
89
+
90
+ export function isWfTerminal(status: string): boolean {
91
+ return (WF_TERMINAL as readonly string[]).includes(status);
92
+ }
93
+
94
+ export function useWorkflowsApi() {
95
+ const { apiBaseUrl } = useDevToolsConfig();
96
+
97
+ const listExecutions = useCallback(
98
+ async (): Promise<WfListItem[]> =>
99
+ jsonOrThrow(
100
+ await fetch(`${apiBaseUrl}/_console/wf`, {
101
+ credentials: "include",
102
+ headers: authHeaders(),
103
+ }),
104
+ ),
105
+ [apiBaseUrl],
106
+ );
107
+
108
+ const getExecution = useCallback(
109
+ async (id: string): Promise<WfDetail> =>
110
+ jsonOrThrow(
111
+ await fetch(`${apiBaseUrl}/_console/wf/${encodeURIComponent(id)}`, {
112
+ credentials: "include",
113
+ headers: authHeaders(),
114
+ }),
115
+ ),
116
+ [apiBaseUrl],
117
+ );
118
+
119
+ const getStats = useCallback(
120
+ async (): Promise<WfStats> =>
121
+ jsonOrThrow(
122
+ await fetch(`${apiBaseUrl}/_console/wf/stats`, {
123
+ credentials: "include",
124
+ headers: authHeaders(),
125
+ }),
126
+ ),
127
+ [apiBaseUrl],
128
+ );
129
+
130
+ const getVersions = useCallback(
131
+ async (): Promise<WfVersion[]> =>
132
+ jsonOrThrow(
133
+ await fetch(`${apiBaseUrl}/_console/wf/versions`, {
134
+ credentials: "include",
135
+ headers: authHeaders(),
136
+ }),
137
+ ),
138
+ [apiBaseUrl],
139
+ );
140
+
141
+ const cancel = useCallback(
142
+ async (id: string): Promise<WfExecReply> =>
143
+ jsonOrThrow(
144
+ await fetch(`${apiBaseUrl}/_console/wf/cancel`, {
145
+ method: "POST",
146
+ credentials: "include",
147
+ headers: authHeaders({ "content-type": "application/json" }),
148
+ body: JSON.stringify({ id }),
149
+ }),
150
+ ),
151
+ [apiBaseUrl],
152
+ );
153
+
154
+ const advance = useCallback(
155
+ async (id: string): Promise<WfExecReply> =>
156
+ jsonOrThrow(
157
+ await fetch(`${apiBaseUrl}/_console/wf/advance`, {
158
+ method: "POST",
159
+ credentials: "include",
160
+ headers: authHeaders({ "content-type": "application/json" }),
161
+ body: JSON.stringify({ id }),
162
+ }),
163
+ ),
164
+ [apiBaseUrl],
165
+ );
166
+
167
+ return useMemo(
168
+ () => ({ listExecutions, getExecution, getStats, getVersions, cancel, advance }),
169
+ [listExecutions, getExecution, getStats, getVersions, cancel, advance],
170
+ );
171
+ }