From 5afbcc0d992e0fc23183bcc94550de2907b0633b Mon Sep 17 00:00:00 2001 From: natew Date: Fri, 7 Feb 2025 14:36:38 -1000 Subject: [PATCH 1/5] feat(lllink): add --unlink --- packages/lllink/src/index.ts | 36 +++++++++++++++++++++++++------ packages/one/src/router/router.ts | 4 ++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/lllink/src/index.ts b/packages/lllink/src/index.ts index f0e9ce62e..68d93480f 100755 --- a/packages/lllink/src/index.ts +++ b/packages/lllink/src/index.ts @@ -5,7 +5,7 @@ * bun run link-workspaces.ts ... */ -import { readdir, readFile, stat, rm, symlink } from 'node:fs/promises' +import { readdir, readFile, stat, rm, symlink, rename, mkdir } from 'node:fs/promises' import { homedir } from 'node:os' import { join, relative, resolve } from 'node:path' @@ -63,22 +63,46 @@ async function findLocalPackages(rootDir: string): Promise) { + const backupDir = join(process.cwd(), 'node_modules', '.cache', 'lllink', 'moved') + await mkdir(backupDir, { recursive: true }) for (const pkgName of Object.keys(localPackages)) { const nmPath = join(process.cwd(), 'node_modules', ...pkgName.split('/')) try { const existingStat = await stat(nmPath) - if (existingStat) { - await rm(nmPath, { recursive: true, force: true }) - const localPath = localPackages[pkgName] - console.info(`${relative(process.cwd(), nmPath)} -> ${localPath.replace(homedir(), '~')}`) - await symlink(localPath, nmPath, 'dir') + if (existingStat && (existingStat.isDirectory() || existingStat.isSymbolicLink())) { + await rename(nmPath, join(backupDir, pkgName.replace('/', '__'))) } + const localPath = localPackages[pkgName] + console.info(`${relative(process.cwd(), nmPath)} -> ${localPath.replace(homedir(), '~')}`) + await symlink(localPath, nmPath, 'dir') } catch {} } } +async function undoLinks() { + const backupDir = join(process.cwd(), 'node_modules', '.cache', 'lllink', 'moved') + let movedItems: string[] + try { + movedItems = await readdir(backupDir) + } catch { + console.info('Nothing to undo.') + return + } + for (const item of movedItems) { + const originalName = item.replace('__', '/') + const nmPath = join(process.cwd(), 'node_modules', ...originalName.split('/')) + await rm(nmPath, { recursive: true, force: true }).catch(() => {}) + await rename(join(backupDir, item), nmPath).catch(() => {}) + console.info(`Restored: ${originalName}`) + } +} + async function main() { const args = process.argv.slice(2) + if (args.includes('--unlink')) { + await undoLinks() + process.exit(0) + } if (args.length === 0) { console.info('No workspace directories provided.') process.exit(0) diff --git a/packages/one/src/router/router.ts b/packages/one/src/router/router.ts index 9abad7def..ef216177b 100644 --- a/packages/one/src/router/router.ts +++ b/packages/one/src/router/router.ts @@ -138,6 +138,10 @@ function subscribeToNavigationChanges() { nextState = undefined if (state && state !== rootState) { + if (process.env.NODE_ENV === 'development') { + console.info(` [one] router`, state) + } + updateState(state, undefined) shouldUpdateSubscribers = true } From c5c14794ced80ad0aca6355613393d7da09d171e Mon Sep 17 00:00:00 2001 From: natew Date: Fri, 7 Feb 2025 19:44:47 -1000 Subject: [PATCH 2/5] add up --- packages/one/package.json | 1 + packages/one/src/cli.ts | 14 ++ packages/one/src/cli/up.tsx | 5 + packages/one/types/cli/up.d.ts | 44 ++++++ packages/up/biome.json | 78 ++++++++++ packages/up/build.ts | 42 ++++++ packages/up/package.json | 47 ++++++ packages/up/src/docker.ts | 113 +++++++++++++++ packages/up/src/index.tsx | 10 ++ packages/up/src/tui.tsx | 257 +++++++++++++++++++++++++++++++++ packages/up/src/tuiTypes.ts | 0 packages/up/src/typeHelpers.ts | 36 +++++ packages/up/src/utils.ts | 27 ++++ packages/up/tsconfig.json | 15 ++ yarn.lock | 214 ++++++++++++++++++++++++++- 15 files changed, 900 insertions(+), 3 deletions(-) create mode 100644 packages/one/src/cli/up.tsx create mode 100644 packages/one/types/cli/up.d.ts create mode 100644 packages/up/biome.json create mode 100644 packages/up/build.ts create mode 100644 packages/up/package.json create mode 100644 packages/up/src/docker.ts create mode 100644 packages/up/src/index.tsx create mode 100644 packages/up/src/tui.tsx create mode 100644 packages/up/src/tuiTypes.ts create mode 100644 packages/up/src/typeHelpers.ts create mode 100644 packages/up/src/utils.ts create mode 100644 packages/up/tsconfig.json diff --git a/packages/one/package.json b/packages/one/package.json index 7b5d191aa..89c1b8600 100644 --- a/packages/one/package.json +++ b/packages/one/package.json @@ -98,6 +98,7 @@ "@vxrn/resolve": "workspace:*", "@vxrn/tslib-lite": "workspace:*", "@vxrn/universal-color-scheme": "workspace:*", + "@vxrn/up": "workspace:*", "@vxrn/use-isomorphic-layout-effect": "workspace:*", "babel-dead-code-elimination": "^1.0.6", "citty": "^0.1.6", diff --git a/packages/one/src/cli.ts b/packages/one/src/cli.ts index 9634b0d70..5f83e7fc0 100644 --- a/packages/one/src/cli.ts +++ b/packages/one/src/cli.ts @@ -244,7 +244,21 @@ const patch = defineCommand({ }, }) +const up = defineCommand({ + meta: { + name: 'up', + version: '0.0.0', + description: '', + }, + args: {}, + async run({ args }) { + const { run } = await import('./cli/up') + await run(args) + }, +}) + const subCommands = { + up, dev, clean, build: buildCommand, diff --git a/packages/one/src/cli/up.tsx b/packages/one/src/cli/up.tsx new file mode 100644 index 000000000..adacda053 --- /dev/null +++ b/packages/one/src/cli/up.tsx @@ -0,0 +1,5 @@ +import { up } from '@vxrn/up' + +export function run(args: {}) { + up() +} diff --git a/packages/one/types/cli/up.d.ts b/packages/one/types/cli/up.d.ts new file mode 100644 index 000000000..0f2e24a8e --- /dev/null +++ b/packages/one/types/cli/up.d.ts @@ -0,0 +1,44 @@ +export declare function run(args: {}): void; +interface DockerConfig { + socketPath?: string; + host?: string; + port?: number; + protocol?: string; + containerFilters?: string; +} +interface HostInfo { + Containers: number; + ContainersRunning: number; + ContainersPaused: number; + ContainersStopped: number; + Images: number; + OperatingSystem: string; + Architecture: string; + MemTotal: number; + Host: string; + ServerVersion: string; + ApiVersion: string; +} +declare const createDockerBridge: (config: DockerConfig) => { + ping: () => Promise; + listImages: () => Promise; + systemDf: () => Promise; + listContainers: () => Promise; + listServices: () => Promise; + stopAllContainers: () => Promise; + removeAllContainers: () => Promise; + removeAllImages: () => Promise; + getInfo: () => Promise; + getContainer: (containerId: string) => Promise; + getService: (serviceId: string) => Promise; + getImage: (imageId: string) => Promise; + restartContainer: (containerId: string) => Promise; + stopContainer: (containerId: string) => Promise; + removeContainer: (containerId: string) => Promise; + removeImage: (imageId: string) => Promise; + getContainerStats: (containerId: string) => Promise; + getContainerLogs: (containerId: string) => Promise; + getServiceLogs: (serviceId: string) => Promise; +}; +export default createDockerBridge; +//# sourceMappingURL=up.d.ts.map \ No newline at end of file diff --git a/packages/up/biome.json b/packages/up/biome.json new file mode 100644 index 000000000..1f4cb687f --- /dev/null +++ b/packages/up/biome.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.1/schema.json", + "organizeImports": { + "enabled": false + }, + "linter": { + "enabled": true, + "ignore": ["**/*/generated-new.ts", "**/*/generated-v2.ts", ".tamagui"], + "rules": { + "correctness": { + "useExhaustiveDependencies": "off", + "noInnerDeclarations": "off", + "noUnnecessaryContinue": "off", + "noConstructorReturn": "off" + }, + "suspicious": { + "noImplicitAnyLet": "off", + "noConfusingVoidType": "off", + "noEmptyInterface": "off", + "noExplicitAny": "off", + "noArrayIndexKey": "off", + "noDoubleEquals": "off", + "noConsoleLog": "error", + "noAssignInExpressions": "off", + "noRedeclare": "off" + }, + "style": { + "noParameterAssign": "off", + "noNonNullAssertion": "off", + "noArguments": "off", + "noUnusedTemplateLiteral": "off", + "useDefaultParameterLast": "off", + "useConst": "off", + "useEnumInitializers": "off", + "useTemplate": "off", + "useSelfClosingElements": "off" + }, + "security": { + "noDangerouslySetInnerHtml": "off", + "noDangerouslySetInnerHtmlWithChildren": "off" + }, + "performance": { + "noDelete": "off", + "noAccumulatingSpread": "off" + }, + "complexity": { + "noForEach": "off", + "noBannedTypes": "off", + "useLiteralKeys": "off", + "useSimplifiedLogicExpression": "off", + "useOptionalChain": "off" + }, + "a11y": { + "noSvgWithoutTitle": "off", + "useMediaCaption": "off", + "noHeaderScope": "off", + "useAltText": "off", + "useButtonType": "off" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "ignore": ["**/*/generated-new.ts", "**/*/generated-v2.ts", ".tamagui"] + }, + "javascript": { + "formatter": { + "trailingComma": "es5", + "jsxQuoteStyle": "double", + "semicolons": "asNeeded", + "quoteStyle": "single" + } + } +} diff --git a/packages/up/build.ts b/packages/up/build.ts new file mode 100644 index 000000000..38296d7c8 --- /dev/null +++ b/packages/up/build.ts @@ -0,0 +1,42 @@ +import esbuild from 'esbuild' + +async function main() { + const watch = process.argv.includes('--watch') + + const buildOptions = { + entryPoints: ['./src/index.tsx'], + outdir: 'dist', + outExtension: { + '.js': '.cjs', + }, + bundle: true, + format: 'cjs', + platform: 'node', + target: 'esnext', + external: ['dockerode', 'mono-layout/wasm', 'yogini/wasm', 'vscode-oniguruma', 'node-pty'], + sourcemap: true, + loader: { + '.ts': 'ts', + '.tsx': 'tsx', + }, + } satisfies esbuild.BuildOptions + + if (watch) { + const context = await esbuild.context(buildOptions) + await context.watch() + console.info('Watching...') + + // Handle process exit + process.on('SIGTERM', async () => { + await context.dispose() + process.exit(0) + }) + } else { + await esbuild.build(buildOptions) + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/packages/up/package.json b/packages/up/package.json new file mode 100644 index 000000000..5e5d767e3 --- /dev/null +++ b/packages/up/package.json @@ -0,0 +1,47 @@ +{ + "name": "@vxrn/up", + "version": "1.1.437", + "license": "BSD-3-Clause", + "sideEffects": [ + "setup.mjs", + "setup.js" + ], + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./src/index.tsx", + "default": "./dist/index.cjs" + } + }, + "main": "dist/index.cjs", + "source": "src/index.ts", + "files": [ + "src", + "types", + "dist", + "vendor", + "LICENSE" + ], + "scripts": { + "build": "bun ./build.ts", + "clean": "bun ./build.ts --watch", + "clean:build": "rm -r dist || true", + "typecheck": "tsc --noEmit", + "lint:fix": "../../node_modules/.bin/biome check --write --unsafe src", + "watch": "yarn build --watch" + }, + "dependencies": { + "@onreza/docker-api-typescript": "^0.5.1-0", + "fast-glob": "^3.3.3", + "js-yaml": "^4.1.0", + "react": "18.3.1", + "react-reconciler": "0.23.0", + "terminosaurus": "^3.0.0-rc.5" + }, + "devDependencies": { + "@tamagui/build": "^1.124.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/up/src/docker.ts b/packages/up/src/docker.ts new file mode 100644 index 000000000..2bd22e254 --- /dev/null +++ b/packages/up/src/docker.ts @@ -0,0 +1,113 @@ +import Docker from '@onreza/docker-api-typescript' +import { load } from 'js-yaml' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { objectEntries, objectFromEntries } from './typeHelpers' + +const docker = new Docker({ socketPath: '/var/run/docker.sock' }) + +export async function getContainers() { + return await docker.containers.containerList().readBody() +} + +export async function getDockerComposeContainers() { + const composePath = join(process.cwd(), 'docker-compose.yml') + const compose = await parseDockerComposeYAML(composePath) + const containers = await getContainers() + + return objectEntries(compose.services).map(([name, service]) => { + return { + name, + ...service, + instance: containers.find((container) => { + return ( + container.Labels?.['com.docker.compose.project.config_files'] === composePath && + container.Labels?.['com.docker.compose.service'] === name + ) + }), + } + }) +} + +interface DockerComposeConfig { + version: string + services: Record + networks?: Record + volumes?: Record + secrets?: Record + configs?: Record +} + +interface ServiceConfig { + image?: string + build?: { + context?: string + dockerfile?: string + args?: Record + target?: string + } + command?: string | string[] + container_name?: string + deploy?: { + mode?: 'replicated' | 'global' + replicas?: number + resources?: { + limits?: { cpus?: string; memory?: string } + reservations?: { cpus?: string; memory?: string } + } + } + environment?: Record | string[] + env_file?: string | string[] + expose?: number[] + ports?: string[] + volumes?: string[] + networks?: string[] + depends_on?: string[] + restart?: 'no' | 'always' | 'on-failure' | 'unless-stopped' + healthcheck?: { + test: string | string[] + interval?: string + timeout?: string + retries?: number + start_period?: string + } +} + +interface NetworkConfig { + driver?: 'bridge' | 'overlay' | 'host' | 'none' + driver_opts?: Record + attachable?: boolean + enable_ipv6?: boolean + internal?: boolean + labels?: Record +} + +interface VolumeConfig { + driver?: string + driver_opts?: Record + external?: boolean + labels?: Record +} + +interface SecretConfig { + file?: string + external?: boolean + name?: string +} + +interface ConfigConfig { + file?: string + external?: boolean + name?: string +} + +export async function parseDockerComposeYAML(filePath: string): Promise { + const fileContents = readFileSync(filePath, 'utf8') + const parsedYaml = load(fileContents) as DockerComposeConfig + + if (!parsedYaml.services || Object.keys(parsedYaml.services).length === 0) { + throw new Error('At least one service is required') + } + + return parsedYaml +} diff --git a/packages/up/src/index.tsx b/packages/up/src/index.tsx new file mode 100644 index 000000000..6b431a169 --- /dev/null +++ b/packages/up/src/index.tsx @@ -0,0 +1,10 @@ +import { render } from 'terminosaurus/react' +import { debug, OneUpTUI } from './tui' + +export async function up() { + if (await debug()) { + return + } + + render({}, ) +} diff --git a/packages/up/src/tui.tsx b/packages/up/src/tui.tsx new file mode 100644 index 000000000..2045375b3 --- /dev/null +++ b/packages/up/src/tui.tsx @@ -0,0 +1,257 @@ +import { useEffect, useState } from 'react' +import { getContainers, getDockerComposeContainers, parseDockerComposeYAML } from './docker' +import { useAsync } from './utils' + +declare global { + namespace JSX { + interface IntrinsicElements { + 'term:div': any + 'term:text': any + 'term:form': any + 'term:input': any + } + } +} + +export async function debug() { + console.info(await getDockerComposeContainers()) + // return true +} + +export function OneUpTUI() { + const containers = useAsync(getDockerComposeContainers) + const [input, setInput] = useState('') + + useEffect(() => { + containers.execute() + }, []) + + const handleSubmit = async () => {} + + return ( + e.target.rootNode.queueDirtyRect()} + > + {/* {!apiKey ? ( + dispatch(anthropicSlice.actions.openKeyModal())} + > + + Click here to set your Anthropic API key and start chatting! + + + ) : ( + + + Connected as Anthropic User + + + Logout + + + )} */} + + + + {/* Discussions list */} + + {containers.data?.map((container) => ( + + // dispatch( + // anthropicSlice.actions.setDiscussionModel({ + // discussionId: currentDiscussionId, + // model, + // }) + // ) + // } + > + {container.name} + + ))} + + + {/* */} + {/* {discussions.map((discussion) => ( + dispatch(setCurrentDiscussion(discussion.id))} + > + + {discussion.title ?? `Untitled`} + + + ))} */} + {/* */} + + + + + {/* {currentDiscussion.messages.length === 0 && !isProcessing && ( + + + Welcome to the Terminosaurus Anthropic Chat! + + + This is a terminal-based chat interface for{' '} + {hyperlink(`Anthropic`, `https://anthropic.com/`)}. To use this application: + + + 1. Click the yellow banner to set your API key + + + 2. Type your message in the input box below and press Enter + + 3. Use /new to start a new conversation + + Note: The Anthropic API key is only used locally to make API calls. It will never + be transmitted to any other server. + + + )} + + {currentDiscussion.messages.map((message, index) => ( + + + {message.role === 'user' ? 'You' : 'Anthropic'}: + + {message.content} + + ))} + + {error && ( + + + {error} + + + )} + + {isProcessing && ( + + + Anthropic is thinking + + + + )} */} + + + + setInput(e.target.text)} /> + + + + + {/* {showKeyModal && ( + dispatch(anthropicSlice.actions.closeKeyModal())} /> + )} */} + + ) +} + +function ProgressDots() { + const [dots, setDots] = useState(0) + + useEffect(() => { + const interval = setInterval(() => { + setDots((dots) => (dots === 2 ? 0 : dots + 1)) + }, 500) + + return () => clearInterval(interval) + }, []) + + return `.`.repeat(dots + 1) +} + +function KeyModal({ onClose }: { onClose: () => void }) { + const [key, setKey] = useState('') + const [rememberKey, setRememberKey] = useState(false) + // const dispatch = useAppDispatch() + + const handleSubmit = () => { + // if (rememberKey && typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, key) + // dispatch(anthropicSlice.actions.setApiKey(key)) + // onClose() + } + + return ( + + + Enter your Anthropic API Key + + Your key will only be used locally to make API calls. + It will never be transmitted to any other server. + + + setKey(e.target.text)} + /> + + + + setRememberKey(!rememberKey)} + paddingRight={1} + > + [{rememberKey ? 'X' : ' '}] Remember this key into localStorage + + + + + + + Save + + + + + Cancel + + + + + ) +} + +// Note: should move that to term-strings +const hyperlink = (text: string, url: string) => { + return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07` +} diff --git a/packages/up/src/tuiTypes.ts b/packages/up/src/tuiTypes.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/up/src/typeHelpers.ts b/packages/up/src/typeHelpers.ts new file mode 100644 index 000000000..ec6f117f7 --- /dev/null +++ b/packages/up/src/typeHelpers.ts @@ -0,0 +1,36 @@ +type ObjectType = Record + +type PickByValue = // From https://stackoverflow.com/a/55153000 + Pick + +type ObjectEntries = // From https://stackoverflow.com/a/60142095 + { [K in keyof OBJ_T]: [keyof PickByValue, OBJ_T[K]] }[keyof OBJ_T][] + +export const objectKeys = (obj: O) => Object.keys(obj) as Array + +export function objectEntries(obj: OBJ_T): ObjectEntries { + return Object.entries(obj) as ObjectEntries +} + +type EntriesType = [PropertyKey, unknown][] | ReadonlyArray + +// Existing Utils +type DeepWritable = { -readonly [P in keyof OBJ_T]: DeepWritable } +type UnionToIntersection = // From https://stackoverflow.com/a/50375286 + (UNION_T extends any ? (k: UNION_T) => void : never) extends (k: infer I) => void ? I : never + +// New Utils +type UnionObjectFromArrayOfPairs = + DeepWritable extends (infer R)[] + ? R extends [infer key, infer val] + ? { [prop in key & PropertyKey]: val } + : never + : never +type MergeIntersectingObjects = { [key in keyof ObjT]: ObjT[key] } +type EntriesToObject = MergeIntersectingObjects< + UnionToIntersection> +> + +export function objectFromEntries(arr: ARR_T): EntriesToObject { + return Object.fromEntries(arr) as EntriesToObject +} diff --git a/packages/up/src/utils.ts b/packages/up/src/utils.ts new file mode 100644 index 000000000..8b53d1bcc --- /dev/null +++ b/packages/up/src/utils.ts @@ -0,0 +1,27 @@ +import { useState, useCallback } from 'react' + +interface AsyncState { + loading: boolean + error: Error | null + data: T | null +} + +export const useAsync = (asyncFunction: () => Promise, args?: any[]) => { + const [state, setState] = useState>({ + loading: false, + error: null, + data: null, + }) + + const execute = useCallback(async () => { + try { + setState({ loading: true, error: null, data: null }) + const result = await asyncFunction() + setState({ loading: false, error: null, data: result }) + } catch (error) { + setState({ loading: false, error: error as Error, data: null }) + } + }, [asyncFunction]) + + return { ...state, execute } +} diff --git a/packages/up/tsconfig.json b/packages/up/tsconfig.json new file mode 100644 index 000000000..022b275e5 --- /dev/null +++ b/packages/up/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.base.json", + "exclude": [ + "vitest.config.ts", + "**/test", + "**/dist", + "**/types", + "**/__tests__" + ], + "compilerOptions": { + "composite": true + }, + "references": [ + ] +} diff --git a/yarn.lock b/yarn.lock index d9b1b3c46..e36c29399 100644 --- a/yarn.lock +++ b/yarn.lock @@ -444,6 +444,13 @@ __metadata: languageName: node linkType: hard +"@arcanis/slice-ansi@npm:^2.0.1": + version: 2.0.1 + resolution: "@arcanis/slice-ansi@npm:2.0.1" + checksum: 10/9c70e2e45dc416992483b0df0fe34f38f0f9490363951eb44e226e5159192ce3d609376b3f9aaddadca2b58a7fb475dc779e0454101ab49e000b8551161dd062 + languageName: node + linkType: hard + "@azure/core-asynciterator-polyfill@npm:^1.0.2": version: 1.0.2 resolution: "@azure/core-asynciterator-polyfill@npm:1.0.2" @@ -5709,6 +5716,15 @@ __metadata: languageName: node linkType: hard +"@onreza/docker-api-typescript@npm:^0.5.1-0": + version: 0.5.1-0 + resolution: "@onreza/docker-api-typescript@npm:0.5.1-0" + dependencies: + undici: "npm:^6.19.8" + checksum: 10/bee1c67aeabafe5eb90f4eb790babf67660db3fc97bf45d31a5b720fabc5012531b0047416c7e5bc1e1b243751e18d5180bea852b371213a2b6858b73ea11e41 + languageName: node + linkType: hard + "@opentelemetry/api-logs@npm:0.56.0": version: 0.56.0 resolution: "@opentelemetry/api-logs@npm:0.56.0" @@ -10954,6 +10970,13 @@ __metadata: languageName: node linkType: hard +"@types/zen-observable@npm:^0.8.3": + version: 0.8.7 + resolution: "@types/zen-observable@npm:0.8.7" + checksum: 10/6d4cdccded97f8e21b18224a70524e559abe2568863ed21ea000580aa3446c6d583253e83e8ecc0c93b4060ef3fb7f9d14739a57ee18925cc30e0e7d79713a15 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -11545,6 +11568,20 @@ __metadata: languageName: unknown linkType: soft +"@vxrn/up@workspace:*, @vxrn/up@workspace:packages/up": + version: 0.0.0-use.local + resolution: "@vxrn/up@workspace:packages/up" + dependencies: + "@onreza/docker-api-typescript": "npm:^0.5.1-0" + "@tamagui/build": "npm:^1.124.1" + fast-glob: "npm:^3.3.3" + js-yaml: "npm:^4.1.0" + react: "npm:18.3.1" + react-reconciler: "npm:0.23.0" + terminosaurus: "npm:^3.0.0-rc.5" + languageName: unknown + linkType: soft + "@vxrn/url-parse@workspace:*, @vxrn/url-parse@workspace:packages/url-parse": version: 0.0.0-use.local resolution: "@vxrn/url-parse@workspace:packages/url-parse" @@ -11749,6 +11786,15 @@ __metadata: languageName: node linkType: hard +"@xterm/addon-fit@npm:^0.9.0": + version: 0.9.0 + resolution: "@xterm/addon-fit@npm:0.9.0" + peerDependencies: + "@xterm/xterm": ^5.0.0 + checksum: 10/2417b3f9a6e2cd671aac179df6d71570aed81f27a9e4fa704a7af002dc754dce7a8f28c9d7d5865d79b4ae45c3be099ea0aa8cac6bd093dd5cd56ad883d81800 + languageName: node + linkType: hard + "@yarnpkg/lockfile@npm:^1.1.0": version: 1.1.0 resolution: "@yarnpkg/lockfile@npm:1.1.0" @@ -13594,6 +13640,13 @@ __metadata: languageName: node linkType: hard +"color-diff@npm:^1.2.0": + version: 1.4.0 + resolution: "color-diff@npm:1.4.0" + checksum: 10/37df0a8aa74bc8c93472ea421b8a746d2303c3d9606854349a494ec259bd1855d7a00b3374ee66d6c574810403c1836b8cdc2af9a84618e59cb64e291658b17c + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -16569,6 +16622,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.3.3": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -21566,6 +21632,13 @@ __metadata: languageName: node linkType: hard +"mono-layout@npm:^0.14.3": + version: 0.14.3 + resolution: "mono-layout@npm:0.14.3" + checksum: 10/1d5b8513f9bae180399fead7e07147d2338ce2fce642dd3bcd329ea1dadefc8ff8f23b74ac3f478ee986dd9fee6bc40576c7ea65cafe57338efc98d2424fa40a + languageName: node + linkType: hard + "moti@npm:^0.29.0": version: 0.29.0 resolution: "moti@npm:0.29.0" @@ -21741,6 +21814,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^7.1.0": + version: 7.1.1 + resolution: "node-addon-api@npm:7.1.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10/ee1e1ed6284a2f8cd1d59ac6175ecbabf8978dcf570345e9a8095a9d0a2b9ced591074ae77f9009287b00c402352b38aa9322a34f2199cdc9f567b842a636b94 + languageName: node + linkType: hard + "node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -21823,6 +21905,16 @@ __metadata: languageName: node linkType: hard +"node-pty@npm:1.1.0-beta27": + version: 1.1.0-beta27 + resolution: "node-pty@npm:1.1.0-beta27" + dependencies: + node-addon-api: "npm:^7.1.0" + node-gyp: "npm:latest" + checksum: 10/0de4ea0d6918b755edba01ac93d8fd620013a6b194d60d5d529219c21fe20d4b9d3210dadcd836a69becdb5633d1569db556defdfaa63aa2eea677e83bf56378 + languageName: node + linkType: hard + "node-releases@npm:^2.0.19": version: 2.0.19 resolution: "node-releases@npm:2.0.19" @@ -22190,6 +22282,7 @@ __metadata: "@vxrn/resolve": "workspace:*" "@vxrn/tslib-lite": "workspace:*" "@vxrn/universal-color-scheme": "workspace:*" + "@vxrn/up": "workspace:*" "@vxrn/use-isomorphic-layout-effect": "workspace:*" babel-dead-code-elimination: "npm:^1.0.6" citty: "npm:^0.1.6" @@ -23653,7 +23746,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:15.8.1, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:15.8.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -24279,6 +24372,20 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:0.23.0": + version: 0.23.0 + resolution: "react-reconciler@npm:0.23.0" + dependencies: + loose-envify: "npm:^1.1.0" + object-assign: "npm:^4.1.1" + prop-types: "npm:^15.6.2" + scheduler: "npm:^0.17.0" + peerDependencies: + react: ^16.0.0 + checksum: 10/77cb14c40c195fc648634c85e826e7f1adbc28605a28d14a24fba53edd86ca65b0c7bd6701e16b3a92fde0291de563915c2738419570a93f2903dca50a6c7a9c + languageName: node + linkType: hard + "react-reconciler@npm:0.27.0": version: 0.27.0 resolution: "react-reconciler@npm:0.27.0" @@ -24291,6 +24398,18 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:^0.29.0": + version: 0.29.2 + resolution: "react-reconciler@npm:0.29.2" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.2" + peerDependencies: + react: ^18.3.1 + checksum: 10/f9ef98a88ec07efaf520ce4508bc4f499cfbec6c929549b4b802a09c2c7cd1b7b893f197ab0505dc03398a991b4f57d7b6572ae53d2699db74496cadde541cfc + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0, react-refresh@npm:^0.14.2": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -24429,6 +24548,15 @@ __metadata: languageName: node linkType: hard +"react@npm:18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/261137d3f3993eaa2368a83110466fc0e558bc2c7f7ae7ca52d94f03aac945f45146bd85e5f481044db1758a1dbb57879e2fcdd33924e2dde1bdc550ce73f7bf + languageName: node + linkType: hard + "react@npm:^18.3.1 || ^19.0.0, react@npm:^19.0.0": version: 19.0.0 resolution: "react@npm:19.0.0" @@ -25489,6 +25617,16 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.17.0": + version: 0.17.0 + resolution: "scheduler@npm:0.17.0" + dependencies: + loose-envify: "npm:^1.1.0" + object-assign: "npm:^4.1.1" + checksum: 10/7fcf855eb30307cd0a04dc1feee313e1a68ac272497c4a82265d9139ebdeb7a00319c82d57f73f275fe55993a12b0683ab0d20735949580008da5a0e222c547d + languageName: node + linkType: hard + "scheduler@npm:^0.21.0": version: 0.21.0 resolution: "scheduler@npm:0.21.0" @@ -25498,7 +25636,7 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": +"scheduler@npm:^0.23.0, scheduler@npm:^0.23.2": version: 0.23.2 resolution: "scheduler@npm:0.23.2" dependencies: @@ -27033,6 +27171,20 @@ __metadata: languageName: node linkType: hard +"term-strings@npm:^0.16.2": + version: 0.16.2 + resolution: "term-strings@npm:0.16.2" + dependencies: + "@types/zen-observable": "npm:^0.8.3" + color-diff: "npm:^1.2.0" + zen-observable: "npm:^0.8.15" + bin: + term-strings: ./build/bin/term-strings.js + term-strings-seqdbg: ./build/bin/term-strings-seqdbg.js + checksum: 10/141c77338041bd4d1a1f70f58e13c7d653af001c137a466cdc0ceb7ec6360a2f17d6582a672209a97d50c610774c1f942ea472a1d498cf182c7479e96937570b + languageName: node + linkType: hard + "terminal-link@npm:^2.1.1": version: 2.1.1 resolution: "terminal-link@npm:2.1.1" @@ -27043,6 +27195,27 @@ __metadata: languageName: node linkType: hard +"terminosaurus@npm:^3.0.0-rc.5": + version: 3.0.0-rc.5 + resolution: "terminosaurus@npm:3.0.0-rc.5" + dependencies: + "@arcanis/slice-ansi": "npm:^2.0.1" + "@xterm/addon-fit": "npm:^0.9.0" + mono-layout: "npm:^0.14.3" + node-pty: "npm:1.1.0-beta27" + react-reconciler: "npm:^0.29.0" + react-refresh: "npm:^0.14.0" + term-strings: "npm:^0.16.2" + vscode-oniguruma: "npm:^1.7.0" + vscode-textmate: "npm:^8.0.0" + yogini: "npm:^0.4.2" + zen-observable: "npm:^0.9.0" + peerDependencies: + react: ^18.2.0 + checksum: 10/a58145a789780d673fa440fe071aeefdbb900cd40261859cd32bff7e25a64a7054ebd11e2db48e8a09e191b1705d891189433cf2700b2d5eae6198fbb0662770 + languageName: node + linkType: hard + "terser@npm:^5.15.0": version: 5.32.0 resolution: "terser@npm:5.32.0" @@ -27904,6 +28077,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.19.8": + version: 6.21.1 + resolution: "undici@npm:6.21.1" + checksum: 10/eeccc07e9073ae8e755fdc0dc8cdfaa426c01ec6f815425c3ecedba2e5394cea4993962c040dd168951714a82f0d001a13018c3ae3ad4534f0fa97afe425c08d + languageName: node + linkType: hard + "unenv@npm:unenv-nightly@2.0.0-20241204-140205-a5d5190": version: 2.0.0-20241204-140205-a5d5190 resolution: "unenv-nightly@npm:2.0.0-20241204-140205-a5d5190" @@ -28804,6 +28984,20 @@ __metadata: languageName: node linkType: hard +"vscode-oniguruma@npm:^1.7.0": + version: 1.7.0 + resolution: "vscode-oniguruma@npm:1.7.0" + checksum: 10/7da9d21459f9788544b258a5fd1b9752df6edd8b406a19eea0209c6bf76507d5717277016799301c4da0d536095f9ca8c06afd1ab8f4001189090c804ca4814e + languageName: node + linkType: hard + +"vscode-textmate@npm:^8.0.0": + version: 8.0.0 + resolution: "vscode-textmate@npm:8.0.0" + checksum: 10/9fa7d66d6042cb090d116c2d8820d34c8870cfcbaed6e404da89f66b899970ed0ac47b59a2e30fc40a25af5414822bb3ea27974f714e9b91910d69c894be95f7 + languageName: node + linkType: hard + "vue-demi@npm:>=0.14.10": version: 0.14.10 resolution: "vue-demi@npm:0.14.10" @@ -29768,6 +29962,13 @@ __metadata: languageName: node linkType: hard +"yogini@npm:^0.4.2": + version: 0.4.2 + resolution: "yogini@npm:0.4.2" + checksum: 10/abc041568e6a5c486ff80d6bca674d9ec01f43b6902eac72e5cd8c6af67f3b5a7496c04ab395fee2a5ac698e204e7521443884496eb66ea903c717d4db8cc519 + languageName: node + linkType: hard + "youch@npm:^3.2.2": version: 3.3.4 resolution: "youch@npm:3.3.4" @@ -29788,13 +29989,20 @@ __metadata: languageName: node linkType: hard -"zen-observable@npm:0.8.15": +"zen-observable@npm:0.8.15, zen-observable@npm:^0.8.15": version: 0.8.15 resolution: "zen-observable@npm:0.8.15" checksum: 10/30eac3f4055d33f446b4cd075d3543da347c2c8e68fbc35c3f5a19fb43be67c6ed27ee136bc8f8933efa547be7ce04957809ad00ee7f1b00a964f199ae6fb514 languageName: node linkType: hard +"zen-observable@npm:^0.9.0": + version: 0.9.0 + resolution: "zen-observable@npm:0.9.0" + checksum: 10/a82ee08f816ebd957d7dd0225d0714eac595a7c486fb6fc9eb52641a34a0bd501d845eaddd2e08e2b2ee1526cbafe2a114d5d8e1e5c058e16049b158e29b763d + languageName: node + linkType: hard + "zip-stream@npm:^6.0.1": version: 6.0.1 resolution: "zip-stream@npm:6.0.1" From f79ed9ee0c9331ddb775fc8bd00db3cc5a0c0804 Mon Sep 17 00:00:00 2001 From: natew Date: Fri, 7 Feb 2025 20:14:34 -1000 Subject: [PATCH 3/5] --- packages/up/docker-run.sh | 9 +++ packages/up/package.json | 6 +- packages/up/src/index.tsx | 1 + packages/up/src/tui.tsx | 139 ++++++++++++-------------------------- yarn.lock | 36 ++++++++++ 5 files changed, 94 insertions(+), 97 deletions(-) create mode 100755 packages/up/docker-run.sh diff --git a/packages/up/docker-run.sh b/packages/up/docker-run.sh new file mode 100755 index 000000000..564165631 --- /dev/null +++ b/packages/up/docker-run.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# disable mouse as input source +printf '\e[?1000l' + +docker exec -it "$1" /bin/sh -c "[ -e /bin/bash ] && /bin/bash || /bin/sh" + +# enable mouse as input source for dockly +#printf '\e[?1000h' diff --git a/packages/up/package.json b/packages/up/package.json index 5e5d767e3..93789e65e 100644 --- a/packages/up/package.json +++ b/packages/up/package.json @@ -36,10 +36,12 @@ "js-yaml": "^4.1.0", "react": "18.3.1", "react-reconciler": "0.23.0", - "terminosaurus": "^3.0.0-rc.5" + "terminosaurus": "^3.0.0-rc.5", + "valtio": "^2.1.3" }, "devDependencies": { - "@tamagui/build": "^1.124.1" + "@tamagui/build": "^1.124.1", + "@types/react": "^18" }, "publishConfig": { "access": "public" diff --git a/packages/up/src/index.tsx b/packages/up/src/index.tsx index 6b431a169..ddb756fbe 100644 --- a/packages/up/src/index.tsx +++ b/packages/up/src/index.tsx @@ -2,6 +2,7 @@ import { render } from 'terminosaurus/react' import { debug, OneUpTUI } from './tui' export async function up() { + // @ts-ignore if (await debug()) { return } diff --git a/packages/up/src/tui.tsx b/packages/up/src/tui.tsx index 2045375b3..fd229e9e7 100644 --- a/packages/up/src/tui.tsx +++ b/packages/up/src/tui.tsx @@ -1,6 +1,8 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useSyncExternalStore } from 'react' import { getContainers, getDockerComposeContainers, parseDockerComposeYAML } from './docker' import { useAsync } from './utils' +import type { TermPty } from 'terminosaurus' +import { proxy, useSnapshot } from 'valtio' declare global { namespace JSX { @@ -9,23 +11,46 @@ declare global { 'term:text': any 'term:form': any 'term:input': any + 'term:pty': any } } } +const globalState = proxy({ + selected: 0, +}) + +const useGlobalState = () => useSnapshot(globalState) + export async function debug() { console.info(await getDockerComposeContainers()) // return true } export function OneUpTUI() { + const [pty, setPty] = useState(null) const containers = useAsync(getDockerComposeContainers) + const state = useGlobalState() + const activeContainer = containers.data?.[state.selected] const [input, setInput] = useState('') useEffect(() => { containers.execute() }, []) + useEffect(() => { + if (activeContainer) { + pty!.spawn(`docker`, [ + 'exec', + '-it', + activeContainer.instance?.Id || '', + '/bin/sh', + '-c', + '[ -e /bin/bash ] && /bin/bash || /bin/sh', + ]) + } + }, [activeContainer]) + const handleSubmit = async () => {} return ( @@ -35,56 +60,27 @@ export function OneUpTUI() { height="100%" onClick={(e) => e.target.rootNode.queueDirtyRect()} > - {/* {!apiKey ? ( - dispatch(anthropicSlice.actions.openKeyModal())} - > - - Click here to set your Anthropic API key and start chatting! - - - ) : ( - - - Connected as Anthropic User - - - Logout - - - )} */} - {/* Discussions list */} - {containers.data?.map((container) => ( - - // dispatch( - // anthropicSlice.actions.setDiscussionModel({ - // discussionId: currentDiscussionId, - // model, - // }) - // ) - // } - > - {container.name} - - ))} + {containers.data?.map((container, index) => { + const isActive = container === activeContainer + return ( + { + globalState.selected = index + }} + backgroundColor={isActive ? 'blue' : undefined} + > + {container.name} + {container.instance?.Status || ''} + + ) + })} {/* */} @@ -113,54 +109,7 @@ export function OneUpTUI() { paddingLeft={1} paddingRight={1} > - {/* {currentDiscussion.messages.length === 0 && !isProcessing && ( - - - Welcome to the Terminosaurus Anthropic Chat! - - - This is a terminal-based chat interface for{' '} - {hyperlink(`Anthropic`, `https://anthropic.com/`)}. To use this application: - - - 1. Click the yellow banner to set your API key - - - 2. Type your message in the input box below and press Enter - - 3. Use /new to start a new conversation - - Note: The Anthropic API key is only used locally to make API calls. It will never - be transmitted to any other server. - - - )} - - {currentDiscussion.messages.map((message, index) => ( - - - {message.role === 'user' ? 'You' : 'Anthropic'}: - - {message.content} - - ))} - - {error && ( - - - {error} - - - )} - - {isProcessing && ( - - - Anthropic is thinking - - - - )} */} + diff --git a/yarn.lock b/yarn.lock index e36c29399..390401247 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10823,6 +10823,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18": + version: 18.3.18 + resolution: "@types/react@npm:18.3.18" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 10/7fdd8b853e0d291d4138133f93f8d5c333da918e5804afcea61a923aab4bdfc9bb15eb21a5640959b452972b8715ddf10ffb12b3bd071898b9e37738636463f2 + languageName: node + linkType: hard + "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -11574,11 +11584,13 @@ __metadata: dependencies: "@onreza/docker-api-typescript": "npm:^0.5.1-0" "@tamagui/build": "npm:^1.124.1" + "@types/react": "npm:^18" fast-glob: "npm:^3.3.3" js-yaml: "npm:^4.1.0" react: "npm:18.3.1" react-reconciler: "npm:0.23.0" terminosaurus: "npm:^3.0.0-rc.5" + valtio: "npm:^2.1.3" languageName: unknown linkType: soft @@ -23810,6 +23822,13 @@ __metadata: languageName: node linkType: hard +"proxy-compare@npm:^3.0.1": + version: 3.0.1 + resolution: "proxy-compare@npm:3.0.1" + checksum: 10/fa9ae15adc53577054405254da64cecb2b76ab30024c424f14e4556a165f6693773ee2eb73667e6d4be11244fa0581c235720165cc32e7ff77ed7156c4f88379 + languageName: node + linkType: hard + "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -28599,6 +28618,23 @@ __metadata: languageName: node linkType: hard +"valtio@npm:^2.1.3": + version: 2.1.3 + resolution: "valtio@npm:2.1.3" + dependencies: + proxy-compare: "npm:^3.0.1" + peerDependencies: + "@types/react": ">=18.0.0" + react: ">=18.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 10/460a553fe6b8cee16f6488f6fc581326bed1366fd8069ce03ca54e0b6eb2f2d17118cbe0bd62fca718bdb6dcde3a82dccfbd25bc39c9f67332d42f8d23d9bfdf + languageName: node + linkType: hard + "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" From 3a33ecea4a75ec15608a4318c33eb80e87808181 Mon Sep 17 00:00:00 2001 From: natew Date: Fri, 7 Feb 2025 20:14:39 -1000 Subject: [PATCH 4/5] --- packages/up/docker-run.sh | 9 --------- 1 file changed, 9 deletions(-) delete mode 100755 packages/up/docker-run.sh diff --git a/packages/up/docker-run.sh b/packages/up/docker-run.sh deleted file mode 100755 index 564165631..000000000 --- a/packages/up/docker-run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -# disable mouse as input source -printf '\e[?1000l' - -docker exec -it "$1" /bin/sh -c "[ -e /bin/bash ] && /bin/bash || /bin/sh" - -# enable mouse as input source for dockly -#printf '\e[?1000h' From 8d3d33d26f6c19265cfaed9dff5e83c4a4715e04 Mon Sep 17 00:00:00 2001 From: natew Date: Fri, 7 Feb 2025 21:58:12 -1000 Subject: [PATCH 5/5] --- packages/up/package.json | 3 +- packages/up/src/package.ts | 31 ++++++++++ packages/up/src/tui.tsx | 117 +++++++++++++++++++++++++++++-------- packages/up/src/utils.ts | 11 ++-- yarn.lock | 1 + 5 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 packages/up/src/package.ts diff --git a/packages/up/package.json b/packages/up/package.json index 93789e65e..417c1d46a 100644 --- a/packages/up/package.json +++ b/packages/up/package.json @@ -37,7 +37,8 @@ "react": "18.3.1", "react-reconciler": "0.23.0", "terminosaurus": "^3.0.0-rc.5", - "valtio": "^2.1.3" + "valtio": "^2.1.3", + "zod": "^3.24.1" }, "devDependencies": { "@tamagui/build": "^1.124.1", diff --git a/packages/up/src/package.ts b/packages/up/src/package.ts new file mode 100644 index 000000000..d8cf5e308 --- /dev/null +++ b/packages/up/src/package.ts @@ -0,0 +1,31 @@ +import { readFile } from 'node:fs/promises' +import { join } from 'node:path' +import { z } from 'zod' + +const PackageJsonSchema = z.object({ + name: z.string(), + version: z.string(), + description: z.string().optional(), + main: z.string().optional(), + scripts: z.record(z.string()).optional(), + dependencies: z.record(z.string()).optional(), + devDependencies: z.record(z.string()).optional(), + peerDependencies: z.record(z.string()).optional(), + private: z.boolean().optional(), + type: z.enum(['module', 'commonjs']).optional(), +}) + +type PackageJson = z.infer + +export async function readPackageJSON(dir: string): Promise { + try { + const raw = await readFile(join(dir, 'package.json'), 'utf-8') + const json = JSON.parse(raw) + return PackageJsonSchema.parse(json) + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to read package.json: ${error.message}`) + } + throw error + } +} diff --git a/packages/up/src/tui.tsx b/packages/up/src/tui.tsx index fd229e9e7..dbdf0e582 100644 --- a/packages/up/src/tui.tsx +++ b/packages/up/src/tui.tsx @@ -1,8 +1,10 @@ -import { useEffect, useState, useSyncExternalStore } from 'react' -import { getContainers, getDockerComposeContainers, parseDockerComposeYAML } from './docker' -import { useAsync } from './utils' +import { useEffect, useState } from 'react' import type { TermPty } from 'terminosaurus' import { proxy, useSnapshot } from 'valtio' +import { getDockerComposeContainers } from './docker' +import { readPackageJSON } from './package' +import { objectEntries } from './typeHelpers' +import { useAsync } from './utils' declare global { namespace JSX { @@ -18,8 +20,14 @@ declare global { const globalState = proxy({ selected: 0, + processes: {} as Record, + logs: [] as string[], }) +function log(...args: string[]) { + globalState.logs.push(args.join(' ')) +} + const useGlobalState = () => useSnapshot(globalState) export async function debug() { @@ -27,29 +35,54 @@ export async function debug() { // return true } +type Process = { + name: string + run: string + type: 'docker' | 'script' + status: 'idle' | 'running' + pty?: TermPty | null +} + +const ptys = new Set() + export function OneUpTUI() { - const [pty, setPty] = useState(null) + const runnableScripts = useAsync(readRunnableScripts, 'development') const containers = useAsync(getDockerComposeContainers) + const inactiveContainers = containers.data?.filter((x) => !x.instance?.Status) const state = useGlobalState() - const activeContainer = containers.data?.[state.selected] const [input, setInput] = useState('') useEffect(() => { containers.execute() + runnableScripts.execute() }, []) - useEffect(() => { - if (activeContainer) { - pty!.spawn(`docker`, [ - 'exec', - '-it', - activeContainer.instance?.Id || '', - '/bin/sh', - '-c', - '[ -e /bin/bash ] && /bin/bash || /bin/sh', - ]) - } - }, [activeContainer]) + const containerProcesses = containers.data?.map((container) => { + return { + name: container.name, + run: `docker exec -it ${container.instance?.Id || ''} /bin/sh -c [ -e /bin/bash ] && /bin/bash || /bin/sh`, + status: 'idle', + type: 'docker', + } as Process + }) + + const scriptProcesses = runnableScripts.data?.map((script) => { + return { + name: script.name, + run: `bun ${script.command}`, + status: 'idle', + type: 'script', + } as Process + }) + + const processes = [...(scriptProcesses || []), ...(containerProcesses || [])] + const activeProc = processes[state.selected] + + // useEffect(() => { + // if (activeContainer?.instance?.Status) { + // pty!.spawn() + // } + // }, [activeContainer]) const handleSubmit = async () => {} @@ -60,15 +93,17 @@ export function OneUpTUI() { height="100%" onClick={(e) => e.target.rootNode.queueDirtyRect()} > + {/* {JSON.stringify(runnableScripts.data) || ''} */} + {state.logs.join('\n')} {/* Discussions list */} - {containers.data?.map((container, index) => { - const isActive = container === activeContainer + {processes.map((proc, index) => { + const isActive = proc === activeProc return ( { @@ -76,8 +111,8 @@ export function OneUpTUI() { }} backgroundColor={isActive ? 'blue' : undefined} > - {container.name} - {container.instance?.Status || ''} + {proc.name} + {proc.status || 'Starting...'} ) })} @@ -109,7 +144,28 @@ export function OneUpTUI() { paddingLeft={1} paddingRight={1} > - + {processes.map((proc, index) => { + return ( + { + if (ref && !ptys.has(ref)) { + ptys.add(ref) + const [command, ...args] = proc.run.split(' ') + ref.spawn(command, args) + // log(`spawn ${proc.name} ${typeof ref}`) + } + }} + flexGrow={1} + display={ + index === state.selected + ? // || index === state.selected + 1 + 'flex' + : 'none' + } + /> + ) + })} @@ -204,3 +260,18 @@ function KeyModal({ onClose }: { onClose: () => void }) { const hyperlink = (text: string, url: string) => { return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07` } + +const readRunnableScripts = async (mode: 'development' | 'production') => { + const json = await readPackageJSON('.') + return objectEntries(json.scripts || {}).flatMap(([name, command]) => { + if (name === 'dev' || name.startsWith('dev:') || name === 'watch') { + return [ + { + name, + command, + }, + ] + } + return [] + }) +} diff --git a/packages/up/src/utils.ts b/packages/up/src/utils.ts index 8b53d1bcc..adfb07676 100644 --- a/packages/up/src/utils.ts +++ b/packages/up/src/utils.ts @@ -6,22 +6,25 @@ interface AsyncState { data: T | null } -export const useAsync = (asyncFunction: () => Promise, args?: any[]) => { +export const useAsync = ( + asyncFunction: (...args: Args) => Promise, + ...args: Args +) => { const [state, setState] = useState>({ loading: false, error: null, data: null, }) - const execute = useCallback(async () => { + const execute = async () => { try { setState({ loading: true, error: null, data: null }) - const result = await asyncFunction() + const result = await asyncFunction(...args) setState({ loading: false, error: null, data: result }) } catch (error) { setState({ loading: false, error: error as Error, data: null }) } - }, [asyncFunction]) + } return { ...state, execute } } diff --git a/yarn.lock b/yarn.lock index 390401247..050434af0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11591,6 +11591,7 @@ __metadata: react-reconciler: "npm:0.23.0" terminosaurus: "npm:^3.0.0-rc.5" valtio: "npm:^2.1.3" + zod: "npm:^3.24.1" languageName: unknown linkType: soft