diff --git a/package.json b/package.json index a5e96ed46..2082900bc 100644 --- a/package.json +++ b/package.json @@ -848,6 +848,12 @@ "type": "string", "default": "http://localhost:8080/api", "description": "The base URL of the AI service." + }, + "runme.app.faqUrl": { + "type": "string", + "scope": "window", + "default": "https://docs.runme.dev/faq", + "markdownDescription": "Frequently Asked Questions page" } } } diff --git a/src/extension/executors/aws.ts b/src/extension/executors/aws.ts index 6727c9f2a..141b153fc 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,14 @@ 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 execRes: string | undefined - const onData = (data: string | Uint8Array) => { - if (execRes === undefined) { - execRes = '' - } - execRes += data.toString() - } - - 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' + const result = await kernel.runProgram(programOptions) + const profile = result.code === 0 ? result.output : 'default' credentials = fromIni({ profile }) switch (awsResolver.view) { diff --git a/src/extension/extension.ts b/src/extension/extension.ts index 5cb8c14b7..50478738a 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 { + getFaqUrl, getForceNewWindowConfig, getRunmeAppUrl, getSessionOutputs, @@ -80,6 +81,7 @@ import { NotebookCellStatusBarProvider } from './provider/cellStatusBar/notebook import { SessionOutputCellStatusBarProvider } from './provider/cellStatusBar/sessionOutput' import * as generate from './ai/generate' import * as manager from './ai/manager' + export class RunmeExtension { protected serializer?: SerializerBase @@ -129,6 +131,23 @@ export class RunmeExtension { */ try { await server.launch() + kernel.runProgram('echo $0').then(async ({ output }) => { + const supportedShells = ['zsh', 'bash'] + const isSupported = supportedShells.some((sh) => output?.includes(sh)) + + if (isSupported) { + return + } + + const showMore = 'Show more' + const answer = await window.showWarningMessage('Unsupported shell', showMore) + + if (answer === showMore) { + const dashboardUri = getFaqUrl('supported-shells') + const uri = Uri.parse(dashboardUri) + env.openExternal(uri) + } + }) } catch (e) { // Unrecoverable error happened if (e instanceof KernelServerError) { diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts index 3146330c2..f8d61c972 100644 --- a/src/extension/kernel.ts +++ b/src/extension/kernel.ts @@ -72,7 +72,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, RunnerExitReason, RunProgramOptions } from './runner' import { IRunnerEnvironment } from './runner/environment' import { IKernelRunnerOptions, executeRunner } from './executors/runner' import { ITerminalState, NotebookTerminalType } from './terminal/terminalState' @@ -1119,4 +1119,75 @@ export class Kernel implements Disposable { public getPlainCache(cacheId: string): Promise | undefined { return this.serializer?.getPlainCache(cacheId) } + + async runProgram(program?: RunProgramOptions | string) { + let programOptions: RunProgramOptions + + if (typeof program === 'object') { + programOptions = program + } else if (typeof program === 'string') { + programOptions = { + programName: '', + background: false, + exec: { + type: 'script', + script: program, + }, + languageId: 'sh', + storeLastOutput: false, + tty: true, + } + } else { + return Promise.reject(new Error('Invalid runProgram arguments')) + } + + 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) + programSession.run() + + return new Promise<{ exitReason?: RunnerExitReason; code?: number | void; output?: string }>( + (resolve, reject) => { + programSession.onDidClose(async (code) => { + return resolve({ code, output: execRes?.trim() }) + }) + + programSession.onInternalErr((e) => { + reject(e) + }) + + const exitReason = programSession.hasExited() + + if (exitReason) { + switch (exitReason.type) { + case 'error': + { + reject({ exitReason, output: execRes?.trim() }) + } + break + + case 'exit': + { + resolve({ exitReason, code: exitReason.code, output: execRes?.trim() }) + } + break + + default: { + resolve({ exitReason, output: execRes?.trim() }) + } + } + } + }, + ) + } } diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 238f5f546..a5f5f970e 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_FAQ_URL = 'https://docs.runme.dev/faq' 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), + faqUrl: z.string().default(DEFAULT_FAQ_URL), }, } @@ -432,6 +434,11 @@ const isPlatformAuthEnabled = (): boolean => { return getCloudConfigurationValue('platformAuth', false) } +const getFaqUrl = (hash: string): string => { + const baseUrl = getCloudConfigurationValue('faqUrl', DEFAULT_FAQ_URL) + return `${baseUrl}#${hash}` +} + export { enableServerLogs, getActionsOpenViewInEditor, @@ -462,4 +469,5 @@ export { getSessionOutputs, getMaskOutputs, getLoginPrompt, + getFaqUrl, }