@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.
- package/.turbo/turbo-build.log +27 -0
- package/LICENSE +373 -0
- package/dist/chunk-5NAXJKRC.js +62 -0
- package/dist/chunk-5NAXJKRC.js.map +1 -0
- package/dist/chunk-FQPGY42H.js +183 -0
- package/dist/chunk-FQPGY42H.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/runner.d.ts +87 -0
- package/dist/runner.js +15 -0
- package/dist/runner.js.map +1 -0
- package/dist/setup.d.ts +40 -0
- package/dist/setup.js +9 -0
- package/dist/setup.js.map +1 -0
- package/package.json +53 -0
- package/src/index.ts +48 -0
- package/src/runner.ts +331 -0
- package/src/setup.ts +123 -0
- package/tsconfig.json +10 -0
- package/tsup.config.ts +11 -0
|
@@ -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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -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"]}
|
package/dist/runner.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/dist/setup.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|