diff --git a/package.json b/package.json
index 724cd4753..078b97944 100644
--- a/package.json
+++ b/package.json
@@ -587,6 +587,12 @@
"default": true,
"markdownDescription": "If set to `true`, the extension will bring up the GRPC server."
},
+ "runme.experiments.shellWarning": {
+ "type": "boolean",
+ "scope": "window",
+ "default": false,
+ "markdownDescription": "If set to `true`, the extension will display an error message if an unsupported shell is detected."
+ },
"runme.checkout.projectDir": {
"type": "string",
"scope": "machine",
@@ -844,6 +850,12 @@
"type": "string",
"default": "http://localhost:8877/api",
"description": "The base URL of the AI service."
+ },
+ "runme.app.docsUrl": {
+ "type": "string",
+ "scope": "window",
+ "default": "https://docs.runme.dev",
+ "markdownDescription": "Documentation Base URL"
}
}
}
diff --git a/src/client/components/configuration/annotations.ts b/src/client/components/configuration/annotations.ts
index 4ccd9f870..f0dcde2eb 100644
--- a/src/client/components/configuration/annotations.ts
+++ b/src/client/components/configuration/annotations.ts
@@ -2,7 +2,12 @@ import { LitElement, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'
import { when } from 'lit/directives/when.js'
-import type { ClientMessage, CellAnnotations, CellAnnotationsErrorResult } from '../../../types'
+import type {
+ ClientMessage,
+ CellAnnotations,
+ CellAnnotationsErrorResult,
+ Settings,
+} from '../../../types'
import { CellAnnotationsSchema, AnnotationSchema } from '../../../schema'
import {
ClientMessages,
@@ -90,6 +95,9 @@ export class Annotations extends LitElement {
@property({ type: Array })
categories: string[] = []
+ @property({ type: Object })
+ settings: Settings = {}
+
#getTargetValue(e: Target) {
switch (e.target.type) {
case 'text':
@@ -228,7 +236,7 @@ export class Annotations extends LitElement {
}
renderDocsLink(id: string) {
- const link = `https://docs.runme.dev/r/extension/${id}`
+ const link = `${this.settings?.docsUrl}/r/extension/${id}`
return html`(docs ${ExternalLinkIcon})`
}
@@ -298,6 +306,7 @@ export class Annotations extends LitElement {
identifier="${id}"
@onChange=${this.onCategorySelectorChange}
@onCreateNewCategory=${this.createNewCategoryClick}
+ @settings=${this.settings}
>
`
diff --git a/src/client/components/configuration/categorySelector.ts b/src/client/components/configuration/categorySelector.ts
index 2d8788c41..c60e368b4 100644
--- a/src/client/components/configuration/categorySelector.ts
+++ b/src/client/components/configuration/categorySelector.ts
@@ -5,6 +5,7 @@ import type { TextField } from '@vscode/webview-ui-toolkit'
import { ExternalLinkIcon } from '../icons/external'
import { CATEGORY_SEPARATOR } from '../../../constants'
+import { Settings } from '../../../types'
export interface ISelectedCategory {
name: string
@@ -302,6 +303,9 @@ export class CategorySelector extends LitElement {
@property()
identifier: string | undefined
+ @property({ type: Object })
+ settings: Settings = {}
+
private dispatchComponentEvent(name: string, e: Event) {
if (e.defaultPrevented) {
e.preventDefault()
@@ -334,7 +338,8 @@ export class CategorySelector extends LitElement {
}
renderLink() {
- return html`(docs ${ExternalLinkIcon})`
}
diff --git a/src/client/index.ts b/src/client/index.ts
index 6c0d20fd3..9bc0b5825 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -1,4 +1,4 @@
-import type { ActivationFunction, RendererContext } from 'vscode-notebook-renderer'
+import type { ActivationFunction } from 'vscode-notebook-renderer'
import { OutputType, RENDERERS } from '../constants'
import type { CellOutput } from '../types'
@@ -13,7 +13,7 @@ import './components'
// rendering logic inside of the `render()` function.
// ----------------------------------------------------------------------------
-export const activate: ActivationFunction = (context: RendererContext) => {
+export const activate: ActivationFunction = (context) => {
setContext(context)
return {
renderOutputItem(outputItem, element) {
@@ -90,6 +90,7 @@ export const activate: ActivationFunction = (context: RendererContext) =>
'validationErrors',
JSON.stringify(payload.output.validationErrors ?? []),
)
+ annoElem.setAttribute('settings', JSON.stringify(payload.output.settings))
element.appendChild(annoElem)
break
case OutputType.terminal:
diff --git a/src/extension/cell.ts b/src/extension/cell.ts
index 1f6f5a9a9..44bcd78f8 100644
--- a/src/extension/cell.ts
+++ b/src/extension/cell.ts
@@ -32,6 +32,7 @@ import {
} from '../types'
import { Mutex } from '../utils/sync'
import {
+ getDocsUrl,
getNotebookTerminalConfigurations,
getSessionOutputs,
isPlatformAuthEnabled,
@@ -169,6 +170,9 @@ export class NotebookCellOutputManager {
annotations: getAnnotations(cell),
validationErrors: validateAnnotations(cell),
id: cell.metadata['runme.dev/id'],
+ settings: {
+ docsUrl: getDocsUrl(),
+ },
},
}
diff --git a/src/extension/executors/aws.ts b/src/extension/executors/aws.ts
index 6727c9f2a..37154ecd8 100644
--- a/src/extension/executors/aws.ts
+++ b/src/extension/executors/aws.ts
@@ -14,7 +14,7 @@ import { resolveProgramOptionsScript } from './runner'
import { IKernelExecutor } from '.'
export const aws: IKernelExecutor = async (executor) => {
- const { cellText, exec, runner, runnerEnv, doc, outputs, context } = executor
+ const { cellText, exec, runner, runnerEnv, doc, outputs, kernel } = executor
const annotations = getAnnotations(exec.cell)
@@ -41,67 +41,19 @@ export const aws: IKernelExecutor = async (executor) => {
cellId,
})
- // todo(sebastian): move down into kernel?
switch (programOptions.exec?.type) {
case 'script':
- {
- programOptions.exec.script = 'echo $AWS_PROFILE'
- }
+ programOptions.exec.script = 'echo $AWS_PROFILE'
break
}
- const program = await runner.createProgramSession(programOptions)
- context.subscriptions.push(program)
+ let profile = ''
+ const result = await kernel.runProgram(programOptions)
- let execRes: string | undefined
- const onData = (data: string | Uint8Array) => {
- if (execRes === undefined) {
- execRes = ''
- }
- execRes += data.toString()
+ if (result) {
+ profile = result
}
- program.onDidWrite(onData)
- program.onDidErr(onData)
- program.run()
-
- const success = await new Promise((resolve, reject) => {
- program.onDidClose(async (code) => {
- if (code !== 0) {
- return resolve(false)
- }
- return resolve(true)
- })
-
- program.onInternalErr((e) => {
- reject(e)
- })
-
- const exitReason = program.hasExited()
-
- // unexpected early return, likely an error
- if (exitReason) {
- switch (exitReason.type) {
- case 'error':
- {
- reject(exitReason.error)
- }
- break
-
- case 'exit':
- {
- resolve(exitReason.code === 0)
- }
- break
-
- default: {
- resolve(false)
- }
- }
- }
- })
-
- const profile = success ? execRes?.trim() : 'default'
credentials = fromIni({ profile })
switch (awsResolver.view) {
diff --git a/src/extension/executors/resource.ts b/src/extension/executors/resource.ts
index 10798f806..03a838760 100644
--- a/src/extension/executors/resource.ts
+++ b/src/extension/executors/resource.ts
@@ -69,9 +69,8 @@ export const uri: IKernelRunner = async ({
program.onDidWrite(onData)
program.onDidErr(onData)
- program.run()
- const success = await new Promise((resolve, reject) => {
+ const success = new Promise((resolve, reject) => {
program.onDidClose(async (code) => {
if (code !== 0) {
return resolve(false)
@@ -107,6 +106,9 @@ export const uri: IKernelRunner = async ({
}
})
- const cellText = success ? execRes?.trim() : undefined
- return runScript?.(cellText) || success
+ program.run()
+ const result = await success
+
+ const cellText = result ? execRes?.trim() : undefined
+ return runScript?.(cellText) || result
}
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 6730d2b63..bbab8caf1 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -15,6 +15,7 @@ import Channel from 'tangle/webviews'
import { NotebookUiEvent, Serializer, SyncSchema } from '../types'
import {
+ getDocsUrlFor,
getForceNewWindowConfig,
getRunmeAppUrl,
getSessionOutputs,
@@ -79,6 +80,8 @@ import { NotebookPanel as EnvStorePanel } from './panels/notebook'
import { NotebookCellStatusBarProvider } from './provider/cellStatusBar/notebook'
import { SessionOutputCellStatusBarProvider } from './provider/cellStatusBar/sessionOutput'
import * as manager from './ai/manager'
+import getLogger from './logger'
+
export class RunmeExtension {
protected serializer?: SerializerBase
@@ -356,6 +359,40 @@ export class RunmeExtension {
}
await bootFile(context)
+
+ if (kernel.hasExperimentEnabled('shellWarning', false)) {
+ const showUnsupportedShellMessage = async () => {
+ const showMore = 'Show more'
+
+ const answer = await window.showErrorMessage('Unsupported shell', showMore)
+ if (answer === showMore) {
+ const url = getDocsUrlFor('/r/extension/supported-shells')
+ env.openExternal(Uri.parse(url))
+ }
+ }
+
+ const logger = getLogger('runme.beta.shellWarning')
+
+ kernel
+ .runProgram('echo $SHELL')
+ .then((output) => {
+ if (output === false) {
+ return
+ }
+
+ const supportedShells = ['bash', 'zsh']
+ const isSupported = supportedShells.some((sh) => output?.includes(sh))
+ logger.info(`Shell: ${output}`)
+
+ if (!isSupported) {
+ showUnsupportedShellMessage()
+ }
+ })
+ .catch((e) => {
+ logger.error(e)
+ showUnsupportedShellMessage()
+ })
+ }
}
protected handleMasking(kernel: Kernel, maskingIsOn: boolean): (e: NotebookUiEvent) => void {
diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts
index b40703263..1d804e3af 100644
--- a/src/extension/kernel.ts
+++ b/src/extension/kernel.ts
@@ -68,7 +68,7 @@ import {
import { getSystemShellPath, isShellLanguage } from './executors/utils'
import './wasm/wasm_exec.js'
import { RpcError } from './grpc/client'
-import { IRunner, IRunnerReady } from './runner'
+import { IRunner, IRunnerReady, RunProgramOptions } from './runner'
import { IRunnerEnvironment } from './runner/environment'
import { IKernelRunnerOptions, executeRunner } from './executors/runner'
import { ITerminalState, NotebookTerminalType } from './terminal/terminalState'
@@ -141,6 +141,7 @@ export class Kernel implements Disposable {
this.#experiments.set('escalationButton', config.get('escalationButton', false))
this.#experiments.set('smartEnvStore', config.get('smartEnvStore', false))
this.#experiments.set('aiLogs', config.get('aiLogs', false))
+ this.#experiments.set('shellWarning', config.get('shellWarning', false))
this.cellManager = new NotebookCellManager(this.#controller)
this.#controller.supportsExecutionOrder = getNotebookExecutionOrder()
@@ -1102,4 +1103,89 @@ export class Kernel implements Disposable {
public getPlainCache(cacheId: string): Promise | undefined {
return this.serializer?.getPlainCache(cacheId)
}
+
+ async runProgram(program?: RunProgramOptions | string) {
+ let programOptions: RunProgramOptions
+ const logger = getLogger('runProgram')
+
+ if (!this.runner) {
+ logger.error('No runner available')
+ return false
+ }
+
+ if (typeof program === 'object') {
+ programOptions = program
+ } else if (typeof program === 'string') {
+ programOptions = {
+ programName: 'bash',
+ background: false,
+ exec: {
+ type: 'script',
+ script: program,
+ },
+ languageId: 'sh',
+ commandMode: CommandModeEnum().INLINE_SHELL,
+ storeLastOutput: false,
+ tty: false,
+ }
+ } else {
+ logger.error('Invalid program options')
+ return
+ }
+
+ const programSession = await this.runner.createProgramSession(programOptions)
+
+ this.context.subscriptions.push(programSession)
+
+ let execRes: string | undefined
+ const onData = (data: string | Uint8Array) => {
+ if (execRes === undefined) {
+ execRes = ''
+ }
+ execRes += data.toString()
+ }
+
+ programSession.onDidWrite(onData)
+ programSession.onDidErr(onData)
+
+ const success = new Promise((resolve, reject) => {
+ programSession.onDidClose(async (code) => {
+ if (code !== 0) {
+ return resolve(false)
+ }
+ return resolve(true)
+ })
+
+ programSession.onInternalErr((e) => {
+ reject(e)
+ })
+
+ const exitReason = programSession.hasExited()
+
+ if (exitReason) {
+ switch (exitReason.type) {
+ case 'error':
+ {
+ reject(exitReason.error)
+ }
+ break
+
+ case 'exit':
+ {
+ resolve(exitReason.code === 0)
+ }
+ break
+
+ default: {
+ resolve(false)
+ }
+ }
+ }
+ })
+
+ programSession.run()
+ const result = await success
+
+ return result ? execRes?.trim() : undefined
+ }
}
diff --git a/src/types.ts b/src/types.ts
index ab183e179..d90819180 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -361,16 +361,18 @@ export interface AWSEKSClustersState {
view: AWSSupportedView.EKSClusters
}
+export interface AnnotationsPayload {
+ annotations?: CellAnnotations
+ validationErrors?: CellAnnotationsErrorResult
+ id?: string
+}
+
interface Payload {
[OutputType.error]: string
[OutputType.deno]?: DenoState
[OutputType.vercel]: VercelState
[OutputType.outputItems]: OutputItemsPayload
- [OutputType.annotations]: {
- annotations?: CellAnnotations
- validationErrors?: CellAnnotationsErrorResult
- id?: string
- }
+ [OutputType.annotations]: AnnotationsPayload & { settings: Settings }
[OutputType.terminal]: TerminalConfiguration & {
['runme.dev/id']: string
content?: string
@@ -719,3 +721,7 @@ export type NotebookUiEvent = {
}
ui: boolean
}
+
+export type Settings = {
+ docsUrl?: string
+}
diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts
index 000b46d19..0684617ff 100644
--- a/src/utils/configuration.ts
+++ b/src/utils/configuration.ts
@@ -23,6 +23,7 @@ const DEFAULT_WORKSPACE_FILE_ORDER = ['.env.local', '.env']
const DEFAULT_RUNME_APP_API_URL = 'https://platform.stateful.com'
const DEFAULT_RUNME_BASE_DOMAIN = 'platform.stateful.com'
const DEFAULT_RUNME_REMOTE_DEV = 'staging.platform.stateful.com'
+const DEFAULT_DOCS_URL = 'https://docs.runme.dev'
const APP_LOOPBACKS = ['127.0.0.1', 'localhost']
const APP_LOOPBACK_MAPPING = new Map([
['api.', ':4000'],
@@ -90,6 +91,7 @@ const configurationSchema = {
maskOutputs: z.boolean().default(true),
loginPrompt: z.boolean().default(true),
platformAuth: z.boolean().default(false),
+ docsUrl: z.string().default(DEFAULT_DOCS_URL),
},
}
@@ -432,6 +434,15 @@ const isPlatformAuthEnabled = (): boolean => {
return getCloudConfigurationValue('platformAuth', false)
}
+const getDocsUrl = (): string => {
+ return getCloudConfigurationValue('docsUrl', DEFAULT_DOCS_URL)
+}
+
+const getDocsUrlFor = (path: string): string => {
+ const baseUrl = getDocsUrl()
+ return `${baseUrl}${path}`
+}
+
export {
enableServerLogs,
getActionsOpenViewInEditor,
@@ -462,4 +473,6 @@ export {
getSessionOutputs,
getMaskOutputs,
getLoginPrompt,
+ getDocsUrlFor,
+ getDocsUrl,
}