Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion docs/content/2.module/1.utils-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ of the Vite DevTools terminals host
the [migration guide](/module/migration-v4#ndt_dep_0004).
::

Start a sub process using `tinyexec` and create a terminal tab in DevTools.
Start a sub process using `tinyexec` and surface it as a read-only session in
the built-in **Terminals** dock.

```ts
import { startSubprocess } from '@nuxt/devtools-kit'
Expand Down Expand Up @@ -110,6 +111,32 @@ subprocess.restart()
subprocess.terminate()
```

### `startDevToolsTerminal()`

Spawn a terminal that is **owned by the Vite DevTools terminals host** and
surfaced in the built-in **Terminals** dock. Unlike `startSubprocess()` (where
your module owns the process and only streams output to a read-only session),
DevTools spawns and owns the process here.

Set `interactive: true` for a fully interactive PTY (stdin + resize,
TUI-capable) — powered by `zigpty` with a graceful pipe fallback where the
native bindings can't load.

```ts
import { startDevToolsTerminal } from '@nuxt/devtools-kit'

startDevToolsTerminal({
id: 'my-module:shell',
name: 'Shell',
command: 'bash',
interactive: true,
})
```

Under the hood this calls the `devtools:terminal:spawn` Nuxt hook, which the
DevTools bridge maps to `ctx.terminals.startPtySession(...)` (interactive) or
`ctx.terminals.startChildProcess(...)` (output-only).

### `extendServerRpc()`

::warning
Expand Down
21 changes: 10 additions & 11 deletions packages/devtools-kit/src/_types/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { ModuleCustomTab } from './custom-tabs'
import type { NuxtDevtoolsInfo } from './server-ctx'
import type { TerminalState } from './terminals'
import type { SpawnTerminalOptions, TerminalState } from './terminals'

declare module '@nuxt/schema' {
interface NuxtHooks {
Expand Down Expand Up @@ -37,7 +37,11 @@ declare module '@nuxt/schema' {
'devtools:customTabs:refresh': () => void

/**
* Register a terminal.
* Register a terminal whose process is owned by the caller (module).
*
* The registered session is surfaced **read-only** in the built-in Vite
* DevTools **Terminals** dock; stream output into it via
* `devtools:terminal:write`.
*/
'devtools:terminal:register': (terminal: TerminalState) => void

Expand All @@ -59,18 +63,13 @@ declare module '@nuxt/schema' {
* Mark a terminal as terminated.
*/
'devtools:terminal:exit': (_: { id: string, code?: number }) => void
}
}

declare module '@nuxt/schema' {
/**
* Runtime Hooks
*/
interface RuntimeNuxtHooks {
/**
* On terminal data.
* Spawn a terminal whose process is owned by the Vite DevTools terminals
* host (surfaced in the built-in **Terminals** dock). Set
* `interactive: true` for a fully interactive PTY (stdin + resize).
*/
'devtools:terminal:data': (payload: { id: string, data: string }) => void
'devtools:terminal:spawn': (options: SpawnTerminalOptions) => void | Promise<void>
}
}

Expand Down
12 changes: 5 additions & 7 deletions packages/devtools-kit/src/_types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import type { ModuleCustomTab } from './custom-tabs'
import type { AssetEntry, AssetInfo, AutoImportsWithMetadata, ComponentRelationship, HookInfo, ImageMeta, NpmCommandOptions, NpmCommandType, PackageUpdateInfo, ScannedNitroTasks, ServerRouteInfo } from './integrations'
import type { ModuleOptions, NuxtDevToolsOptions } from './options'
import type { InstallModuleReturn, ServerDebugContext } from './server-ctx'
import type { TerminalAction, TerminalInfo } from './terminals'

export interface ServerFunctions {
// Static RPCs (can be provide on production build in the future)
Expand Down Expand Up @@ -38,11 +37,6 @@ export interface ServerFunctions {
getNpmCommand: (command: NpmCommandType, packageName: string, options?: NpmCommandOptions) => Promise<string[] | undefined>
runNpmCommand: (command: NpmCommandType, packageName: string, options?: NpmCommandOptions) => Promise<{ processId: string } | undefined>

// Terminal
getTerminals: () => TerminalInfo[]
getTerminalDetail: (id: string) => Promise<TerminalInfo | undefined>
runTerminalAction: (id: string, action: TerminalAction) => Promise<boolean>

// Storage
getStorageMounts: () => Promise<StorageMounts>
getStorageKeys: (base?: string) => Promise<string[]>
Expand Down Expand Up @@ -83,7 +77,11 @@ export interface ClientFunctions {
callHook: (hook: string, ...args: any[]) => Promise<void>
navigateTo: (path: string) => void

onTerminalData: (_: { id: string, data: string }) => void
/**
* Server→client signal that a terminal session has exited. Kept so the
* client can clear transient subprocess UI state (installing-modules /
* analyze-build / npm updates) when the underlying process finishes.
*/
onTerminalExit: (_: { id: string, code?: number }) => void
}

Expand Down
34 changes: 34 additions & 0 deletions packages/devtools-kit/src/_types/terminals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,37 @@ export interface TerminalState extends TerminalInfo {
*/
onActionTerminate?: () => Promise<void> | void
}

/**
* Options for spawning a DevTools terminal that is owned by the Vite DevTools
* terminals host (surfaced in the built-in **Terminals** dock).
*
* Unlike the `devtools:terminal:register`/`:write` hooks (where the *module*
* owns the process and merely streams output), the process is spawned and
* owned by DevTools. Set {@link interactive} to `true` to get a fully
* interactive PTY (stdin + resize, TUI-capable) instead of an output-only
* child process.
*/
export interface SpawnTerminalOptions extends TerminalBase {
/** Command to execute. */
command: string
/** Command arguments. */
args?: string[]
/** Working directory. Defaults to the process cwd. */
cwd?: string
/** Extra environment variables. */
env?: Record<string, string>
/**
* Spawn a fully interactive pseudo-terminal (PTY) instead of an output-only
* child process. Interactive sessions accept keystrokes and resizing in the
* built-in Terminals dock. Powered by `zigpty`; where its native bindings
* can't load, it gracefully degrades to pipe-based emulation.
*
* @default false
*/
interactive?: boolean
/** Initial column count (PTY only). Default: 80. */
cols?: number
/** Initial row count (PTY only). Default: 24. */
rows?: number
}
26 changes: 25 additions & 1 deletion packages/devtools-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ViteDevToolsNodeContext } from '@vitejs/devtools-kit'
import type { BirpcGroup } from 'birpc'
import type { ChildProcess } from 'node:child_process'
import type { Result } from 'tinyexec'
import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SubprocessOptions, TerminalState } from './types'
import type { ModuleCustomTab, NuxtDevtoolsInfo, NuxtDevtoolsServerContext, SpawnTerminalOptions, SubprocessOptions, TerminalState } from './types'
import { useNuxt } from '@nuxt/kit'
import { x } from 'tinyexec'
import { deprecate } from './diagnostics'
Expand Down Expand Up @@ -195,6 +195,30 @@ export function onDevToolsInitialized(fn: (info: NuxtDevtoolsInfo) => void, nuxt
nuxt.hook('devtools:initialized', fn)
}

/**
* Spawn a terminal owned by the Vite DevTools terminals host, surfaced in the
* built-in **Terminals** dock.
*
* Unlike {@link startSubprocess} (where the module owns the process and only
* streams output to a read-only session), DevTools spawns and owns the process
* here. Set `interactive: true` for a fully interactive PTY (stdin + resize,
* TUI-capable) — powered by `zigpty` with a graceful pipe fallback where the
* native bindings can't load.
*
* @example
* ```ts
* startDevToolsTerminal({
* id: 'my-module:shell',
* name: 'Shell',
* command: 'bash',
* interactive: true,
* })
* ```
*/
export function startDevToolsTerminal(options: SpawnTerminalOptions, nuxt = useNuxt()): Promise<void> {
return Promise.resolve(nuxt.callHook('devtools:terminal:spawn', options))
}

/**
* Run a callback once the Vite DevTools kit has connected, receiving the
* connected `ViteDevToolsNodeContext`.
Expand Down
9 changes: 3 additions & 6 deletions packages/devtools/client/components/ModuleItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { InstalledModuleInfo } from '../../src/types'
import { computed } from 'vue'
import { useCurrentTerminalId } from '~/composables/state-routes'

const props = defineProps<{
mod: InstalledModuleInfo
Expand All @@ -13,7 +12,6 @@ const data = computed(() => ({
...props.mod,
...staticInfo.value,
}))
const terminalId = useCurrentTerminalId()
</script>

<template>
Expand All @@ -27,15 +25,14 @@ const terminalId = useCurrentTerminalId()
<!-- NPM Version bump -->
<NpmVersionCheck v-if="data.npm" :key="data.npm" :package-name="data.npm" :options="{ dev: true }">
<template #default="{ info, update, state, id, restart }">
<NuxtLink
<div
v-if="state === 'running'" flex="~ gap-2"
animate-pulse items-center
:to="id ? '/modules/terminals' : undefined"
@click="id ? terminalId = id : undefined"
:title="id ? 'Follow the output in the Terminals dock' : undefined"
>
<span i-carbon-circle-dash flex-none animate-spin text-lg op50 />
<code text-sm op50>Upgrading...</code>
</NuxtLink>
</div>
<div v-else-if="state === 'updated'" mx--2>
<button
flex="~ gap-2"
Expand Down
12 changes: 0 additions & 12 deletions packages/devtools/client/components/NpmVersionCheck.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
import type { NpmCommandOptions } from '../../src/types'
import { createTemplatePromise } from '@vueuse/core'
import { ref } from 'vue'
import { useRouter } from '#app/composables/router'
import { useRestartDialogs } from '~/composables/dialog'
import { usePackageUpdate } from '~/composables/npm'
import { useCurrentTerminalId } from '~/composables/state-routes'
import { telemetry } from '~/composables/telemetry'

const props = withDefaults(
Expand All @@ -19,7 +17,6 @@ const props = withDefaults(
},
)

const router = useRouter()
const {
info,
update,
Expand All @@ -28,12 +25,10 @@ const {
restart,
} = usePackageUpdate(props.packageName, props.options)

const shouldGotoTerminal = ref(true)
const shouldRestartServer = ref(true)
const restartDialogs = useRestartDialogs()

const PromiseConfirm = createTemplatePromise<boolean, [string]>()
const terminalId = useCurrentTerminalId()

async function updateWithConfirm() {
const processId = await update(async (command) => {
Expand All @@ -51,10 +46,6 @@ async function updateWithConfirm() {
message: `${props.packageName} has been updated. Do you want to restart the Nuxt server now?`,
})
}
if (processId && shouldGotoTerminal.value) {
terminalId.value = processId
router.push('/modules/terminals')
}
}
</script>

Expand Down Expand Up @@ -88,9 +79,6 @@ async function updateWithConfirm() {
The following command will be executed in your terminal:
</p>
<NCodeBlock :code="args[0]" lang="bash" my3 px4 py2 border="~ base rounded" :lines="false" />
<NCheckbox v-model="shouldGotoTerminal" n="primary">
Navigate to terminal
</NCheckbox>
<NCheckbox v-model="shouldRestartServer" n="primary">
Restart Nuxt server after update
</NCheckbox>
Expand Down
64 changes: 0 additions & 64 deletions packages/devtools/client/components/TerminalPage.vue

This file was deleted.

Loading
Loading