Repository: stateful/vscode-runme
Pull request #460 (2023-04-27 17:19:30)

Add "promptEnv" setting to toggle prompting for environment vars
Part of #430 

mxsdev: CI keeps failing on build :\ not sure why
mxsdev: @christian-bromann 

Created stateful/runme.dev#140, let me know if that is sufficient to merge

Patch:
diff --git a/src/client/components/annotations.ts b/src/client/components/annotations.ts
index 5c3f2972..fefedc3d 100644
--- a/src/client/components/annotations.ts
+++ b/src/client/components/annotations.ts
@@ -88,6 +88,7 @@ export class Annotations extends LitElement {
     ['background', 'Run cell as background process (default: false)'],
     ['interactive', 'Run cell inside terminal to allow for interactive input (default: true)'],
     ['closeTerminalOnSuccess', 'Hide terminal after cell successful execution (default: true)'],
+    ['promptEnv', 'Prompt user input for exported environment variables (default: true)'],
     ['mimeType', 'Cell\'s ouput content MIME type (default: text/plain)'],
     ['name', 'Cell\'s canonical name for easy referencing in the CLI (default: auto-generated)'],
   ])
diff --git a/src/constants.ts b/src/constants.ts
index f72ef8ae..602ec1a7 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -89,3 +89,5 @@ export const LANGUAGES = new Map([
 
 export const SERVER_ADDRESS = 'localhost'
 export const SERVER_PORT = 7863
+
+export const DEFAULT_PROMPT_ENV = true
diff --git a/src/extension/executors/runner.ts b/src/extension/executors/runner.ts
index e4cbb6fc..43354058 100644
--- a/src/extension/executors/runner.ts
+++ b/src/extension/executors/runner.ts
@@ -52,7 +52,7 @@ export async function executeRunner(
   environmentManager?: IEnvironmentManager
 ) {
   const annotations = getAnnotations(exec.cell)
-  const { interactive, mimeType, background, closeTerminalOnSuccess } = annotations
+  const { interactive, mimeType, background, closeTerminalOnSuccess, promptEnv } = annotations
 
   // enforce background tasks as singleton instanes
   // to do this,
@@ -79,7 +79,7 @@ export async function executeRunner(
     RUNME_ID
   }
 
-  const commands = await parseCommandSeq(cellText, prepareCmdSeq)
+  const commands = await parseCommandSeq(cellText, promptEnv, prepareCmdSeq)
   if(!commands) { return false }
 
   if (commands.length === 0) {
diff --git a/src/extension/executors/task.ts b/src/extension/executors/task.ts
index dfecb3cf..03f148d9 100644
--- a/src/extension/executors/task.ts
+++ b/src/extension/executors/task.ts
@@ -35,8 +35,10 @@ async function taskExecutor(
   doc: TextDocument,
   outputs: NotebookCellOutputManager,
 ): Promise<boolean> {
+  const { interactive: isInteractive, promptEnv } = getAnnotations(exec.cell)
+
   const cwd = path.dirname(doc.uri.fsPath)
-  const cellText = await retrieveShellCommand(exec)
+  const cellText = await retrieveShellCommand(exec, promptEnv)
   if (typeof cellText !== 'string') {
     return false
   }
@@ -63,7 +65,6 @@ async function taskExecutor(
    * run as non interactive shell script if set as configuration or annotated
    * in markdown section
    */
-  const isInteractive = getAnnotations(exec.cell).interactive
   if (!isInteractive) {
     return inlineSh.call(this, exec, cmdLine, cwd, env, outputs)
   }
diff --git a/src/extension/executors/utils.ts b/src/extension/executors/utils.ts
index f5f7ae3e..0b51d93a 100644
--- a/src/extension/executors/utils.ts
+++ b/src/extension/executors/utils.ts
@@ -4,7 +4,7 @@ import path from 'node:path'
 import { NotebookCellOutput, NotebookCellExecution, NotebookCellOutputItem, window } from 'vscode'
 
 import { ENV_STORE } from '../constants'
-import { OutputType } from '../../constants'
+import { DEFAULT_PROMPT_ENV, OutputType } from '../../constants'
 import type { CellOutputPayload } from '../../types'
 import { NotebookCellOutputManager } from '../cell'
 
@@ -60,7 +60,8 @@ export async function promptUserForVariable(
 
 export function getCommandExportExtractMatches(
   rawText: string,
-  supportsDirect = true
+  supportsDirect = true,
+  supportsPrompt = true,
 ): CommandExportExtractMatch[] {
   const test = new RegExp(EXPORT_EXTRACT_REGEX)
 
@@ -83,7 +84,7 @@ export function getCommandExportExtractMatches(
     if (placeHolder.startsWith('$(') && placeHolder.endsWith(')')) {
       matchType = 'exec'
       value = placeHolder.slice(2, -1)
-    } else if (!placeHolder.includes('\n')) {
+    } else if (!placeHolder.includes('\n') && supportsPrompt) {
       matchType = 'prompt'
     } else if (supportsDirect) {
       matchType = 'direct'
@@ -109,12 +110,12 @@ export function getCommandExportExtractMatches(
  * @param exec NotebookCellExecution
  * @returns cell text if all operation to retrieve the cell text could be executed, undefined otherwise
  */
-export async function retrieveShellCommand (exec: NotebookCellExecution) {
+export async function retrieveShellCommand (exec: NotebookCellExecution, promptForEnv = DEFAULT_PROMPT_ENV) {
   let cellText = exec.cell.document.getText()
   const cwd = path.dirname(exec.cell.document.uri.fsPath)
   const rawText = exec.cell.document.getText()
 
-  const exportMatches = getCommandExportExtractMatches(rawText)
+  const exportMatches = getCommandExportExtractMatches(rawText, true, promptForEnv)
 
   const stateEnv = Object.fromEntries(ENV_STORE)
 
@@ -194,11 +195,12 @@ export function getShellPath(execKey?: string): string|undefined {
  */
 export async function parseCommandSeq(
   cellText: string,
+  promptForEnv = DEFAULT_PROMPT_ENV,
   parseBlock?: (block: string) => string[]
 ): Promise<string[]|undefined> {
   parseBlock ??= (s) => s ? s.split('\n') : []
 
-  const exportMatches = getCommandExportExtractMatches(cellText, false)
+  const exportMatches = getCommandExportExtractMatches(cellText, false, promptForEnv)
 
   type CommandBlock =
     |{
diff --git a/src/extension/provider/runmeTask.ts b/src/extension/provider/runmeTask.ts
index dd685fcd..19256f20 100644
--- a/src/extension/provider/runmeTask.ts
+++ b/src/extension/provider/runmeTask.ts
@@ -116,7 +116,7 @@ export class RunmeTaskProvider implements TaskProvider {
         const cwd = options.cwd || path.dirname(filePath)
 
         const cellContent = 'value' in cell ? cell.value : cell.document.getText()
-        const commands = await parseCommandSeq(cellContent, prepareCmdSeq)
+        const commands = await parseCommandSeq(cellContent, undefined, prepareCmdSeq)
 
         const runOpts: RunProgramOptions = {
           programName: getShellPath() ?? 'sh',
diff --git a/src/schema.ts b/src/schema.ts
index 1d2026a7..27d10b85 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -1,5 +1,7 @@
 import { z } from 'zod'
 
+import { DEFAULT_PROMPT_ENV } from './constants'
+
 const cleanAnnotation = (value: string, character: string): string => {
     return value.trim().replaceAll(/[ ]{2,}/g, '').replaceAll(character, '')
 }
@@ -42,6 +44,7 @@ export const AnnotationSchema = {
     background: boolify(false),
     interactive: boolify(true),
     closeTerminalOnSuccess: boolify(true),
+    promptEnv: boolify(true),
     name: z.preprocess(
         (value) =>
             typeof value === 'string' ?
@@ -57,7 +60,7 @@ export const AnnotationSchema = {
             }
             return true
         }, 'mime type specification invalid format')
-        .default('text/plain')
+        .default('text/plain'),
 }
 
 export const SafeCellAnnotationsSchema = z.object({
@@ -65,6 +68,7 @@ export const SafeCellAnnotationsSchema = z.object({
     background: falseyBoolean(false),
     interactive: falseyBoolean(true),
     closeTerminalOnSuccess: falseyBoolean(true),
+    promptEnv: falseyBoolean(DEFAULT_PROMPT_ENV),
 })
 
 export const CellAnnotationsSchema = z.object({
diff --git a/src/types.ts b/src/types.ts
index 28e4e4bf..3d565fed 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -31,6 +31,7 @@ export namespace Serializer {
     interactive?: string
     closeTerminalOnSuccess?: string
     mimeType?: string
+    promptEnv?: string
     ['runme.dev/name']?: string
     ['runme.dev/uuid']?: string
     ['runme.dev/denoState']?: DenoState


Test patch:
diff --git a/tests/extension/executors/utils.test.ts b/tests/extension/executors/utils.test.ts
index 3752f473..a56ec70b 100644
--- a/tests/extension/executors/utils.test.ts
+++ b/tests/extension/executors/utils.test.ts
@@ -197,6 +197,10 @@ test('supports multiline exports', async () => {
 })
 
 suite('parseCommandSeq', () => {
+  beforeEach(() => {
+    vi.mocked(window.showInputBox).mockReset()
+  })
+
   test('single-line export', async () => {
     vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
 
@@ -204,11 +208,25 @@ suite('parseCommandSeq', () => {
       'export TEST="<placeholder>"'
     ].join('\n'))
 
-    expect(res).toBeTruthy
+    expect(res).toBeTruthy()
     expect(res).toHaveLength(1)
     expect(res?.[0]).toBe('export TEST="test value"')
   })
 
+  test('single-line export with prompt disabled', async () => {
+    vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
+
+    const res = await parseCommandSeq([
+      'export TEST="placeholder"'
+    ].join('\n'), false)
+
+    expect(window.showInputBox).toBeCalledTimes(0)
+
+    expect(res).toBeTruthy()
+    expect(res).toHaveLength(1)
+    expect(res![0]).toBe('export TEST="placeholder"')
+  })
+
   test('single line export with cancelled prompt', async () => {
     vi.mocked(window.showInputBox).mockImplementationOnce(async () => undefined)
 
@@ -286,7 +304,7 @@ suite('parseCommandSeq', () => {
       'echo $TEST_MULTILINE'
     ]
 
-    const res = await parseCommandSeq(cmdLines.join('\n'), getCmdSeq)
+    const res = await parseCommandSeq(cmdLines.join('\n'), true, getCmdSeq)
 
     expect(res).toBeTruthy()
     expect(res).toStrictEqual([
diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts
index 09306d7e..6f0afb4b 100644
--- a/tests/extension/utils.test.ts
+++ b/tests/extension/utils.test.ts
@@ -281,6 +281,7 @@ suite('#getAnnotations', () => {
         interactive: true,
         mimeType: 'text/plain',
         name: 'command-123',
+        promptEnv: true,
         'runme.dev/uuid': '48d86c43-84a4-469d-8c78-963513b0f9d0'
       }
     )
@@ -300,6 +301,7 @@ suite('#getAnnotations', () => {
       interactive: true,
       mimeType: 'text/plain',
       name: 'echo-hello',
+      promptEnv: true,
     })
   })
 })