@aprovan/patchwork-ink 0.1.0-dev.03aaf5b

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts"],"sourcesContent":["/**\n * @aprovan/patchwork-ink - Runner\n *\n * Provides the runtime mounting logic for Ink terminal widgets.\n * The image owns all ink/react dependencies and mounting code.\n */\n\nimport type { WriteStream } from 'node:tty';\nimport { render } from 'ink';\nimport React from 'react';\n\n/**\n * Global injections for compiling widgets with this image\n *\n * These tell the compiler which imports to transform into global variable references.\n * The evaluateWidget function will provide these globals at runtime.\n */\nexport interface GlobalInjection {\n module: string;\n globalName: string;\n}\n\nexport function getGlobals(): GlobalInjection[] {\n return [\n { module: 'react', globalName: '__REACT__' },\n { module: 'ink', globalName: '__INK__' },\n ];\n}\n\nexport interface RunnerOptions {\n /** Service proxy for UTCP calls */\n proxy?: {\n call(\n namespace: string,\n procedure: string,\n args: unknown[],\n ): Promise<unknown>;\n };\n /** Initial props/inputs to pass to widget */\n inputs?: Record<string, unknown>;\n /** Output stream (default: process.stdout) */\n stdout?: WriteStream;\n /** Input stream (default: process.stdin) */\n stdin?: NodeJS.ReadStream;\n /** Exit on Ctrl+C (default: true) */\n exitOnCtrlC?: boolean;\n}\n\nexport interface RunnerInstance {\n /** Unique mount ID */\n id: string;\n /** Unmount the widget */\n unmount: () => void;\n /** Wait until the widget exits */\n waitUntilExit: () => Promise<void>;\n /** Rerender with new props */\n rerender: (props: Record<string, unknown>) => void;\n /** Clear the terminal output */\n clear: () => void;\n}\n\nlet mountCounter = 0;\n\nfunction generateMountId(): string {\n return `patchwork-ink-${Date.now()}-${++mountCounter}`;\n}\n\n/**\n * Generate namespace globals that proxy calls to a service proxy\n *\n * Given services like [\"git.branch\", \"git.status\", \"github.repos.get\"],\n * generates globals with appropriate methods.\n */\nfunction generateNamespaceGlobals(\n services: string[],\n proxy: RunnerOptions['proxy'],\n): Record<string, unknown> {\n if (!proxy) return {};\n\n const namespaces: Record<string, Record<string, unknown>> = {};\n\n for (const service of services) {\n const parts = service.split('.');\n if (parts.length < 2) continue;\n\n const namespace = parts[0] as string;\n const procedurePath = parts.slice(1);\n\n if (!namespaces[namespace]) {\n namespaces[namespace] = {};\n }\n\n let current = namespaces[namespace] as Record<string, unknown>;\n for (let i = 0; i < procedurePath.length - 1; i++) {\n const key = procedurePath[i] as string;\n if (!current[key]) {\n current[key] = {};\n }\n current = current[key] as Record<string, unknown>;\n }\n\n const finalKey = procedurePath[procedurePath.length - 1] as string;\n const fullProcedure = procedurePath.join('.');\n current[finalKey] = (...args: unknown[]) =>\n proxy.call(namespace, fullProcedure, args);\n }\n\n return namespaces;\n}\n\n/**\n * Extract unique namespace names from services array\n */\nfunction extractNamespaces(services: string[]): string[] {\n const namespaces = new Set<string>();\n for (const service of services) {\n const parts = service.split('.');\n if (parts[0]) {\n namespaces.add(parts[0]);\n }\n }\n return Array.from(namespaces);\n}\n\n/**\n * Inject namespace globals into globalThis\n */\nfunction injectNamespaceGlobals(namespaces: Record<string, unknown>): void {\n for (const [name, value] of Object.entries(namespaces)) {\n (globalThis as Record<string, unknown>)[name] = value;\n }\n}\n\n/**\n * Remove namespace globals from globalThis\n */\nfunction removeNamespaceGlobals(namespaceNames: string[]): void {\n for (const name of namespaceNames) {\n delete (globalThis as Record<string, unknown>)[name];\n }\n}\n\nexport interface CompiledWidget {\n /** Compiled ESM code */\n code: string;\n /** Content hash for caching */\n hash: string;\n /** Original manifest */\n manifest: {\n name: string;\n services?: string[];\n [key: string]: unknown;\n };\n}\n\n/**\n * Run a compiled widget using Ink\n *\n * This is the main entry point for running terminal widgets.\n * The image owns all React/Ink dependencies.\n */\nexport async function run(\n widget: CompiledWidget,\n options: RunnerOptions = {},\n): Promise<RunnerInstance> {\n const {\n proxy,\n inputs = {},\n stdout = process.stdout as WriteStream,\n stdin = process.stdin,\n exitOnCtrlC = true,\n } = options;\n const mountId = generateMountId();\n\n // Inject namespace globals for services\n const services = widget.manifest.services || [];\n const namespaceNames = extractNamespaces(services);\n const namespaces = generateNamespaceGlobals(services, proxy);\n injectNamespaceGlobals(namespaces);\n\n // Import the widget module from code\n const dataUri = `data:text/javascript;base64,${Buffer.from(\n widget.code,\n ).toString('base64')}`;\n\n let module: { default?: unknown };\n try {\n module = await import(/* webpackIgnore: true */ /* @vite-ignore */ dataUri);\n } catch {\n // Fallback: use Function-based loading\n const AsyncFunction = Object.getPrototypeOf(async function () {})\n .constructor as new (argName: string, code: string) => (\n exports: Record<string, unknown>,\n ) => Promise<Record<string, unknown>>;\n const exports: Record<string, unknown> = {};\n const fn = new AsyncFunction('exports', widget.code + '\\nreturn exports;');\n module = await fn(exports);\n }\n\n const Component = module.default;\n if (!Component) {\n removeNamespaceGlobals(namespaceNames);\n throw new Error('Widget must export a default component');\n }\n\n if (typeof Component !== 'function') {\n removeNamespaceGlobals(namespaceNames);\n throw new Error('Widget default export must be a function/component');\n }\n\n // Render using Ink\n let currentInputs = { ...inputs };\n const element = React.createElement(\n Component as React.ComponentType,\n currentInputs,\n );\n const instance = render(element, {\n stdout,\n stdin,\n exitOnCtrlC,\n });\n\n return {\n id: mountId,\n unmount() {\n instance.unmount();\n removeNamespaceGlobals(namespaceNames);\n },\n waitUntilExit() {\n return instance.waitUntilExit();\n },\n rerender(newInputs: Record<string, unknown>) {\n currentInputs = { ...currentInputs, ...newInputs };\n const newElement = React.createElement(\n Component as React.ComponentType,\n currentInputs,\n );\n instance.rerender(newElement);\n },\n clear() {\n instance.clear();\n },\n };\n}\n\n/**\n * Run a widget once and wait for exit\n */\nexport async function runOnce(\n widget: CompiledWidget,\n options: RunnerOptions = {},\n): Promise<void> {\n const instance = await run(widget, options);\n await instance.waitUntilExit();\n instance.unmount();\n}\n\n/**\n * Evaluate widget code and return the component\n *\n * This is used for more advanced scenarios where you need\n * direct access to the component.\n */\nexport async function evaluateWidget(\n code: string,\n services: Record<string, unknown> = {},\n): Promise<React.ComponentType<{ services?: Record<string, unknown> }>> {\n // Store services for widget access\n (globalThis as Record<string, unknown>).__PATCHWORK_SERVICES__ = services;\n\n // Inject globals that the compiled code expects\n const __EXPORTS__: Record<string, unknown> = {};\n const __REACT__ = React;\n const __INK__ = await import('ink');\n\n // Execute the transformed code with injected globals\n const fn = new Function('__EXPORTS__', '__REACT__', '__INK__', code);\n fn(__EXPORTS__, __REACT__, __INK__);\n\n const Component =\n __EXPORTS__.default ||\n __EXPORTS__.Widget ||\n Object.values(__EXPORTS__).find(\n (v): v is React.ComponentType => typeof v === 'function',\n );\n\n if (!Component) {\n throw new Error('No default export or Widget component found');\n }\n\n return Component as React.ComponentType<{\n services?: Record<string, unknown>;\n }>;\n}\n\n/**\n * Render a component directly with Ink\n *\n * For cases where you already have an evaluated component.\n */\nexport function renderComponent(\n Component: React.ComponentType<Record<string, unknown>>,\n props: Record<string, unknown> = {},\n options: Omit<RunnerOptions, 'proxy' | 'inputs'> = {},\n): RunnerInstance {\n const {\n stdout = process.stdout as WriteStream,\n stdin = process.stdin,\n exitOnCtrlC = true,\n } = options;\n const mountId = generateMountId();\n\n let currentProps = { ...props };\n const element = React.createElement(Component, currentProps);\n const instance = render(element, {\n stdout,\n stdin,\n exitOnCtrlC,\n });\n\n return {\n id: mountId,\n unmount: () => instance.unmount(),\n waitUntilExit: () => instance.waitUntilExit(),\n rerender(newProps: Record<string, unknown>) {\n currentProps = { ...currentProps, ...newProps };\n instance.rerender(React.createElement(Component, currentProps));\n },\n clear: () => instance.clear(),\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,6 @@
1
+ export { InkEnvironment, SetupOptions, cleanup, setup } from './setup.js';
2
+ export { CompiledWidget, GlobalInjection, RunnerInstance, RunnerOptions, evaluateWidget, getGlobals, renderComponent, run, runOnce } from './runner.js';
3
+ export { Box, Newline, Spacer, Static, Text, Transform, render, useApp, useFocus, useFocusManager, useInput, useStderr, useStdin, useStdout } from 'ink';
4
+ export { default as React } from 'react';
5
+ export { default as chalk } from 'chalk';
6
+ import 'node:tty';
package/dist/index.js ADDED
@@ -0,0 +1,57 @@
1
+ import {
2
+ evaluateWidget,
3
+ getGlobals,
4
+ renderComponent,
5
+ run,
6
+ runOnce
7
+ } from "./chunk-FQPGY42H.js";
8
+ import {
9
+ cleanup,
10
+ setup
11
+ } from "./chunk-5NAXJKRC.js";
12
+
13
+ // src/index.ts
14
+ import {
15
+ render,
16
+ Box,
17
+ Text,
18
+ Static,
19
+ Transform,
20
+ Newline,
21
+ Spacer,
22
+ useInput,
23
+ useApp,
24
+ useFocus,
25
+ useFocusManager,
26
+ useStdin,
27
+ useStdout,
28
+ useStderr
29
+ } from "ink";
30
+ import { default as default2 } from "react";
31
+ import { default as default3 } from "chalk";
32
+ export {
33
+ Box,
34
+ Newline,
35
+ default2 as React,
36
+ Spacer,
37
+ Static,
38
+ Text,
39
+ Transform,
40
+ default3 as chalk,
41
+ cleanup,
42
+ evaluateWidget,
43
+ getGlobals,
44
+ render,
45
+ renderComponent,
46
+ run,
47
+ runOnce,
48
+ setup,
49
+ useApp,
50
+ useFocus,
51
+ useFocusManager,
52
+ useInput,
53
+ useStderr,
54
+ useStdin,
55
+ useStdout
56
+ };
57
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @aprovan/patchwork-image-ink\n *\n * Ink terminal UI image for CLI widgets.\n * Provides React components for building terminal interfaces.\n */\n\nexport { setup, cleanup } from './setup.js';\nexport type { SetupOptions, InkEnvironment } from './setup.js';\n\n// Runner - mounting/execution for terminal widgets\nexport {\n run,\n runOnce,\n evaluateWidget,\n renderComponent,\n getGlobals,\n} from './runner.js';\nexport type {\n RunnerOptions,\n RunnerInstance,\n CompiledWidget,\n GlobalInjection,\n} from './runner.js';\n\n// Re-export Ink components for convenience\nexport {\n render,\n Box,\n Text,\n Static,\n Transform,\n Newline,\n Spacer,\n useInput,\n useApp,\n useFocus,\n useFocusManager,\n useStdin,\n useStdout,\n useStderr,\n} from 'ink';\n\n// Re-export React for widget authors\nexport { default as React } from 'react';\n\n// Re-export chalk for styling\nexport { default as chalk } from 'chalk';\n"],"mappings":";;;;;;;;;;;;;AA0BA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAoB,WAAXA,gBAAwB;AAGjC,SAAoB,WAAXA,gBAAwB;","names":["default"]}
@@ -0,0 +1,87 @@
1
+ import { WriteStream } from 'node:tty';
2
+ import React from 'react';
3
+
4
+ /**
5
+ * @aprovan/patchwork-ink - Runner
6
+ *
7
+ * Provides the runtime mounting logic for Ink terminal widgets.
8
+ * The image owns all ink/react dependencies and mounting code.
9
+ */
10
+
11
+ /**
12
+ * Global injections for compiling widgets with this image
13
+ *
14
+ * These tell the compiler which imports to transform into global variable references.
15
+ * The evaluateWidget function will provide these globals at runtime.
16
+ */
17
+ interface GlobalInjection {
18
+ module: string;
19
+ globalName: string;
20
+ }
21
+ declare function getGlobals(): GlobalInjection[];
22
+ interface RunnerOptions {
23
+ /** Service proxy for UTCP calls */
24
+ proxy?: {
25
+ call(namespace: string, procedure: string, args: unknown[]): Promise<unknown>;
26
+ };
27
+ /** Initial props/inputs to pass to widget */
28
+ inputs?: Record<string, unknown>;
29
+ /** Output stream (default: process.stdout) */
30
+ stdout?: WriteStream;
31
+ /** Input stream (default: process.stdin) */
32
+ stdin?: NodeJS.ReadStream;
33
+ /** Exit on Ctrl+C (default: true) */
34
+ exitOnCtrlC?: boolean;
35
+ }
36
+ interface RunnerInstance {
37
+ /** Unique mount ID */
38
+ id: string;
39
+ /** Unmount the widget */
40
+ unmount: () => void;
41
+ /** Wait until the widget exits */
42
+ waitUntilExit: () => Promise<void>;
43
+ /** Rerender with new props */
44
+ rerender: (props: Record<string, unknown>) => void;
45
+ /** Clear the terminal output */
46
+ clear: () => void;
47
+ }
48
+ interface CompiledWidget {
49
+ /** Compiled ESM code */
50
+ code: string;
51
+ /** Content hash for caching */
52
+ hash: string;
53
+ /** Original manifest */
54
+ manifest: {
55
+ name: string;
56
+ services?: string[];
57
+ [key: string]: unknown;
58
+ };
59
+ }
60
+ /**
61
+ * Run a compiled widget using Ink
62
+ *
63
+ * This is the main entry point for running terminal widgets.
64
+ * The image owns all React/Ink dependencies.
65
+ */
66
+ declare function run(widget: CompiledWidget, options?: RunnerOptions): Promise<RunnerInstance>;
67
+ /**
68
+ * Run a widget once and wait for exit
69
+ */
70
+ declare function runOnce(widget: CompiledWidget, options?: RunnerOptions): Promise<void>;
71
+ /**
72
+ * Evaluate widget code and return the component
73
+ *
74
+ * This is used for more advanced scenarios where you need
75
+ * direct access to the component.
76
+ */
77
+ declare function evaluateWidget(code: string, services?: Record<string, unknown>): Promise<React.ComponentType<{
78
+ services?: Record<string, unknown>;
79
+ }>>;
80
+ /**
81
+ * Render a component directly with Ink
82
+ *
83
+ * For cases where you already have an evaluated component.
84
+ */
85
+ declare function renderComponent(Component: React.ComponentType<Record<string, unknown>>, props?: Record<string, unknown>, options?: Omit<RunnerOptions, 'proxy' | 'inputs'>): RunnerInstance;
86
+
87
+ export { type CompiledWidget, type GlobalInjection, type RunnerInstance, type RunnerOptions, evaluateWidget, getGlobals, renderComponent, run, runOnce };
package/dist/runner.js ADDED
@@ -0,0 +1,15 @@
1
+ import {
2
+ evaluateWidget,
3
+ getGlobals,
4
+ renderComponent,
5
+ run,
6
+ runOnce
7
+ } from "./chunk-FQPGY42H.js";
8
+ export {
9
+ evaluateWidget,
10
+ getGlobals,
11
+ renderComponent,
12
+ run,
13
+ runOnce
14
+ };
15
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,40 @@
1
+ import { WriteStream } from 'node:tty';
2
+
3
+ /**
4
+ * @aprovan/patchwork-image-ink
5
+ *
6
+ * Setup function for the Ink terminal UI image.
7
+ * Handles terminal environment configuration for CLI widgets.
8
+ */
9
+
10
+ interface SetupOptions {
11
+ /** Output stream (default: process.stdout) */
12
+ stdout?: WriteStream;
13
+ /** Input stream (default: process.stdin) */
14
+ stdin?: NodeJS.ReadStream;
15
+ /** Enable color support detection override */
16
+ colorMode?: 'detect' | 'ansi' | 'ansi256' | 'truecolor' | 'none';
17
+ /** Enable debug mode (default: false) */
18
+ debug?: boolean;
19
+ }
20
+ interface InkEnvironment {
21
+ stdout: WriteStream;
22
+ stdin: NodeJS.ReadStream;
23
+ colorSupport: 'none' | 'ansi' | 'ansi256' | 'truecolor';
24
+ isInteractive: boolean;
25
+ columns: number;
26
+ rows: number;
27
+ }
28
+ /**
29
+ * Setup the Ink terminal UI image runtime environment
30
+ *
31
+ * @param options - Optional configuration
32
+ * @returns Environment configuration for Ink
33
+ */
34
+ declare function setup(options?: SetupOptions): InkEnvironment;
35
+ /**
36
+ * Cleanup - no-op for CLI but provided for API consistency
37
+ */
38
+ declare function cleanup(): void;
39
+
40
+ export { type InkEnvironment, type SetupOptions, cleanup, setup };
package/dist/setup.js ADDED
@@ -0,0 +1,9 @@
1
+ import {
2
+ cleanup,
3
+ setup
4
+ } from "./chunk-5NAXJKRC.js";
5
+ export {
6
+ cleanup,
7
+ setup
8
+ };
9
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@aprovan/patchwork-ink",
3
+ "version": "0.1.0-dev.03aaf5b",
4
+ "description": "Patchwork image: Ink terminal UI for CLI widgets",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./runner": {
14
+ "types": "./dist/runner.d.ts",
15
+ "import": "./dist/runner.js"
16
+ },
17
+ "./setup": {
18
+ "types": "./dist/setup.d.ts",
19
+ "import": "./dist/setup.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "patchwork": {
24
+ "platform": "cli",
25
+ "esbuild": {
26
+ "target": "node20",
27
+ "format": "esm",
28
+ "jsx": "automatic"
29
+ }
30
+ },
31
+ "dependencies": {
32
+ "chalk": "^5.0.0",
33
+ "ink": "^5.0.0",
34
+ "ink-box": "^2.0.0",
35
+ "ink-link": "^4.0.0",
36
+ "ink-spinner": "^5.0.0",
37
+ "react": "^18.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.10.5",
41
+ "@types/react": "^18.0.0",
42
+ "tsup": "^8.3.5",
43
+ "typescript": "^5.7.3"
44
+ },
45
+ "engines": {
46
+ "node": ">=20.0.0"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit"
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @aprovan/patchwork-image-ink
3
+ *
4
+ * Ink terminal UI image for CLI widgets.
5
+ * Provides React components for building terminal interfaces.
6
+ */
7
+
8
+ export { setup, cleanup } from './setup.js';
9
+ export type { SetupOptions, InkEnvironment } from './setup.js';
10
+
11
+ // Runner - mounting/execution for terminal widgets
12
+ export {
13
+ run,
14
+ runOnce,
15
+ evaluateWidget,
16
+ renderComponent,
17
+ getGlobals,
18
+ } from './runner.js';
19
+ export type {
20
+ RunnerOptions,
21
+ RunnerInstance,
22
+ CompiledWidget,
23
+ GlobalInjection,
24
+ } from './runner.js';
25
+
26
+ // Re-export Ink components for convenience
27
+ export {
28
+ render,
29
+ Box,
30
+ Text,
31
+ Static,
32
+ Transform,
33
+ Newline,
34
+ Spacer,
35
+ useInput,
36
+ useApp,
37
+ useFocus,
38
+ useFocusManager,
39
+ useStdin,
40
+ useStdout,
41
+ useStderr,
42
+ } from 'ink';
43
+
44
+ // Re-export React for widget authors
45
+ export { default as React } from 'react';
46
+
47
+ // Re-export chalk for styling
48
+ export { default as chalk } from 'chalk';
package/src/runner.ts ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * @aprovan/patchwork-ink - Runner
3
+ *
4
+ * Provides the runtime mounting logic for Ink terminal widgets.
5
+ * The image owns all ink/react dependencies and mounting code.
6
+ */
7
+
8
+ import type { WriteStream } from 'node:tty';
9
+ import { render } from 'ink';
10
+ import React from 'react';
11
+
12
+ /**
13
+ * Global injections for compiling widgets with this image
14
+ *
15
+ * These tell the compiler which imports to transform into global variable references.
16
+ * The evaluateWidget function will provide these globals at runtime.
17
+ */
18
+ export interface GlobalInjection {
19
+ module: string;
20
+ globalName: string;
21
+ }
22
+
23
+ export function getGlobals(): GlobalInjection[] {
24
+ return [
25
+ { module: 'react', globalName: '__REACT__' },
26
+ { module: 'ink', globalName: '__INK__' },
27
+ ];
28
+ }
29
+
30
+ export interface RunnerOptions {
31
+ /** Service proxy for UTCP calls */
32
+ proxy?: {
33
+ call(
34
+ namespace: string,
35
+ procedure: string,
36
+ args: unknown[],
37
+ ): Promise<unknown>;
38
+ };
39
+ /** Initial props/inputs to pass to widget */
40
+ inputs?: Record<string, unknown>;
41
+ /** Output stream (default: process.stdout) */
42
+ stdout?: WriteStream;
43
+ /** Input stream (default: process.stdin) */
44
+ stdin?: NodeJS.ReadStream;
45
+ /** Exit on Ctrl+C (default: true) */
46
+ exitOnCtrlC?: boolean;
47
+ }
48
+
49
+ export interface RunnerInstance {
50
+ /** Unique mount ID */
51
+ id: string;
52
+ /** Unmount the widget */
53
+ unmount: () => void;
54
+ /** Wait until the widget exits */
55
+ waitUntilExit: () => Promise<void>;
56
+ /** Rerender with new props */
57
+ rerender: (props: Record<string, unknown>) => void;
58
+ /** Clear the terminal output */
59
+ clear: () => void;
60
+ }
61
+
62
+ let mountCounter = 0;
63
+
64
+ function generateMountId(): string {
65
+ return `patchwork-ink-${Date.now()}-${++mountCounter}`;
66
+ }
67
+
68
+ /**
69
+ * Generate namespace globals that proxy calls to a service proxy
70
+ *
71
+ * Given services like ["git.branch", "git.status", "github.repos.get"],
72
+ * generates globals with appropriate methods.
73
+ */
74
+ function generateNamespaceGlobals(
75
+ services: string[],
76
+ proxy: RunnerOptions['proxy'],
77
+ ): Record<string, unknown> {
78
+ if (!proxy) return {};
79
+
80
+ const namespaces: Record<string, Record<string, unknown>> = {};
81
+
82
+ for (const service of services) {
83
+ const parts = service.split('.');
84
+ if (parts.length < 2) continue;
85
+
86
+ const namespace = parts[0] as string;
87
+ const procedurePath = parts.slice(1);
88
+
89
+ if (!namespaces[namespace]) {
90
+ namespaces[namespace] = {};
91
+ }
92
+
93
+ let current = namespaces[namespace] as Record<string, unknown>;
94
+ for (let i = 0; i < procedurePath.length - 1; i++) {
95
+ const key = procedurePath[i] as string;
96
+ if (!current[key]) {
97
+ current[key] = {};
98
+ }
99
+ current = current[key] as Record<string, unknown>;
100
+ }
101
+
102
+ const finalKey = procedurePath[procedurePath.length - 1] as string;
103
+ const fullProcedure = procedurePath.join('.');
104
+ current[finalKey] = (...args: unknown[]) =>
105
+ proxy.call(namespace, fullProcedure, args);
106
+ }
107
+
108
+ return namespaces;
109
+ }
110
+
111
+ /**
112
+ * Extract unique namespace names from services array
113
+ */
114
+ function extractNamespaces(services: string[]): string[] {
115
+ const namespaces = new Set<string>();
116
+ for (const service of services) {
117
+ const parts = service.split('.');
118
+ if (parts[0]) {
119
+ namespaces.add(parts[0]);
120
+ }
121
+ }
122
+ return Array.from(namespaces);
123
+ }
124
+
125
+ /**
126
+ * Inject namespace globals into globalThis
127
+ */
128
+ function injectNamespaceGlobals(namespaces: Record<string, unknown>): void {
129
+ for (const [name, value] of Object.entries(namespaces)) {
130
+ (globalThis as Record<string, unknown>)[name] = value;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Remove namespace globals from globalThis
136
+ */
137
+ function removeNamespaceGlobals(namespaceNames: string[]): void {
138
+ for (const name of namespaceNames) {
139
+ delete (globalThis as Record<string, unknown>)[name];
140
+ }
141
+ }
142
+
143
+ export interface CompiledWidget {
144
+ /** Compiled ESM code */
145
+ code: string;
146
+ /** Content hash for caching */
147
+ hash: string;
148
+ /** Original manifest */
149
+ manifest: {
150
+ name: string;
151
+ services?: string[];
152
+ [key: string]: unknown;
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Run a compiled widget using Ink
158
+ *
159
+ * This is the main entry point for running terminal widgets.
160
+ * The image owns all React/Ink dependencies.
161
+ */
162
+ export async function run(
163
+ widget: CompiledWidget,
164
+ options: RunnerOptions = {},
165
+ ): Promise<RunnerInstance> {
166
+ const {
167
+ proxy,
168
+ inputs = {},
169
+ stdout = process.stdout as WriteStream,
170
+ stdin = process.stdin,
171
+ exitOnCtrlC = true,
172
+ } = options;
173
+ const mountId = generateMountId();
174
+
175
+ // Inject namespace globals for services
176
+ const services = widget.manifest.services || [];
177
+ const namespaceNames = extractNamespaces(services);
178
+ const namespaces = generateNamespaceGlobals(services, proxy);
179
+ injectNamespaceGlobals(namespaces);
180
+
181
+ // Import the widget module from code
182
+ const dataUri = `data:text/javascript;base64,${Buffer.from(
183
+ widget.code,
184
+ ).toString('base64')}`;
185
+
186
+ let module: { default?: unknown };
187
+ try {
188
+ module = await import(/* webpackIgnore: true */ /* @vite-ignore */ dataUri);
189
+ } catch {
190
+ // Fallback: use Function-based loading
191
+ const AsyncFunction = Object.getPrototypeOf(async function () {})
192
+ .constructor as new (argName: string, code: string) => (
193
+ exports: Record<string, unknown>,
194
+ ) => Promise<Record<string, unknown>>;
195
+ const exports: Record<string, unknown> = {};
196
+ const fn = new AsyncFunction('exports', widget.code + '\nreturn exports;');
197
+ module = await fn(exports);
198
+ }
199
+
200
+ const Component = module.default;
201
+ if (!Component) {
202
+ removeNamespaceGlobals(namespaceNames);
203
+ throw new Error('Widget must export a default component');
204
+ }
205
+
206
+ if (typeof Component !== 'function') {
207
+ removeNamespaceGlobals(namespaceNames);
208
+ throw new Error('Widget default export must be a function/component');
209
+ }
210
+
211
+ // Render using Ink
212
+ let currentInputs = { ...inputs };
213
+ const element = React.createElement(
214
+ Component as React.ComponentType,
215
+ currentInputs,
216
+ );
217
+ const instance = render(element, {
218
+ stdout,
219
+ stdin,
220
+ exitOnCtrlC,
221
+ });
222
+
223
+ return {
224
+ id: mountId,
225
+ unmount() {
226
+ instance.unmount();
227
+ removeNamespaceGlobals(namespaceNames);
228
+ },
229
+ waitUntilExit() {
230
+ return instance.waitUntilExit();
231
+ },
232
+ rerender(newInputs: Record<string, unknown>) {
233
+ currentInputs = { ...currentInputs, ...newInputs };
234
+ const newElement = React.createElement(
235
+ Component as React.ComponentType,
236
+ currentInputs,
237
+ );
238
+ instance.rerender(newElement);
239
+ },
240
+ clear() {
241
+ instance.clear();
242
+ },
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Run a widget once and wait for exit
248
+ */
249
+ export async function runOnce(
250
+ widget: CompiledWidget,
251
+ options: RunnerOptions = {},
252
+ ): Promise<void> {
253
+ const instance = await run(widget, options);
254
+ await instance.waitUntilExit();
255
+ instance.unmount();
256
+ }
257
+
258
+ /**
259
+ * Evaluate widget code and return the component
260
+ *
261
+ * This is used for more advanced scenarios where you need
262
+ * direct access to the component.
263
+ */
264
+ export async function evaluateWidget(
265
+ code: string,
266
+ services: Record<string, unknown> = {},
267
+ ): Promise<React.ComponentType<{ services?: Record<string, unknown> }>> {
268
+ // Store services for widget access
269
+ (globalThis as Record<string, unknown>).__PATCHWORK_SERVICES__ = services;
270
+
271
+ // Inject globals that the compiled code expects
272
+ const __EXPORTS__: Record<string, unknown> = {};
273
+ const __REACT__ = React;
274
+ const __INK__ = await import('ink');
275
+
276
+ // Execute the transformed code with injected globals
277
+ const fn = new Function('__EXPORTS__', '__REACT__', '__INK__', code);
278
+ fn(__EXPORTS__, __REACT__, __INK__);
279
+
280
+ const Component =
281
+ __EXPORTS__.default ||
282
+ __EXPORTS__.Widget ||
283
+ Object.values(__EXPORTS__).find(
284
+ (v): v is React.ComponentType => typeof v === 'function',
285
+ );
286
+
287
+ if (!Component) {
288
+ throw new Error('No default export or Widget component found');
289
+ }
290
+
291
+ return Component as React.ComponentType<{
292
+ services?: Record<string, unknown>;
293
+ }>;
294
+ }
295
+
296
+ /**
297
+ * Render a component directly with Ink
298
+ *
299
+ * For cases where you already have an evaluated component.
300
+ */
301
+ export function renderComponent(
302
+ Component: React.ComponentType<Record<string, unknown>>,
303
+ props: Record<string, unknown> = {},
304
+ options: Omit<RunnerOptions, 'proxy' | 'inputs'> = {},
305
+ ): RunnerInstance {
306
+ const {
307
+ stdout = process.stdout as WriteStream,
308
+ stdin = process.stdin,
309
+ exitOnCtrlC = true,
310
+ } = options;
311
+ const mountId = generateMountId();
312
+
313
+ let currentProps = { ...props };
314
+ const element = React.createElement(Component, currentProps);
315
+ const instance = render(element, {
316
+ stdout,
317
+ stdin,
318
+ exitOnCtrlC,
319
+ });
320
+
321
+ return {
322
+ id: mountId,
323
+ unmount: () => instance.unmount(),
324
+ waitUntilExit: () => instance.waitUntilExit(),
325
+ rerender(newProps: Record<string, unknown>) {
326
+ currentProps = { ...currentProps, ...newProps };
327
+ instance.rerender(React.createElement(Component, currentProps));
328
+ },
329
+ clear: () => instance.clear(),
330
+ };
331
+ }