This file is a merged representation of a subset of the codebase, containing specifically included files, combined into a single document by Repomix.
<file_summary> This section contains a summary of this file.
This file contains a packed representation of a subset of the repository's contents that is considered the most important context. It is designed to be easily consumable by AI systems for analysis, code review, or other automated processes.<file_format> The content is organized as follows:
- This summary section
- Repository information
- Directory structure
- Repository files (if enabled)
- Multiple file entries, each consisting of:
- File path as an attribute
- Full contents of the file </file_format>
<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the original repository files, not this packed version.
- When processing this file, use the file path to distinguish between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with the same level of security as you would the original repository. </usage_guidelines>
</file_summary>
<directory_structure> scripts/ test-post.js test-pre.js src/ api/ kit.ts send-result.ts core/ db.ts utils.test.ts utils.ts main/ index.ts run/ app-prompt.ts setup/ create-env.ts degit-kenv.ts link-kenv-to-kit.ts setup.test.js setup.ts types/ index.d.ts test-sdk/ ava.config.js config.js main.test.js package.json </directory_structure>
This section contains the contents of the repository's files. import { rimraf } from "rimraf"await import("../test-sdk/config.js")
if (test("-d", kitMockPath())) { await rimraf(kitMockPath()) }
process.env.KENV = home(".kenv")
await exec(kit ${kitPath("cli", "refresh-scripts-db.js")})
import { Channel, Value } from "../core/enum.js" import { run, cmd, getMainScriptPath, isScriptlet, isSnippet, } from "../core/utils.js" import type { Choice, Scriptlet, Script, } from "../types/core.js" import { mainMenu, scriptFlags, actions, modifiers, errorPrompt, getFlagsFromActions, } from "../api/kit.js" import type { Open } from "../types/packages.js" import { parseShebang } from "../core/shebang.js" import "./../target/path/path.js"
console.clear()
if (env.KIT_EDITOR !== "code") {
scriptFlags["code"] = {
group: "Script Actions",
name: "Open Kenv in VS Code",
description: "Open the script's kenv in VS Code",
shortcut: ${cmd}+shift+o,
}
}
let panel = ``
// let submitted = false
// let onInput = input => {
// if (input.startsWith("/")) submit("/")
// if (input.startsWith("")) submit("")
// if (input.startsWith(">")) submit(">")
// submitted = true
// }
let onNoChoices = async (input, state) => { // if (submitted) return if (input && state.flaggedValue === "") { let regex = /[!@#$%^&*()_+=[]{};':"\|,.<>/?]/g let invalid = regex.test(input)
if (invalid) {
panel = md(`# No matches found
No matches found for ${input}`)
setPanel(panel)
return
}
let scriptName = input
.replace(/[^\w\s-]/g, "")
.trim()
.replace(/\s/g, "-")
.toLowerCase()
panel = md(`# Quick New Script
Create a script named ${scriptName}
`)
setPanel(panel)
}
}
/*
terminal ~ browse home / browse root ' snippets " word api : emoji search ; app launcher , "sticky note" . file search < clipboard history 0-9 calculator ? docs */
let isApp = false let isPass = false let input = "" let focused: Choice | undefined
trace.instant({
args: "mainMenu",
})
let script = await mainMenu({
name: "Main",
description: "Script Kit",
placeholder: "Script Kit",
enter: "Run",
strict: false,
flags: scriptFlags,
onMenuToggle: async (input, state) => {
if (!state?.flag) {
let menuFlags = {
...(scriptFlags as object),
...getFlagsFromActions(actions),
}
setFlags(menuFlags)
}
},
onKeyword: async (input, state) => {
let { keyword, value } = state
if (keyword) {
if (value?.filePath) {
preload(value?.filePath)
await run(value.filePath, --keyword, keyword)
}
}
},
onSubmit: i => { if (i) { input = i.trim() } }, onBlur: async (input, state) => { hide() exit() }, onNoChoices, onChoiceFocus: async (input, state) => { isApp = state?.focused?.group === "Apps" || state?.focused?.group === "Community" isPass = state?.focused?.group === "Pass" && !state?.focused?.exact
focused = state?.focused
},
// footer: Script Options: ${cmd}+k,
shortcodes: {
// "=": kitPath("handler", "equals-handler.js"),
// ">": kitPath("handler", "greaterthan-handler.js"),
// "/": kitPath("main", "browse.js"),
// "~": kitPath("handler", "tilde-handler.js"),
// "'": kitPath("handler", "quote-handler.js"),
// '"': kitPath("handler", "doublequote-handler.js"),
// ";": kitPath("handler", "semicolon-handler.js"),
// ":": kitPath("handler", "colon-handler.js"),
// ".": kitPath("handler", "period-handler.js"),
// "\": kitPath("handler", "backslash-handler.js"),
// "|": kitPath("handler", "pipe-handler.js"),
// ",": kitPath("handler", "comma-handler.js"),
// "": kitPath("handler", "backtick-handler.js"), // "<": kitPath("handler", "lessthan-handler.js"), // "-": kitPath("handler", "minus-handler.js"), // "[": kitPath("handler", "leftbracket-handler.js"), "1": ${kitPath("handler", "number-handler.js")} 1, "2": ${kitPath("handler", "number-handler.js")} 2, "3": ${kitPath("handler", "number-handler.js")} 3, "4": ${kitPath("handler", "number-handler.js")} 4, "5": ${kitPath("handler", "number-handler.js")} 5, "6": ${kitPath("handler", "number-handler.js")} 6, "7": ${kitPath("handler", "number-handler.js")} 7, "8": ${kitPath("handler", "number-handler.js")} 8, "9": ${kitPath("handler", "number-handler.js")} 9`,
// "0": kitPath("handler", "zero-handler.js"),
// "?": kitPath("handler", "question-handler.js"),
},
actions, input: arg?.input || "", })
trace.instant({ args: "mainMenu submitted", })
if (!script && Object.keys(flag).length === 0) {
global.warn(
Running error action because of no script or flag detected
)
await errorPrompt({
message: An unknown error occurred. Please try again.,
name: "No Script or Flag Detected",
})
}
if (typeof script === "boolean" && !script) { exit() }
const runScript = async (script: Script | string) => { if (isApp && typeof script === "string") { return await Promise.all([ hide({ preloadScript: getMainScriptPath(), }), (open as unknown as Open)(script as string), ]) }
if (isPass || (script as Script)?.postfix) { let hardPass = (script as any).postfix || input if (typeof global?.flag === "object") { global.flag.hardPass = hardPass } return await run( (script as Script)?.filePath, "--pass", hardPass ) }
if ( script === Value.NoValue || typeof script === "undefined" ) { console.warn("🤔 No script selected", script) return }
if (typeof script === "string") { if (script === "kit-sponsor") { return await run(kitPath("main", "sponsor.js")) }
let scriptPath = script as string
let [maybeScript, numarg] = scriptPath.split(/\s(?=\d)/)
if (await isFile(maybeScript)) {
return await run(maybeScript, numarg)
}
return await run(
`${kitPath("cli", "new")}.js`,
scriptPath.trim().replace(/\s/g, "-").toLowerCase(),
`--scriptName`,
scriptPath.trim()
)
}
let shouldEdit = flag?.open
let selectedFlag: string | undefined = Object.keys( flag ).find(f => { return f && !modifiers[f] })
if (selectedFlag && flag?.code) {
return await exec(
open -a 'Visual Studio Code' '${path.dirname( path.dirname(script.filePath) )}'
)
}
if (selectedFlag && selectedFlag === "settings") { return await run(kitPath("main", "kit.js")) } if (selectedFlag?.startsWith("kenv")) { let k = script.kenv || "main" if (selectedFlag === "kenv-term") { k = path.dirname(path.dirname(script.filePath)) }
return await run(
`${kitPath("cli", selectedFlag)}.js`,
k
)
}
if (selectedFlag?.endsWith("menu")) {
return await run(${kitPath("cli", selectedFlag)}.js)
}
if (selectedFlag && !flag?.open) {
return await run(
${kitPath("cli", selectedFlag)}.js,
script.filePath
)
}
if (flag[modifiers.opt]) { return showLogWindow(script?.filePath) }
if (script.background) { return await run( kitPath("cli", "toggle-background.js"), script?.filePath ) }
if (shouldEdit) { return await edit(script.filePath, kenvPath()) }
if (isSnippet(script)) { send(Channel.STAMP_SCRIPT, script as Script)
return await run(
kitPath("app", "paste-snippet.js"),
"--filePath",
script.filePath
)
}
if (isScriptlet(script)) { let { runScriptlet } = await import("./scriptlet.js") updateArgs(args) await runScriptlet(script, script.inputs || [], flag) return }
if (Array.isArray(script)) { let { runScriptlet } = await import("./scriptlet.js") updateArgs(args) await runScriptlet(focused as Scriptlet, script, flag) return }
if ((script as Script)?.shebang) { const shebang = parseShebang(script as Script) return await sendWait(Channel.SHEBANG, shebang) }
if (script?.filePath) {
preload(script?.filePath)
let runP = run(
script.filePath,
...Object.keys(flag).map(f => --${f})
)
return await runP
}
return await arg("How did you get here?") }
await runScript(script)
process.env.KIT_TARGET = 'app-prompt' import { Channel, Trigger } from '../core/enum.js' import os from 'node:os' import { configEnv, run } from '../core/utils.js'// TODO: Fix the types around accepting an early Scriptlet let script: any = '' let args = [] let tooEarlyHandler = (data) => { if (data.channel === Channel.VALUE_SUBMITTED) { script = data?.value?.scriptlet ? data?.value : data?.value?.script || data?.state?.value?.filePath args = data?.value?.args || data?.state?.value?.args || [] global.headers = data?.value?.headers || {}
// const value = `${process.pid}: ${
// data?.channel
// }: ${script} ${performance.now()}ms`
// process.send({
// channel: Channel.CONSOLE_LOG,
// value,
// });
} }
process.send({ channel: Channel.KIT_LOADING, value: 'app-prompt.ts' })
process.on('message', tooEarlyHandler)
await import('../api/global.js') let { initTrace } = await import('../api/kit.js') await initTrace() await import('../api/pro.js') await import('../api/lib.js') await import('../platform/base.js')
let platform = os.platform()
try {
await import(../platform/${platform}.js)
} catch (error) {
// console.log(No ./platform/${platform}.js)
}
await import('../target/app.js')
if (process.env.KIT_MEASURE) {
let { PerformanceObserver, performance } = await import('node:perf_hooks')
let obs = new PerformanceObserver((list) => {
let entry = list.getEntries()[0]
log(⌚️ [Perf] ${entry.name}: ${entry.duration}ms)
})
obs.observe({ entryTypes: ['measure'] })
}
try { await silentAttemptImport(kenvPath('globals.ts')) } catch (error) { log('No user-defined globals.ts') }
let trigger = '' let name = '' let result = null let choices = [] let scriptlet = null process.title = 'Kit Idle - App Prompt' process.send({ channel: Channel.KIT_READY, value: result })
try { result = await new Promise((resolve, reject) => { process.off('message', tooEarlyHandler)
if (script) {
// process.send({
// channel: Channel.CONSOLE_LOG,
// value: `Too early ${tooEarly}...`,
// })
// TODO: Revisit what causes "too early" and the edge-cases here
if (script?.scriptlet) {
resolve(script)
return
}
resolve({
script,
args,
trigger: Trigger.Trigger
})
return
}
type MessageData = {
channel: Channel
value: any
}
let messageHandler = (data: MessageData) => {
if (data.channel === Channel.HEARTBEAT) {
send(Channel.HEARTBEAT)
}
if (data.channel === Channel.VALUE_SUBMITTED) {
trace.instant({
name: 'app-prompt.ts -> VALUE_SUBMITTED',
args: data
})
global.headers = data?.value?.headers || {}
process.off('message', messageHandler)
resolve(data.value)
}
}
process.on('message', messageHandler)
}) } catch (e) { global.warn(e) exit() } ;({ script, args, trigger, choices, name, scriptlet } = result)
process.env.KIT_TRIGGER = trigger
configEnv()
process.title = Kit - ${path.basename(script)}
process.once('beforeExit', () => { if (global?.trace?.flush) { global.trace.flush() global.trace = null } send(Channel.BEFORE_EXIT) })
performance.mark('run')
if (choices?.length > 0) { global.kitScript = scriptlet?.filePath let inputs: string[] = []
if (choices[0].inputs?.length > 0) { inputs = await arg<string[]>( { name, scriptlet: true, resize: true, onEscape: () => { exit() } }, choices ) } let { runScriptlet } = await import('../main/scriptlet.js') updateArgs(args) await runScriptlet(scriptlet, inputs, flag) } else { if (script.includes('.md')) { log({ script, ugh: '❌' }) exit() } await run(script, ...args) }
import { backupEnvFile, mergeEnvFiles, formatEnvContent } from "../core/env-backup.js" import { safeWriteEnvFile } from "../core/env-file-lock.js" import chalk from "chalk"// Check if .env already exists const envPath = kenvPath(".env") const alreadyExists = await pathExists(envPath)
if (!alreadyExists) { // Create new .env from template let envTemplatePath = kitPath("templates", "env", "template.env")
let envTemplate = await readFile(envTemplatePath, "utf8")
let envTemplateCompiler = compile(envTemplate)
let compiledEnvTemplate = envTemplateCompiler({
...process.env,
KIT_MAIN_SHORTCUT: process.platform === "win32" ? "ctrl ;" : "cmd ;"
})
const templateLines = compiledEnvTemplate.split(/\r?\n/)
await safeWriteEnvFile(templateLines, envPath)
global.log?.(chalk.green(`✅ Created new .env file with template variables`))
} else {
// Merge template variables with existing .env file
global.log?.(chalk.blue(📝 .env file exists, checking for new template variables...))
// Create backup first
const backupResult = await backupEnvFile()
if (backupResult.success) {
try {
let envTemplatePath = kitPath("templates", "env", "template.env")
let envTemplate = await readFile(envTemplatePath, "utf8")
let envTemplateCompiler = compile(envTemplate)
let compiledEnvTemplate = envTemplateCompiler({
...process.env,
KIT_MAIN_SHORTCUT: process.platform === "win32" ? "ctrl ;" : "cmd ;"
})
// Write template to temporary file
const tempTemplatePath = kenvPath(".env.template.tmp")
await writeFile(tempTemplatePath, compiledEnvTemplate, "utf8")
// Merge existing .env with new template
const merged = await mergeEnvFiles(envPath, tempTemplatePath)
// Format merged content using the corrected function
const mergedContentString = formatEnvContent(merged)
const mergedLines = mergedContentString.split('\n');
// Write merged content safely
await safeWriteEnvFile(mergedLines, envPath)
// Clean up temporary template file
await unlink(tempTemplatePath).catch(() => { })
global.log?.(chalk.green(`✅ Merged .env file with ${merged.size} variables (preserving your existing settings)`))
// Clean up backup since merge was successful
if (backupResult.backupPath) {
await unlink(backupResult.backupPath).catch(() => { })
}
} catch (error) {
global.log?.(chalk.red(`❌ Failed to merge .env template: ${error}`))
// Restore from backup if merge failed
if (backupResult.backupPath) {
try {
await copyFile(backupResult.backupPath, envPath)
await unlink(backupResult.backupPath)
global.log?.(chalk.yellow(`🔄 Restored .env from backup after merge failure`))
} catch (restoreError) {
global.log?.(chalk.red(`❌ Failed to restore .env backup: ${restoreError}`))
}
}
}
} else {
global.log?.(chalk.yellow(`⚠️ Could not backup .env file: ${backupResult.error}`))
global.log?.(chalk.yellow(`⚠️ Skipping template merge to avoid potential data loss`))
}
}
export { }
await trash(kenvPath(".git"))export {}
import ava from "ava" import os from "node:os" import "../../test-sdk/config.js" import { pathToFileURL } from "node:url"/** @type {import("../core/utils")} */ let { isFile, KIT_FIRST_PATH } = await import( pathToFileURL(kitPath("core", "utils.js")).href )
let KIT = kitPath() let KENV = kenvTestPath
let kenvSetupMockPath = (...parts) => { return path.resolve(KENV, ...parts) }
/** @type {import("child_process").SpawnSyncOptions} */ const options = { cwd: KIT, encoding: "utf-8", env: { KIT, KENV, KIT_NODE_PATH: process.execPath, PATH: KIT_FIRST_PATH } }
ava.before(Run setup script, (t) => {
const setupResult = spawnSync(./script, [./setup/setup.js], options)
})
ava.serial("env was created", async (t) => { let envPath = kenvSetupMockPath(".env") t.log({ envPath }) let checkEnv = await isFile(envPath) let contents = await readFile(envPath, "utf-8")
t.true(checkEnv, `env was created`)
t.false(contents.includes("{{"), `Check if .env was compiled`)
})
ava.serial("kenv linked to kit", async (t) => { let pkg = await readJson(kenvSetupMockPath("package.json"))
t.assert(
pkg.devDependencies?.["@johnlindquist/kit"],
"file:../.kit",
`kenv linked to kit`
)
})
ava.serial("kenv degit", async (t) => { let files = await readdir(kenvSetupMockPath())
t.false(files.includes(".git"), ".git was removed from kenv")
})
ava.serial("chmod", async (t) => { if (process.platform === "win32") { t.pass("Skipping chmod test on Windows") return }
let { access } = await import("node:fs/promises")
let { constants } = await import("node:fs")
let bins = ["scripts", "kar", "bin k", "bin kit", "bin sk"]
for (let b of bins) {
let binPath = kitPath(...b.split(" "))
t.log(binPath)
let result = await access(binPath, constants.X_OK)
t.true(isUndefined(result), "bins can be executed")
}
})
ava.serial("example script exists", async (t) => { t.truthy(await pathExists(kenvPath("scripts", "browse-scriptkit.ts"))) })
await setup("create-env") await setup("link-kenv-to-kit") await setup("chmod-helpers") await setup("switch-windows-kit-to-bat") await setup("ensure-snippets") await setup("ensure-scriptlets") // await setup("setup-pnpm")export {}
import "./globals.d.ts" import type { AppApi } from "./kitapp.ts" import type { KitApi, Run } from "./kit.ts" import type { PackagesApi } from "./packages.ts" import type { PlatformApi } from "./platform.ts" import type { ProAPI } from "./pro.ts"export type GlobalApi = KitApi & PackagesApi & PlatformApi & AppApi & ProAPI
declare global { var kit: GlobalApi & Run interface Global extends GlobalApi {} }
export * from "./core.js" export * from "../core/utils.js"
export default kit
export default { environmentVariables: { KIT_TEST: "true", }, verbose: true, files: [ "src/**/*.test.js" "test/**/*.test.js", ], } import path from "node:path" import os from "node:os" import { pathToFileURL } from "node:url"process.env.KIT = process.env.KIT || path.resolve(os.homedir(), ".kit")
let importKit = async (...parts) => { let partsPath = path.resolve(process.env.KIT, ...parts) await import(pathToFileURL(partsPath).href) }
await importKit("api/global.js") await importKit("api/kit.js") await importKit("api/lib.js") await importKit("target/terminal.js") await importKit("platform/base.js")
let platform = os.platform()
try {
await importKit(platform/${platform}.js)
} catch (error) {
// console.log(No ./platform/${platform}.js)
}
export let kitMockPath = (...parts) => path.resolve(home(".kit-mock-path"), ...parts)
export let kenvTestPath = kitMockPath(".kenv-test") export let kenvSetupPath = kitMockPath(".kenv-setup")
process.env.KENV = kenvTestPath
/** @type {import("../src/core/utils.js")} /
let { KIT_APP, KIT_APP_PROMPT, KIT_FIRST_PATH } = await import(
pathToFileURL(path.resolve(${process.env.KIT}, "core", "utils.js")).href
)
/* @type {import("../src/core/enum.js")} */
let { Channel } = await import(
pathToFileURL(path.resolve(${process.env.KIT}, "core", "enum.js")).href
)
process.env.PATH = KIT_FIRST_PATH
let execOptions = { env: { PATH: KIT_FIRST_PATH } } global.kenvTestPath = kenvTestPath global.kenvSetupPath = kenvSetupPath global.kitMockPath = kitMockPath global.execOptions = execOptions
let testScript = async (name, content, type = "js") => {
await exec(kit new ${name} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: type
}
})
let scriptPath = kenvPath("scripts", `${name}.js`)
await appendFile(scriptPath, content)
let { stdout, stderr } = await exec(`${kenvPath("bin", name)} --trust`)
return { stdout, stderr, scriptPath }
}
global.testScript = testScript
export { Channel, KIT_APP, KIT_APP_PROMPT, testScript }
import type { CallToolResult } from '@modelcontextprotocol/sdk/types' import { Channel } from '../core/enum.js'// Content types based on MCP spec interface TextContent { type: 'text' text: string }
interface ImageContent { type: 'image' data: string // base64-encoded mimeType: string }
interface AudioContent { type: 'audio' data: string // base64-encoded mimeType: string }
interface ResourceContent { type: 'resource' resource: { uri: string mimeType?: string text?: string blob?: string // base64-encoded } }
// Union of all content types type ContentItem = TextContent | ImageContent | AudioContent | ResourceContent
// Additional result options interface ResultOptions { isError?: boolean structuredContent?: Record<string, unknown> _meta?: Record<string, unknown> }
// Result object combines content with options type ResultObject = ContentItem & ResultOptions
// Main function overloads export function sendResult(content: string): Promise export function sendResult(content: ResultObject): Promise export function sendResult(content: ContentItem[]): Promise
export async function sendResult( content: string | ResultObject | ContentItem[] ): Promise { let toolResult: CallToolResult
if (typeof content === 'string') { // Simple string - wrap as text content toolResult = { content: [{ type: 'text', text: content }] } } else if (Array.isArray(content)) { // Array of content items toolResult = { content: content as any } } else { // Single object - extract type and options const { type, isError, structuredContent, _meta, ...contentData } = content as ResultObject & { type: string }
// Build content item based on type
const contentItem: ContentItem = { type, ...contentData } as ContentItem
// Build result with options
toolResult = {
content: [contentItem] as any
}
if (isError !== undefined) toolResult.isError = isError
if (structuredContent !== undefined) toolResult.structuredContent = structuredContent
if (_meta !== undefined) toolResult._meta = _meta
}
// Send via MCP channel await global.sendWait(Channel.RESPONSE, { body: toolResult, statusCode: 200, headers: { 'Content-Type': 'application/json' } }) }
// Export types for users export type { TextContent, ImageContent, AudioContent, ResourceContent, ContentItem, ResultObject, ResultOptions }
import * as path from 'node:path' import { rm } from 'node:fs/promises' import { kitPath, kenvPath, prefsPath, promptDbPath, isDir, isFile, extensionRegex, resolveScriptToCommand, scriptsSort, scriptsDbPath, statsPath, userDbPath, getScriptFiles, getKenvs, processInBatches, parseSnippets } from './utils.js'import { parseScript } from './parser.js'
import { parseScriptlets } from './scriptlets.js'
import { writeJson, readJson } from '../globals/fs-extra.js'
import type { Choice, Script, PromptDb } from '../types/core' import { Low } from 'lowdb' import { JSONFile } from 'lowdb/node' import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods' import type { Keyv } from 'keyv' import type { DBData, DBKeyOrPath, DBReturnType } from '../types/kit.js' import { Env } from './enum.js'
export const resolveKenv = (...parts: string[]) => { if (global.kitScript) { return path.resolve(global.kitScript, '..', '..', ...parts) }
return kenvPath(...parts) }
export let store = async (nameOrPath: string, initialData: object | (() => Promise) = {}): Promise => {
let isPath = nameOrPath.includes('/') || nameOrPath.includes('\')
let { default: Keyv } = await import('keyv')
let { KeyvFile } = await import('keyv-file')
let dbPath = isPath ? nameOrPath : kenvPath('db', ${nameOrPath}.json)
let fileExists = await isFile(dbPath)
let keyv = new Keyv({ store: new KeyvFile({ filename: dbPath // Not all options are required... } as any) })
if (!fileExists) { let dataToInit: Record<string, any> = {}
if (typeof initialData === 'function') {
dataToInit = await (initialData as () => Promise<any>)()
} else {
dataToInit = initialData
}
for await (let [key, value] of Object.entries(dataToInit)) {
await keyv.set(key, value)
}
}
return keyv }
export async function db( dataOrKeyOrPath?: DBKeyOrPath, data?: DBData, fromCache = true ): Promise<DBReturnType> { let dbPath = ''
// If 'data' is undefined and 'dataOrKeyOrPath' is not a string, // treat 'dataOrKeyOrPath' as 'data' and generate a default key/path if (typeof data === 'undefined' && typeof dataOrKeyOrPath !== 'string') { data = dataOrKeyOrPath dataOrKeyOrPath = '_' + resolveScriptToCommand(global.kitScript) }
// Handle case when 'dataOrKeyOrPath' is a string (key or path) if (typeof dataOrKeyOrPath === 'string') { // Initialize or reset the cache map based on 'fromCache' global.__kitDbMap = fromCache ? global.__kitDbMap || new Map() : new Map()
// Return cached database if available
if (global.__kitDbMap.has(dataOrKeyOrPath)) {
return global.__kitDbMap.get(dataOrKeyOrPath)
}
dbPath = dataOrKeyOrPath
// Ensure the database file has a '.json' extension and resolve its full path
if (!dbPath.endsWith('.json')) {
dbPath = resolveKenv('db', `${dbPath}.json`)
}
}
// Check if the parent directory of 'dbPath' exists
const parentExists = await isDir(path.dirname(dbPath))
if (!parentExists) {
dbPath = kenvPath('db', ${path.basename(dbPath)})
}
// Always ensure the directory exists before creating the database await ensureDir(path.dirname(dbPath))
let _db: Low
// Initialize the database const init = async () => { const jsonFile = new JSONFile(dbPath) const result = await jsonFile.read() _db = new Low(jsonFile, result)
try {
// Read existing data
await _db.read()
} catch (error) {
// Log error and attempt to recover if possible
global.warn?.(error)
if (path.dirname(dbPath) === kitPath('db')) {
// Attempt to reinitialize the database
// await rm(dbPath); // This line is commented out in the original code
_db = new Low(jsonFile, result)
await _db.read()
}
}
// If no data or not using cache, initialize with provided data
if (!_db.data || !fromCache) {
const getData = async () => {
if (typeof data === 'function') {
const result = await (data as () => Promise<T>)()
return Array.isArray(result) ? { items: result } : result
}
return Array.isArray(data) ? { items: data } : data
}
_db.data = await getData()
try {
// Write initial data to the database
await _db.write()
} catch (error) {
global.log?.(error)
// On Windows, sometimes the rename fails due to timing issues
// Retry once after a short delay
if (process.platform === 'win32' && error?.code === 'ENOENT') {
await new Promise(resolve => setTimeout(resolve, 100))
try {
await _db.write()
} catch (retryError) {
global.log?.('Retry write also failed:', retryError)
}
}
}
}
}
await init()
// Define database API with additional methods const dbAPI = { dbPath, clear: async () => { await rm(dbPath) }, reset: async () => { await rm(dbPath) await init() } }
// Create a proxy to handle property access and modification
const dbProxy = new Proxy(dbAPI as any, {
get: (_target, key: string) => {
if (key === 'then') return _db
if (key in dbAPI) {
return typeof dbAPI[key] === 'function' ? dbAPI[key].bind(dbAPI) : dbAPI[key]
}
const dbInstance = _db as any
if (dbInstance[key]) {
return typeof dbInstance[key] === 'function' ? dbInstance[key].bind(dbInstance) : dbInstance[key]
}
return _db.data?.[key]
},
set: (_target: any, key: string, value: any) => {
try {
;(_db as any).data[key] = value
// Optionally send data to a parent process if connected
// if (process.send) {
// send(DB_SET_${key} as any, value);
// }
return true
} catch (error) {
return false
}
}
})
// Cache the database instance if a key/path is provided if (typeof dataOrKeyOrPath === 'string') { global.__kitDbMap.set(dataOrKeyOrPath, dbProxy) }
return dbProxy }
global.db = db global.store = store
export let parseScripts = async (ignoreKenvPattern = /^ignore$/) => { let scriptFiles = await getScriptFiles() let kenvDirs = await getKenvs(ignoreKenvPattern)
for await (let kenvDir of kenvDirs) { let scripts = await getScriptFiles(kenvDir) scriptFiles = [...scriptFiles, ...scripts] }
let scriptInfoPromises = [] for (const file of scriptFiles) { let asyncScriptInfoFunction = parseScript(file)
scriptInfoPromises.push(asyncScriptInfoFunction)
}
let scriptInfo = await processInBatches(scriptInfoPromises, 5)
let timestamps = [] try { let timestampsDb = await getTimestamps() timestamps = timestampsDb.stamps } catch {}
scriptInfo.sort(scriptsSort(timestamps))
return scriptInfo }
export let getScriptsDb = async (fromCache = true, ignoreKenvPattern = /^ignore$/) => { let dbResult = await db<{ scripts: Script[] }>( scriptsDbPath, async () => { const [scripts, scriptlets, snippets] = await Promise.all([ parseScripts(ignoreKenvPattern), parseScriptlets(), parseSnippets() ]) return { scripts: scripts.concat(scriptlets, snippets) as Script[] } }, fromCache )
return dbResult }
export let setScriptTimestamp = async (stamp: Stamp): Promise<Script[]> => { let timestampsDb = await getTimestamps() let index = timestampsDb.stamps.findIndex((s) => s.filePath === stamp.filePath)
let oldStamp = timestampsDb.stamps[index]
stamp.timestamp = Date.now() if (stamp.runCount) { stamp.runCount = oldStamp?.runCount ? oldStamp.runCount + 1 : 1 } if (oldStamp) { timestampsDb.stamps[index] = { ...oldStamp, ...stamp } } else { timestampsDb.stamps.push(stamp) }
try { await timestampsDb.write() } catch (error) { if (global.log) global.log(error) }
let scriptsDb = await getScriptsDb(false) let script = scriptsDb.scripts.find((s) => s.filePath === stamp.filePath)
if (script) { scriptsDb.scripts = scriptsDb.scripts.sort(scriptsSort(timestampsDb.stamps)) try { await scriptsDb.write() } catch (error) {} }
return scriptsDb.scripts }
// export let removeScriptFromDb = async ( // filePath: string // ): Promise<Script[]> => { // let scriptsDb = await getScriptsDb() // let script = scriptsDb.scripts.find( // s => s.filePath === filePath // )
// if (script) { // scriptsDb.scripts = scriptsDb.scripts.filter( // s => s.filePath !== filePath // ) // await scriptsDb.write() // }
// return scriptsDb.scripts // }
global.__kitScriptsFromCache = true export let refreshScripts = async () => { await getScripts(false) }
export let getPrefs = async () => { return await db(kitPath('db', 'prefs.json')) }
export type Stamp = { filePath: string timestamp?: number compileStamp?: number compileMessage?: string executionTime?: number changeStamp?: number exitStamp?: number runStamp?: number runCount?: number }
export let getTimestamps = async (fromCache = true) => { return await db<{ stamps: Stamp[] }>( statsPath, { stamps: [] }, fromCache ) }
export let getScriptFromString = async (script: string): Promise<Script> => { let scripts = await getScripts(false)
// Check if the string contains any path separators (both Unix and Windows style) const containsPathSeparator = script.includes('/') || script.includes('\')
if (!containsPathSeparator) { let result = scripts.find((s) => s.name === script || s.command === script.replace(extensionRegex, ''))
if (!result) {
// Provide detailed error information for debugging
const availableNames = scripts.map(s => s.name).slice(0, 10).join(', ')
const availableCommands = scripts.map(s => s.command).slice(0, 10).join(', ')
const totalScripts = scripts.length
throw new Error(
`Cannot find script based on name or command: ${script}\n` +
`Total scripts available: ${totalScripts}\n` +
`Sample script names: ${availableNames}${totalScripts > 10 ? '...' : ''}\n` +
`Sample commands: ${availableCommands}${totalScripts > 10 ? '...' : ''}`
)
}
return result
}
// Helper function to normalize paths for cross-platform comparison const normalizePath = (p: string): string => { // Replace all backslashes with forward slashes for consistent comparison return p.replace(/\/g, '/') }
// For case-insensitive comparison on Windows const isWindows = process.platform === 'win32' const compareStrings = (a: string, b: string): boolean => { if (isWindows) { return a.toLowerCase() === b.toLowerCase() } return a === b }
// Normalize input path const normalizedInput = normalizePath(script)
// Try to find the script let result = scripts.find((s) => { const normalizedScriptPath = normalizePath(s.filePath)
// Direct comparison (handles most cases including scriptlets with anchors)
if (compareStrings(normalizedScriptPath, normalizedInput)) {
return true
}
// For scriptlets with anchors, also try more flexible matching
if (s.filePath.includes('#') && script.includes('#')) {
const [inputBase, inputAnchor] = normalizedInput.split('#')
const [scriptBase, scriptAnchor] = normalizedScriptPath.split('#')
// Compare base paths and anchors separately
if (compareStrings(inputBase, scriptBase) && inputAnchor === scriptAnchor) {
return true
}
}
return false
})
if (!result) { // Provide detailed error information for path-based searches const availablePaths = scripts .map(s => s.filePath) .filter(p => p.toLowerCase().includes(path.basename(script).toLowerCase())) .slice(0, 5)
const pathInfo = {
input: script,
normalized: normalizedInput,
basename: path.basename(script),
dirname: path.dirname(script),
separator: path.sep,
platform: process.platform
}
throw new Error(
`Cannot find script based on path: ${script}\n` +
`Path details: ${JSON.stringify(pathInfo, null, 2)}\n` +
`Similar paths found:\n${availablePaths.map(p => ` - ${p}`).join('\n')}` +
(availablePaths.length === 0 ? ' (none found)' : '')
)
}
return result }
export let getScripts = async (fromCache = true, ignoreKenvPattern = /^ignore$/) => { global.__kitScriptsFromCache = fromCache return (await getScriptsDb(fromCache, ignoreKenvPattern)).scripts }
export type ScriptValue = (pluck: keyof Script, fromCache?: boolean) => () => Promise<Choice[]>
export let scriptValue: ScriptValue = (pluck, fromCache) => async () => { let menuItems: Script[] = await getScripts(fromCache)
return menuItems.map((script: Script) => ({ ...script, value: script[pluck] })) }
export type AppDb = { version: string openAtLogin: boolean previewScripts: boolean autoUpdate: boolean tray: boolean authorized: boolean searchDebounce?: boolean termFont?: string convertKeymap?: boolean cachePrompt?: boolean mini?: boolean disableGpu?: boolean disableBlurEffect?: boolean }
export type UserDb = Partial<RestEndpointMethodTypes['users']['getAuthenticated']['response']['data']>
export let setUserJson = async (user: UserDb) => { await global.cli('set-env-var', 'KIT_LOGIN', user?.login || Env.REMOVE) await writeJson(userDbPath, user) }
export let getUserJson = async (): Promise => { let user: any = {} let userDbExists = await isFile(userDbPath) if (userDbExists) { try { user = await readJson(userDbPath) } catch (error) { await setUserJson({}) user = {} } }
return user }
type PrefsDb = { showJoin: boolean } export let getPrefsDb = async () => { return await db(prefsPath, { showJoin: true }) }
export let getPromptDb = async () => { return await db<PromptDb & { clear?: boolean }>(promptDbPath, { screens: {}, clear: false }) }
let copyIfNotExists = async (p: string, dest: string) => { let exists = await isFile(dest) console.log({ p, dest, exists: exists ? "true" : "false", }) if (!exists) await copyFile(p, dest) }let writeIfNotExists = async (p: string, dest: string) => { if (!(await isFile(p))) await writeFile(p, dest) }
let npmRc = registry=https://registry.npmjs.org install-links=false.trim()
await writeIfNotExists(kenvPath(".npmrc"), npmRc)
// add install-links=false to kenv's .npmrc if it doesn't exist
let npmrcContent = await readFile(
kenvPath(".npmrc"),
"utf-8"
)
if (!npmrcContent.match(/^install-links=false$/gm)) {
if (npmrcContent.split("\n").at(-1) !== "") {
await appendFile(kenvPath(".npmrc"), "\n")
}
await appendFile(
kenvPath(".npmrc"),
install-links=false
)
}
await cli("install", "${kitPath()}")
// ensure kenvPath('package.json') has a "type": "module"
let defaultPackageJson = {
type: "module",
engines: {
node: "22.17.1",
},
devDependencies: {
"@johnlindquist/kit": link:${(process.env.KIT || home(".kit"))?.replace(/\\/g, '/')},
"@typescript/lib-dom":
"npm:@johnlindquist/no-dom@^1.0.2",
},
}
let packageJson = await ensureReadFile( kenvPath("package.json"), JSON.stringify(defaultPackageJson, null, 2) ) let packageJsonObj = JSON.parse(packageJson) if (!packageJsonObj.type) { packageJsonObj.type = "module" packageJsonObj.engines = defaultPackageJson.engines await writeFile( kenvPath("package.json"), JSON.stringify(packageJsonObj, null, 2) ) }
export { }
/** @type {import("/Users/johnlindquist/.kit")} */import { pathToFileURL } from "node:url" import { rimraf } from "rimraf"
async function importRelativePath(relativePath) { const path = await import("node:path") const { fileURLToPath, pathToFileURL } = await import("node:url") const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const absolutePath = path.join(__dirname, relativePath) const fileURL = pathToFileURL(absolutePath).href return import(fileURL) }
await importRelativePath("../test-sdk/config.js") console.log({ kenvTestPath })
let escapePathPeriods = (p) => p.replace(/./g, "\.")
let userKenv = (...parts) => {
return pathToFileURL(home(".kenv", ...parts.filter(Boolean))).href
}
let userBinPath = userKenv("bin")
if (await isDir(userBinPath)) {
let staleMocks = userKenv("bin", "mock*")
console.log(Removing stale mocks: ${staleMocks})
await rimraf(escapePathPeriods(staleMocks))
}
if (await isDir("-d", kitMockPath())) { await rimraf(escapePathPeriods(kitMockPath())) }
if (await isDir(kenvTestPath)) {
console.log(Clearing ${kenvTestPath})
await rimraf(escapePathPeriods(kenvTestPath))
}
let { stdout: branch, stderr } = await exec("git branch --show-current")
if (stderr || !branch.match(/main|beta|alpha|next/)) exit(1)
branch = branch.trim()
let repo = johnlindquist/kenv#${branch}
console.log(Cloning ${repo} to ${kenvTestPath})
await degit(repo, { force: true }).clone(kenvTestPath)
console.log(Cloning ${repo} to ${kenvSetupPath})
await degit(repo, { force: true }).clone(kenvSetupPath)
console.log({ kitPath: kitPath() })
process.env.KENV = kenvTestPath
console.log({ kitPath: kitPath() })
await rimraf(escapePathPeriods(kitPath("db", "scripts.json")))
const { stdout: setupStdout, stderr: setupStderr } = await exec(
kit "${kitPath("setup", "setup.js")}" --no-edit
)
console.log({ setupStdout })
if (setupStderr) {
console.log({ setupStderr })
exit(1)
}
// console.log(
// await readFile(kenvPath("package.json"), "utf-8")
// )
const { stdout: refreshScriptsDbStdout, stderr: refreshScriptsDbStderr } =
await exec(kit "${kitPath("cli", "refresh-scripts-db.js")}")
console.log({ refreshScriptsDbStdout })
if (refreshScriptsDbStderr) {
console.log({ refreshScriptsDbStderr })
exit(1)
}
export {}
import ava from 'ava' import { parseScript, parseMarkdownAsScriptlets, shortcutNormalizer, getKenvFromPath, home, kenvPath, processPlatformSpecificTheme, parseSnippets, parseScriptletsFromPath, scriptsSort, templatePlaceholdersRegex } from './utils' import { outputTmpFile } from '../api/kit' import { ensureDir } from '../globals/fs-extra' import { cmd } from './constants' import slugify from 'slugify' import type { Stamp } from './db' import type { CronExpression, Script, Snippet } from '../types' import path from 'path'// Helper function to create a temporary snippet file process.env.KENV = home('.mock-kenv') async function createTempSnippet(fileName: string, content: string) { const snippetDir = kenvPath('snippets') await ensureDir(snippetDir) return await outputTmpFile(path.join(snippetDir, fileName), content) }
/**
- [IMPORTANT]
- These test create files in the tmp directory.
- They each need unique names or tests will fail */
ava('parseScript name comment metadata', async (t) => { let name = 'Testing Parse Script Comment' let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
// Name: ${name} `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.filePath, scriptPath) })
ava('parseScript comment full metadata', async (t) => {
let name = 'Testing Parse Script Comment Full Metadata'
let description = 'This is a test description'
let schedule = '0 0 * * *'
let shortcut = ${cmd}+9
let normalizedShortcut = shortcutNormalizer(shortcut)
let timeout = 15000
let fileName = slugify(name, { lower: true })
let scriptContent = `
import "@johnlindquist/kit"
// Name:
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.description, description) t.is(script.schedule, schedule as CronExpression) t.is(script.filePath, scriptPath) t.is(script.shortcut, normalizedShortcut) t.is(script.timeout, timeout) })
ava('parseScript multiline description in global metadata', async (t) => {
let name = 'Testing Multiline Description Global'
let description = This is a multiline description that spans multiple lines and should be properly parsed
let fileName = slugify(name, { lower: true })
let scriptContent = `
import "@johnlindquist/kit"
metadata = { name: "${name}", description: `${description}` } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.description, description) })
ava('parseScript multiline description in export metadata', async (t) => {
let name = 'Testing Multiline Description Export'
let description = This is a multiline description that spans multiple lines and should be properly parsed
let fileName = slugify(name, { lower: true })
let scriptContent = `
import "@johnlindquist/kit"
export const metadata = { name: "${name}", description: `${description}` } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.description, description) })
ava('parseScript export convention metadata name', async (t) => { let name = 'Testing Parse Script Convention' let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
export const metadata = { name: "${name}" } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.filePath, scriptPath) })
ava('parseScript timeout metadata from comments', async (t) => { let name = 'Testing Timeout Metadata' let timeout = 5000 let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
// Name:
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.timeout, timeout) t.is(script.filePath, scriptPath) })
ava('parseScript timeout metadata from export', async (t) => { let name = 'Testing Timeout Export Metadata' let timeout = 10000 let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
export const metadata = { name: "${name}", timeout: ${timeout} } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.timeout, timeout) t.is(script.filePath, scriptPath) })
ava('parseScript timeout metadata from global', async (t) => { let name = 'Testing Timeout Global Metadata' let timeout = 30000 let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
metadata = { name: "${name}", timeout: ${timeout} } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.timeout, timeout) t.is(script.filePath, scriptPath) })
ava('parseScript global convention metadata name', async (t) => { let name = 'Testing Parse Script Convention Global' let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
metadata = { name: "${name}" } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) t.is(script.name, name) t.is(script.filePath, scriptPath) })
ava('parseScript ignore metadata variable name', async (t) => { let name = 'Testing Parse Script Convention Ignore Metadata Variable Name' let fileName = slugify(name, { lower: true }) let scriptContent = ` import "@johnlindquist/kit"
const metadata = { name: "${name}" } `.trim()
let scriptPath = await outputTmpFile(${fileName}.ts, scriptContent)
let script = await parseScript(scriptPath) // Don't pick up on the metadata variable name, so it's the slugified version t.is(script.name, fileName) t.is(script.filePath, scriptPath) })
ava('parseMarkdownAsScripts', async (t) => { let markdown = `
Open Script Kit
```bash open -a 'Google Chrome' https://scriptkit.com/{{user}} ```
This Script Opens the Script Kit URL
I hope you enjoy!
Append Note
```kit await appendFile(home("{{File Name}}.txt"), {{Note}}) ``` `
const scripts = await parseMarkdownAsScriptlets(markdown) t.log(scripts) // t.is(scripts.length, 2) t.is(scripts[0].name, 'Open Script Kit') t.is(scripts[0].trigger, 'sk') t.is(scripts[0].tool, 'bash') t.is(scripts[0].scriptlet, "open -a 'Google Chrome' https://scriptkit.com/{{user}}") t.is(scripts[0].group, 'Scriptlets') t.deepEqual(scripts[0].inputs, ['user'])
t.is(scripts[1].name, 'Append Note') t.is(scripts[1].tool, 'kit') t.is(scripts[1].scriptlet, 'await appendFile(home("{{File Name}}.txt"), {{Note}})') t.is(scripts[1].group, 'Scriptlets') t.deepEqual(scripts[1].inputs, ['File Name', 'Note']) })
ava('parseMarkdownAsScripts handles quotes in name and formats command', async (t) => { let markdown = `
What's This?
```bash echo "This is a test script" ``` `
const scripts = await parseMarkdownAsScriptlets(markdown) t.is(scripts.length, 1) t.is(scripts[0].name, "What's This?") t.is(scripts[0].trigger, 'test-quotes') t.is(scripts[0].tool, 'bash') t.is(scripts[0].scriptlet, 'echo "This is a test script"') t.is(scripts[0].command, 'whats-this') })
ava('parseMarkdownAsScripts allow JavaScript objects', async (t) => { let markdown = `
Open Script Kit
```bash open -a 'Google Chrome' https://scriptkit.com/{{user}} ```
This Script Opens the Script Kit URL
I hope you enjoy!
Append Note
```kit await appendFile(home("{{File Name}}.txt"), {{Note}}) ``` `
const scripts = await parseMarkdownAsScriptlets(markdown) // t.log(scripts) // t.is(scripts.length, 2) t.is(scripts[0].name, 'Open Script Kit') t.is(scripts[0].trigger, 'sk') t.is(scripts[0].tool, 'bash') t.is(scripts[0].scriptlet, "open -a 'Google Chrome' https://scriptkit.com/{{user}}") t.is(scripts[0].group, 'Scriptlets') t.deepEqual(scripts[0].inputs, ['user'])
t.is(scripts[1].name, 'Append Note') t.is(scripts[1].tool, 'kit') t.is(scripts[1].scriptlet, 'await appendFile(home("{{File Name}}.txt"), {{Note}})') t.is(scripts[1].group, 'Scriptlets') t.deepEqual(scripts[1].inputs, ['File Name', 'Note']) })
ava('parseMarkdownAsScripts allow JavaScript imports, exports, ${', async (t) => { let markdown = `
Open Script Kit
```bash open -a 'Google Chrome' https://scriptkit.com/{{user}} ```
This Script Opens the Script Kit URL
I hope you enjoy!
Append Note
```kit import { appendFile } from "fs" let note = "This is a note" await exec(`echo "${note}" >> foo.txt`) await appendFile(home("{{File Name}}.txt"), {{Note}}) export { note } ``` `
const scripts = await parseMarkdownAsScriptlets(markdown) // t.log(scripts) // t.is(scripts.length, 2) t.is(scripts[0].name, 'Open Script Kit') t.is(scripts[0].trigger, 'sk') t.is(scripts[0].tool, 'bash') t.is(scripts[0].tag, 'trigger: sk') t.is(scripts[0].scriptlet, "open -a 'Google Chrome' https://scriptkit.com/{{user}}") t.is(scripts[0].group, 'Scriptlets') t.deepEqual(scripts[0].inputs, ['user'])
t.is(scripts[1].name, 'Append Note')
t.is(scripts[1].tool, 'kit')
t.is(scripts[1].cwd, '~/Downloads')
t.is(scripts[1].prepend, 'PATH=/usr/local/bin')
t.is(scripts[1].append, '| grep "foo"')
if (process.platform === 'darwin') {
t.is(scripts[1].tag, 'cmd+o')
} else {
t.is(scripts[1].tag, 'ctrl+o')
}
t.is(
scripts[1].scriptlet,
import { appendFile } from "fs" let note = "This is a note" await exec(\echo "${note}" >> foo.txt`)
await appendFile(home("{{File Name}}.txt"), {{Note}})
export { note }
`.trim()
)
t.is(scripts[1].group, 'Scriptlets')
t.deepEqual(scripts[1].inputs, ['File Name', 'Note'])
})
ava("parseMarkdownAsScripts allow doesn't create multiple inputs for the same template variable", async (t) => { let markdown = `
Open Script Kit
```bash open -a 'Google Chrome' https://scriptkit.com/{{user}} && echo "{{user}}" ```
This Script Opens the Script Kit URL
I hope you enjoy!
Append Note
```kit import { appendFile } from "fs" let note = "This is a note" await exec(`echo "${note}" >> foo.txt`) await appendFile(home("{{File Name}}.txt"), {{Note}}) console.log("Creating {{Note}}") export { note } ``` `
const scripts = await parseMarkdownAsScriptlets(markdown)
t.deepEqual(scripts[0].inputs, ['user']) t.deepEqual(scripts[1].inputs, ['File Name', 'Note']) })
ava('parseScriptlets tool default to bash or cmd', async (t) => { let scriptlet = await parseMarkdownAsScriptlets(`
Append Note
``` echo "hello world" ```
`)
t.is(scriptlet[0].tool, process.platform === 'win32' ? 'cmd' : 'bash') })
ava("parseMarkdownAsScriptlets doesn't error on empty string", async (t) => { let scriptlets = await parseMarkdownAsScriptlets('') t.is(scriptlets.length, 0) })
ava('parseScriptletsFromPath - valid markdown file', async (t) => { const markdown = `
Test
Test Scriptlet
```js console.log("Hello, world!") ``` ` const filePath = await outputTmpFile('test-scriptlet.md', markdown) const scripts = await parseScriptletsFromPath(filePath)
// t.log(scripts[0])
t.is(scripts.length, 1)
t.is(scripts[0].name, 'Test Scriptlet')
if (process.platform === 'darwin') {
t.is(scripts[0].friendlyShortcut, 'cmd+t')
} else {
t.is(scripts[0].friendlyShortcut, 'ctrl+t')
}
t.is(scripts[0].scriptlet.trim(), 'console.log("Hello, world!")')
t.is(scripts[0].group, 'Test')
t.is(scripts[0].filePath, ${filePath}#Test-Scriptlet)
t.is(scripts[0].kenv, '')
})
ava('parseScriptletsFromPath - empty file', async (t) => { const filePath = await outputTmpFile('empty-scriptlet.md', '') const scripts = await parseScriptletsFromPath(filePath)
t.is(scripts.length, 0) })
ava('parseScriptletsFromPath me- file with multiple scriptlets', async (t) => { const markdown = `
Scriptlet 1
```js console.log("Scriptlet 1") ```
Scriptlet 2
```js console.log("Scriptlet 2") ``` ` const filePath = await outputTmpFile('multiple-scriptlets.md', markdown) const scripts = await parseScriptletsFromPath(filePath)
t.is(scripts.length, 2) t.is(scripts[0].name, 'Scriptlet 1') t.is(scripts[1].name, 'Scriptlet 2') })
ava('parseScriptletsFromPath - h1 as group', async (t) => { const markdown = `
Group A
Scriptlet 1
```js console.log("Scriptlet 1") ```
Scriptlet 2
```js console.log("Scriptlet 2") ``` ``` ` const filePath = await outputTmpFile('grouped-scriptlets.md', markdown) const scripts = await parseScriptletsFromPath(filePath)
t.is(scripts.length, 2) t.is(scripts[0].name, 'Scriptlet 1') t.is(scripts[0].group, 'Group A') t.is(scripts[1].name, 'Scriptlet 2') t.is(scripts[1].group, 'Group A') })
ava('getKenvFromPath - main kenv', async (t) => { let scriptletsPath = kenvPath('script', 'kit.md') let kenv = getKenvFromPath(scriptletsPath) t.is(kenv, '') })
ava('getKenvFromPath - sub kenv', async (t) => { let scriptletsPath = kenvPath('kenvs', 'test', 'script', 'kit.md') let kenv = getKenvFromPath(scriptletsPath) t.is(kenv, 'test') })
ava('getKenvFromPath - no kenv, empty string', async (t) => { let scriptletsPath = home('kit.md') t.is(getKenvFromPath(scriptletsPath), '') })
ava('processPlatformSpecificTheme - Mac specific', (t) => { const originalPlatform = process.platform Object.defineProperty(process, 'platform', { value: 'darwin' })
const input = --color-primary-mac: #ff0000; --color-secondary-win: #00ff00; --color-tertiary-other: #0000ff; --color-neutral: #cccccc;
const expected = --color-primary: #ff0000; --color-neutral: #cccccc;
const result = processPlatformSpecificTheme(input) t.is(result.trim(), expected.trim())
Object.defineProperty(process, 'platform', { value: originalPlatform }) })
ava('processPlatformSpecificTheme - Windows specific', (t) => { const originalPlatform = process.platform Object.defineProperty(process, 'platform', { value: 'win32' })
const input = --color-primary-mac: #ff0000; --color-secondary-win: #00ff00; --color-tertiary-other: #0000ff; --color-neutral: #cccccc;
const expected = --color-secondary: #00ff00; --color-neutral: #cccccc;
const result = processPlatformSpecificTheme(input) t.is(result.trim(), expected.trim())
Object.defineProperty(process, 'platform', { value: originalPlatform }) })
ava('processPlatformSpecificTheme - Other platform', (t) => { const originalPlatform = process.platform Object.defineProperty(process, 'platform', { value: 'linux' })
const input = --color-primary-mac: #ff0000; --color-secondary-win: #00ff00; --color-tertiary-other: #0000ff; --color-neutral: #cccccc;
const expected = --color-tertiary: #0000ff; --color-neutral: #cccccc;
const result = processPlatformSpecificTheme(input) t.is(result.trim(), expected.trim())
Object.defineProperty(process, 'platform', { value: originalPlatform }) })
ava('processPlatformSpecificTheme - No platform-specific variables', (t) => {
const input = --color-primary: #ff0000; --color-secondary: #00ff00; --color-tertiary: #0000ff; --color-neutral: #cccccc;
const result = processPlatformSpecificTheme(input) t.is(result.trim(), input.trim()) })
ava('processPlatformSpecificTheme - Empty input', (t) => { const input = '' const result = processPlatformSpecificTheme(input) t.is(result, '') })
// TODO: Figure out process.env.KENV = on windows
if (process.platform !== 'win32') {
ava('parseSnippets - basic snippet', async (t) => {
const content = // Name: Test Snippet // Snippet: test console.log("Hello, world!");.trim()
await createTempSnippet('test-snippet.txt', content)
const snippets = await parseSnippets()
const testSnippet = snippets.find((s) => s.name === 'Test Snippet')
t.truthy(testSnippet)
if (testSnippet) {
t.is(testSnippet.name, 'Test Snippet')
t.is(testSnippet.tag, 'test')
t.is(testSnippet.text.trim(), 'console.log("Hello, world!");')
t.is(testSnippet.group, 'Snippets')
t.is(testSnippet.kenv, '')
t.is(testSnippet.expand, 'test')
}
})
ava('parseSnippets - snippet without metadata', async (t) => {
const content = console.log("No metadata");
const fileName = 'no-metadata-snippet.txt'
const filePath = await createTempSnippet(fileName, content)
const snippets = await parseSnippets()
const testSnippet = snippets.find((s) => s.filePath === filePath)
t.truthy(testSnippet)
if (testSnippet) {
t.is(testSnippet.name, path.basename(filePath))
t.is(testSnippet.tag, '')
t.is(testSnippet.expand, '')
t.is(testSnippet.text.trim(), content)
}
})
ava('parseSnippets - snippet with HTML content', async (t) => { const content = ` // Name: HTML Snippet
Hello, world!
await createTempSnippet('html-snippet.txt', content)
const snippets = await parseSnippets()
const testSnippet = snippets.find((s) => s.name === 'HTML Snippet')
t.truthy(testSnippet)
if (testSnippet) {
t.is(testSnippet.name, 'HTML Snippet')
t.is(testSnippet.text.trim(), '<div>\n <h1>Hello, world!</h1>\n</div>')
const expectedPreview = `<div class="p-4">\n <style>\n p{\n margin-bottom: 1rem;\n }\n li{\n margin-bottom: .25rem;\n }\n \n </style>\n <div>\n <h1>Hello, world!</h1>\n</div>\n</div>`.trim()
if (testSnippet.preview && typeof testSnippet.preview === 'string') {
t.is(testSnippet.preview.replace(/\r\n/g, '\n').trim(), expectedPreview)
}
}
})
ava('parseSnippets - multiple snippets', async (t) => {
const snippet1 = // Name: Snippet 1 // Snippet: s1 console.log("Snippet 1");.trim()
const snippet2 = `
// Name: Snippet 2 // Snippet: s2 console.log("Snippet 2"); `.trim()
await createTempSnippet('snippet1.txt', snippet1)
await createTempSnippet('snippet2.txt', snippet2)
const snippets = await parseSnippets()
const testSnippet1 = snippets.find((s) => s.name === 'Snippet 1')
const testSnippet2 = snippets.find((s) => s.name === 'Snippet 2')
t.truthy(testSnippet1)
t.truthy(testSnippet2)
// Sorted by name by default
const definedSnippets = [testSnippet1, testSnippet2].filter(Boolean) as Snippet[]
const testSnippets = definedSnippets.sort((a, b) => a.name.localeCompare(b.name))
t.is(testSnippets.length, 2)
if (testSnippets[0] && testSnippets[1]) {
t.is(testSnippets[0].tag, 's1')
t.is(testSnippets[1].tag, 's2')
t.is(testSnippets[0].value, 'console.log("Snippet 1");')
t.is(testSnippets[1].value, 'console.log("Snippet 2");')
}
}) }
// Clean up temporary files after all tests // ava.after.always(async () => { // const snippetDir = path.join(kenvPath(), "snippets") // await rmdir(snippetDir, { recursive: true }) // })
if (process.platform !== 'win32') { ava('parseScriptlets no tool preview uses bash codeblock', async (t) => { let scriptlet = await parseMarkdownAsScriptlets(`
Append Note
``` echo "hello world" ```
`)
t.log(scriptlet[0].preview)
t.true((scriptlet[0].preview as string)?.includes('language-bash'))
})
ava('parseScriptlets with tool preview uses tool codeblock', async (t) => { let scriptlet = await parseMarkdownAsScriptlets(` ## Append Note
<!--
Shortcut: cmd o
cwd: ~/Downloads
-->
\`\`\`python
echo "hello world"
\`\`\`
`)
t.log(scriptlet[0].preview)
t.true((scriptlet[0].preview as string)?.includes('language-python'))
}) }
ava('parseScriptlets with a shell tool and without inputs uses shebang', async (t) => { let scriptlet = await parseMarkdownAsScriptlets(`
Append Note
``` echo "hello world" ```
`)
// t.log(scriptlet[0])
t.truthy(scriptlet[0].shebang) })
ava("parseScriptlets with a shell tool with inputs doesn't use shebang", async (t) => { let scriptlet = await parseMarkdownAsScriptlets(`
Append Note
``` echo "hello {{who}}" ```
`)
// t.log(scriptlet[0])
t.falsy(scriptlet[0].shebang) })
ava('scriptsSort - sorts by index when timestamps are equal', (t) => { const timestamps: Stamp[] = [] const scripts = [ { index: 2, name: 'B', filePath: 'b.ts' }, { index: 1, name: 'A', filePath: 'a.ts' }, { index: 3, name: 'C', filePath: 'c.ts' } ] as Script[]
const sortedScripts = [...scripts].sort(scriptsSort(timestamps))
t.is(sortedScripts[0].name, 'A') t.is(sortedScripts[1].name, 'B') t.is(sortedScripts[2].name, 'C') })
ava('scriptsSort - treats missing index as 9999', (t) => { const timestamps: Stamp[] = [] const scripts = [ { name: 'No Index', filePath: 'no-index.ts' }, { index: 1, name: 'Has Index', filePath: 'has-index.ts' } ] as Script[]
const sortedScripts = [...scripts].sort(scriptsSort(timestamps))
t.is(sortedScripts[0].name, 'Has Index') t.is(sortedScripts[1].name, 'No Index') })
ava('scriptsSort - timestamps take precedence over index', (t) => { const now = Date.now() const timestamps: Stamp[] = [ { filePath: 'b.ts', timestamp: now }, { filePath: 'a.ts', timestamp: now - 1000 } ]
const scripts = [ { index: 2, name: 'B', filePath: 'b.ts' }, { index: 1, name: 'A', filePath: 'a.ts' } ] as Script[]
const sortedScripts = [...scripts].sort(scriptsSort(timestamps))
t.is(sortedScripts[0].name, 'B', 'More recent timestamp should come first') t.is(sortedScripts[1].name, 'A') })
ava('scriptsSort - falls back to name when no timestamps or index', (t) => { const timestamps: Stamp[] = [] const scripts = [ { name: 'Charlie', filePath: 'c.ts' }, { name: 'Alpha', filePath: 'a.ts' }, { name: 'Bravo', filePath: 'b.ts' } ] as Script[]
const sortedScripts = [...scripts].sort(scriptsSort(timestamps))
t.is(sortedScripts[0].name, 'Alpha') t.is(sortedScripts[1].name, 'Bravo') t.is(sortedScripts[2].name, 'Charlie') })
ava('templatePlaceholdersRegex - detects VS Code snippet variables', (t) => { // Valid patterns t.true(templatePlaceholdersRegex.test('${1:default}')) t.true(templatePlaceholdersRegex.test('${foo|bar}')) t.true(templatePlaceholdersRegex.test('${name}')) t.true(templatePlaceholdersRegex.test('$1')) t.true(templatePlaceholdersRegex.test('${1}')) t.true(templatePlaceholdersRegex.test('${foo|bar|baz}')) // Multiple choices t.true(templatePlaceholdersRegex.test('${1:foo bar}')) // Spaces in default t.true(templatePlaceholdersRegex.test('${foo-bar}')) // Dashes in names t.true(templatePlaceholdersRegex.test('${1:foo:bar}')) // Colons in default value
// Invalid patterns t.false(templatePlaceholdersRegex.test('$')) t.false(templatePlaceholdersRegex.test('${')) t.false(templatePlaceholdersRegex.test('${}')) t.false(templatePlaceholdersRegex.test('${|}')) t.false(templatePlaceholdersRegex.test('$foo')) t.false(templatePlaceholdersRegex.test('${nested{}}')) t.false(templatePlaceholdersRegex.test('${foo|}')) // Empty last choice t.false(templatePlaceholdersRegex.test('${|foo}')) // Empty first choice t.false(templatePlaceholdersRegex.test('${foo||bar}')) // Double pipe t.false(templatePlaceholdersRegex.test('${foo|bar|}')) // Trailing pipe })
import ava from 'ava'; import slugify from 'slugify'; import { Channel, KIT_APP_PROMPT } from './config.js'; import { pathToFileURL } from 'url';process.env.NODE_NO_WARNINGS = 1;
process.env.KIT = process.env.KIT || path.resolve(os.homedir(), '.kit');
ava.serial('app-prompt.js', async (t) => {
let script = 'mock-script-with-arg';
let scriptPath = kenvPath('scripts', ${script}.js);
let placeholder = 'hello';
let contents = await arg("${placeholder}");
await exec(kit new ${script} main --no-edit);
await writeFile(scriptPath, contents);
t.log('Starting app-prompt.js...'); let mockApp = fork(KIT_APP_PROMPT, { env: { NODE_NO_WARNINGS: '1', KIT: home('.kit'), KENV: kenvPath(), KIT_CONTEXT: 'app' } });
let command = 'mock-script-with-arg'; let value = { script: command, args: ['hello'] };
t.log('Waiting for app-prompt.js to start...'); let result = await new Promise((resolve, reject) => { /** channel: Channel pid: number newPid?: number state: AppState widgetId?: number * */ mockApp.on('message', (data) => { console.log('received', data); if (data.channel === Channel.SET_SCRIPT) { // The mockApp will hang waiting for input if you don't submit a value mockApp.send({ channel: Channel.VALUE_SUBMITTED, value: 'done' }); resolve(data); } });
mockApp.on('spawn', () => {
mockApp.send(
{
channel: Channel.VALUE_SUBMITTED,
value
},
(error) => { }
);
});
});
t.log({ result, command }); t.is(result.value.command, command); });
ava.serial('kit setup', async (t) => { let envPath = kenvPath('.env'); let fileCreated = test('-f', envPath);
t.true(fileCreated);
let contents = await readFile(envPath, 'utf-8'); t.true(contents.includes('KIT_TEMPLATE=default')); });
// Flaky test
ava('TypeScript support', async (t) => {
let tsScript = 'mock-typescript-script';
await exec(kit set-env-var KIT_MODE ts);
await wait(100);
await exec(kit new ${tsScript} main --no-edit);
let tsScriptPath = kenvPath('scripts', ${tsScript}.ts);
t.true(await pathExists(tsScriptPath), Should create ${tsScriptPath});
t.is( await readFile(tsScriptPath, 'utf-8'), await readFile(kenvPath('templates', 'default.ts'), 'utf-8'), 'Generated TypeScript file matches TypeScript template' );
await appendFile(
tsScriptPath,
console.log(await arg())
);
let message = 'success';
let { stdout, stderr } = await exec(kit ${tsScript} ${message});
t.is(stderr, '');
t.regex(stdout, new RegExp(${message}), 'TypeScript script worked');
let JSofTSExists = await pathExists(tsScriptPath.replace(/.ts$/, '.js'));
t.false(JSofTSExists, 'Should remove generated JS file');
let envContents = await readFile(kenvPath('.env'), 'utf-8');
t.log({ envContents });
t.true(envContents.includes('KIT_MODE=ts'), Should set KIT_MODE=ts ${envContents});
});
ava('TypeScript import from lib', async (t) => {
let tsScript = 'mock-typescript-script-load-lib';
await exec(kit set-env-var KIT_MODE ts);
await exec(kit new ${tsScript} main --no-edit);
let tsScriptPath = kenvPath('scripts', ${tsScript}.ts);
t.true(await pathExists(tsScriptPath), Should create ${tsScript}.ts);
t.is(
await readFile(tsScriptPath, 'utf-8'),
await readFile(kenvPath('templates', 'default.ts'), 'utf-8'),
'Generated TypeScript file matches TypeScript template'
);
await outputFile(
kenvPath('lib', 'yo.ts'),
import "@johnlindquist/kit" export let go = async ()=> await arg()
);
t.log(await readdir(kenvPath('lib')));
await appendFile(
tsScriptPath,
import { go } from "../lib/yo" console.log(await go())
);
let message = 'success';
let { stdout, stderr } = await exec(kit ${tsScript} ${message});
t.is(stderr, '');
t.regex(stdout, new RegExp(${message}), 'TypeScript script worked');
let JSofTSExists = await pathExists(tsScriptPath.replace(/.ts$/, '.js'));
t.false(JSofTSExists, 'Should remove generated JS file'); });
ava.serial('JavaScript support', async (t) => {
let script = 'mock-javascript-script';
await exec(kit new ${script} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let scriptPath = kenvPath('scripts', ${script}.js);
t.true(await pathExists(scriptPath));
let scriptContents = await readFile(scriptPath, 'utf-8'); let defaultTemplateContents = await readFile(kenvPath('templates', 'default.js'), 'utf-8');
t.is(scriptContents, defaultTemplateContents, 'Generated JavaScript file matches JavaScript template'); });
ava.serial('kit new, run, and rm', async (t) => {
let command = 'mock-script-for-new-run-rm';
let scriptContents = let value = await arg() console.log(\${command} ${value} 🎉!`)
`;
let { stdout, stderr } = await exec(kit new ${command} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let scriptPath = kenvPath('scripts', ${command}.js);
let binPath = kenvPath('bin', ${command});
if (process.platform === 'win32') { binPath += '.cmd'; }
t.true(stderr === '', 'kit new errored out'); t.true(test('-f', scriptPath), 'script created'); await writeFile(scriptPath, scriptContents);
t.true(test('-f', binPath), 'bin created');
let message = 'success';
({ stdout, stderr } = await exec(${binPath} ${message}));
t.true(stdout.includes(message), stdout includes ${message});
let { stdout: rmStdout, stderr: rmStderr } = await exec(kit rm ${command} --confirm);
let scripts = await readdir(kenvPath('scripts')); let bins = await readdir(kenvPath('bin')); t.log({ scripts, bins, rmStdout, rmStderr });
let fileRmed = !scripts.includes(command); let binRmed = !(await isFile(binPath));
t.true(fileRmed); t.true(binRmed); });
ava.serial('kit hook', async (t) => {
let script = 'mock-script-with-export';
let contents = export let value = await arg();
await exec(kit new ${script} main --no-edit);
await writeFile(kenvPath('scripts', ${script}.js), contents);
let message = 'hello';
await import(pathToFileURL(kitPath('index.js')).href);
let result = await kit(${script} ${message});
t.is(result.value, message);
});
ava.serial('kit script-output-hello', async (t) => {
let script = 'mock-script-output-hello';
let contents = 'console.log(await arg())';
await exec(kit new ${script} main --no-edit);
await writeFile(kenvPath('scripts', ${script}.js), contents);
let { stdout } = await exec(kit ${script} "hello");
t.log({ stdout });
t.true(stdout.includes('hello')); });
ava.serial('kit script in random dir', async (t) => {
let someRandomDir = kitMockPath('.kit-some-random-dir');
let script = 'mock-some-random-script';
let contents = 'console.log(await arg())';
let scriptPath = path.resolve(someRandomDir, ${script}.js);
await outputFile(scriptPath, contents);
try {
let command = kit "${scriptPath}" "hello";
let { stdout, stderr } = await exec(command);
t.log({ stdout, stderr, scriptPath, contents, command });
t.true(stdout.includes('hello'), "Expected 'hello' in stdout");
} catch (error) {
t.log({ error: error.message, scriptPath, contents });
t.fail(Error executing script: ${error.message});
}
// Verify the file contents let actualContents = await readFile(scriptPath, 'utf-8'); t.is(actualContents, contents, 'Script file contents should match'); });
ava.serial('Run both JS and TS scripts', async (t) => { let jsCommand = 'mock-js-script'; let tsCommand = 'mock-ts-script';
let newJSCommandResult = await exec(kit new ${jsCommand} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let newTSCommandResult = await exec(kit new ${tsCommand} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'ts'
}
});
process.env.PATH = ${kenvPath('bin')}${path.delimiter}${process.env.PATH};
let jsCommandResult = await exec(${jsCommand});
let tsCommandResult = await exec(${tsCommand});
t.log({ newJSCommandResult, newTSCommandResult, jsCommandResult, tsCommandResult });
t.is(jsCommandResult.stderr, ''); t.is(tsCommandResult.stderr, ''); });
ava.serial('Run kit from package.json', async (t) => {
let command = 'mock-pkg-json-script';
let scriptPath = kenvPath('scripts', ${command}.js);
await exec(kit new ${command} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
await appendFile(
scriptPath,
let value = await arg() console.log(value)
);
let pkgPath = kenvPath('package.json'); let pkgJson = await readJson(pkgPath); let npmScript = 'run-kit';
let message = 'success';
pkgJson.scripts = {
[npmScript]: kit ${command} ${message}
};
await writeJson(pkgPath, pkgJson);
pkgJson = await readJson(pkgPath); t.log(pkgJson);
cd(kenvPath());
let { stdout, stderr } = await exec(pnpm run ${npmScript});
t.is(stderr, '');
t.regex(stdout, new RegExp(${message}));
});
ava.serial('Run a script with --flag values: pass hello instead of one and two', async (t) => {
let command = 'mock-boolean-flag-values-pass-hello-instead-of-one-and-two';
let scriptPath = kenvPath('scripts', ${command}.js);
await exec(kit new ${command} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let success = 'success'; let fail = 'fail';
await appendFile(
scriptPath,
let value = await arg() if(flag.one === "one" && flag.two === "two"){ console.log("${success}") }else{ console.log("${fail}") }
);
cd(kenvPath());
({ stdout, stderr } = await exec(kit ${command} hello));
t.is(stderr, ''); t.regex(stdout, new RegExp(fail)); });
ava.serial('Run a script with --flag values: ones and twos match', async (t) => {
let command = 'mock-boolean-flag-values-ones-and-twos-match';
let scriptPath = kenvPath('scripts', ${command}.js);
await exec(kit new ${command} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let success = 'success'; let fail = 'fail';
await appendFile(
scriptPath,
let value = await arg() if(flag.one === "one" && flag.two === "two"){ console.log("${success}") }else{ console.log("${fail}") }
);
cd(kenvPath());
let { stdout, stderr } = await exec(kit ${command} hello --one one --two two);
t.is(stderr, ''); t.regex(stdout, new RegExp(success)); });
ava.serial('Run a script with --flag values: ones match, twos mismatch', async (t) => {
let command = 'mock-boolean-flag-values-ones-match-twos-mismatch';
let scriptPath = kenvPath('scripts', ${command}.js);
await exec(kit new ${command} main --no-edit, {
env: {
...process.env,
KIT_NODE_PATH: process.execPath,
KIT_MODE: 'js'
}
});
let success = 'success'; let fail = 'fail';
await appendFile(
scriptPath,
let value = await arg() if(flag.one === "one" && flag.two === "two"){ console.log("${success}") }else{ console.log("${fail}") }
);
cd(kenvPath());
({ stdout, stderr } = await exec(kit ${command} hello --one one --two three));
t.is(stderr, ''); t.regex(stdout, new RegExp(fail)); });
ava.serial('Run a scriptlet from a .md file', async (t) => {
let scriptlet = 'mock-scriptlet-from-md-file';
let scriptletPath = kenvPath('scriptlets', ${scriptlet}.md);
let testFilePathContents = 'Success!';
let scriptletName = 'Test Scriptlet';
let scriptletNameSlug = slugify(scriptletName);
await ensureDir(kenvPath('scriptlets'));
let content = `
${scriptletName}
```ts await writeFile(kenvPath("test.md"), "${testFilePathContents}") ``` `.trim();
await writeFile(scriptletPath, content);
let { stdout, stderr } = await exec(kit "${scriptletPath}#${scriptletNameSlug}");
t.log({ stdout, stderr, content });
let testFilePathFinalContents = await readFile(kenvPath('test.md'), 'utf8');
t.is(testFilePathFinalContents, testFilePathContents);
});
ava.serial('Run a scriptlet from a .md file with args', async (t) => {
let scriptlet = 'mock-scriptlet-from-md-file-with-args';
let scriptletPath = kenvPath('scriptlets', ${scriptlet}.md);
let scriptletDir = path.parse(scriptletPath).dir;
t.log;
await ensureDir(scriptletDir);
let scriptletName = 'Test Scriptlet With Args';
t.log(Slugifying ${scriptletName});
let scriptletNameSlug = slugify(scriptletName);
t.log(Writing file: ${scriptletPath});
let scriptletContent = `
${scriptletName}
```ts let scope = await arg("scope") let message = await arg("message") console.log(scope + ": " + message) ``` `.trim(); t.log({ scriptletPath, scriptletNameSlug, scriptletContent }); try { await writeFile(scriptletPath, scriptletContent); } catch (error) { t.log(error); }
let fullCommand = kit ${scriptletPath}#${scriptletNameSlug} test "Hello, world!";
t.log({ fullCommand });
let { stdout } = await exec(fullCommand);
t.is(stdout, 'test: Hello, world!'); });
ava.serial('Run a bash scriptlet from a .md file with args', async (t) => { if (process.platform === 'win32') { t.pass('Skipping test on Windows'); return; }
let scriptlet = 'mock-bash-scriptlet-from-md-file-with-args';
let scriptletPath = kenvPath('scriptlets', ${scriptlet}.md);
let scriptletDir = path.parse(scriptletPath).dir; t.log; await ensureDir(scriptletDir); let scriptletName = 'Test Bash Scriptlet With Args'; let scriptletNameSlug = slugify(scriptletName);
let scriptletContent = `
${scriptletName}
```bash echo "fix($1): $2" ``` `.trim(); t.log({ scriptletPath, scriptletNameSlug, scriptletContent }); try { await writeFile(scriptletPath, scriptletContent); } catch (error) { t.log(error); }
let fullCommand = kit ${scriptletPath}#${scriptletNameSlug} test "Hello, world!";
t.log({ fullCommand });
let { stdout } = await exec(fullCommand);
t.is(stdout, 'fix(test): Hello, world!'); });
import '../globals/index.js' import { config } from 'dotenv-flow'import { md as globalMd, marked } from '../globals/marked.js'
import * as path from 'node:path'
import type { Script, Metadata, Shortcut, Scriptlet, Snippet } from '../types/core' import { lstatSync, realpathSync } from 'node:fs' import { lstat, readdir } from 'node:fs/promises' import { execSync } from 'node:child_process'
import { Channel, ProcessType } from './enum.js' import { type AssignmentExpression, type Identifier, type ObjectExpression, Parser, type Program } from 'acorn' import tsPlugin from 'acorn-typescript' import type { Stamp } from './db' import { pathToFileURL } from 'node:url' import { parseScript } from './parser.js' import { kitPath, kenvPath } from './resolvers.js' import { cmd, scriptsDbPath, statsPath } from './constants.js' import { isBin, isJsh, isDir, isWin, isMac } from './is.js' import { stat } from "node:fs/promises"; import { parentPort } from 'node:worker_threads';
// Module-level variables to store the last known mtimes for the DB files // These are global for this utility, shared by any cache using it. let utilLastScriptsDbMtimeMs: number = 0; let utilLastStatsPathMtimeMs: number = 0;
export async function checkDbAndInvalidateCache( cacheMap: Map<any, any>, cacheName: string // For logging/debugging purposes ): Promise { let currentScriptsDbMtimeMs = 0; let currentStatsPathMtimeMs = 0;
try {
currentScriptsDbMtimeMs = (await stat(scriptsDbPath)).mtimeMs;
} catch (dbError) {
// console.warn(Could not stat scriptsDbPath "${scriptsDbPath}" for ${cacheName} cache:, dbError);
currentScriptsDbMtimeMs = -1; // Mark as different/error
}
try {
currentStatsPathMtimeMs = (await stat(statsPath)).mtimeMs;
} catch (dbError) {
// console.warn(Could not stat statsPath "${statsPath}" for ${cacheName} cache:, dbError);
currentStatsPathMtimeMs = -1; // Mark as different/error
}
if (
currentScriptsDbMtimeMs !== utilLastScriptsDbMtimeMs ||
currentStatsPathMtimeMs !== utilLastStatsPathMtimeMs
) {
if (parentPort) {
parentPort.postMessage({
channel: Channel.LOG_TO_PARENT,
value: [CacheUtil] '${cacheName}' cache cleared due to ${currentScriptsDbMtimeMs !== utilLastScriptsDbMtimeMs ? 'scriptsDb' : ''} ${currentStatsPathMtimeMs !== utilLastStatsPathMtimeMs ? 'stats' : ''} file changes/inaccessibility.
});
}
cacheMap.clear();
// Update the utility's last known mtimes
utilLastScriptsDbMtimeMs = currentScriptsDbMtimeMs;
utilLastStatsPathMtimeMs = currentStatsPathMtimeMs;
} else {
// DB files haven't changed AND were accessible (or still inaccessible as before)
// No need to clear cache based on DB files.
}
}
export let extensionRegex = /.(mjs|ts|js)$/g
// Regex to detect VS Code snippet variables like:
// $1,
export let wait = async (time: number): Promise => new Promise((res) => setTimeout(res, time))
export let checkProcess = (pid: string | number) => {
return execSync(kill -0 + pid).buffer.toString()
}
export let combinePath = (arrayOfPaths: string[]): string => { const pathSet = new Set()
for (const p of arrayOfPaths) { if (p) { const paths = p.split(path.delimiter) for (const singlePath of paths) { if (singlePath) { pathSet.add(singlePath) } } } }
return Array.from(pathSet).join(path.delimiter) }
const DEFAULT_PATH = process.env.PATH export const resetPATH = () => { process.env.PATH = DEFAULT_PATH } const UNIX_DEFAULT_PATH = combinePath(['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin'])
const WIN_DEFAULT_PATH = combinePath([])
export const KIT_DEFAULT_PATH = isWin ? WIN_DEFAULT_PATH : UNIX_DEFAULT_PATH
export const KIT_BIN_PATHS = combinePath([ kitPath('bin'), ...(isWin ? [] : [kitPath('override', 'code')]), kenvPath('bin') ])
export const KIT_FIRST_PATH = combinePath([KIT_BIN_PATHS, process?.env?.PATH, KIT_DEFAULT_PATH])
export const KIT_LAST_PATH = combinePath([process.env.PATH, KIT_DEFAULT_PATH, KIT_BIN_PATHS])
export let assignPropsTo = ( source: { [s: string]: unknown } | ArrayLike, target: { [x: string]: unknown } ) => { Object.entries(source).forEach(([key, value]) => { target[key] = value }) }
//app let fileExists = (path: string) => { try { return lstatSync(path, { throwIfNoEntry: false })?.isFile() } catch { return false } }
export let isScriptletPath = (filePath: unknown) => { return typeof filePath === 'string' && filePath.includes('.md#') }
//app export let resolveToScriptPath = (rawScript: string, cwd: string = process.cwd()): string => { let extensions = ['', '.js', '.ts', '.md'] let resolvedScriptPath = ''
// Remove anchor from the end let script = rawScript.replace(/#.*$/, '')
// if (!script.match(/(.js|.mjs|.ts)$/)) script += ".js" if (fileExists(script)) return script
// Check sibling scripts if (global.kitScript) { let currentRealScriptPath = realpathSync(global.kitScript) let maybeSiblingScriptPath = path.join(path.dirname(currentRealScriptPath), script) if (fileExists(maybeSiblingScriptPath)) { return maybeSiblingScriptPath }
if (fileExists(maybeSiblingScriptPath + '.js')) {
return maybeSiblingScriptPath + '.js'
}
if (fileExists(maybeSiblingScriptPath + '.ts')) {
return maybeSiblingScriptPath + '.ts'
}
}
// Check main kenv
for (let ext of extensions) { resolvedScriptPath = kenvPath('scripts', script + ext) if (fileExists(resolvedScriptPath)) return resolvedScriptPath }
// Check other kenvs let [k, s] = script.split('/') if (s) { for (let ext of extensions) { resolvedScriptPath = kenvPath('kenvs', k, 'scripts', s + ext) if (fileExists(resolvedScriptPath)) return resolvedScriptPath } }
// Check scripts dir
for (let ext of extensions) { resolvedScriptPath = path.resolve(cwd, 'scripts', script + ext) if (fileExists(resolvedScriptPath)) return resolvedScriptPath }
// Check anywhere
for (let ext of extensions) { resolvedScriptPath = path.resolve(cwd, script + ext) if (fileExists(resolvedScriptPath)) return resolvedScriptPath }
throw new Error(${script} not found)
}
export let resolveScriptToCommand = (script: string) => {
return path.basename(script).replace(new RegExp(\\${path.extname(script)}$), '')
}
//app export const shortcutNormalizer = (shortcut: string) => shortcut ? shortcut .replace(/(option|opt|alt)/i, isMac ? 'Option' : 'Alt') .replace(/(ctl|cntrl|ctrl|control)/, 'Control') .replace(/(command|cmd)/i, isMac ? 'Command' : 'Control') .replace(/(shift|shft)/i, 'Shift') .split(/\s/) .filter(Boolean) .map((part) => (part[0].toUpperCase() + part.slice(1)).trim()) .join('+') : ''
export const friendlyShortcut = (shortcut: string) => { let f = '' if (shortcut.includes('Command+')) f += 'cmd+' if (shortcut.match(/(?<!Or)Control+/)) f += 'ctrl+' if (shortcut.includes('Alt+')) f += 'alt+' if (shortcut.includes('Option+')) f += 'opt+' if (shortcut.includes('Shift+')) f += 'shift+' if (shortcut.includes('+')) f += shortcut.split('+').pop()?.toLowerCase()
return f }
export let setMetadata = (
contents: string,
overrides: {
[key: string]: string
}
) => {
Object.entries(overrides).forEach(([key, value]) => {
let k = key[0].toUpperCase() + key.slice(1)
// if not exists, then add
if (!contents.match(new RegExp(^\/\/\\s*(${key}|${k}):.*, 'gm'))) {
// uppercase first letter
contents = // ${k}: ${value} ${contents}.trim()
} else {
// if exists, then replace
contents = contents.replace(new RegExp(^\/\/\\s*(${key}|${k}):.*$, 'gm'), // ${k}: ${value})
}
})
return contents
}
// Exhaustive, compile-time-checked list of metadata keys.
// satisfies ensures every entry is a valid keyof Metadata and
// warns if we add an invalid key. Missing keys will surface when hovering the
// _MissingKeys helper type during development.
const META_KEYS = [
"author",
"name",
"description",
"enter",
"alias",
"image",
"emoji",
"shortcut",
"shortcode",
"trigger",
"snippet", // Keep deprecated for now
"expand",
"keyword",
"pass",
"group",
"exclude",
"watch",
"log",
"background",
"system",
"schedule",
"index",
"access",
"response",
"tag",
"longRunning",
"mcp",
'timeout',
'cache',
'bin'
] as const satisfies readonly (keyof Metadata)[];
// Optional development-time check for forgotten keys. type _MissingKeys = Exclude<keyof Metadata, typeof META_KEYS[number]>; // should be never
export const VALID_METADATA_KEYS_SET: ReadonlySet = new Set(META_KEYS);
const getMetadataFromComments = (contents: string): Record<string, any> => { const lines = contents.split('\n') const metadata = {} let commentStyle = null let inMultilineComment = false let multilineCommentEnd = null
// Valid metadata key pattern: starts with a letter, can contain letters, numbers, and underscores const validKeyPattern = /^[a-zA-Z][a-zA-Z0-9_]*$/ // Common prefixes to ignore const ignoreKeyPrefixes = ['TODO', 'FIXME', 'NOTE', 'HACK', 'XXX', 'BUG']
// Regex to match comment lines with metadata const commentRegex = { '//': /^//\s*([^:]+):(.)$/, '#': /^#\s([^:]+):(.*)$/ }
for (const line of lines) { // Check for the start of a multiline comment block if ( !inMultilineComment && (line.trim().startsWith('/') || line.trim().startsWith("'''") || line.trim().startsWith('"""') || line.trim().match(/^: '/)) ) { inMultilineComment = true multilineCommentEnd = line.trim().startsWith('/') ? '*/' : line.trim().startsWith(": '") ? "'" : line.trim().startsWith("'''") ? "'''" : '"""' }
// Check for the end of a multiline comment block
if (inMultilineComment && line.trim().endsWith(multilineCommentEnd)) {
inMultilineComment = false
multilineCommentEnd = null
continue // Skip the end line of a multiline comment block
}
// Skip lines that are part of a multiline comment block
if (inMultilineComment) continue
// Determine comment style and try to match metadata
let match = null
if (line.startsWith('//')) {
match = line.match(commentRegex['//'])
commentStyle = '//'
} else if (line.startsWith('#')) {
match = line.match(commentRegex['#'])
commentStyle = '#'
}
if (!match) continue
// Extract and trim the key and value
const [, rawKey, value] = match
const trimmedKey = rawKey.trim()
const trimmedValue = value.trim()
// Skip if key starts with common prefixes to ignore
if (ignoreKeyPrefixes.some(prefix => trimmedKey.toUpperCase().startsWith(prefix))) continue
// Skip if key doesn't match valid pattern
if (!validKeyPattern.test(trimmedKey)) continue
// Transform the key case
let key = trimmedKey
if (key?.length > 0) {
key = key[0].toLowerCase() + key.slice(1)
}
// Skip empty keys or values
if (!key || !trimmedValue) {
continue
}
let parsedValue: string | boolean | number
let lowerValue = trimmedValue.toLowerCase()
let lowerKey = key.toLowerCase()
switch (true) {
case lowerValue === 'true':
parsedValue = true
break
case lowerValue === 'false':
parsedValue = false
break
case lowerKey === 'timeout':
parsedValue = Number.parseInt(trimmedValue, 10)
break
default:
parsedValue = trimmedValue
}
// Only assign if the key hasn't been assigned before AND is in the valid set
// Cast key to keyof Metadata because Set.has expects this type due to Set<keyof Metadata>.
// We trust the string `key` corresponds if .has returns true.
if (!(key in metadata) && VALID_METADATA_KEYS_SET.has(key as keyof Metadata)) {
metadata[key] = parsedValue
}
}
return metadata }
function parseTypeScript(code: string) { const parser = Parser.extend( // @ts-expect-error Somehow these are not 100% compatible tsPlugin({ allowSatisfies: true }) ) return parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' }) }
function isOfType<T extends { type: string }, TType extends string>(node: T, type: TType): node is T & { type: TType } { return node.type === type }
function parseMetadataProperties(properties: ObjectExpression['properties']) { return properties.reduce((acc, prop) => { if (!isOfType(prop, 'Property')) { throw Error('Not a Property') }
const key = prop.key
const value = prop.value
if (!isOfType(key, 'Identifier')) {
throw Error('Key is not an Identifier')
}
// Handle both Literal and TemplateLiteral
if (isOfType(value, 'Literal')) {
acc[key.name] = value.value
} else if (isOfType(value, 'TemplateLiteral')) {
// For template literals, concatenate all the quasi elements
// Template literals without expressions will have quasis with the full content
if (value.expressions.length === 0 && value.quasis.length === 1) {
// Simple template literal without expressions
acc[key.name] = value.quasis[0].value.cooked
} else {
// Template literal with expressions - throw an error with helpful message
throw Error(`Template literals with expressions are not supported in metadata. The metadata.${key.name} property contains a template literal with ${value.expressions.length} expression(s). Please use a plain string or a template literal without expressions.`)
}
} else {
throw Error(`value is not a Literal or TemplateLiteral, but a ${value.type}`)
}
return acc
}, {}) }
function getMetadataFromExport(ast: Program): Partial { for (const node of ast.body) { const isExpressionStatement = isOfType(node, 'ExpressionStatement')
if (isExpressionStatement) {
const expression = node.expression as AssignmentExpression
const isMetadata = (expression.left as Identifier).name === 'metadata'
const isEquals = expression.operator === '='
const properties = (expression.right as ObjectExpression).properties
const isGlobalMetadata = isMetadata && isEquals
if (isGlobalMetadata) {
return parseMetadataProperties(properties)
}
}
const isExportNamedDeclaration = isOfType(node, 'ExportNamedDeclaration')
if (!isExportNamedDeclaration || !node.declaration) {
continue
}
const declaration = node.declaration
if (declaration.type !== 'VariableDeclaration' || !declaration.declarations[0]) {
continue
}
const namedExport = declaration.declarations[0]
if (!('name' in namedExport.id) || namedExport.id.name !== 'metadata') {
continue
}
if (namedExport.init?.type !== 'ObjectExpression') {
continue
}
const properties = namedExport.init?.properties
return parseMetadataProperties(properties)
}
// Nothing found return {} }
//app export let getMetadata = (contents: string): Metadata => { const fromComments = getMetadataFromComments(contents)
// if ( // !/(const|var|let) metadata/g.test(contents) && // !/^metadata = {/g.test(contents) // ) { // // No named export in file, return early // return fromComments // }
let ast: Program
try {
ast = parseTypeScript(contents)
} catch (err) {
// TODO: May wanna introduce some error handling here. In my script version, I automatically added an error
// message near the top of the user's file, indicating that their input couldn't be parsed...
// acorn-typescript unfortunately doesn't support very modern syntax, like const T generics.
// But it works in most cases.
return fromComments
}
try { const fromExport = getMetadataFromExport(ast) return { ...fromComments, ...fromExport } } catch (err) { return fromComments } }
export let getLastSlashSeparated = (string: string, count: number) => { return string.replace(//$/, '').split('/').slice(-count).join('/') || '' }
export let kenvFromFilePath = (filePath: string) => { let { dir } = path.parse(filePath) let { name: scriptsName, dir: kenvDir } = path.parse(dir) if (scriptsName !== 'scripts') return '.kit' let { name: kenv } = path.parse(kenvDir) if (path.relative(kenvDir, kenvPath()) === '') return '' return kenv }
//app export let getLogFromScriptPath = (filePath: string) => { let { name, dir } = path.parse(filePath) let { name: scriptsName, dir: kenvDir } = path.parse(dir) if (scriptsName !== 'scripts') return kitPath('logs', 'main.log')
return path.resolve(kenvDir, 'logs', ${name}.log)
}
//new RegExp(`(^//([^(:|\W)]+
export let stripMetadata = (fileContents: string, exclude: string[] = []) => {
let excludeWithCommon = [http, https, TODO, FIXME, NOTE].concat(exclude);
// Regex to capture the metadata key and the colon
// It matches lines starting with //, followed by a key (word characters), then a colon.
// It uses a negative lookbehind for exclusions.
const regex = new RegExp(
^(//\\s*([^(:|\\W|\\n)]+${exclude.length ? (?<!\b(${excludeWithCommon.join('|')})\b) : ''}):).*$\n?,
'gim'
);
return fileContents.replace(regex, (match, group1) => {
// group1 contains the key part like "// Name:" or "// Shortcode:"
// We want to keep this part and just remove the value after it, then add a newline.
return ${group1.trimEnd()}\n;
});
}
export let stripName = (name: string) => { let strippedName = path.parse(name).name strippedName = strippedName.trim().replace(/\s+/g, '-') // Only lowercase if there's no hyphen in the original input if (!name.includes('-')) { strippedName = strippedName.toLowerCase() } strippedName = strippedName.replace(/[^\w-]+/g, '') strippedName = strippedName.replace(/-{2,}/g, '-') return strippedName }
//validator
export let checkIfCommandExists = async (input: string) => {
if (await isBin(kenvPath('bin', input))) {
return global.chalk{red.bold ${input}} already exists. Try again:
}
if (await isDir(kenvPath('bin', input))) {
return global.chalk{red.bold ${input}} exists as group. Enter different name:
}
if (await isBin(input)) {
return global.chalk{red.bold ${input}} is a system command. Enter different name:
}
if (!input.match(/^([a-z]|[0-9]|-|/)+$/g)) {
return global.chalk{red.bold ${input}} can only include lowercase, numbers, and -. Enter different name:
}
return true }
export let getKenvs = async (ignorePattern = /^ignore$/): Promise<string[]> => { if (!(await isDir(kenvPath('kenvs')))) return []
let dirs = await readdir(kenvPath('kenvs'), { withFileTypes: true })
let kenvs = [] for (let dir of dirs) { if (!dir.name.match(ignorePattern) && (dir.isDirectory() || dir.isSymbolicLink())) { kenvs.push(kenvPath('kenvs', dir.name)) } }
return kenvs }
export let kitMode = () => (process.env.KIT_MODE || 'ts').toLowerCase()
global.__kitRun = false
let kitGlobalRunCount = 0 export let run = async (command: string, ...commandArgs: string[]) => { performance.mark('run') kitGlobalRunCount++ let kitLocalRunCount = kitGlobalRunCount
let scriptArgs = [] let script = '' let match // This regex splits the command string into parts: // - Matches single-quoted strings: '[^']+?' // - Matches double-quoted strings: "[^"]+?" // - Matches one or more whitespace characters: \s+ // This allows us to preserve quoted arguments as single units let splitRegex = /('[^']+?')|("[^"]+?")|\s+/ let quoteRegex = /'|"/g let parts = command.split(splitRegex).filter(Boolean)
for (let item of parts) { if (!script) { script = item.replace(quoteRegex, '') } else if (!item.match(quoteRegex)) { scriptArgs.push(...item.trim().split(/\s+/)) } else { scriptArgs.push(item.replace(quoteRegex, '')) } } // In case a script is passed with a path, we want to use the full command if (script.includes(path.sep)) { script = command scriptArgs = [] } let resolvedScript = resolveToScriptPath(script) global.projectPath = (...args) => path.resolve(path.dirname(path.dirname(resolvedScript)), ...args)
global.onTabs = [] global.kitScript = resolvedScript global.kitCommand = resolveScriptToCommand(resolvedScript) let realProjectPath = projectPath() updateEnv(realProjectPath) if (process.env.KIT_CONTEXT === 'app') { let script = await parseScript(global.kitScript)
if (commandArgs.includes(`--${cmd}`)) {
script.debug = true
global.send(Channel.DEBUG_SCRIPT, script)
return await Promise.resolve('Debugging...')
}
cd(realProjectPath)
global.send(Channel.SET_SCRIPT, script)
}
let result = await global.attemptImport(resolvedScript, ...scriptArgs, ...commandArgs)
global.flag.tab = ''
return result }
export let updateEnv = (scriptProjectPath: string) => { let { parsed, error } = config({ node_env: process.env.NODE_ENV || 'development', path: scriptProjectPath, silent: true })
if (parsed) { assignPropsTo(process.env, global.env) }
if (error) { let isCwdKenv = path.normalize(cwd()) === path.normalize(kenvPath()) if (isCwdKenv && !error?.message?.includes('files matching pattern') && !process.env.CI) { global.log(error.message) } } }
export let configEnv = () => { let { parsed, error } = config({ node_env: process.env.NODE_ENV || 'development', path: process.env.KIT_DOTENV_PATH || kenvPath(), silent: true })
if (error) { let isCwdKenv = path.normalize(cwd()) === path.normalize(kenvPath()) if (isCwdKenv && !error?.message?.includes('files matching pattern') && !process.env.CI) { global.log(error.message) } }
process.env.PATH_FROM_DOTENV = combinePath([parsed?.PATH || process.env.PATH])
process.env.PATH = combinePath([process.env.PARSED_PATH, KIT_FIRST_PATH])
assignPropsTo(process.env, global.env)
return parsed }
export let trashScriptBin = async (script: Script) => { let { command, kenv, filePath } = script let { pathExists } = await import('fs-extra')
let binJSPath = isJsh()
? kenvPath('node_modules', '.bin', command + '.js')
: kenvPath(kenv && kenvs/${kenv}, 'bin', command + '.js')
let binJS = await pathExists(binJSPath) let { name, dir } = path.parse(filePath) let commandBinPath = path.resolve(path.dirname(dir), 'bin', name)
if (process.platform === 'win32') { if (!commandBinPath.endsWith('.cmd')) { commandBinPath += '.cmd' } }
if (binJS) { let binPath = isJsh() ? kenvPath('node_modules', '.bin', command) : commandBinPath
await global.trash([binPath, ...(binJS ? [binJSPath] : [])])
}
if (await pathExists(commandBinPath)) { await global.trash(commandBinPath) } }
export let trashScript = async (script: Script) => { let { filePath } = script
await trashScriptBin(script)
let { pathExists } = await import('fs-extra')
await global.trash([...((await pathExists(filePath)) ? [filePath] : [])])
await wait(100) }
export let getScriptFiles = async (kenv = kenvPath()) => { let scriptsPath = path.join(kenv, 'scripts') try { let dirEntries = await readdir(scriptsPath) let scriptFiles: string[] = [] for (let fileName of dirEntries) { if (!fileName.startsWith('.')) { let fullPath = path.join(scriptsPath, fileName) if (path.extname(fileName)) { scriptFiles.push(fullPath) } else { try { let stats = await lstat(fullPath) if (!stats.isDirectory()) { scriptFiles.push(fullPath) } } catch (error) { log(error) } } } } return scriptFiles } catch { return [] } }
export let scriptsSort = (timestamps: Stamp[]) => (a: Script, b: Script) => { let aTimestamp = timestamps.find((t) => t.filePath === a.filePath) let bTimestamp = timestamps.find((t) => t.filePath === b.filePath)
if (aTimestamp && bTimestamp) { return bTimestamp.timestamp - aTimestamp.timestamp }
if (aTimestamp) { return -1 }
if (bTimestamp) { return 1 }
if (a?.index || b?.index) { if ((a?.index || 9999) < (b?.index || 9999)) { return -1 } return 1 }
let aName = (a?.name || '').toLowerCase() let bName = (b?.name || '').toLowerCase()
return aName > bName ? 1 : aName < bName ? -1 : 0 }
export let isParentOfDir = (parent: string, dir: string) => { let relative = path.relative(parent, dir) return relative && !relative.startsWith('..') && !path.isAbsolute(relative) }
export let isInDir = (parentDir: string) => (dir: string) => { const relative = path.relative(parentDir, dir) return relative && !relative.startsWith('..') && !path.isAbsolute(relative) }
export let escapeShortcut: Shortcut = {
name: Escape,
key: escape,
bar: 'left',
onPress: async () => {
exit()
}
}
export let backToMainShortcut: Shortcut = {
name: Back,
key: escape,
bar: 'left',
onPress: async () => {
await mainScript()
}
}
export let closeShortcut: Shortcut = {
name: 'Exit',
key: ${cmd}+w,
bar: 'right',
onPress: () => {
exit()
}
}
export let editScriptShortcut: Shortcut = {
name: 'Edit Script',
key: ${cmd}+o,
onPress: async (input, { script }) => {
await run(kitPath('cli', 'edit-script.js'), script?.filePath)
exit()
},
bar: 'right'
}
export let submitShortcut: Shortcut = {
name: 'Submit',
key: ${cmd}+s,
bar: 'right',
onPress: async (input) => {
await submit(input)
}
}
export let viewLogShortcut: Shortcut = {
name: 'View Log',
key: ${cmd}+l,
onPress: async (input, { focused }) => {
await run(kitPath('cli', 'open-script-log.js'), focused?.value?.scriptPath)
},
bar: 'right',
visible: true
}
export let terminateProcessShortcut: Shortcut = {
name: 'Terminate Process',
key: ${cmd}+enter,
onPress: async (input, { focused }) => {
await sendWait(Channel.TERMINATE_PROCESS, focused?.value?.pid)
},
bar: 'right',
visible: true
}
export let terminateAllProcessesShortcut: Shortcut = {
name: 'Terminate All Processes',
key: ${cmd}+shift+enter,
onPress: async () => {
await sendWait(Channel.TERMINATE_ALL_PROCESSES)
},
bar: 'right',
visible: true
}
export let smallShortcuts: Shortcut[] = [ // escapeShortcut, closeShortcut ]
export let argShortcuts: Shortcut[] = [ // escapeShortcut, closeShortcut, editScriptShortcut ]
export let editorShortcuts: Shortcut[] = [closeShortcut, editScriptShortcut, submitShortcut]
export let defaultShortcuts: Shortcut[] = [ // escapeShortcut, closeShortcut, editScriptShortcut, submitShortcut ]
export let divShortcuts: Shortcut[] = [ // escapeShortcut, closeShortcut, { ...editScriptShortcut, bar: '' } ]
export let formShortcuts: Shortcut[] = [
// escapeShortcut,
{
...editScriptShortcut,
bar: ''
},
closeShortcut,
{
name: 'Reset',
key: ${cmd}+alt+r,
bar: ''
}
]
export let cliShortcuts: Shortcut[] = [ // escapeShortcut, closeShortcut ]
let kitFilePath = (...paths: string[]) => pathToFileURL(kitPath('images', ...paths)).href let iconPath = kitFilePath('icon.svg') let kentPath = kitFilePath('kent.jpg') let mattPath = kitFilePath('matt.jpg')
const checkmarkStyles = `
<style> .checkmark-list { list-style-type: none !important; padding-left: 0 !important; } .checkmark-list li { padding-left: 1.5em; position: relative; } .checkmark-list li::before { content: "✓"; position: absolute; left: 0; color: var(--color-primary); } .checkmark-list li::marker { content: none !important; } </style>export let proPane = () =>
${checkmarkStyles}
Pro Features
- Unlimited Active Prompts
- Built-in Debugger
- Script Log Window
- Vite Widgets
- Webcam Capture
- Basic Screenshots
- Desktop Color Picker
- Support through Discord
Planned Features...
- Sync Scripts to GitHub Repo
- Run Script Remotely as GitHub Actions
- Advanced Screenshots
- Screen Recording
- Measure Tool
What the community is saying
I forgot that a lot of people don't know what Script Kit is. You're missing out! I use it to easily open projects in VSCode, start a zoom meeting and put the link in my clipboard, download Twitter images, upload images to cloudinary, and so much more!
export const getShellSeparator = () => { let separator = '&&' if (process.platform === 'win32') { separator = '&' } // if powershell if ( process.env.KIT_SHELL?.includes('pwsh') || process.env.KIT_SHELL?.includes('powershell') || process.env.SHELL?.includes('pwsh') || process.env.SHELL?.includes('powershell') || process.env.ComSpec?.includes('powershell') || process.env.ComSpec?.includes('pwsh') ) { separator = ';' }
if (process.env.KIT_SHELL?.includes('fish') || process.env.SHELL?.includes('fish')) { separator = ';' }
return separator }
export let getTrustedKenvsKey = () => { let username = process.env?.USER || process.env?.USERNAME || 'NO_USER_ENV_FOUND'
let formattedUsername = username.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()
let trustedKenvKey = KIT_${formattedUsername}_DANGEROUSLY_TRUST_KENVS
return trustedKenvKey }
export const uniq = (array: any[]): any[] => { if (!Array.isArray(array)) { throw new Error('Input should be an array') } return [...new Set(array)] }
interface DebounceSettings { leading?: boolean trailing?: boolean }
type Procedure = (...args: any[]) => void
type DebouncedFunc = (...args: Parameters) => void
export const debounce = ( func: T, waitMilliseconds = 0, options: DebounceSettings = {} ): DebouncedFunc => { let timeoutId: ReturnType | undefined
return (...args: Parameters) => { const doLater = () => { timeoutId = undefined // If trailing is enabled, we invoke the function only if the function was invoked during the wait period if (options.trailing !== false) { func(...args) } }
const shouldCallNow = options.leading && timeoutId === undefined
// Always clear the timeout
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(doLater, waitMilliseconds)
// If leading is enabled and no function call has been scheduled, we call the function immediately
if (shouldCallNow) {
func(...args)
}
} }
export const range = (start: number, end: number, step = 1): number[] => { return Array.from({ length: Math.ceil((end - start) / step) }, (_, i) => start + i * step) }
type Iteratee = ((item: T) => any) | keyof T
export let sortBy = (collection: T[], iteratees: Iteratee[]): T[] => { const iterateeFuncs = iteratees.map((iteratee) => typeof iteratee === 'function' ? iteratee : (item: T) => item[iteratee as keyof T] )
return [...collection].sort((a, b) => { for (const iteratee of iterateeFuncs) { const valueA = iteratee(a) const valueB = iteratee(b)
if (valueA < valueB) {
return -1
} else if (valueA > valueB) {
return 1
}
}
return 0
}) }
export let isUndefined = (value: any): value is undefined => { return value === undefined }
export let isString = (value: any): value is string => { return typeof value === 'string' }
export let getCachePath = (filePath: string, type: string) => { // Normalize file path const normalizedPath = path.normalize(filePath)
// Replace all non-alphanumeric characters and path separators with dashes let dashedName = normalizedPath.replace(/[^a-zA-Z0-9]/g, '-')
// Remove leading dashes while (dashedName.charAt(0) === '-') { dashedName = dashedName.substr(1) }
// Replace multiple consecutive dashes with a single dash dashedName = dashedName.replace(/-+/g, '-')
// Append .json extension
return kitPath('cache', type, ${dashedName}.json)
}
export let adjustPackageName = (packageName: string) => {
let adjustedPackageName = ''
if (packageName.startsWith('@')) {
let parts = packageName.split('/')
adjustedPackageName = ${parts[0]}/${parts[1]}
} else {
adjustedPackageName = packageName.split('/')[0]
}
return adjustedPackageName }
export let keywordInputTransformer = (keyword: string) => { if (!keyword) return (input: string) => input
let keywordRegex = new RegExp((?<=${global.arg.keyword}\\s)(.*), 'gi')
return (input: string) => { return input.match(keywordRegex)?.[0] || '' } }
export let escapeHTML = (text: string) => { // Handle null or undefined input if (!text || typeof text !== 'string') return ''
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
// Perform HTML escape on the updated text text = text.replace(/[&<>"']/g, function (m) { return map[m] })
// Convert tabs to spaces text = text.replace(/\t/g, ' ')
// Convert newline characters to
return text.replace(/\n/g, '
')
}
// Optimized for worker: larger batch size, no retries for local ops, fast path for small arrays export let processInBatches = async (items: Promise[], batchSize: number = 500, maxRetries = 0): Promise<T[]> => { if (!items.length) return [] if (items.length <= batchSize) { return Promise.all(items) } let result: T[] = [] for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize) const batchResults = await Promise.all(batch) result = result.concat(batchResults.filter((item): item is Awaited => item !== undefined)) } return result }
export let md = (content = '', containerClasses = 'p-5 prose prose-sm') => { return globalMd(content + '\n', containerClasses) }
export let highlight = async (markdown: string, containerClass = 'p-5 leading-loose', injectStyles = '') => { let { default: highlight } = global.__kitHighlight || (await import('highlight.js')) if (!global.__kitHighlight) global.__kitHighlight = { default: highlight }
let renderer = new marked.Renderer()
renderer.paragraph = (p) => {
// Convert a tag with href .mov, .mp4, or .ogg video links to video tags
const text = p.text || ''
if (text.match(/.</a>/)) {
let url = text.match(/href="(.)"/)[1]
return <video controls src="${url}" style="max-width: 100%;"></video>
}
return `<p>${p.text}</p>`
}
let highlightedMarkdown = marked(markdown)
let result = `
${highlightedMarkdown}
return result }
export let tagger = (script: Script) => { if (!script.tag) { let tags = []
if (script.friendlyShortcut) {
tags.push(script.friendlyShortcut)
} else if (script.shortcut) {
tags.push(friendlyShortcut(shortcutNormalizer(script.shortcut)))
}
if (script.kenv && script.kenv !== '.kit') {
tags.push(script.kenv)
}
if (script.trigger) tags.push(`trigger: ${script.trigger}`)
if (script.keyword) tags.push(`keyword: ${script.keyword}`)
if (script.snippet) tags.push(`snippet ${script.snippet}`)
if (script.expand) {
tags.push(`expand: ${script.expand}`)
}
if (typeof script.pass === 'string' && script.pass !== 'true') {
tags.push(script.pass.startsWith('/') ? `pattern: ${script.pass}` : `postfix: ${script.pass}`)
}
script.tag = tags.join(' ')
} }
export let getKenvFromPath = (filePath: string): string => { let normalizedPath = path.normalize(filePath) let normalizedKenvPath = path.normalize(kenvPath())
if (!normalizedPath.startsWith(normalizedKenvPath)) { return '' }
let relativePath = normalizedPath.replace(normalizedKenvPath, '') if (!relativePath.includes('kenvs')) { return '' }
let parts = relativePath.split(path.sep) let kenvIndex = parts.indexOf('kenvs') return kenvIndex !== -1 && parts[kenvIndex + 1] ? parts[kenvIndex + 1] : '' }
export let isScriptlet = (script: Script | Scriptlet): script is Scriptlet => { return 'scriptlet' in script }
export let isSnippet = (script: Script): script is Snippet => { return 'text' in script || script?.filePath?.endsWith('.txt') }
export let processPlatformSpecificTheme = (cssString: string): string => { const platform = process.platform const platformSuffix = platform === 'darwin' ? '-mac' : platform === 'win32' ? '-win' : '-other'
// Split the CSS string into lines const lines = cssString.split('\n')
// Process each line const processedLines = lines.map((line) => { // Check if the line contains a CSS variable if (line.includes('--') && line.includes(':')) { const parts = line.split(':') const variableName = parts[0].trim()
// Check if the variable ends with a platform suffix
if (variableName.endsWith('-mac') || variableName.endsWith('-win') || variableName.endsWith('-other')) {
// If it matches the current platform, remove the suffix
if (variableName.endsWith(platformSuffix)) {
return ` ${variableName.slice(0, -platformSuffix.length)}: ${parts[1].trim()}`
}
// If it doesn't match, remove the line
return null
}
}
// If it's not a platform-specific variable, keep the line as is
return line
})
// Join the processed lines, filtering out null values return processedLines.filter((line) => line !== null).join('\n') }
export let infoPane = (title: string, description?: string) => { return `
${title}
${description}
// TODO: Clean-up re-exports export { parseScript, commandFromFilePath, getShebangFromContents, iconFromKenv, parseFilePath, parseMetadata, postprocessMetadata } from './parser.js'
export { defaultGroupClassName, defaultGroupNameClassName, formatChoices } from './format.js'
export { groupChoices } from './group.js' export { parseScriptletsFromPath, parseMarkdownAsScriptlets, parseScriptlets } from './scriptlets.js'
export { getSnippet, parseSnippets } from './snippets.js'
export { createPathResolver, home, kitPath, kenvPath, kitPnpmPath, kitDotEnvPath } from './resolvers.js'
export { isBin, isFile, isJsh, isDir, isLinux, isMac, isWin } from './is.js' export { cmd, returnOrEnter, scriptsDbPath, timestampsPath, statsPath, prefsPath, promptDbPath, themeDbPath, userDbPath, tmpClipboardDir, tmpDownloadsDir, getMainScriptPath, kitDocsPath, KENV_SCRIPTS, KENV_APP, KENV_BIN, KIT_APP, KIT_APP_PROMPT, KIT_APP_INDEX, SHELL_TOOLS } from './constants.js'
import path from 'node:path' import { existsSync, lstatSync } from 'node:fs' import { readJson } from '../globals/fs-extra.js' import { readFile } from '../globals/fs.js' import * as os from 'node:os' import { pathToFileURL } from 'node:url' import * as JSONSafe from 'safe-stable-stringify' import { QuickScore, quickScore, createConfig, type Options, type ConfigOptions } from 'quick-score' import { formatDistanceToNow } from '../utils/date.js' import type { Action, Choice, FlagsObject, FlagsWithKeys, PromptConfig, ScoredChoice, Script, Scriptlet, Shortcut } from '../types/core' import { Channel, PROMPT } from '../core/enum.js'import { kitPath, kenvPath, resolveScriptToCommand, run, home, isFile, getKenvs, groupChoices, formatChoices, parseScript, processInBatches, highlight, md as mdUtil, tagger } from '../core/utils.js' import { getScripts, getScriptFromString, getUserJson, getTimestamps, type Stamp, setUserJson } from '../core/db.js'
import { default as stripAnsi } from 'strip-ansi'
import type { CallToolResult, Kenv } from '../types/kit' import type { Fields as TraceFields } from 'chrome-trace-event' import dotenv from 'dotenv' import type { kenvEnv } from '../types/env' import { getRecentLimit } from './recent.js'
global.__kitActionsMap = new Map<string, Action | Shortcut>()
export async function initTrace() { if (process.env.KIT_TRACE || (process.env.KIT_TRACE_DATA && !global?.trace?.enabled)) { let timestamp = Date.now() let { default: Trace } = await import('chrome-trace-event') let tracer = new Trace.Tracer({ noStream: true })
await ensureDir(kitPath('trace'))
let writeStream = createWriteStream(kitPath('trace', `trace-${timestamp}.json`))
tracer.pipe(writeStream)
const tidCache = new Map()
function updateFields(channel) {
let tid
if (channel) {
let cachedTid = tidCache.get(channel)
if (cachedTid === undefined) {
cachedTid = Object.entries(Channel).findIndex(([, value]) => value === channel)
tidCache.set(channel, cachedTid)
}
tid = cachedTid
}
return tid
}
function createTraceFunction(eventType: 'B' | 'E' | 'I') {
return function (fields: TraceFields) {
fields.tid = updateFields(fields?.channel) || 1
if (!process.env.KIT_TRACE_DATA) {
fields.args = undefined
}
return tracer.mkEventFunc(eventType)(fields)
}
}
global.trace = {
begin: createTraceFunction('B'),
end: createTraceFunction('E'),
instant: createTraceFunction('I'),
flush: () => {
tracer.flush()
},
enabled: true
}
global.trace.instant({
name: 'Init Trace',
args: {
timestamp
}
})
} }
global.trace ||= { begin: () => { }, end: () => { }, instant: () => { }, flush: () => { }, enabled: false }
global.isWin = os.platform().startsWith('win') global.isMac = os.platform().startsWith('darwin') global.isLinux = os.platform().startsWith('linux') global.cmd = global.isMac ? 'cmd' : 'ctrl'
let isErrored = false
export let errorPrompt = async (error: Error) => {
if (isErrored) {
return
}
isErrored = true
if (global.__kitAbandoned) {
let { name } = path.parse(global.kitScript)
let errorLog = path.resolve(path.dirname(path.dirname(global.kitScript)), 'logs', ${name}.log)
await appendFile(errorLog, `\nAbandonned. Exiting...`)
exit()
}
if (process.env.KIT_CONTEXT === 'app') {
global.warn(☠️ ERROR PROMPT SHOULD SHOW ☠️)
// Use the new formatter for better error handling
const { SourcemapErrorFormatter } = await import('../core/sourcemap-formatter.js')
const formattedError = SourcemapErrorFormatter.formatError(error)
global.warn(formattedError.stack)
let errorFile = global.kitScript
let line = 1
let col = 1
// Extract location using the formatter
const errorLocation = SourcemapErrorFormatter.extractErrorLocation(error)
if (errorLocation) {
errorFile = errorLocation.file
line = errorLocation.line
col = errorLocation.column
}
let script = global.kitScript.replace(/.*\//, '')
let errorToCopy = formattedError.stack
let dashedDate = () => new Date().toISOString().replace('T', '-').replace(/:/g, '-').split('.')[0]
let errorJsonPath = global.tmp(`error-${dashedDate()}.txt`)
await global.writeFile(errorJsonPath, errorToCopy)
try {
if (global?.args.length > 0) {
log({ args })
args = []
}
global.warn(`Running error action because of`, {
script,
error: formattedError.message
})
await run(kitPath('cli', 'error-action.js'), script, errorJsonPath, errorFile, String(line), String(col))
} catch (error) {
global.warn(error)
}
} else { global.console.log(error) } }
export let outputTmpFile = async (fileName: string, contents: string) => { let outputPath = path.resolve(os.tmpdir(), 'kit', fileName) await outputFile(outputPath, contents) return outputPath }
export let copyTmpFile = async (fromFile: string, fileName: string) => await outputTmpFile(fileName, await global.readFile(fromFile, 'utf-8'))
export let buildWidget = async (scriptPath, outPath = '') => { let outfile = outPath || scriptPath
let templateContent = await readFile(kenvPath('templates', 'widget.html'), 'utf8')
let REACT_PATH = kitPath('node_modules', 'react', 'index.js') let REACT_DOM_PATH = kitPath('node_modules', 'react-dom', 'index.js')
let REACT_CONTENT = ` let { default: React } = await import( kitPath("node_modules", "react", "umd", "react.development.js") ) let { default: ReactDOM } = await import( kitPath("node_modules", "react-dom", "umd", "react-dom.deveolpment.js") )
let __renderToString = (x, y, z)=> Server.renderToString(React.createElement(x, y, z))
`
let templateCompiler = compile(templateContent) let result = templateCompiler({ REACT_PATH, REACT_DOM_PATH, REACT_CONTENT })
let contents = await readFile(outfile, 'utf8')
await writeFile(outfile, result) }
let getMissingPackages = (e: string): string[] => { let missingPackage = [] if (e.includes('Cannot find package')) { missingPackage = e.match(/(?<=Cannot find package ['"]).(?=['"])/g) } else if (e.includes('Could not resolve')) { missingPackage = e.match(/(?<=Could not resolve ['"]).(?=['"])/g) } else if (e.includes('Cannot find module')) { missingPackage = e.match(/(?<=Cannot find module ['"]).*(?=['"])/g) }
return (missingPackage || []).map((s) => s.trim()).filter(Boolean) }
global.attemptImport = async (scriptPath, ..._args) => { let cachedArgs = args.slice(0) let importResult = undefined try { global.updateArgs(_args)
let href = pathToFileURL(scriptPath).href
let kitImport = `${href}?now=${Date.now()}.kit`
importResult = await import(kitImport)
} catch (error) { let e = error.toString() global.warn(e) if (process.env.KIT_CONTEXT === 'app') { await errorPrompt(error) } else { throw error } }
return importResult }
global.silentAttemptImport = async (scriptPath, ..._args) => { let cachedArgs = args.slice(0) let importResult = undefined try { global.updateArgs(_args)
let href = pathToFileURL(scriptPath).href
let kitImport = `${href}?now=${Date.now()}.kit`
importResult = await import(kitImport)
} catch (error) { }
return importResult }
global.__kitAbandoned = false global.send = (channel: Channel, value?: any) => { if (global.__kitAbandoned) return null if (process?.send) { try { let payload = { pid: process.pid, promptId: global.__kitPromptId, kitScript: global.kitScript, channel, value }
global.trace.instant({
name: `Send ${channel}`,
channel,
args: payload
})
process.send(payload)
} catch (e) {
global.warn(e)
}
} else { // console.log(from, ...args) } }
global.sendResponse = (body: any, headers: Record<string, string> = {}) => { let statusCode = 200 if (headers['Status-Code']) { statusCode = Number.parseInt(headers['Status-Code'], 10) headers['Status-Code'] = undefined }
const responseHeaders = { ...headers } if (!responseHeaders['Content-Type']) { responseHeaders['Content-Type'] = 'application/json' }
const response = { body, statusCode, headers: responseHeaders }
return global.sendWait(Channel.RESPONSE, response) }
// Import sendResult implementation import { sendResult as sendResultImpl } from './send-result.js'
// Assign to global with the implementation global.sendResult = sendResultImpl
// Import and export params function import { params } from './params.js' global.params = params
let _consoleLog = global.console.log.bind(global.console) let _consoleWarn = global.console.warn.bind(global.console) let _consoleClear = global.console.clear.bind(global.console) let _consoleError = global.console.error.bind(global.console) let _consoleInfo = global.console.info.bind(global.console)
global.log = (...args) => { if (process?.send && process.env.KIT_CONTEXT === 'app') { global.send(Channel.KIT_LOG, args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')) } else { _consoleLog(...args) } } global.warn = (...args) => { if (process?.send && process.env.KIT_CONTEXT === 'app') { global.send(Channel.KIT_WARN, args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')) } else { _consoleWarn(...args) } } global.clear = () => { if (process?.send && process.env.KIT_CONTEXT === 'app') { global.send(Channel.KIT_CLEAR) } else { _consoleClear() } }
if (process?.send && process.env.KIT_CONTEXT === 'app') { global.console.log = (...args) => { let log = args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')
global.send(Channel.CONSOLE_LOG, log)
}
global.console.warn = (...args) => { let warn = args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')
if (process?.send && process.env.KIT_CONTEXT === 'app') {
global.send(Channel.CONSOLE_WARN, warn)
} else {
_consoleWarn(...args)
}
}
global.console.clear = () => { if (process?.send && process.env.KIT_CONTEXT === 'app') { global.send(Channel.CONSOLE_CLEAR) } else { _consoleClear() } }
global.console.error = (...args) => { let error = args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')
if (process?.send && process.env.KIT_CONTEXT === 'app') {
global.send(Channel.CONSOLE_ERROR, error)
} else {
_consoleError(...args)
}
}
global.console.info = (...args) => { let info = args.map((a) => (typeof a !== 'string' ? JSONSafe.stringify(a) : a)).join(' ')
if (process?.send && process.env.KIT_CONTEXT === 'app') {
global.send(Channel.CONSOLE_INFO, info)
} else {
_consoleInfo(...args)
}
} }
global.dev = async (data) => { await global.sendWait(Channel.DEV_TOOLS, data) } global.devTools = global.dev
global.showImage = async (html, options) => {
await global.widget(
md(## \showImage` is Deprecated
Please use the new `widget` function instead.
#745 `) ) // global.send(Channel.SHOW, { options, html }) }
global.setPlaceholder = async (text) => { await global.sendWait(Channel.SET_PLACEHOLDER, stripAnsi(text)) }
global.setEnter = async (text) => { await global.sendWait(Channel.SET_ENTER, text) }
global.main = async (scriptPath: string, ..._args) => { let kitScriptPath = kitPath('main', scriptPath) + '.js' return await global.attemptImport(kitScriptPath, ..._args) }
global.lib = async (lib: string, ..._args) => { let libScriptPath = path.resolve(global.kitScript, '..', '..', 'lib', lib) + '.js' return await global.attemptImport(libScriptPath, ..._args) }
global.cli = async (cliPath, ..._args) => { let cliScriptPath = kitPath('cli', cliPath) + '.js'
return await global.attemptImport(cliScriptPath, ..._args) }
global.setup = async (setupPath, ..._args) => {
global.setPlaceholder(>_ setup: ${setupPath}...)
let setupScriptPath = kitPath('setup', setupPath) + '.js'
return await global.attemptImport(setupScriptPath, ..._args)
}
global.kenvTmpPath = (...parts) => { let command = resolveScriptToCommand(global.kitScript) let scriptTmpDir = kenvPath('tmp', command, ...parts)
mkdir('-p', path.dirname(scriptTmpDir)) return scriptTmpDir }
export let tmpPath = (...parts: string[]) => { let command = global?.kitScript ? resolveScriptToCommand(global.kitScript) : ''
let tmpCommandDir = path.resolve(os.tmpdir(), 'kit', command)
let scriptTmpDir = path.resolve(tmpCommandDir, ...parts)
let kenvTmpCommandPath = kenvPath('tmp', command)
global.ensureDirSync(tmpCommandDir) // symlink to kenvPath("command") // Check if tmpCommandDir exists and is not a symlink before creating the symlink if (!existsSync(kenvTmpCommandPath) || lstatSync(kenvTmpCommandPath).isSymbolicLink()) { global.ensureSymlinkSync(tmpCommandDir, kenvTmpCommandPath) }
return scriptTmpDir }
global.tmpPath = tmpPath /**
- @deprecated use
tmpPathinstead */ global.tmp = global.tmpPath global.inspect = async (data, fileName) => { let dashedDate = () => new Date().toISOString().replace('T', '-').replace(/:/g, '-').split('.')[0]
let formattedData = data let tmpFullPath = ''
if (typeof data !== 'string') { formattedData = JSONSafe.stringify(data, null, '\t') }
if (fileName) {
tmpFullPath = tmpPath(fileName)
} else if (typeof data === 'object') {
tmpFullPath = tmpPath(${dashedDate()}.json)
} else {
tmpFullPath = tmpPath(${dashedDate()}.txt)
}
await global.writeFile(tmpFullPath, formattedData)
await global.edit(tmpFullPath) }
global.compileTemplate = async (template, vars) => { let templateContent = await global.readFile(kenvPath('templates', template), 'utf8') let templateCompiler = global.compile(templateContent) return templateCompiler(vars) }
global.currentOnTab = null global.onTabs = [] global.onTabIndex = 0 global.onTab = (name, tabFunction) => { let fn = async (...args) => { await tabFunction(...args) } global.onTabs.push({ name, fn }) if (global.flag?.tab) { if (global.flag?.tab === name) { let tabIndex = global.onTabs.length - 1 global.onTabIndex = tabIndex global.send(Channel.SET_TAB_INDEX, tabIndex) global.currentOnTab = fn() } } else if (global.onTabs.length === 1) { global.onTabIndex = 0 global.send(Channel.SET_TAB_INDEX, 0) global.currentOnTab = fn() } }
global.kitPrevChoices = []
global.groupChoices = groupChoices global.formatChoices = formatChoices
global.addChoice = async (choice: string | Choice) => { if (typeof choice !== 'object') { choice = { name: String(choice), value: String(choice) } }
choice.id ||= global.uuid() return await global.sendWait(Channel.ADD_CHOICE, choice) }
global.appendChoices = async (choices: string[] | Choice[]) => { return await global.sendWait(Channel.APPEND_CHOICES, choices) }
// TODO: Add an option to avoid sorting global.createChoiceSearch = async ( choices: Choice[], config: Partial<Omit<Options, 'keys'> & ConfigOptions & { keys: string[] }> = { minimumScore: 0.3, maxIterations: 3, keys: ['name'] } ) => { if (!config?.minimumScore) config.minimumScore = 0.3 if (!config?.maxIterations) config.maxIterations = 3 if (config?.keys && Array.isArray(config.keys)) { config.keys = config.keys.map((key) => { if (key === 'name') return 'slicedName' if (key === 'description') return 'slicedDescription' return key }) }
let formattedChoices = await global.___kitFormatChoices(choices) function scorer(string: string, query: string, matches: number[][]) { return quickScore(string, query, matches as any, undefined, undefined, createConfig(config)) }
const keys = (config?.keys || ['slicedName']).map((name) => ({ name, scorer }))
let qs = new QuickScore(formattedChoices, { keys, ...config })
return (query: string) => { let result = qs.search(query) as ScoredChoice[] if (result.find((c) => c?.item?.group)) { let createScoredChoice = (item: Choice): ScoredChoice => { return { item, score: 0, matches: {}, _: '' } } const groups: Set = new Set() const keepGroups: Set = new Set() const filteredBySearch: ScoredChoice[] = []
// Build a map for constant time access
const resultMap = new Map(result.map((r) => [r.item.id, r]))
for (const choice of formattedChoices) {
if (choice?.skip) {
const scoredSkip = createScoredChoice(choice)
filteredBySearch.push(scoredSkip)
if (choice?.group) groups.add(choice.group)
} else {
const scored = resultMap.get(choice?.id)
if (scored) {
filteredBySearch.push(scored)
if (choice?.group && groups.has(choice.group)) {
keepGroups.add(choice.group)
}
}
}
}
result = filteredBySearch.filter((sc) => {
if (sc?.item?.skip) {
if (!keepGroups.has(sc?.item?.group)) return false
}
return true
})
}
return result
} }
global.setScoredChoices = async (choices: ScoredChoice[]) => { return await global.sendWait(Channel.SET_SCORED_CHOICES, choices) }
global.___kitFormatChoices = async (choices, className = '') => { if (!Array.isArray(choices)) { return choices } let formattedChoices = formatChoices(choices, className) let { __currentPromptConfig } = global as any let { shortcuts: globalShortcuts } = __currentPromptConfig || {}
if (globalShortcuts && choices?.[0]) { let shortcuts = globalShortcuts.filter((shortcut) => { if (shortcut?.condition) { return shortcut.condition(choices?.[0]) } return true })
global.send(Channel.SET_SHORTCUTS, shortcuts)
} global.kitPrevChoices = formattedChoices
global.setLoading(false) return formattedChoices }
global.setChoices = async (choices, config) => { let formattedChoices = await global.___kitFormatChoices(choices, config?.className || '') global.send(Channel.SET_CHOICES, { choices: formattedChoices, skipInitialSearch: config?.skipInitialSearch, inputRegex: config?.inputRegex || '', generated: Boolean(config?.generated) })
performance.measure('SET_CHOICES', 'run') }
global.flag ||= {} global.prepFlags = (flagsOptions: FlagsObject): FlagsObject => { for (let key of Object.keys(global?.flag)) { delete global?.flag?.[key] }
if (!flagsOptions || Object.entries(flagsOptions)?.length === 0) { return false }
let validFlags = { sortChoicesKey: (flagsOptions as FlagsWithKeys)?.sortChoicesKey || [], order: (flagsOptions as FlagsWithKeys)?.order || [] } let currentFlags = Object.entries(flagsOptions) for (let [key, value] of currentFlags) { if (key === 'order') continue if (key === 'sortChoicesKey') continue
// Strip non-serializable functions before sending to avoid IPC serialization issues
const { onAction, condition, preview, ...rest } = value ?? {}
let validFlag = {
...rest,
name: value?.name || key,
shortcut: value?.shortcut || '',
description: value?.description || '',
value: key,
bar: value?.bar || '',
preview: typeof preview === 'string' ? preview : '',
hasAction: Boolean(onAction)
}
validFlags[key] = validFlag
if (value?.group) {
validFlags[key].group = value.group
}
}
for (const [key, value] of currentFlags) { if (key === 'order') continue if (key === 'sortChoicesKey') continue const choice = { id: key, name: value?.name || key, value: key, description: value?.description || '', preview: value?.preview || '
', shortcut: value?.shortcut || '', onAction: value?.onAction || null } as Choiceif (value?.group) {
choice.group = value.group
}
// Use the flag key (not the display name) as the map key, since that's what the UI sends back
global.__kitActionsMap.set(key, choice)
// console.log(`[SDK] Storing action in map with key: ${key}`, {
// name: choice.name,
// onAction: typeof value?.onAction,
// shortcut: choice.shortcut
// })
}
return validFlags }
global.setFlags = async (flags: FlagsObject, options = {}) => {
let flagsMessage = {
flags: global.prepFlags(flags),
options: {
name: options?.name || '',
placeholder: options?.placeholder || '',
active: options?.active || 'Actions'
}
}
// TODO: Move props from FlagsObject like "order", "sortChoicesKey" to the options
// console.log([SDK] Sending flags to app:, {
// flagKeys: Object.keys(flagsMessage.flags),
// flagsWithHasAction: Object.entries(flagsMessage.flags).map(([k, v]) => ({
// key: k,
// name: (v as any).name,
// hasAction: (v as any).hasAction,
// shortcut: (v as any).shortcut
// }))
// })
await global.sendWait(Channel.SET_FLAGS, flagsMessage)
}
function sortArrayByIndex(arr) { const sortedArr = [] const indexedItems = []
// Separate indexed items from non-indexed items arr.forEach((item, i) => { if (item.hasOwnProperty('index')) { indexedItems.push({ item, index: item.index }) } else { sortedArr.push(item) } })
// Sort indexed items based on their index indexedItems.sort((a, b) => a.index - b.index)
// Insert indexed items into the sorted array at their respective positions for (const { item, index } of indexedItems) { sortedArr.splice(index, 0, item) }
return sortedArr }
export let getFlagsFromActions = (actions: PromptConfig['actions']) => { let flags: FlagsObject = {} let indices = new Set() for (let a of actions as Action[]) { if (a?.index) { indices.add(a.index) } } let groups = new Set() if (Array.isArray(actions)) { const sortedActions = sortArrayByIndex(actions) for (let i = 0; i < sortedActions.length; i++) { let action = sortedActions[i] if (typeof action === 'string') { action = { name: action, flag: action } } if (action?.group) { groups.add(action.group) }
let flagAction = {
flag: action.flag || action.name,
index: i,
close: true,
...action,
hasAction: !!action?.onAction,
bar: action?.visible ? 'right' : ''
} as Action
flags[action.flag || action.name] = flagAction
}
}
flags.sortChoicesKey = Array.from(groups).map((g) => 'index')
return flags }
global.setActions = async (actions: Action[], options = {}) => { let flags = getFlagsFromActions(actions) await setFlags(flags, options) }
global.openActions = async () => { await sendWait(Channel.OPEN_ACTIONS) }
global.closeActions = async () => { await sendWait(Channel.CLOSE_ACTIONS) }
global.setFlagValue = (value: any) => { return global.sendWait(Channel.SET_FLAG_VALUE, value) }
global.hide = async (hideOptions = {}) => { await global.sendWait(Channel.HIDE_APP, hideOptions) if (process.env.KIT_HIDE_DELAY) { await wait(-process.env.KIT_HIDE_DELAY) } }
global.show = async () => { await global.sendWait(Channel.SHOW_APP) }
global.blur = async () => { await global.sendWait(Channel.BLUR_APP, {}) }
global.run = run
let wrapCode = (html: string, containerClass: string, codeStyles = '') => { return `
<style type="text/css">
code{
font-size: 0.9rem !important;
width: 100%;
${codeStyles}
}
pre{
display: flex;
}
p{
margin-bottom: 1rem;
}
</style>
${html.trim()}
`
}
let getLanguage = (language: string) => { if (language.includes('python')) return 'python' if (language.includes('ruby')) return 'ruby' if (language.includes('php')) return 'php' if (language.includes('perl')) return 'perl'
switch (language) { case 'node': language = 'javascript' break
case 'sh':
case 'zsh':
language = 'bash'
break
case 'irb':
language = 'ruby'
break
case 'raku':
case 'perl6':
language = 'perl'
break
case 'ps1':
case 'pwsh':
language = 'powershell'
break
case 'tclsh':
language = 'tcl'
break
case 'erl':
case 'escript':
language = 'erlang'
break
case 'iex':
language = 'elixir'
break
case 'rscript':
case 'r':
language = 'r'
break
case 'ghci':
case 'hugs':
language = 'haskell'
break
default:
// If the language is not recognized or already has the correct syntax, leave it as is.
break
}
return language }
export let highlightJavaScript = async (filePath: string, shebang = ''): Promise => { let isPathAFile = await isFile(filePath) let contents = `` if (isPathAFile) { contents = await readFile(filePath, 'utf8') } else { contents = filePath.trim() }
let { default: highlight } = global.__kitHighlight || (await import('highlight.js')) if (!global.__kitHighlight) global.__kitHighlight = { default: highlight } let highlightedContents = `` if (shebang) { // split shebang into command and args let [command, ...shebangArgs] = shebang.split(' ')
let language = command.endsWith('env') ? shebangArgs?.[0] : command.split('/').pop() || 'bash'
language = getLanguage(language)
highlightedContents = highlight.highlight(contents, {
language
}).value
} else { highlightedContents = highlight.highlight(contents, { language: 'javascript' }).value }
let wrapped = wrapCode(highlightedContents, 'px-5') return wrapped }
let order = [ 'Script Actions', 'New', 'Copy', 'Debug', 'Kenv', 'Git', 'Share', 'Export', // "DB", 'Run' ]
export let actions: Action[] = [
// {
// name: "New Menu",
// key: ${cmd}+shift+n,
// onPress: async () => {
// await run(kitPath("cli", "new-menu.js"))
// },
// },
{
name: 'New Script',
description: 'Create a new script',
shortcut: ${cmd}+n,
onAction: async () => {
await run(kitPath('cli', 'new.js'))
},
group: 'New'
},
{
name: 'Generate Script with AI',
description: 'Generate a new script with AI',
shortcut: ${cmd}+g,
onAction: async () => {
await run(kitPath('cli', 'generate.js'))
},
group: 'New'
},
{
name: 'New Scriptlet',
description: 'Create a new scriptlet',
shortcut: ${cmd}+shift+n,
onAction: async () => {
await run(kitPath('cli', 'new-scriptlet.js'))
},
group: 'New'
},
{
name: 'New Snippet',
description: 'Create a new snippet',
shortcut: ${cmd}+opt+n,
onAction: async () => {
await run(kitPath('cli', 'new-snippet.js'))
},
group: 'New'
},
{
name: 'New Theme',
description: 'Create a new theme',
onAction: async () => {
await run(kitPath('cli', 'new-theme.js'))
},
group: 'New'
},
{
name: 'Sign In',
description: 'Log in to GitHub to Script Kit',
flag: 'sign-in-to-script-kit',
shortcut: ${cmd}+shift+opt+s,
onAction: async () => {
await run(kitPath('main', 'account-v2.js'))
},
group: 'Settings'
},
{
name: 'List Processes',
description: 'List running processes',
shortcut: ${cmd}+p,
onAction: async () => {
let processes = await getProcesses()
if (processes.filter((p) => p?.scriptPath)?.length > 1) {
await run(kitPath('cli', 'processes.js'))
} else {
toast('No running processes found...')
}
},
group: 'Debug'
},
{
name: 'Find Script',
description: 'Search for a script by contents',
shortcut: ${cmd}+f,
onAction: async () => {
// Don't clear all flags - preserve existing action flags
// global.setFlags({})
await run(kitPath('cli', 'find.js'))
},
group: 'Script Actions'
},
{
name: 'Reset Prompt',
shortcut: ${cmd}+0,
onAction: async () => {
await run(kitPath('cli', 'kit-clear-prompt.js'))
},
group: 'Script Actions'
},
// TODO: Figure out why setFlags is being called twice and overridden here
// {
// name: "Share",
// description: "Share {{name}}",
// shortcut: ${cmd}+s,
// condition: c => !c.needsDebugger,
// onAction: async (input, { focused }) => {
// let shareFlags = {}
// for (let [k, v] of Object.entries(scriptFlags)) {
// if (k.startsWith("share")) {
// shareFlags[k] = v
// delete shareFlags[k].group
// }
// }
// await setFlags(shareFlags)
// await setFlagValue(focused?.value)
// },
// group: "Script Actions",
// },
{
name: 'Debug',
shortcut: ${cmd}+enter,
condition: (c) => c.needsDebugger,
onAction: async (input, { focused }) => {
flag.cmd = true
submit(focused)
},
group: 'Debug'
},
{
name: 'Support',
shortcut: ${cmd}+i,
close: false,
onAction: async () => {
let userJson = await getUserJson()
let loggedIn = userJson?.login
let helpActions: Action[] = [
...(loggedIn
? [
{
name: 'Sign Out',
description: 'Sign out of Script Kit',
onAction: async () => {
await deauthenticate()
}
}
]
: [
{
name: 'Sign In',
description: 'Sign in to Script Kit',
onAction: async () => {
await run(kitPath('main', 'account-v2.js'))
}
}
]),
{
name: 'Read Docs',
description: 'Read the docs',
onAction: async () => {
await open('https://scriptkit.com/docs')
exit()
}
},
{
name: 'Ask a Question',
description: 'Open GitHub Discussions',
onAction: async () => {
await open(https://github.com/johnlindquist/kit/discussions)
exit()
}
},
{
name: 'Report a Bug',
description: 'Open GitHub Issues',
onAction: async () => {
await open(https://github.com/johnlindquist/kit/issues)
exit()
}
},
{
name: 'Join Discord Server',
description: 'Hang out on Discord',
onAction: async () => {
let response = await get('https://scriptkit.com/api/discord-invite')
await open(response.data)
exit()
}
}
]
await setActions(helpActions, {
name: Script Kit ${process.env.KIT_APP_VERSION},
placeholder: 'Support',
active: 'Script Kit Support'
})
openActions()
},
group: 'Support'
}
]
export let modifiers = { cmd: 'cmd', shift: 'shift', opt: 'opt', ctrl: 'ctrl' }
export let scriptFlags: FlagsObject = {
order,
sortChoicesKey: order.map((o) => ''),
// open: {
// name: "Script Actions",
// description: "Open {{name}} in your editor",
// shortcut: ${cmd}+o,
// action: "right",
// },
// ["new-menu"]: {
// name: "New",
// description: "Create a new script",
// shortcut: ${cmd}+n,
// action: "left",
// },
['edit-script']: {
name: 'Edit',
shortcut: ${cmd}+o,
group: 'Script Actions',
description: 'Open {{name}} in your editor',
preview: async (input, state) => {
let flaggedFilePath = state?.flaggedValue?.filePath
if (!flaggedFilePath) return
// Get last modified time
let { size, mtime, mtimeMs } = await stat(flaggedFilePath)
let lastModified = new Date(mtimeMs)
let stamps = await getTimestamps()
let stamp = stamps.stamps.find((s) => s.filePath === flaggedFilePath)
let composeBlock = (...lines) => lines.filter(Boolean).join('\n')
let compileMessage = stamp?.compileMessage?.trim() || ''
let compileStamp = stamp?.compileStamp
? `Last compiled: ${formatDistanceToNow(new Date(stamp?.compileStamp), {
addSuffix: false
})} ago`
: ''
let executionTime = stamp?.executionTime ? `Last run duration: ${stamp?.executionTime}ms` : ''
let runCount = stamp?.runCount ? `Run count: ${stamp?.runCount}` : ''
let compileBlock = composeBlock(compileMessage && `* ${compileMessage}`, compileStamp && `* ${compileStamp}`)
if (compileBlock) {
compileBlock = `### Compile Info\n${compileBlock}`.trim()
}
let executionBlock = composeBlock(runCount && `* ${runCount}`, executionTime && `* ${executionTime}`)
if (executionBlock) {
executionBlock = `### Execution Info\n${executionBlock}`.trim()
}
let lastRunBlock = ''
if (stamp) {
let lastRunDate = new Date(stamp.timestamp)
lastRunBlock = `### Last Run
-
${lastRunDate.toLocaleString()}
-
${formatDistanceToNow(lastRunDate, { addSuffix: false })} ago `.trim() }
let modifiedBlock = `### Last Modified
-
${lastModified.toLocaleString()}
-
${formatDistanceToNow(lastModified, { addSuffix: false })} ago`
let info = md( `# Stats
${flaggedFilePath}
${compileBlock}
${executionBlock}
${modifiedBlock}
${lastRunBlock}
.trim() ) return info } }, [cmd]: { group: 'Debug', name: 'Debug Script', description: 'Open inspector. Pause on debugger statements.', shortcut: ${cmd}+enter`,
flag: cmd
},
[modifiers.opt]: {
group: 'Debug',
name: 'Open Log Window',
description: 'Open a log window for {{name}}',
shortcut: 'alt+enter',
flag: modifiers.opt
},
'push-script': {
group: 'Git',
name: 'Push to Git Repo',
description: 'Push {{name}} to a git repo'
},
'pull-script': {
group: 'Git',
name: 'Pull from Git Repo',
description: 'Pull {{name}} from a git repo'
},
'edit-doc': {
group: 'Script Actions',
name: 'Create/Edit Doc',
shortcut: ${cmd}+.,
description: "Open {{name}}'s markdown in your editor"
},
'share-script-to-scriptkit': {
group: 'Share',
name: 'Share to ScriptKit.com',
description: 'Share {{name}} to the community script library',
shortcut: ${cmd}+s
},
'share-script-as-discussion': {
group: 'Share',
name: 'Post to Community Scripts',
description: 'Share {{name}} on GitHub Discussions',
shortcut: ${cmd}+opt+s
},
'share-script-as-link': {
group: 'Share',
name: 'Create Install URL',
description: 'Create a link which will install the script',
shortcut: ${cmd}+shift+s
},
'share-script-as-kit-link': {
group: 'Share',
name: 'Share as private kit:// link',
description: 'Create a private link which will install the script'
},
'share-script': {
group: 'Share',
name: 'Share as Gist',
description: 'Share {{name}} as a gist'
},
'share-script-as-markdown': {
group: 'Share',
name: 'Share as Markdown',
description: 'Copies script contents in fenced JS Markdown'
},
'share-copy': {
group: 'Copy',
name: 'Copy script contents to clipboard',
description: 'Copy script contents to clipboard',
shortcut: ${cmd}+c
},
'copy-path': {
group: 'Copy',
name: 'Copy Path',
description: 'Copy full path of script to clipboard'
},
'paste-as-markdown': {
group: 'Copy',
name: 'Paste as Markdown',
description: 'Paste the contents of the script as Markdown',
shortcut: ${cmd}+shift+p
},
duplicate: {
group: 'Script Actions',
name: 'Duplicate',
description: 'Duplicate {{name}}',
shortcut: ${cmd}+d
},
rename: {
group: 'Script Actions',
name: 'Rename',
description: 'Rename {{name}}',
shortcut: ${cmd}+shift+r
},
remove: {
group: 'Script Actions',
name: 'Remove',
description: 'Delete {{name}}',
shortcut: ${cmd}+shift+backspace
},
'remove-from-recent': {
group: 'Script Actions',
name: 'Remove from Recent',
description: 'Remove {{name}} from the recent list'
},
'clear-recent': {
group: 'Script Actions',
name: 'Clear Recent',
description: 'Clear the recent list of scripts'
},
// ["open-script-database"]: {
// group: "DB",
// name: "Open Database",
// description: "Open the db file for {{name}}",
// shortcut: ${cmd}+b,
// },
// ["clear-script-database"]: {
// group: "DB",
// name: "Delete Database",
// description:
// "Delete the db file for {{name}}",
// },
'reveal-script': {
group: 'Script Actions',
name: 'Reveal',
description: Reveal {{name}} in ${isMac ? 'Finder' : 'Explorer'},
shortcut: ${cmd}+shift+f
},
'kenv-term': {
group: 'Kenv',
name: 'Open Script Kenv in a Terminal',
description: "Open {{name}}'s kenv in a terminal"
},
'kenv-trust': {
group: 'Kenv',
name: 'Trust Script Kenv',
description: "Trust {{name}}'s kenv"
},
'kenv-view': {
group: 'Kenv',
name: 'View Script Kenv',
description: "View {{name}}'s kenv"
},
'kenv-visit': {
group: 'Kenv',
name: 'Open Script Repo',
description: "Visit {{name}}'s kenv in your browser"
},
// ["share"]: {
// name: "Share",
// description: "Share {{name}}",
// shortcut: ${cmd}+s,
// bar: "right",
// },
// ["share-script"]: {
// name: "Share as Gist",
// description: "Share {{name}} as a gist",
// shortcut: ${cmd}+g,
// },
// ["share-script-as-kit-link"]: {
// name: "Share as kit:// link",
// description:
// "Create a link which will install the script",
// shortcut: "option+s",
// },
// ["share-script-as-link"]: {
// name: "Share as URL",
// description:
// "Create a URL which will install the script",
// shortcut: ${cmd}+u,
// },
// ["share-script-as-discussion"]: {
// name: "Share as GitHub Discussion",
// description:
// "Copies shareable info to clipboard and opens GitHub Discussions",
// },
// ["share-script-as-markdown"]: {
// name: "Share as Markdown",
// description:
// "Copies script contents in fenced JS Markdown",
// shortcut: ${cmd}+m,
// },
'change-shortcut': {
group: 'Script Actions',
name: 'Change Shortcut',
description: 'Prompts to pick a new shortcut for the script'
},
move: {
group: 'Kenv',
name: 'Move Script to Kenv',
description: 'Move the script between Kit Environments'
},
'stream-deck': {
group: 'Export',
name: 'Prepare Script for Stream Deck',
description: 'Create a .sh file around the script for Stream Decks'
},
'open-script-log': {
group: 'Debug',
name: 'Open Log File',
description: 'Open the log file for {{name}}',
shortcut: ${cmd}+l
},
[modifiers.shift]: {
group: 'Run',
name: 'Run script w/ shift flag',
shortcut: 'shift+enter',
flag: 'shift'
},
[modifiers.ctrl]: {
group: 'Run',
name: 'Run script w/ ctrl flag',
shortcut: 'ctrl+enter',
flag: 'ctrl'
},
settings: {
group: 'Settings',
name: 'Settings',
description: 'Open the settings menu',
shortcut: ${cmd}+,
}
}
export function buildScriptConfig(message: string | PromptConfig): PromptConfig { let scriptsConfig = typeof message === 'string' ? { placeholder: message } : message scriptsConfig.scripts = true scriptsConfig.resize = false scriptsConfig.enter ||= 'Select' scriptsConfig.preventCollapse = true return scriptsConfig }
async function getScriptResult(script: Script | string, message: string | PromptConfig): Promise<Script> { if (typeof script === 'string' && (typeof message === 'string' || message?.strict === true)) { return await getScriptFromString(script) } return script as Script //hmm... }
export let getApps = async () => { let { choices } = await readJson(kitPath('db', 'apps.json')).catch((error) => ({ choices: [] }))
if (choices.length === 0) { return [] }
let groupedApps = choices.map((c) => { c.group = 'Apps' return c })
return groupedApps }
export let splitEnvVarIntoArray = (envVar: string | undefined, fallback: string[]) => { return envVar ? envVar .split(',') .map((s) => s.trim()) .filter(Boolean) : fallback }
let groupScripts = (scripts) => { let excludeGroups = global?.env?.KIT_EXCLUDE_KENVS?.split(',').map((k) => k.trim()) || []
return groupChoices(scripts, { groupKey: 'kenv', missingGroupName: 'Main', order: splitEnvVarIntoArray(process?.env?.KIT_MAIN_ORDER, ['Favorite', 'Main', 'Scriptlets', 'Kit']),
endOrder: splitEnvVarIntoArray(process?.env?.KIT_MAIN_END_ORDER, ['Apps', 'Pass']),
recentKey: 'timestamp',
excludeGroups,
recentLimit: getRecentLimit(),
hideWithoutInput: splitEnvVarIntoArray(process?.env?.KIT_HIDE_WITHOUT_INPUT, []),
tagger
}) }
let processedScripts = [] export let getProcessedScripts = async (fromCache = true) => { if (fromCache && global.__kitScriptsFromCache && processedScripts.length) { return processedScripts }
trace.begin({ name: 'getScripts' }) let scripts: Script[] = await getScripts(fromCache) trace.end({ name: 'getScripts' })
trace.begin({ name: 'getTimestamps' }) let timestampsDb = await getTimestamps() trace.end({ name: 'getTimestamps' })
global.__kitScriptsFromCache = true
trace.begin({ name: 'processedScripts = await Promise.all' }) processedScripts = await processInBatches(scripts.map(processScript(timestampsDb.stamps)), 100)
trace.end({ name: 'processedScripts = await Promise.all' })
return processedScripts }
export let getGroupedScripts = async (fromCache = true) => { trace.begin({ name: 'getProcessedScripts' }) let processedscripts = await getProcessedScripts(fromCache) trace.end({ name: 'getProcessedScripts' })
let apps = (await getApps()).map((a) => { a.ignoreFlags = true return a }) if (apps.length) { processedscripts = processedscripts.concat(apps) }
let kitScripts = [ // kitPath("cli", "new.js"), kitPath('cli', 'generate.js'), kitPath('cli', 'new-menu.js'), kitPath('cli', 'new-scriptlet.js'), kitPath('cli', 'new-snippet.js'), kitPath('cli', 'new-theme.js'), kitPath('cli', 'share.js'), kitPath('cli', 'find.js'), // kitPath('main', 'docs.js'), kitPath('main', 'kit.js'), kitPath('cli', 'processes.js'), kitPath('cli', 'kenv-manage.js'), kitPath('main', 'kit-windows.js'), kitPath('main', 'file-search.js') // kitPath("main", "google.js"), ]
if (global?.env?.KIT_LOGIN) { kitScripts.push(kitPath('main', 'account-v2.js')) } else { kitScripts.push(kitPath('main', 'sign-in.js')) }
if (global?.env?.KIT_PRO !== 'true') { kitScripts.push(kitPath('main', 'sponsor.js')) }
kitScripts = kitScripts.concat([ kitPath("main", "docs.js"), // kitPath("main", "api.js"), // kitPath("main", "guide.js"), // kitPath("main", "tips.js"), // kitPath("main", "suggest.js"), kitPath('main', 'datamuse.js'), kitPath('main', 'giphy.js'), kitPath('main', 'browse.js'), kitPath('main', 'app-launcher.js'), // kitPath("main", "account.js"), kitPath('main', 'dev.js'), // kitPath('main', 'hot.js'), kitPath('main', 'snippets.js'), kitPath('main', 'term.js'), kitPath('main', 'sticky.js'), kitPath('main', 'spell.js'), kitPath('main', 'define.js'), kitPath('main', 'rhyme.js'), kitPath('cli', 'manage-npm.js'), kitPath('main', 'clipboard-history.js'), kitPath('main', 'emoji.js'),
kitPath('pro', 'theme-selector.js')
])
if (isMac) { kitScripts.push(kitPath('main', 'system-commands.js')) kitScripts.push(kitPath('main', 'focus-window.js'))
if (!Boolean(global?.env?.KIT_ACCESSIBILITY)) {
kitScripts.push(kitPath('main', 'accessibility.js'))
}
}
if (process.env.KIT_HIDE_KIT_SCRIPTS) { kitScripts = [] }
trace.begin({ name: 'parsedKitScripts' }) let parsedKitScripts = await processInBatches( kitScripts.map(async (scriptPath) => { let script = await parseScript(scriptPath)
script.group = 'Kit'
script.ignoreFlags = true
script.preview = `<div></div>`
processPreviewPath(script)
return script
}),
5
)
trace.end({ name: 'parsedKitScripts' })
processedscripts = processedscripts.concat(parsedKitScripts)
// let getHot = async () => { // let hotPath = kitPath("data", "hot.json") // if (await isFile(hotPath)) { // return await readJson(hotPath) // }
// return [] // }
// let loadHotChoices = async () => { // try { // let hot = await getHot()
// return hot.map(choice => { // choice.preview = async () => { // if (choice?.body) { // return await highlight(choice?.body) // }
// return "" // }
// choice.group = "Community" // choice.enter = "View Discussion" // choice.lastGroup = true
// return choice // }) // } catch (error) { // return [error.message] // } // }
// let communityScripts = await loadHotChoices()
// processedscripts = processedscripts.concat( // communityScripts // )
// let scraps = await parseScraps() // processedscripts = processedscripts.concat(scraps)
trace.begin({ name: 'groupScripts' }) let groupedScripts = groupScripts(processedscripts) trace.end({ name: 'groupScripts' })
groupedScripts = groupedScripts.map((s) => { if (s.group === 'Pass') { s.ignoreFlags = true }
return s
})
return groupedScripts }
export let mainMenu = async (message: string | PromptConfig = 'Select a script'): Promise<Script | string> => {
// if (global.trace) {
// global.trace.addBegin({
// name: "buildScriptConfig",
// tid: 0,
// args: Build main menu,
// })
// }
trace.begin({ name: 'buildScriptConfig' })
let scriptsConfig = buildScriptConfig(message)
trace.end({ name: 'buildScriptConfig' })
scriptsConfig.keepPreview = true
// We preload from an in-memory cache, then replace with the actual scripts global.__kitScriptsFromCache = false
trace.begin({ name: 'getGroupedScripts' }) let groupedScripts = await getGroupedScripts() trace.end({ name: 'getGroupedScripts' })
process.send({ channel: Channel.MAIN_MENU_READY, scripts: groupedScripts.length }) let script = await global.arg(scriptsConfig, groupedScripts) return await getScriptResult(script, message) }
export let selectScript = async ( message: string | PromptConfig = 'Select a script', fromCache = true, xf = (x: Script[]) => x, ignoreKenvPattern = /^ignore$/ ): Promise<Script> => { let scripts: Script[] = xf(await getScripts(fromCache, ignoreKenvPattern)) let scriptsConfig = buildScriptConfig(message)
if (process.env.KIT_CONTEXT === 'terminal') { let script = await global.arg(scriptsConfig, scripts) return await getScriptResult(script, message) } let groupedScripts = groupScripts(scripts)
scriptsConfig.keepPreview = true
let script = await global.arg(scriptsConfig, groupedScripts) return await getScriptResult(script, message) }
export let processPreviewPath = (s: Script) => { if (s.previewPath) { s.preview = async () => { let previewPath = getPreviewPath(s)
let preview = `<div></div>`
if (await isFile(previewPath)) {
preview = md(await readFile(previewPath, 'utf8'))
}
return preview
}
} }
export let processScriptPreview = (script: Script, infoBlock = '') => async () => { if ((script as Scriptlet)?.scriptlet) { return script.preview } let previewPath = getPreviewPath(script) let preview = ``
if (await isFile(previewPath)) {
preview = await processWithPreviewFile(script, previewPath, infoBlock)
} else if (typeof script?.preview === 'string') {
preview = await processWithStringPreview(script, infoBlock)
} else {
preview = await processWithNoPreview(script, infoBlock)
}
return preview
}
// TODO: The logic around scripts + stats/timestamps is confusing. Clean it up. export let processScript = (timestamps: Stamp[] = []) => async (s: Script): Promise<Script> => { let stamp = timestamps.find((t) => t.filePath === s.filePath)
let infoBlock = ``
if (stamp) {
s.compileStamp = stamp.compileStamp
s.compileMessage = stamp.compileMessage
s.timestamp = stamp.timestamp
if (stamp.compileMessage && stamp.compileStamp) {
infoBlock = `~~~
return s
}
export let getPreviewPath = (s: Script): string => { if (s?.previewPath) { return path.normalize(s.previewPath.replace('~', home()).replace('$KIT', kitPath())) } return path.resolve(path.dirname(path.dirname(s.filePath)), 'docs', path.parse(s.filePath).name + '.md') }
export let processWithPreviewFile = async (s: Script, previewPath: string, infoBlock: string): Promise => {
let processedPreview = ``
try {
let preview = await readFile(previewPath, 'utf8')
let content = await highlightJavaScript(s.filePath, s.shebang)
processedPreview = md(infoBlock + preview) + content
} catch (error) {
processedPreview = md(Could not find doc file ${previewPath} for ${s.name})
warn(`Could not find doc file ${previewPath} for ${s.name}`)
}
return processedPreview }
export let processWithStringPreview = async (s: Script, infoBlock: string) => {
let processedPreview = if (s?.preview === 'false') { processedPreview = `<div/>` } else { try { let content = await readFile(path.resolve(path.dirname(s.filePath), s?.preview as string), 'utf-8') processedPreview = infoBlock ? md(infoBlock) : + md(content)
} catch (error) {
processedPreview = Error: ${error.message}
}
}
return processedPreview }
export let processWithNoPreview = async (s: Script, infoBlock: string): Promise => { let processedPreview = `` let preview = await readFile(s.filePath, 'utf8')
if (preview.startsWith('/') && preview.includes('/')) { let index = preview.indexOf('*/') let content = preview.slice(2, index).trim() let markdown = md(infoBlock + content) let js = await highlightJavaScript(preview.slice(index + 2).trim()) return markdown + js }
let markdown = md(`# ${s.name}
${path.basename(s?.filePath)}
> ${s.note} : ''}
`)
let content = await highlightJavaScript(preview, s?.shebang || '')
processedPreview = markdown + (infoBlock ? md(infoBlock) : `` + content) return processedPreview }
global.selectScript = selectScript
export let selectKenv = async (
config = {
placeholder: 'Select a Kenv',
enter: 'Select Kenv'
} as PromptConfig,
// ignorePattern ignores examples and sponsors
ignorePattern = /^(examples|sponsors)$/
) => {
let homeKenv = {
name: 'main',
description: Your main kenv: ${kenvPath()},
value: {
name: 'main',
dirPath: kenvPath()
}
}
let selectedKenv: Kenv | string = homeKenv.value
let kenvs = await getKenvs(ignorePattern) if (kenvs.length) { let kenvChoices = [ homeKenv, ...kenvs.map((p) => { let name = path.basename(p) return { name, description: p, value: { name, dirPath: p } } }) ]
selectedKenv = await global.arg(config, kenvChoices)
if (typeof selectedKenv === 'string') {
return kenvChoices.find(
(c) => c.value.name === selectedKenv || path.resolve(c.value.dirPath) === path.resolve(selectedKenv as string)
).value
}
} return selectedKenv as Kenv }
global.selectKenv = selectKenv
global.highlight = highlight
global.setTab = async (tabName: string) => { let i = global.onTabs.findIndex(({ name }) => name === tabName) await global.sendWait(Channel.SET_TAB_INDEX, i) global.onTabs[i].fn() }
global.execLog = (command: string, logger = global.log) => { let writeableStream = new Writable() writeableStream._write = (chunk, encoding, next) => { logger(chunk.toString().trim()) next() }
let child = exec(command, { all: true, shell: process?.env?.KIT_SHELL || (process.platform === 'win32' ? 'cmd.exe' : 'zsh') })
child.all.pipe(writeableStream)
return child }
// global.projectPath = (...args) => {
// throw new Error(
// Script not loaded. Can't use projectPath() until a script is imported
// )
// }
global.clearTabs = () => { global.send(Channel.CLEAR_TABS) }
global.md = mdUtil
export let isAuthenticated = async () => { let envPath = kenvPath('.kenv') let envContents = await readFile(envPath, 'utf8') // check if the .env file has a GITHUB_SCRIPTKIT_TOKEN return envContents.match(/^GITHUB_SCRIPTKIT_TOKEN=.*/g) }
export let setEnvVar = async (key: string, value: string) => { await global.cli('set-env-var', key, value) }
export let getEnvVar = async (key: string, fallback = '') => { let kenvEnv = dotenv.parse(await readFile(kenvPath('.env'), 'utf8')) as kenvEnv return kenvEnv?.[key] || fallback }
export let toggleEnvVar = async (key: keyof kenvEnv, defaultValue = 'true') => {
let kenvEnv = dotenv.parse(await readFile(kenvPath('.env'), 'utf8')) as kenvEnv
// Check if the environment variable key exists and if its value is equal to the defaultState
// If it is, toggle the value between "true" and "false"
// If it isn't, set it to the defaultState
await setEnvVar(
key,
kenvEnv?.[key] === defaultValue
? defaultValue === 'true'
? 'false'
: 'true' // Toggle the value
: defaultValue // Set to defaultState if not already set
)
}
//@ts-ignore export let authenticate = async (): Promise => { // @ts-ignore let { Octokit } = await import('../share/auth-scriptkit.js') let octokit = new Octokit({ request: { fetch: global.fetch }, auth: { scopes: ['gist'], env: 'GITHUB_SCRIPTKIT_TOKEN' } })
let user = await octokit.rest.users.getAuthenticated()
let userJson = await getUserJson() await setUserJson({ ...userJson, ...user.data })
return octokit }
export let deauthenticate = async () => { await setUserJson({}) await replace({ files: kenvPath('.env'), from: /GITHUB_SCRIPTKIT_TOKEN=.*/g, to: '', disableGlobs: true }) process.env.GITHUB_SCRIPTKIT_TOKEN = env.GITHUB_SCRIPTKIT_TOKEN = ``
await mainScript() }
global.createGist = async ( content: string, { fileName = 'file.txt', description = 'Gist Created in Script Kit', isPublic = false } = {} ) => { let octokit = await authenticate() let response = await octokit.rest.gists.create({ description, public: isPublic, files: { [fileName]: { content } } })
return response.data }
global.browse = (url: string) => { return (global as any).open(url) }
global.PROMPT = PROMPT
global.preload = (scriptPath?: string) => { if (process.send) { send(Channel.PRELOAD, scriptPath || global.kitScript) } }
global.metadata = {} global.headers = {}
{ "name": "@johnlindquist/kit", "type": "module", "bin": { "kit": "bin/kit", "sk": "bin/sk", "kitblitz": "bin/kitblitz.mjs" }, "engines": { "node": ">=14.8.0" }, "version": "0.0.0-development", "description": "The Script Kit sdk", "repository": { "type": "git", "url": "git+https://github.com/johnlindquist/kit.git" }, "exports": { ".": { "types": "./types/index.d.ts", "import": "./index.js", "default": "./index.js" }, "./*": "./*", "./api/*": "./api/*.js", "./cli/*": "./cli/*.js", "./target/*": "./target/*.js", "./platform/*": "./platform/*.js", "./run/*": "./run/*.js", "./core/*": "./core/*.js", "./workers": "./workers/index.js", "./types/*": "./types/*.js" }, "types": "./types/index.d.ts", "scripts": { "ava": "ava --config ./test/ava.config.mjs --fail-fast", "ava:ci": "ava --config ./test/ava.config.mjs", "ava:watch": "ava --watch --no-worker-threads --config ./test/ava.config.mjs", "ava:reset": "ava reset-cache --config ./test/ava.config.mjs", "ava:debug": "ava debug --config ./test/ava.config.mjs", "coverage": "c8 --reporter=text --reporter=html npm run ava", "coverage:v8": "c8 --reporter=text --reporter=lcov --reporter=html npm run ava", "build-kit": "tsx ./build/build-kit.ts", "build": "tsx ./build/build-kit.ts", "verify": "tsc --noEmit -p tsconfig.verify.json", "commit": "cz", "rebuild-kit": "tsx ./build/rebuild-kit.ts", "download-md": "node ./build/download-md.js", "declaration": "tsc -p ./tsconfig-declaration.json --watch", "pretest:core": "node ./scripts/test-pre.js", "test:core": "cross-env NODE_NO_WARNINGS=1 ava ./src/core/*.test.js --no-worker-threads", "posttest:core": "node ./scripts/test-post.js", "pretest:kit": "node ./scripts/test-pre.js", "test:kit": "cross-env NODE_NO_WARNINGS=1 ava ./src/api/kit.test.js --no-worker-threads", "pretest:sdk": "node ./scripts/test-pre.js", "test:sdk": "cross-env NODE_NO_WARNINGS=1 ava ./test-sdk/*.test.js --no-worker-threads", "posttest:sdk": "node ./scripts/test-post.js", "pretest:api": "node ./scripts/test-pre.js", "test:api": "cross-env NODE_NO_WARNINGS=1 ava ./src/api/*.test.js --no-worker-threads", "posttest:api": "node ./scripts/test-post.js", "pretest:metadata": "node ./scripts/test-pre.js", "test:metadata": "cross-env NODE_NO_WARNINGS=1 ava ./src/core/metadata.test.js --no-worker-threads", "posttest:metadata": "node ./scripts/test-post.js", "pretest": "node ./scripts/test-pre.js", "test": "cross-env NODE_NO_WARNINGS=1 ava --no-worker-threads --fail-fast", "posttest": "node ./scripts/test-post.js", "build-editor-types": "tsx ./build/build-editor-types.ts", "rebuild-test": "npm run rebuild-kit && npm run test -- --fail-fast", "lazy-install": "npm i esbuild@0.23.1 --save-exact --production --prefer-dedupe --loglevel=verbose", "preinstall": "node ./build/preinstall.js" }, "author": "John Lindquist (https://johnlindquist.com)", "license": "ISC", "pnpm": { "overrides": { "typescript": "5.8.3", "esbuild": "0.25.5" } }, "dependencies": { "@ai-sdk/anthropic": "2.0.0-beta.5", "@ai-sdk/google": "2.0.0-beta.8", "@ai-sdk/openai": "2.0.0-beta.7", "@ai-sdk/react": "2.0.0-beta.16", "@ai-sdk/xai": "2.0.0-beta.4", "@johnlindquist/open": "^10.1.1", "@modelcontextprotocol/sdk": "1.13.3", "@octokit/auth-oauth-device": "8.0.1", "@octokit/core": "7.0.2", "@octokit/plugin-paginate-rest": "13.0.0", "@octokit/plugin-rest-endpoint-methods": "15.0.0", "@octokit/plugin-retry": "8.0.1", "@octokit/plugin-throttling": "11.0.1", "@openrouter/ai-sdk-provider": "1.0.0-beta.1", "@types/chalk": "2.2.4", "@types/download": "8.0.5", "@types/fs-extra": "11.0.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/shelljs": "0.8.17", "@typescript/lib-dom": "npm:@johnlindquist/no-dom@^1.0.2", "acorn": "^8.15.0", "acorn-typescript": "^1.4.13", "advanced-calculator": "1.1.1", "ai": "5.0.0-beta.18", "axios": "1.10.0", "body-parser": "^2.2.0", "bottleneck": "^2.19.5", "chalk": "5.4.1", "chalk-template": "1.1.0", "chrome-trace-event": "^1.0.4", "color-name": "2.0.0", "date-fns": "4.1.0", "dotenv": "^17.0.1", "dotenv-flow": "4.1.0", "download": "8.0.0", "enquirer": "2.4.1", "esbuild": "0.25.5", "execa": "9.6.0", "filesize": "10.1.6", "fs-extra": "^11.3.0", "globby": "^14.1.0", "handlebars": "4.7.8", "highlight.js": "^11.11.1", "isomorphic-git": "1.32.1", "jsonfile": "6.1.0", "keyv": "^5.3.4", "keyv-file": "^5.1.2", "lowdb": "7.0.1", "marked": "15.0.12", "marked-extended-tables": "2.0.1", "marked-gfm-heading-id": "4.1.1", "marked-highlight": "2.2.1", "minimist": "1.2.8", "open": "10.1.2", "p-retry": "6.2.1", "project-name-generator": "2.1.9", "quick-score": "^0.2.0", "react": "^19.1.0", "react-dom": "^19.1.0", "replace-in-file": "8.3.0", "rimraf": "6.0.1", "safe-stable-stringify": "^2.5.0", "shelljs": "0.10.0", "slugify": "1.6.6", "source-map-support": "^0.5.21", "strip-ansi": "7.1.0", "suggestion": "2.1.2", "tmp-promise": "3.0.3", "untildify": "5.0.0", "zod": "^4.0.5" }, "devDependencies": { "@types/debug": "4.1.12", "@types/node": "^22.15.30", "@types/node-ipc": "9.2.3", "@types/sinon": "17.0.4", "acorn-walk": "8.3.4", "ava": "^6.4.0", "c8": "10.1.3", "cross-env": "^7.0.3", "cz-conventional-changelog": "^3.3.0", "debug": "4.4.1", "husky": "^9.1.7", "node-stream-zip": "^1.15.0", "semantic-release": "24.2.6", "semantic-release-plugin-update-version-in-files": "2.0.0", "sinon": "20.0.0", "tsc-watch": "7.1.1", "tsx": "4.20.3", "typescript": "5.8.3", "unzipper": "0.12.3", "vite": "6.3.5" }, "ava": { "environmentVariables": { "KIT_TEST": "true" }, "verbose": true, "files": [ "src/**/*.test.js", "test/**/*.test.js", "test-sdk/**/*.test.js" ] }, "release": { "branches": [ "+([0-9]).x", "main", "next", { "name": "beta", "prerelease": true }, { "name": "alpha", "prerelease": true } ], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", [ "@semantic-release/npm", { "pkgRoot": "./.kit" } ], [ "semantic-release-plugin-update-version-in-files", { "files": [ "./.kit/package.json" ] } ] ] }, "volta": { "node": "22.17.1" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog"}, "optionalDependencies": { "@johnlindquist/mac-windows": "1.0.2", "file-icon": "5.1.1", "get-app-icon": "1.0.1" }, "packageManager": "pnpm@10.13.1" }