Skip to content

Commit

Permalink
fix: prompt transformers and operators 馃
Browse files Browse the repository at this point in the history
* fix: adding prompt model and transformer logic

* fix: the get transformer

* fix: make transformer arguments optional completely

* fix: context can now run transformer sequence

* fix: the split transformer

* fix: the run transformer

* fix: attempting to resolve nested resolvers

* fix: quoted arguments for nested transformers and the echo transformer

* fix: lower, snake, title, and upper transformers

* fix: transformer docs
  • Loading branch information
daretodave committed May 10, 2024
1 parent 6349046 commit 533c5be
Show file tree
Hide file tree
Showing 16 changed files with 473 additions and 15 deletions.
158 changes: 158 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,164 @@ export async function query(): Promise<{

> Note the return type is optional, just added above to highlight the typescript engine provided

### Transformers
Sometimes we need to change variables, execution results or perform an operation on strings.

MTERM helps solve this by providing `transformers` as terminal operators. These transformers abstract away somewhat tedious actions in the terminal, they also provide common utilities within the scope of a prompt

<img src="https://github.com/mterm-io/mterm/assets/7341502/d7a0cb6c-6c75-4183-9596-8b1295789d64" alt="drawing" width="250"/>

For the prompt of
```bash
":snake(this is some text)"
```

We get the result
```bash
this_is_some_text
```

`snake` is a mterm transformer that takes a string and converts it to [snake case](https://en.wikipedia.org/wiki/Snake_case)


Transformers can be used anywhere including from within transformers -

```bash
":upper(hmm) :lower(WOW) :snake(:lower(HMM WOW))"
```

Prints out -
```bash
HMM wow hmm_wow
```

#### Transformer Arguments

Some transformers accept arguments. Arguments for transformers are seperated by `:` colon characters

Here is the split transformer on a string -

```bash
":split(1,2,3,4:1:,)"
```
- `1,2,3,4` is the first argument, the split text
- `1` is the index to return (a [split operation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split) is an array, so return a value from this array)
- `,` the final argument is the [delimiter](https://en.wikipedia.org/wiki/Delimiter)

> one way to view this operation is `"1,2,3,4".split(',')[1]`

Note you can use `"` quotes to wrap transformer arguments if there is a colon within the argument

#### Transformers Provided

Here are the provided transformers for use -

##### echo
> print out the argument, proxies the argument to the `echo` on the terminal

`:echo` examples -
```bash
:echo(HELLO) -> "HELLO"
:echo(:upper(hello)) -> "HELLO"
```

##### get
> gets the argument from the [vault](#secrets), if not available gets the argument from the [environment variables](https://en.wikipedia.org/wiki/Environment_variable) from the host machine, if not available returns the fallback

`:get` examples -
```bash
# e.g: Basic find
#
# assuming:
## there is a env variable called PATH=PATH_FROM_SYSTEM
:get(PATH) -> "PATH_FROM_SYSTEM"
# e.g: Fallback keys
#
# assuming:
## there is NOT a env variable called NOT_FOUND_A
## there is NOT a env variable called NOT_FOUND_B
## there is a env variable called FOUND_C=VALUE_OF_C
:get(NOT_FOUND_A,NOT_FOUND_B,FOUND_C) -> "VALUE_OF_C"
# e.g: Fallback variable
#
# assuming:
## there is NOT a env variable called NOT_FOUND_A
## there is a env variable called FOUND_C=VALUE_OF_C
:get(NOT_FOUND_A:im the fallback) -> "im the fallback"
:get(FOUND_C:im the fallback) -> "VALUE_OF_C"
```

##### lower
> transforms the argument to lowercase

`:lower` examples -
```bash
":lower(HELLO)" -> "hello"
:echo(:lower(HELLO)) -> "hello"
```

##### run
> runs the argument against the terminal and proxies the result

`:run` examples -
```bash
# e.g: Run command
#
# assuming:
# there is a `hello` function in `commands.ts`
# the `hello` function is declared as const hello = (arg = 'fallback') => `Hello, ${arg}!`
":run(hello)" -> "Hello, fallback!"
":run(hello argument)" -> "Hello, argument!"
# e.g: Run command within transformer
#
# assuming:
# there is a `hello` function in `commands.ts`
# the `hello` function is declared as const hello = (arg = 'fallback') => `Hello, ${arg}!`
":lower(:run(hello))" -> "hello, fallback!"
":lower(:run(hello)) :upper(:run(hello))" -> "hello, fallback! HELLO, FALLBACK!"
```

##### snake
> transforms the argument to snakecase

`:snake` examples -
```bash
":snake(HELLO WORLD)" -> "hello_world"
```

##### title
> transforms the argument to [titlecase](https://en.wikipedia.org/wiki/Title_case)

`:title` examples -
```bash
":title(hello world)" -> "Hello World"
":title(hello_world)" -> "Hello World"
```

##### split
> splits the provided argument, accepts the index or uses `0`, accepts the [delimiter](https://en.wikipedia.org/wiki/Delimiter) or uses `,`

`:split` examples -
```bash
":split(hello,world)" -> "hello"
":split(hello,world:1)" -> "world"
":split(hello@world:1:@)" -> "world"
:echo(":split(":get(xx,PATH)":0:;)") -> "FIRST_RESULT_IN_PATH"
```

##### upper
> transforms the argument to uppercase

`:title` examples -
```bash
":uppercase(hello world)" -> "HELLO WORLD"
```

### Secrets

Environment variables are a bit unsafe. You set these and leave the host machine all the ability to read and share these. Wonderful for services and backends, not the safest for personal usage.
Expand Down
35 changes: 31 additions & 4 deletions src/main/framework/execute-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Workspace } from './workspace'
import { Command, ResultContentEvent, ResultStreamEvent, Runtime } from './runtime'
import { Command, Prompt, ResultContentEvent, ResultStreamEvent, Runtime } from './runtime'
import { WebContents } from 'electron'
import { readFile } from 'fs-extra'
import short from 'short-uuid'
import { ResultStream } from './result-stream'

import { process } from './transformers'
export interface RuntimeContentHandle {
id: string
update(html: string): void
Expand All @@ -20,7 +20,6 @@ export type RuntimeContentEventCallback = (event: RuntimeContentEvent) => Promis
export class ExecuteContext {
public readonly start: number = Date.now()
public readonly id: string = short.generate()

public readonly events: ResultContentEvent[] = []
private readonly eventHandlers = new Map<string, RuntimeContentEventCallback>()

Expand All @@ -35,9 +34,37 @@ export class ExecuteContext {
enabled: boolean
results: boolean
max: number
}
},
public prompt: Prompt = new Prompt(command.prompt)
) {}

copyForPrompt(prompt: string): ExecuteContext {
const context = new ExecuteContext(
this.platform,
this.sender,
this.workspace,
this.runtime,
{
...this.command,
result: {
code: 0,
stream: [],
edit: undefined
}
},
this.profile,
this.history,
new Prompt(prompt)
)

context.command.context = context

return context
}

async resolve(text: string): Promise<string> {
return await process(this, text)
}
out(text: string, error: boolean = false): ResultStream | null {
const isFinished = this.command.aborted || this.command.complete
if (isFinished) {
Expand Down
4 changes: 4 additions & 0 deletions src/main/framework/runtime-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { HistoricalExecution } from './history'
import { writeFile } from 'fs-extra'
import { ExecuteContext } from './execute-context'
import { ResultStream } from './result-stream'
import { transform } from './transformers'

export function attach({ app, workspace }: BootstrapContext): void {
const eventListForCommand = (command: Command): ResultContentEvent[] => {
Expand Down Expand Up @@ -455,6 +456,9 @@ export function attach({ app, workspace }: BootstrapContext): void {
}
)

// run transformers
context.prompt.value = await transform(context)

// attach context to command
command.context = context

Expand Down
17 changes: 10 additions & 7 deletions src/main/framework/runtime-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,21 @@ const systemCommands: Array<{
Restart
]
export async function execute(context: ExecuteContext): Promise<void | boolean> {
const [cmd, ...args] = context.command.prompt.split(' ')

// check for system commands
for (const systemCommand of systemCommands) {
if (systemCommand.command === cmd || systemCommand?.alias?.includes(cmd)) {
return systemCommand.task(context, ...args)
if (
systemCommand.command === context.prompt.cmd ||
systemCommand?.alias?.includes(context.prompt.cmd)
) {
return systemCommand.task(context, ...context.prompt.args)
}
}

// check for user commands
if (context.workspace.commands.has(cmd)) {
let result = await Promise.resolve(context.workspace.commands.run(context, cmd, ...args))
if (context.workspace.commands.has(context.prompt.cmd)) {
let result = await Promise.resolve(
context.workspace.commands.run(context, context.prompt.cmd, ...context.prompt.args)
)

if (!result) {
// nothing was replied with, assume this is a run that will happen in time
Expand All @@ -70,7 +73,7 @@ export async function execute(context: ExecuteContext): Promise<void | boolean>
const [platformProgram, ...platformProgramArgs] = context.platform.split(' ')

const argsClean = platformProgramArgs.map(
(arg: string) => `${arg.replace('$ARGS', context.command.prompt)}`
(arg: string) => `${arg.replace('$ARGS', context.prompt.value)}`
)

const childSpawn = spawn(platformProgram, argsClean, {
Expand Down
24 changes: 24 additions & 0 deletions src/main/framework/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ export interface EditFileViewModel {
content: string
}

export class Prompt {
private _value: string = ''
public parts: string[] = []
public args: string[] = []
public cmd: string = ''
constructor(value: string = '') {
this.value = value
}

set value(value: string) {
this._value = value
this.parts = value.split(' ')

const [cmd, ...args] = this.parts

this.cmd = cmd
this.args = args
}

get value(): string {
return this._value
}
}

export interface EditFile extends EditFileViewModel {
callback: (text: string) => Promise<void> | void
}
Expand Down
4 changes: 1 addition & 3 deletions src/main/framework/system-commands/cd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { ExecuteContext } from '../execute-context'

export default {
command: 'cd',
async task(context: ExecuteContext): Promise<void> {
const [, ...args] = context.command.prompt.split(' ')

async task(context: ExecuteContext, ...args: string[]): Promise<void> {
const path: string = context.runtime.resolve(args[0] || '.')

const isLocationActive = await pathExists(path)
Expand Down
2 changes: 1 addition & 1 deletion src/main/framework/system-commands/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { pathExists, stat } from 'fs-extra'
import { ExecuteContext } from '../execute-context'

async function edit(context: ExecuteContext): Promise<void> {
const [, ...args] = context.command.prompt.split(' ')
const [, ...args] = context.prompt.split(' ')
const path: string = context.runtime.resolve(args[0] || '.')

const isLocationActive = await pathExists(path)
Expand Down
4 changes: 4 additions & 0 deletions src/main/framework/transformers/echo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { transformer } from './index'
export const echo: transformer = (_, ...args) => {
return `echo "${args.join(':')}"`
}
33 changes: 33 additions & 0 deletions src/main/framework/transformers/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { transformer } from './index'
import { ExecuteContext } from '../execute-context'

function find(context: ExecuteContext, key: string): string | undefined {
if (context.workspace.store.unlocked) {
// is vault unlocked?
// search here first!
const valueMaybe = context.workspace.store.value<string>(key)
if (valueMaybe !== undefined && valueMaybe !== null) {
// value entries can be empty strings
// lets not do a simple falsie here
return valueMaybe
}
}
const valueMaybe = process.env[key]
if (valueMaybe !== undefined) {
return valueMaybe
}

return undefined
}
export const get: transformer = (context, keyList, fallback = '') => {
const keys = keyList.split(',')

for (const key of keys) {
const valueMaybe = find(context, key)
if (valueMaybe !== undefined) {
return valueMaybe
}
}

return fallback
}
Loading

0 comments on commit 533c5be

Please sign in to comment.