@nbt-dev/components 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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", "../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"]
3
+ "sources": ["../src/core/use-cartridge-info.ts", "../src/core/use-gsheets.ts", "../src/core/use-permissions-api.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", "import { useCallback, useEffect, useState } from \"react\";\nimport { useDevToolsConfig } from \"./config\";\nimport { authHeaders } from \"./auth\";\n\n// Operator permission management \u2014 the data layer behind the devtools Permissions\n// tab. Reads the catalog (GET /_console/permissions) and the user\u00D7role matrix\n// (GET /_console/users), and toggles grants (POST /_console/roles/{assign,revoke}).\n// All routes are operator-gated server-side; on an open/dev node they answer\n// freely. Mirrors useLiveBulkRegistry's fetch/abort shape.\n\nexport type CartPermissions = {\n slug: string;\n permissions: string[];\n roles: { name: string; perms: string[] }[];\n};\n\nexport type UserGrant = { cart: string; role: string };\n\nexport type PermUser = {\n id: string;\n email: string;\n name: string;\n capsVersion: number;\n roles: UserGrant[];\n caps: string[];\n};\n\nexport type PermissionsState = {\n carts: CartPermissions[];\n users: PermUser[];\n loading: boolean;\n error: string | null;\n refresh: () => void;\n assign: (userId: string, cart: string, role: string) => Promise<void>;\n revoke: (userId: string, cart: string, role: string) => Promise<void>;\n};\n\nexport function usePermissionsApi(): PermissionsState {\n const { apiBaseUrl } = useDevToolsConfig();\n const [carts, setCarts] = useState<CartPermissions[]>([]);\n const [users, setUsers] = useState<PermUser[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n const [nonce, setNonce] = useState(0);\n\n const refresh = useCallback(() => setNonce((n) => n + 1), []);\n\n useEffect(() => {\n const ac = new AbortController();\n let cancelled = false;\n setLoading(true);\n setError(null);\n (async () => {\n try {\n const [pr, ur] = await Promise.all([\n fetch(`${apiBaseUrl}/_console/permissions`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n }),\n fetch(`${apiBaseUrl}/_console/users`, {\n signal: ac.signal,\n credentials: \"include\",\n headers: authHeaders(),\n }),\n ]);\n if (!pr.ok) throw new Error(`permissions HTTP ${pr.status}`);\n if (!ur.ok) throw new Error(`users HTTP ${ur.status}`);\n const pdata = (await pr.json()) as { carts?: CartPermissions[] };\n const udata = (await ur.json()) as PermUser[];\n if (cancelled) return;\n setCarts(Array.isArray(pdata.carts) ? pdata.carts : []);\n setUsers(Array.isArray(udata) ? udata : []);\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, nonce]);\n\n const mutate = useCallback(\n async (path: string, userId: string, cart: string, role: string) => {\n const r = await fetch(`${apiBaseUrl}${path}`, {\n method: \"POST\",\n credentials: \"include\",\n headers: authHeaders({ \"content-type\": \"application/json\" }),\n body: JSON.stringify({ userId, cart, role }),\n });\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\n refresh();\n },\n [apiBaseUrl, refresh],\n );\n\n const assign = useCallback(\n (userId: string, cart: string, role: string) => mutate(\"/_console/roles/assign\", userId, cart, role),\n [mutate],\n );\n const revoke = useCallback(\n (userId: string, cart: string, role: string) => mutate(\"/_console/roles/revoke\", userId, cart, role),\n [mutate],\n );\n\n return { carts, users, loading, error, refresh, assign, revoke };\n}\n", "// Fetchers + hook for the workflow-engine introspection endpoints\n// (/_console/wf*). The read routes are ungated like /_console/metrics; the\n// mutating one (cancel) is admin-gated server-side, so every request carries the\n// 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 branchId?: string;\n // Provenance of what started this run: \"event:<slug>:<event>\" | \"cron:<id>\" |\n // \"manual\" | \"child:<id>\" | \"continue:<id>\" | \"\" (legacy/unknown).\n trigger?: string;\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 branchId?: string;\n trigger?: string;\n // High-res unix MICROSECONDS (start / last activity). Serialized by\n // cart_record_json as quoted strings (s64 wire form); coerce with Number().\n // 0/absent on legacy rows \u2192 fall back to createdAt/updatedAt.\n startedUs?: string | number;\n endedUs?: string | 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 // unix-ms when this entry was journaled (second-grained DateTime auto-stamp).\n createdAt: number;\n // High-res unix MICROSECONDS this entry was journaled \u2014 the timeline's real\n // start/end source (keeps sub-ms host calls distinct). Raw integer on the wire.\n // 0/absent on legacy rows \u2192 fall back to createdAt.\n tsUs?: number;\n // HOST_CALL_INTENT only: \"<line>:<col>;\u2026\" call-site in final-bundle coords,\n // mapped to authored source via the bundle sourcemap (Phase C).\n loc?: string;\n};\n\nexport type WfDetail = { execution: WfExecution; events: WfEvent[] };\n\n// The compiled workflow bundle for a version + how many leading lines are the\n// auto-prepended runtime prelude (so the inspector can skip past it to the\n// authored call site when resolving a journal entry's `loc`).\nexport type WfBundleSource = { version: number; preludeLines: number; source: string };\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 getBundleSource = useCallback(\n async (version: number): Promise<WfBundleSource> =>\n jsonOrThrow(\n await fetch(`${apiBaseUrl}/_console/wf/source?version=${encodeURIComponent(String(version))}`, {\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 return useMemo(\n () => ({ getBundleSource, cancel }),\n [getBundleSource, cancel],\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["useCallback", "useEffect", "useState", "useState", "useCallback", "useEffect", "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.4",
3
+ "version": "0.1.6",
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",
@@ -58,6 +58,7 @@
58
58
  "@codemirror/state": "^6.5.4",
59
59
  "@codemirror/view": "^6.39.5",
60
60
  "@dagrejs/dagre": "^3.0.0",
61
+ "@replit/codemirror-vscode-keymap": "^6.0.2",
61
62
  "@xyflow/react": "^12.11.0",
62
63
  "clsx": "^2.1.1",
63
64
  "lucide-react": ">=0.5.0",
package/src/core/index.ts CHANGED
@@ -23,6 +23,8 @@ 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 { usePermissionsApi } from "./use-permissions-api";
27
+ export type { CartPermissions, UserGrant, PermUser, PermissionsState } from "./use-permissions-api";
26
28
  export { useWorkflowsApi, isWfTerminal, WF_TERMINAL } from "./use-workflows";
27
29
  export type {
28
30
  WfStatus,
@@ -30,8 +32,7 @@ export type {
30
32
  WfExecution,
31
33
  WfEvent,
32
34
  WfDetail,
33
- WfStats,
34
- WfVersion,
35
+ WfBundleSource,
35
36
  WfExecReply,
36
37
  } from "./use-workflows";
37
38
  export {
@@ -0,0 +1,111 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { useDevToolsConfig } from "./config";
3
+ import { authHeaders } from "./auth";
4
+
5
+ // Operator permission management — the data layer behind the devtools Permissions
6
+ // tab. Reads the catalog (GET /_console/permissions) and the user×role matrix
7
+ // (GET /_console/users), and toggles grants (POST /_console/roles/{assign,revoke}).
8
+ // All routes are operator-gated server-side; on an open/dev node they answer
9
+ // freely. Mirrors useLiveBulkRegistry's fetch/abort shape.
10
+
11
+ export type CartPermissions = {
12
+ slug: string;
13
+ permissions: string[];
14
+ roles: { name: string; perms: string[] }[];
15
+ };
16
+
17
+ export type UserGrant = { cart: string; role: string };
18
+
19
+ export type PermUser = {
20
+ id: string;
21
+ email: string;
22
+ name: string;
23
+ capsVersion: number;
24
+ roles: UserGrant[];
25
+ caps: string[];
26
+ };
27
+
28
+ export type PermissionsState = {
29
+ carts: CartPermissions[];
30
+ users: PermUser[];
31
+ loading: boolean;
32
+ error: string | null;
33
+ refresh: () => void;
34
+ assign: (userId: string, cart: string, role: string) => Promise<void>;
35
+ revoke: (userId: string, cart: string, role: string) => Promise<void>;
36
+ };
37
+
38
+ export function usePermissionsApi(): PermissionsState {
39
+ const { apiBaseUrl } = useDevToolsConfig();
40
+ const [carts, setCarts] = useState<CartPermissions[]>([]);
41
+ const [users, setUsers] = useState<PermUser[]>([]);
42
+ const [loading, setLoading] = useState(true);
43
+ const [error, setError] = useState<string | null>(null);
44
+ const [nonce, setNonce] = useState(0);
45
+
46
+ const refresh = useCallback(() => setNonce((n) => n + 1), []);
47
+
48
+ useEffect(() => {
49
+ const ac = new AbortController();
50
+ let cancelled = false;
51
+ setLoading(true);
52
+ setError(null);
53
+ (async () => {
54
+ try {
55
+ const [pr, ur] = await Promise.all([
56
+ fetch(`${apiBaseUrl}/_console/permissions`, {
57
+ signal: ac.signal,
58
+ credentials: "include",
59
+ headers: authHeaders(),
60
+ }),
61
+ fetch(`${apiBaseUrl}/_console/users`, {
62
+ signal: ac.signal,
63
+ credentials: "include",
64
+ headers: authHeaders(),
65
+ }),
66
+ ]);
67
+ if (!pr.ok) throw new Error(`permissions HTTP ${pr.status}`);
68
+ if (!ur.ok) throw new Error(`users HTTP ${ur.status}`);
69
+ const pdata = (await pr.json()) as { carts?: CartPermissions[] };
70
+ const udata = (await ur.json()) as PermUser[];
71
+ if (cancelled) return;
72
+ setCarts(Array.isArray(pdata.carts) ? pdata.carts : []);
73
+ setUsers(Array.isArray(udata) ? udata : []);
74
+ } catch (e) {
75
+ if (cancelled || (e as { name?: string }).name === "AbortError") return;
76
+ setError(e instanceof Error ? e.message : String(e));
77
+ } finally {
78
+ if (!cancelled) setLoading(false);
79
+ }
80
+ })();
81
+ return () => {
82
+ cancelled = true;
83
+ ac.abort();
84
+ };
85
+ }, [apiBaseUrl, nonce]);
86
+
87
+ const mutate = useCallback(
88
+ async (path: string, userId: string, cart: string, role: string) => {
89
+ const r = await fetch(`${apiBaseUrl}${path}`, {
90
+ method: "POST",
91
+ credentials: "include",
92
+ headers: authHeaders({ "content-type": "application/json" }),
93
+ body: JSON.stringify({ userId, cart, role }),
94
+ });
95
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
96
+ refresh();
97
+ },
98
+ [apiBaseUrl, refresh],
99
+ );
100
+
101
+ const assign = useCallback(
102
+ (userId: string, cart: string, role: string) => mutate("/_console/roles/assign", userId, cart, role),
103
+ [mutate],
104
+ );
105
+ const revoke = useCallback(
106
+ (userId: string, cart: string, role: string) => mutate("/_console/roles/revoke", userId, cart, role),
107
+ [mutate],
108
+ );
109
+
110
+ return { carts, users, loading, error, refresh, assign, revoke };
111
+ }
@@ -1,7 +1,7 @@
1
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.
2
+ // (/_console/wf*). The read routes are ungated like /_console/metrics; the
3
+ // mutating one (cancel) is admin-gated server-side, so every request carries the
4
+ // devtools Bearer token (authHeaders) like the contracts probe.
5
5
 
6
6
  import { useCallback, useMemo } from "react";
7
7
  import { useDevToolsConfig } from "./config";
@@ -28,6 +28,10 @@ export type WfListItem = {
28
28
  updatedAt: number;
29
29
  lane: string;
30
30
  deployVersion: number;
31
+ branchId?: string;
32
+ // Provenance of what started this run: "event:<slug>:<event>" | "cron:<id>" |
33
+ // "manual" | "child:<id>" | "continue:<id>" | "" (legacy/unknown).
34
+ trigger?: string;
31
35
  };
32
36
 
33
37
  // The execution record from GET /_console/wf/:id (raw entity field names).
@@ -43,6 +47,13 @@ export type WfExecution = {
43
47
  deployVersion: number;
44
48
  createdAt: number;
45
49
  updatedAt: number;
50
+ branchId?: string;
51
+ trigger?: string;
52
+ // High-res unix MICROSECONDS (start / last activity). Serialized by
53
+ // cart_record_json as quoted strings (s64 wire form); coerce with Number().
54
+ // 0/absent on legacy rows → fall back to createdAt/updatedAt.
55
+ startedUs?: string | number;
56
+ endedUs?: string | number;
46
57
  };
47
58
 
48
59
  // One journal entry. kind ∈ HOST_CALL_INTENT | HOST_CALL_RESULT | SUSPENDED |
@@ -55,22 +66,23 @@ export type WfEvent = {
55
66
  target?: string;
56
67
  op?: string;
57
68
  payload?: string;
69
+ // unix-ms when this entry was journaled (second-grained DateTime auto-stamp).
70
+ createdAt: number;
71
+ // High-res unix MICROSECONDS this entry was journaled — the timeline's real
72
+ // start/end source (keeps sub-ms host calls distinct). Raw integer on the wire.
73
+ // 0/absent on legacy rows → fall back to createdAt.
74
+ tsUs?: number;
75
+ // HOST_CALL_INTENT only: "<line>:<col>;…" call-site in final-bundle coords,
76
+ // mapped to authored source via the bundle sourcemap (Phase C).
77
+ loc?: string;
58
78
  };
59
79
 
60
80
  export type WfDetail = { execution: WfExecution; events: WfEvent[] };
61
81
 
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 };
82
+ // The compiled workflow bundle for a version + how many leading lines are the
83
+ // auto-prepended runtime prelude (so the inspector can skip past it to the
84
+ // authored call site when resolving a journal entry's `loc`).
85
+ export type WfBundleSource = { version: number; preludeLines: number; source: string };
74
86
 
75
87
  // The exec-reply shape returned by run/signal/advance/cancel.
76
88
  export type WfExecReply = {
@@ -94,43 +106,10 @@ export function isWfTerminal(status: string): boolean {
94
106
  export function useWorkflowsApi() {
95
107
  const { apiBaseUrl } = useDevToolsConfig();
96
108
 
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> =>
109
+ const getBundleSource = useCallback(
110
+ async (version: number): Promise<WfBundleSource> =>
121
111
  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`, {
112
+ await fetch(`${apiBaseUrl}/_console/wf/source?version=${encodeURIComponent(String(version))}`, {
134
113
  credentials: "include",
135
114
  headers: authHeaders(),
136
115
  }),
@@ -151,21 +130,8 @@ export function useWorkflowsApi() {
151
130
  [apiBaseUrl],
152
131
  );
153
132
 
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
133
  return useMemo(
168
- () => ({ listExecutions, getExecution, getStats, getVersions, cancel, advance }),
169
- [listExecutions, getExecution, getStats, getVersions, cancel, advance],
134
+ () => ({ getBundleSource, cancel }),
135
+ [getBundleSource, cancel],
170
136
  );
171
137
  }
@@ -2,7 +2,7 @@
2
2
  // full-text didChange sync (debounced), pushed publishDiagnostics → lint,
3
3
  // completion, hover, and F12 go-to-definition.
4
4
 
5
- import { type Extension, type Text } from "@codemirror/state";
5
+ import { Prec, type Extension, type Text } from "@codemirror/state";
6
6
  import {
7
7
  EditorView,
8
8
  ViewPlugin,
@@ -154,7 +154,7 @@ export function lspExtensions(
154
154
  };
155
155
  });
156
156
 
157
- const gotoDef = keymap.of([
157
+ const gotoDef = Prec.high(keymap.of([
158
158
  {
159
159
  key: "F12",
160
160
  run: (view) => {
@@ -180,11 +180,13 @@ export function lspExtensions(
180
180
  return true;
181
181
  },
182
182
  },
183
- ]);
183
+ ]));
184
184
 
185
185
  return [
186
186
  syncPlugin,
187
- autocompletion({ override: [completionSource] }),
187
+ // The active keymap (vscode or stock CM) owns completion keys; disable
188
+ // autocompletion's own bindings so they don't double up / conflict.
189
+ autocompletion({ override: [completionSource], defaultKeymap: false }),
188
190
  hover,
189
191
  gotoDef,
190
192
  ];
@@ -4,12 +4,14 @@
4
4
  // changing a setting doesn't blow away cursor + scroll.
5
5
 
6
6
  import React from "react";
7
- import { Compartment, EditorState, Prec } from "@codemirror/state";
7
+ import { Compartment, EditorState, Prec, type Extension } from "@codemirror/state";
8
8
  import {
9
9
  EditorView,
10
10
  keymap,
11
11
  lineNumbers,
12
12
  drawSelection,
13
+ rectangularSelection,
14
+ crosshairCursor,
13
15
  highlightActiveLine,
14
16
  } from "@codemirror/view";
15
17
  import {
@@ -27,6 +29,7 @@ import {
27
29
  import { lintGutter } from "@codemirror/lint";
28
30
  import { searchKeymap } from "@codemirror/search";
29
31
  import { completionKeymap, closeBrackets } from "@codemirror/autocomplete";
32
+ import { vscodeKeymap } from "@replit/codemirror-vscode-keymap";
30
33
  import { nbtLanguageSupport } from "./nbt-language";
31
34
  import { themeExtension } from "./editor-themes";
32
35
  import { lspExtensions, type GotoDefHandler } from "./lsp-extensions";
@@ -51,6 +54,32 @@ function indentConfig(tabSize: number) {
51
54
  ]);
52
55
  }
53
56
 
57
+ export type EditorKeymap = "vscode" | "codemirror";
58
+
59
+ // The mode-specific bindings, swapped live via a compartment. `vscode` is the
60
+ // full VSCode set (multi-cursor, history, search, completion, folding — all of
61
+ // it) INCLUDING the Alt-click / Alt-drag modifier for adding cursors (CM6
62
+ // defaults that to Ctrl/Cmd). `codemirror` is the stock CM6 stack — keyboard
63
+ // bindings plus the default Mod-click modifier. The Mod-s save is NOT here —
64
+ // it's a separate high-prec binding so it survives the swap and wins over both.
65
+ function keymapExtension(mode: EditorKeymap): Extension {
66
+ if (mode === "codemirror") {
67
+ return keymap.of([
68
+ ...defaultKeymap,
69
+ ...historyKeymap,
70
+ ...searchKeymap,
71
+ ...completionKeymap,
72
+ indentWithTab,
73
+ ]);
74
+ }
75
+ return [
76
+ keymap.of(vscodeKeymap),
77
+ // VSCode adds a cursor with Alt-click (Alt-drag → box select, already wired
78
+ // via rectangularSelection); CM6's default is Ctrl/Cmd-click.
79
+ EditorView.clickAddsSelectionRange.of((e) => e.altKey),
80
+ ];
81
+ }
82
+
54
83
  export type NbtEditorProps = {
55
84
  uri: string;
56
85
  value: string;
@@ -63,6 +92,7 @@ export type NbtEditorProps = {
63
92
  fontSize?: number;
64
93
  lineWrap?: boolean;
65
94
  tabSize?: number;
95
+ keymap?: EditorKeymap;
66
96
  };
67
97
 
68
98
  export const NbtEditor: React.FC<NbtEditorProps> = ({
@@ -77,6 +107,7 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
77
107
  fontSize = 12,
78
108
  lineWrap = false,
79
109
  tabSize = 2,
110
+ keymap: keymapMode = "vscode",
80
111
  }) => {
81
112
  const hostRef = React.useRef<HTMLDivElement | null>(null);
82
113
  const viewRef = React.useRef<EditorView | null>(null);
@@ -95,8 +126,9 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
95
126
  const themeComp = React.useRef(new Compartment());
96
127
  const wrapComp = React.useRef(new Compartment());
97
128
  const indentComp = React.useRef(new Compartment());
98
- const settingsRef = React.useRef({ theme, fontSize, lineWrap, tabSize });
99
- settingsRef.current = { theme, fontSize, lineWrap, tabSize };
129
+ const keymapComp = React.useRef(new Compartment());
130
+ const settingsRef = React.useRef({ theme, fontSize, lineWrap, tabSize, keymapMode });
131
+ settingsRef.current = { theme, fontSize, lineWrap, tabSize, keymapMode };
100
132
 
101
133
  React.useEffect(() => {
102
134
  const host = hostRef.current;
@@ -107,6 +139,9 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
107
139
  lineNumbers(),
108
140
  history(),
109
141
  drawSelection(),
142
+ rectangularSelection(),
143
+ crosshairCursor(),
144
+ EditorState.allowMultipleSelections.of(true),
110
145
  highlightActiveLine(),
111
146
  bracketMatching(),
112
147
  closeBrackets(),
@@ -116,20 +151,20 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
116
151
  indentComp.current.of(indentConfig(s.tabSize)),
117
152
  themeComp.current.of([sizingTheme(s.fontSize), themeExtension(s.theme)]),
118
153
  wrapComp.current.of(s.lineWrap ? EditorView.lineWrapping : []),
119
- keymap.of([
120
- {
121
- key: "Mod-s",
122
- run: (view) => {
123
- onSaveRef.current?.(view.state.doc.toString());
124
- return true;
154
+ // Save wins over whichever keymap set is active — kept out of the swappable
155
+ // compartment so a mode change can't drop it.
156
+ Prec.high(
157
+ keymap.of([
158
+ {
159
+ key: "Mod-s",
160
+ run: (view) => {
161
+ onSaveRef.current?.(view.state.doc.toString());
162
+ return true;
163
+ },
125
164
  },
126
- },
127
- ...defaultKeymap,
128
- ...historyKeymap,
129
- ...searchKeymap,
130
- ...completionKeymap,
131
- indentWithTab,
132
- ]),
165
+ ]),
166
+ ),
167
+ keymapComp.current.of(keymapExtension(s.keymapMode)),
133
168
  EditorView.updateListener.of((u) => {
134
169
  if (u.docChanged) onChangeRef.current?.(u.state.doc.toString());
135
170
  }),
@@ -168,9 +203,10 @@ export const NbtEditor: React.FC<NbtEditorProps> = ({
168
203
  ]),
169
204
  wrapComp.current.reconfigure(lineWrap ? EditorView.lineWrapping : []),
170
205
  indentComp.current.reconfigure(indentConfig(tabSize)),
206
+ keymapComp.current.reconfigure(keymapExtension(keymapMode)),
171
207
  ],
172
208
  });
173
- }, [theme, fontSize, lineWrap, tabSize]);
209
+ }, [theme, fontSize, lineWrap, tabSize, keymapMode]);
174
210
 
175
211
  return <div ref={hostRef} className="h-full min-h-0 overflow-hidden" />;
176
212
  };