From 1e7f095121e9f21e9e4885104ecfc83c752715ee Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Tue, 2 Dec 2025 18:45:46 +0000 Subject: [PATCH 01/15] feat: initial setup for "create" cmd in cli --- go.mod | 5 +++++ go.sum | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/go.mod b/go.mod index b17f5cc..1efaf1d 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect @@ -37,9 +38,13 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-cobra v1.2.0 // indirect diff --git a/go.sum b/go.sum index f5b74b9..c0d9b2e 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -19,6 +21,7 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= @@ -43,6 +46,7 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= @@ -60,10 +64,13 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -78,9 +85,15 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -161,6 +174,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -183,6 +197,7 @@ golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= From 989bb5cf72f18bfe643b9d95dd04d359116801b1 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Wed, 3 Dec 2025 18:49:27 +0000 Subject: [PATCH 02/15] feat: add ts sample-app template --- typescript/sample-app/README.md | 5 ++ typescript/sample-app/_gitignore | 39 +++++++++ typescript/sample-app/index.ts | 108 ++++++++++++++++++++++++ typescript/sample-app/package-lock.json | 81 ++++++++++++++++++ typescript/sample-app/package.json | 13 +++ typescript/sample-app/pnpm-lock.yaml | 61 +++++++++++++ typescript/sample-app/tsconfig.json | 31 +++++++ 7 files changed, 338 insertions(+) create mode 100644 typescript/sample-app/README.md create mode 100644 typescript/sample-app/_gitignore create mode 100644 typescript/sample-app/index.ts create mode 100644 typescript/sample-app/package-lock.json create mode 100644 typescript/sample-app/package.json create mode 100644 typescript/sample-app/pnpm-lock.yaml create mode 100644 typescript/sample-app/tsconfig.json diff --git a/typescript/sample-app/README.md b/typescript/sample-app/README.md new file mode 100644 index 0000000..1d85657 --- /dev/null +++ b/typescript/sample-app/README.md @@ -0,0 +1,5 @@ +# Kernel Typscript Sample App + +This is a simple Kernel application that extracts the title from a webpage. + +See the [docs](https://onkernel.com/docs/quickstart) for information. \ No newline at end of file diff --git a/typescript/sample-app/_gitignore b/typescript/sample-app/_gitignore new file mode 100644 index 0000000..9325515 --- /dev/null +++ b/typescript/sample-app/_gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ +package-lock.json + +# TypeScript +*.tsbuildinfo +dist/ +build/ + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +.cache/ +.temp/ +.tmp/ \ No newline at end of file diff --git a/typescript/sample-app/index.ts b/typescript/sample-app/index.ts new file mode 100644 index 0000000..ea3a94c --- /dev/null +++ b/typescript/sample-app/index.ts @@ -0,0 +1,108 @@ +import { Kernel, type KernelContext } from "@onkernel/sdk"; +import { chromium } from "playwright"; + +const kernel = new Kernel(); + +const app = kernel.app("ts-basic"); + +/** + * Example app that extracts the title of a webpage + * Args: + * ctx: Kernel context containing invocation information + * payload: An object with a URL property + * Returns: + * A dictionary containing the page title + * Invoke this via CLI: + * kernel login # or: export KERNEL_API_KEY= + * kernel deploy index.ts # If you haven't already deployed this app + * kernel invoke ts-basic get-page-title -p '{"url": "https://www.google.com"}' + * kernel logs ts-basic -f # Open in separate tab + */ +interface PageTitleInput { + url: string; +} + +interface PageTitleOutput { + title: string; +} +app.action( + "get-page-title", + async ( + ctx: KernelContext, + payload?: PageTitleInput + ): Promise => { + if (!payload?.url) { + throw new Error("URL is required"); + } + + if ( + !payload.url.startsWith("http://") && + !payload.url.startsWith("https://") + ) { + payload.url = `https://${payload.url}`; + } + + // Validate the URL + try { + new URL(payload.url); + } catch { + throw new Error(`Invalid URL: ${payload.url}`); + } + + const kernelBrowser = await kernel.browsers.create({ + invocation_id: ctx.invocation_id, + }); + + console.log( + "Kernel browser live view url: ", + kernelBrowser.browser_live_view_url + ); + + const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); + const context = browser.contexts()[0] || (await browser.newContext()); + const page = context.pages()[0] || (await context.newPage()); + + try { + ////////////////////////////////////// + // Your browser automation logic here + ////////////////////////////////////// + await page.goto(payload.url); + const title = await page.title(); + return { title }; + } finally { + await kernel.browsers.deleteByID(kernelBrowser.session_id); + } + } +); + +/** + * Example app that creates a long-running Kernel browser for manual testing + * Invoke this action to test Kernel browsers manually with our browser live view + * https://onkernel.com/docs/browsers/live-view + * Args: + * ctx: Kernel context containing invocation information + * Returns: + * A dictionary containing the browser live view url + * Invoke this via CLI: + * kernel login # or: export KERNEL_API_KEY= + * kernel deploy index.ts # If you haven't already deployed this app + * kernel invoke ts-basic create-browser-for-testing + * kernel logs ts-basic -f # Open in separate tab + */ +interface CreateBrowserForTestingOutput { + browser_live_view_url: string; +} +app.action( + "create-browser-for-testing", + async (ctx: KernelContext): Promise => { + const kernelBrowser = await kernel.browsers.create({ + invocation_id: ctx.invocation_id, + stealth: true, + timeout_seconds: 3600, // Keep browser alive for 1 hour + }); + + return { + browser_live_view_url: kernelBrowser.browser_live_view_url, + }; + } +); diff --git a/typescript/sample-app/package-lock.json b/typescript/sample-app/package-lock.json new file mode 100644 index 0000000..104353f --- /dev/null +++ b/typescript/sample-app/package-lock.json @@ -0,0 +1,81 @@ +{ + "name": "ts-basic", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ts-basic", + "dependencies": { + "@onkernel/sdk": "0.1.0-alpha.16", + "playwright": "^1.52.0" + }, + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/@onkernel/sdk": { + "version": "0.1.0-alpha.16", + "resolved": "https://registry.npmjs.org/@onkernel/sdk/-/sdk-0.1.0-alpha.16.tgz", + "integrity": "sha512-KlC1EFiWoSXWxGLdoI0gh6cyF+5xy0wx4ATJ2MPMQl31H5ElAR/oGO3UE3Ge2cFKra8oWVZRUj7NOkxnpDnQ3g==", + "license": "Apache-2.0" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/typescript/sample-app/package.json b/typescript/sample-app/package.json new file mode 100644 index 0000000..527437d --- /dev/null +++ b/typescript/sample-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "ts-basic", + "module": "index.ts", + "type": "module", + "private": true, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@onkernel/sdk": ">=0.14.2", + "playwright": "^1.52.0" + } +} diff --git a/typescript/sample-app/pnpm-lock.yaml b/typescript/sample-app/pnpm-lock.yaml new file mode 100644 index 0000000..a69bdd0 --- /dev/null +++ b/typescript/sample-app/pnpm-lock.yaml @@ -0,0 +1,61 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@onkernel/sdk': + specifier: '>=0.5.0' + version: 0.5.0 + playwright: + specifier: ^1.52.0 + version: 1.52.0 + typescript: + specifier: ^5 + version: 5.8.3 + +packages: + + '@onkernel/sdk@0.5.0': + resolution: {integrity: sha512-n7gwc7rU0GY/XcDnEV0piHPd76bHTSfuTjQW4qFKUWQji0UK9YUVKDFklqAWbyGlXPUezWCfxh79ELv2cFYOBA==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + playwright-core@1.52.0: + resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.52.0: + resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} + engines: {node: '>=18'} + hasBin: true + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + +snapshots: + + '@onkernel/sdk@0.5.0': {} + + fsevents@2.3.2: + optional: true + + playwright-core@1.52.0: {} + + playwright@1.52.0: + dependencies: + playwright-core: 1.52.0 + optionalDependencies: + fsevents: 2.3.2 + + typescript@5.8.3: {} diff --git a/typescript/sample-app/tsconfig.json b/typescript/sample-app/tsconfig.json new file mode 100644 index 0000000..39959d0 --- /dev/null +++ b/typescript/sample-app/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["./**/*.ts", "./**/*.tsx"], + "exclude": ["node_modules", "dist"] +} + \ No newline at end of file From 58025cc7ab96a4b6154bef2a51ee7e49114c7104 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Thu, 4 Dec 2025 19:38:11 +0000 Subject: [PATCH 03/15] fix: remove old files --- typescript/sample-app/README.md | 5 -- typescript/sample-app/_gitignore | 39 --------- typescript/sample-app/index.ts | 108 ------------------------ typescript/sample-app/package-lock.json | 81 ------------------ typescript/sample-app/package.json | 13 --- typescript/sample-app/pnpm-lock.yaml | 61 ------------- typescript/sample-app/tsconfig.json | 31 ------- 7 files changed, 338 deletions(-) delete mode 100644 typescript/sample-app/README.md delete mode 100644 typescript/sample-app/_gitignore delete mode 100644 typescript/sample-app/index.ts delete mode 100644 typescript/sample-app/package-lock.json delete mode 100644 typescript/sample-app/package.json delete mode 100644 typescript/sample-app/pnpm-lock.yaml delete mode 100644 typescript/sample-app/tsconfig.json diff --git a/typescript/sample-app/README.md b/typescript/sample-app/README.md deleted file mode 100644 index 1d85657..0000000 --- a/typescript/sample-app/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Kernel Typscript Sample App - -This is a simple Kernel application that extracts the title from a webpage. - -See the [docs](https://onkernel.com/docs/quickstart) for information. \ No newline at end of file diff --git a/typescript/sample-app/_gitignore b/typescript/sample-app/_gitignore deleted file mode 100644 index 9325515..0000000 --- a/typescript/sample-app/_gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# Dependencies -node_modules/ -package-lock.json - -# TypeScript -*.tsbuildinfo -dist/ -build/ - -# Environment -.env -.env.local -.env.*.local - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Logs -logs/ -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Testing -coverage/ -.nyc_output/ - -# Misc -.cache/ -.temp/ -.tmp/ \ No newline at end of file diff --git a/typescript/sample-app/index.ts b/typescript/sample-app/index.ts deleted file mode 100644 index ea3a94c..0000000 --- a/typescript/sample-app/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Kernel, type KernelContext } from "@onkernel/sdk"; -import { chromium } from "playwright"; - -const kernel = new Kernel(); - -const app = kernel.app("ts-basic"); - -/** - * Example app that extracts the title of a webpage - * Args: - * ctx: Kernel context containing invocation information - * payload: An object with a URL property - * Returns: - * A dictionary containing the page title - * Invoke this via CLI: - * kernel login # or: export KERNEL_API_KEY= - * kernel deploy index.ts # If you haven't already deployed this app - * kernel invoke ts-basic get-page-title -p '{"url": "https://www.google.com"}' - * kernel logs ts-basic -f # Open in separate tab - */ -interface PageTitleInput { - url: string; -} - -interface PageTitleOutput { - title: string; -} -app.action( - "get-page-title", - async ( - ctx: KernelContext, - payload?: PageTitleInput - ): Promise => { - if (!payload?.url) { - throw new Error("URL is required"); - } - - if ( - !payload.url.startsWith("http://") && - !payload.url.startsWith("https://") - ) { - payload.url = `https://${payload.url}`; - } - - // Validate the URL - try { - new URL(payload.url); - } catch { - throw new Error(`Invalid URL: ${payload.url}`); - } - - const kernelBrowser = await kernel.browsers.create({ - invocation_id: ctx.invocation_id, - }); - - console.log( - "Kernel browser live view url: ", - kernelBrowser.browser_live_view_url - ); - - const browser = await chromium.connectOverCDP(kernelBrowser.cdp_ws_url); - const context = browser.contexts()[0] || (await browser.newContext()); - const page = context.pages()[0] || (await context.newPage()); - - try { - ////////////////////////////////////// - // Your browser automation logic here - ////////////////////////////////////// - await page.goto(payload.url); - const title = await page.title(); - return { title }; - } finally { - await kernel.browsers.deleteByID(kernelBrowser.session_id); - } - } -); - -/** - * Example app that creates a long-running Kernel browser for manual testing - * Invoke this action to test Kernel browsers manually with our browser live view - * https://onkernel.com/docs/browsers/live-view - * Args: - * ctx: Kernel context containing invocation information - * Returns: - * A dictionary containing the browser live view url - * Invoke this via CLI: - * kernel login # or: export KERNEL_API_KEY= - * kernel deploy index.ts # If you haven't already deployed this app - * kernel invoke ts-basic create-browser-for-testing - * kernel logs ts-basic -f # Open in separate tab - */ -interface CreateBrowserForTestingOutput { - browser_live_view_url: string; -} -app.action( - "create-browser-for-testing", - async (ctx: KernelContext): Promise => { - const kernelBrowser = await kernel.browsers.create({ - invocation_id: ctx.invocation_id, - stealth: true, - timeout_seconds: 3600, // Keep browser alive for 1 hour - }); - - return { - browser_live_view_url: kernelBrowser.browser_live_view_url, - }; - } -); diff --git a/typescript/sample-app/package-lock.json b/typescript/sample-app/package-lock.json deleted file mode 100644 index 104353f..0000000 --- a/typescript/sample-app/package-lock.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "ts-basic", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ts-basic", - "dependencies": { - "@onkernel/sdk": "0.1.0-alpha.16", - "playwright": "^1.52.0" - }, - "peerDependencies": { - "typescript": "^5" - } - }, - "node_modules/@onkernel/sdk": { - "version": "0.1.0-alpha.16", - "resolved": "https://registry.npmjs.org/@onkernel/sdk/-/sdk-0.1.0-alpha.16.tgz", - "integrity": "sha512-KlC1EFiWoSXWxGLdoI0gh6cyF+5xy0wx4ATJ2MPMQl31H5ElAR/oGO3UE3Ge2cFKra8oWVZRUj7NOkxnpDnQ3g==", - "license": "Apache-2.0" - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.52.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - } - } -} diff --git a/typescript/sample-app/package.json b/typescript/sample-app/package.json deleted file mode 100644 index 527437d..0000000 --- a/typescript/sample-app/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "ts-basic", - "module": "index.ts", - "type": "module", - "private": true, - "peerDependencies": { - "typescript": "^5" - }, - "dependencies": { - "@onkernel/sdk": ">=0.14.2", - "playwright": "^1.52.0" - } -} diff --git a/typescript/sample-app/pnpm-lock.yaml b/typescript/sample-app/pnpm-lock.yaml deleted file mode 100644 index a69bdd0..0000000 --- a/typescript/sample-app/pnpm-lock.yaml +++ /dev/null @@ -1,61 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@onkernel/sdk': - specifier: '>=0.5.0' - version: 0.5.0 - playwright: - specifier: ^1.52.0 - version: 1.52.0 - typescript: - specifier: ^5 - version: 5.8.3 - -packages: - - '@onkernel/sdk@0.5.0': - resolution: {integrity: sha512-n7gwc7rU0GY/XcDnEV0piHPd76bHTSfuTjQW4qFKUWQji0UK9YUVKDFklqAWbyGlXPUezWCfxh79ELv2cFYOBA==} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - playwright-core@1.52.0: - resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.52.0: - resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} - engines: {node: '>=18'} - hasBin: true - - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - -snapshots: - - '@onkernel/sdk@0.5.0': {} - - fsevents@2.3.2: - optional: true - - playwright-core@1.52.0: {} - - playwright@1.52.0: - dependencies: - playwright-core: 1.52.0 - optionalDependencies: - fsevents: 2.3.2 - - typescript@5.8.3: {} diff --git a/typescript/sample-app/tsconfig.json b/typescript/sample-app/tsconfig.json deleted file mode 100644 index 39959d0..0000000 --- a/typescript/sample-app/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - }, - "include": ["./**/*.ts", "./**/*.tsx"], - "exclude": ["node_modules", "dist"] -} - \ No newline at end of file From 76743f23fbdda775e2bc6131f702a714789df708 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 5 Dec 2025 14:18:18 +0000 Subject: [PATCH 04/15] hide `create` command --- cmd/root.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/root.go b/cmd/root.go index e7553de..c7f8e84 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -135,7 +135,8 @@ func init() { rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) - rootCmd.AddCommand(createCmd) + // Hide create command while WIP + // rootCmd.AddCommand(createCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command From 34f1d7be802ce4885819a167c5c35a7b86daa4ac Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 8 Dec 2025 16:02:19 +0000 Subject: [PATCH 05/15] review: refactor prompting to use pterm --- go.mod | 5 ----- go.sum | 15 --------------- 2 files changed, 20 deletions(-) diff --git a/go.mod b/go.mod index 1efaf1d..b17f5cc 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/charmbracelet/colorprofile v0.3.0 // indirect github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1 // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect @@ -38,13 +37,9 @@ require ( github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.8 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.1.0 // indirect github.com/muesli/mango-cobra v1.2.0 // indirect diff --git a/go.sum b/go.sum index c0d9b2e..f5b74b9 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= -github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= -github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8= github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII= @@ -21,7 +19,6 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= @@ -46,7 +43,6 @@ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNE github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= @@ -64,13 +60,10 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -85,15 +78,9 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= -github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -174,7 +161,6 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -197,7 +183,6 @@ golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= From 8d3babdcb478fc56ae2cecc9e2f7906c0beba81c Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 8 Dec 2025 16:14:56 +0000 Subject: [PATCH 06/15] review: add app name validation with flag --- cmd/root.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index c7f8e84..e7553de 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -135,8 +135,7 @@ func init() { rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) - // Hide create command while WIP - // rootCmd.AddCommand(createCmd) + rootCmd.AddCommand(createCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command From b21a710c16846af3045efe4a304cb0d9b61f9471 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Fri, 5 Dec 2025 18:49:43 +0000 Subject: [PATCH 07/15] remove test --- pkg/create/templates_test.go | 113 ----------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 pkg/create/templates_test.go diff --git a/pkg/create/templates_test.go b/pkg/create/templates_test.go deleted file mode 100644 index 602cf91..0000000 --- a/pkg/create/templates_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package create - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetSupportedTemplatesForLanguage_Deterministic(t *testing.T) { - // Run the function multiple times to ensure consistent ordering - const iterations = 10 - language := LanguageTypeScript - - var firstResult TemplateKeyValues - for i := 0; i < iterations; i++ { - result := GetSupportedTemplatesForLanguage(language) - - if i == 0 { - firstResult = result - } else { - // Verify that each iteration produces the same order - assert.Equal(t, len(firstResult), len(result), "All iterations should return the same number of templates") - for j := range result { - assert.Equal(t, firstResult[j].Key, result[j].Key, "Template at index %d should be consistent across iterations", j) - assert.Equal(t, firstResult[j].Value, result[j].Value, "Template value at index %d should be consistent across iterations", j) - } - } - } -} - -func TestTemplateKeyValues_GetTemplateDisplayValues(t *testing.T) { - templates := TemplateKeyValues{ - {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, - {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, - } - - displayValues := templates.GetTemplateDisplayValues() - - assert.Len(t, displayValues, 2) - assert.Equal(t, "Sample App - Implements basic Kernel apps", displayValues[0]) - assert.Equal(t, "Advanced Sample - Implements sample actions with advanced Kernel configs", displayValues[1]) -} - -func TestTemplateKeyValues_GetTemplateKeyFromValue(t *testing.T) { - templates := TemplateKeyValues{ - {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, - {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, - } - - tests := []struct { - name string - selectedValue string - wantKey string - wantErr bool - }{ - { - name: "Valid value returns correct key", - selectedValue: "Sample App - Implements basic Kernel apps", - wantKey: "sample-app", - wantErr: false, - }, - { - name: "Invalid value returns error", - selectedValue: "Non-existent template", - wantKey: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - key, err := templates.GetTemplateKeyFromValue(tt.selectedValue) - - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.wantKey, key) - } - }) - } -} - -func TestTemplateKeyValues_ContainsKey(t *testing.T) { - templates := TemplateKeyValues{ - {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, - {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, - } - - tests := []struct { - name string - key string - want bool - }{ - { - name: "Existing key returns true", - key: "sample-app", - want: true, - }, - { - name: "Non-existing key returns false", - key: "non-existent", - want: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := templates.ContainsKey(tt.key) - assert.Equal(t, tt.want, got) - }) - } -} From 51e679e626412c54805eb01a1c1a73f257d1147c Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Mon, 8 Dec 2025 18:01:41 +0000 Subject: [PATCH 08/15] fix: adding sorting for UX consistency && tests --- pkg/create/templates_test.go | 113 +++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 pkg/create/templates_test.go diff --git a/pkg/create/templates_test.go b/pkg/create/templates_test.go new file mode 100644 index 0000000..602cf91 --- /dev/null +++ b/pkg/create/templates_test.go @@ -0,0 +1,113 @@ +package create + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetSupportedTemplatesForLanguage_Deterministic(t *testing.T) { + // Run the function multiple times to ensure consistent ordering + const iterations = 10 + language := LanguageTypeScript + + var firstResult TemplateKeyValues + for i := 0; i < iterations; i++ { + result := GetSupportedTemplatesForLanguage(language) + + if i == 0 { + firstResult = result + } else { + // Verify that each iteration produces the same order + assert.Equal(t, len(firstResult), len(result), "All iterations should return the same number of templates") + for j := range result { + assert.Equal(t, firstResult[j].Key, result[j].Key, "Template at index %d should be consistent across iterations", j) + assert.Equal(t, firstResult[j].Value, result[j].Value, "Template value at index %d should be consistent across iterations", j) + } + } + } +} + +func TestTemplateKeyValues_GetTemplateDisplayValues(t *testing.T) { + templates := TemplateKeyValues{ + {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, + {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, + } + + displayValues := templates.GetTemplateDisplayValues() + + assert.Len(t, displayValues, 2) + assert.Equal(t, "Sample App - Implements basic Kernel apps", displayValues[0]) + assert.Equal(t, "Advanced Sample - Implements sample actions with advanced Kernel configs", displayValues[1]) +} + +func TestTemplateKeyValues_GetTemplateKeyFromValue(t *testing.T) { + templates := TemplateKeyValues{ + {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, + {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, + } + + tests := []struct { + name string + selectedValue string + wantKey string + wantErr bool + }{ + { + name: "Valid value returns correct key", + selectedValue: "Sample App - Implements basic Kernel apps", + wantKey: "sample-app", + wantErr: false, + }, + { + name: "Invalid value returns error", + selectedValue: "Non-existent template", + wantKey: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, err := templates.GetTemplateKeyFromValue(tt.selectedValue) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.wantKey, key) + } + }) + } +} + +func TestTemplateKeyValues_ContainsKey(t *testing.T) { + templates := TemplateKeyValues{ + {Key: "sample-app", Value: "Sample App - Implements basic Kernel apps"}, + {Key: "advanced-sample", Value: "Advanced Sample - Implements sample actions with advanced Kernel configs"}, + } + + tests := []struct { + name string + key string + want bool + }{ + { + name: "Existing key returns true", + key: "sample-app", + want: true, + }, + { + name: "Non-existing key returns false", + key: "non-existent", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := templates.ContainsKey(tt.key) + assert.Equal(t, tt.want, got) + }) + } +} From ae22440428ca0600ef8f0c95da2328249a2d119f Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Tue, 9 Dec 2025 16:54:11 +0000 Subject: [PATCH 09/15] feat: add directory overwrite checks --- cmd/create.go | 18 ++++++++++--- cmd/create_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++- pkg/create/prompts.go | 13 +++++++++ 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 31d22ed..d413442 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -27,10 +27,22 @@ func (c CreateCmd) Create(ctx context.Context, ci CreateInput) error { return fmt.Errorf("failed to resolve app path: %w", err) } - // TODO: handle overwrite gracefully (prompt user) - // Check if directory already exists + // Check if directory already exists and prompt for overwrite if _, err := os.Stat(appPath); err == nil { - return fmt.Errorf("directory %s already exists", ci.Name) + overwrite, err := create.PromptForOverwrite(ci.Name) + if err != nil { + return fmt.Errorf("failed to prompt for overwrite: %w", err) + } + + if !overwrite { + pterm.Warning.Println("Operation cancelled.") + return nil + } + + // Remove existing directory + if err := os.RemoveAll(appPath); err != nil { + return fmt.Errorf("failed to remove existing directory: %w", err) + } } if err := os.MkdirAll(appPath, 0755); err != nil { diff --git a/cmd/create_test.go b/cmd/create_test.go index d04e201..835f135 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -14,6 +14,11 @@ import ( "github.com/stretchr/testify/require" ) +const ( + DIR_PERM = 0755 // rwxr-xr-x + FILE_PERM = 0644 // rw-r--r-- +) + func TestCreateCommand(t *testing.T) { tests := []struct { name string @@ -145,7 +150,7 @@ func TestAllTemplatesCreation(t *testing.T) { appPath := filepath.Join(tmpDir, appName) // Create app directory - err := os.MkdirAll(appPath, 0755) + err := os.MkdirAll(appPath, DIR_PERM) require.NoError(t, err, "failed to create app directory") // Copy template files without installing dependencies @@ -352,6 +357,61 @@ func TestCreateCommand_RequiredToolMissing(t *testing.T) { } } +// TestCreateCommand_DirectoryOverwrite tests that overwriting an existing directory +// properly removes old content and creates new content +func TestCreateCommand_DirectoryOverwrite(t *testing.T) { + tmpDir := t.TempDir() + appName := "test-app" + appPath := filepath.Join(tmpDir, appName) + + orgDir, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(tmpDir) + require.NoError(t, err) + + t.Cleanup(func() { + os.Chdir(orgDir) + }) + + // Initialize directory with some files + err = os.MkdirAll(appPath, DIR_PERM) + require.NoError(t, err, "failed to create initial directory") + + // Create some initial files that should be removed after overwrite + oldFile1 := filepath.Join(appPath, "old-file-1.txt") + oldSubDir := filepath.Join(appPath, "old-subdir") + + err = os.WriteFile(oldFile1, []byte("old content 1"), FILE_PERM) + require.NoError(t, err, "failed to create old file 1") + + err = os.MkdirAll(oldSubDir, DIR_PERM) + require.NoError(t, err, "failed to create old subdirectory") + + // Verify initial files exist + assert.FileExists(t, oldFile1, "old file 1 should exist before overwrite") + assert.DirExists(t, oldSubDir, "old subdirectory should exist before overwrite") + + // Manually remove the directory and create the new app + err = os.RemoveAll(appPath) + require.NoError(t, err, "failed to remove existing directory") + + c := CreateCmd{} + err = c.Create(context.Background(), CreateInput{ + Name: appName, + Language: create.LanguageTypeScript, + Template: "sample-app", + }) + require.NoError(t, err, "failed to create new app") + + // Verify old files are gone + assert.NoFileExists(t, oldFile1, "old file 1 should not exist after overwrite") + assert.NoDirExists(t, oldSubDir, "old subdirectory should not exist after overwrite") + + // Verify new template files exist + assert.FileExists(t, filepath.Join(appPath, "index.ts"), "new index.ts should exist") +} + func getTemplateInfo() []struct { name string language string diff --git a/pkg/create/prompts.go b/pkg/create/prompts.go index f353982..9747580 100644 --- a/pkg/create/prompts.go +++ b/pkg/create/prompts.go @@ -122,3 +122,16 @@ func PromptForTemplate(providedTemplate string, providedLanguage string) (string pterm.Warning.Printfln("Template '%s' not found. Please select from available templates.\n", providedTemplate) return handleTemplatePrompt(templateKVs) } + +// PromptForOverwrite prompts the user to confirm overwriting an existing directory. +func PromptForOverwrite(dirName string) (bool, error) { + overwrite, err := pterm.DefaultInteractiveConfirm. + WithDefaultText(fmt.Sprintf("\nDirectory %s already exists. Overwrite?", dirName)). + WithDefaultValue(false). + Show() + if err != nil { + return false, fmt.Errorf("failed to prompt for overwrite: %w", err) + } + + return overwrite, nil +} From 2b00687034630f4abff517ee1f6038e1746d6c40 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Tue, 9 Dec 2025 16:58:42 +0000 Subject: [PATCH 10/15] feat: add dynamic next steps commands --- pkg/create/templates.go | 164 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 156 insertions(+), 8 deletions(-) diff --git a/pkg/create/templates.go b/pkg/create/templates.go index 88077cf..54b7fdc 100644 --- a/pkg/create/templates.go +++ b/pkg/create/templates.go @@ -6,6 +6,18 @@ import ( "sort" ) +// Template key constants +const ( + TemplateSampleApp = "sample-app" + TemplateAdvancedSample = "advanced-sample" + TemplateComputerUse = "computer-use" + TemplateCUA = "cua" + TemplateMagnitude = "magnitude" + TemplateGeminiCUA = "gemini-cua" + TemplateBrowserUse = "browser-use" + TemplateStagehand = "stagehand" +) + type TemplateInfo struct { Name string Description string @@ -20,42 +32,42 @@ type TemplateKeyValue struct { type TemplateKeyValues []TemplateKeyValue var Templates = map[string]TemplateInfo{ - "sample-app": { + TemplateSampleApp: { Name: "Sample App", Description: "Implements basic Kernel apps", Languages: []string{LanguageTypeScript, LanguagePython}, }, - "advanced-sample": { + TemplateAdvancedSample: { Name: "Advanced Sample", Description: "Implements sample actions with advanced Kernel configs", Languages: []string{LanguageTypeScript, LanguagePython}, }, - "computer-use": { + TemplateComputerUse: { Name: "Computer Use", Description: "Implements the Anthropic Computer Use SDK", Languages: []string{LanguageTypeScript, LanguagePython}, }, - "cua": { + TemplateCUA: { Name: "CUA Sample", Description: "Implements a Computer Use Agent (OpenAI CUA) sample", Languages: []string{LanguageTypeScript, LanguagePython}, }, - "magnitude": { + TemplateMagnitude: { Name: "Magnitude", Description: "Implements the Magnitude.run SDK", Languages: []string{LanguageTypeScript}, }, - "gemini-cua": { + TemplateGeminiCUA: { Name: "Gemini CUA", Description: "Implements Gemini 2.5 Computer Use Agent", Languages: []string{LanguageTypeScript}, }, - "browser-use": { + TemplateBrowserUse: { Name: "Browser Use", Description: "Implements Browser Use SDK", Languages: []string{LanguagePython}, }, - "stagehand": { + TemplateStagehand: { Name: "Stagehand", Description: "Implements the Stagehand v3 SDK", Languages: []string{LanguageTypeScript}, @@ -109,3 +121,139 @@ func (tkv TemplateKeyValues) ContainsKey(key string) bool { } return false } + +type DeployConfig struct { + EntryPoint string + EnvVars []string + InvokeCommand string + RegisteredAppName string +} + +var Commands = map[string]map[string]DeployConfig{ + LanguageTypeScript: { + TemplateSampleApp: { + EntryPoint: "index.ts", + EnvVars: []string{}, + InvokeCommand: `kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}'`, + RegisteredAppName: "ts-basic", + }, + TemplateAdvancedSample: { + EntryPoint: "index.ts", + EnvVars: []string{}, + InvokeCommand: "kernel invoke ts-advanced test-captcha-solver", + RegisteredAppName: "ts-advanced", + }, + TemplateStagehand: { + EntryPoint: "index.ts", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-stagehand teamsize-task --payload '{"company": "Kernel"}'`, + RegisteredAppName: "ts-stagehand", + }, + TemplateComputerUse: { + EntryPoint: "index.ts", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, + RegisteredAppName: "ts-cu", + }, + TemplateMagnitude: { + EntryPoint: "index.ts", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-magnitude mag-url-extract --payload '{"url": "https://en.wikipedia.org/wiki/Special:Random"}'`, + RegisteredAppName: "ts-magnitude", + }, + TemplateCUA: { + EntryPoint: "index.ts", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, + RegisteredAppName: "ts-cua", + }, + TemplateGeminiCUA: { + EntryPoint: "index.ts", + EnvVars: []string{"GOOGLE_API_KEY=XXX", "OPENAI_API_KEY=XXX"}, + InvokeCommand: "kernel invoke ts-gemini-cua gemini-cua-task", + RegisteredAppName: "ts-gemini-cua", + }, + }, + LanguagePython: { + TemplateSampleApp: { + EntryPoint: "main.py", + EnvVars: []string{}, + InvokeCommand: `kernel invoke python-basic get-page-title --payload '{"url": "https://www.google.com"}'`, + RegisteredAppName: "python-basic", + }, + TemplateAdvancedSample: { + EntryPoint: "main.py", + EnvVars: []string{}, + InvokeCommand: "kernel invoke python-advanced test-captcha-solver", + RegisteredAppName: "python-advanced", + }, + TemplateBrowserUse: { + EntryPoint: "main.py", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-bu bu-task --payload '{"task": "Compare the price of gpt-4o and DeepSeek-V3"}'`, + RegisteredAppName: "python-bu", + }, + TemplateComputerUse: { + EntryPoint: "main.py", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, + RegisteredAppName: "python-cu", + }, + TemplateCUA: { + EntryPoint: "main.py", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, + RegisteredAppName: "python-cua", + }, + }, +} + +// GetDeployCommand returns the full deploy command string for a given language and template +func GetDeployCommand(language, template string) string { + langCommands, ok := Commands[language] + if !ok { + return "" + } + + config, ok := langCommands[template] + if !ok { + return "" + } + + cmd := "kernel deploy " + config.EntryPoint + for _, env := range config.EnvVars { + cmd += " --env " + env + } + + return cmd +} + +// GetInvokeSample returns the sample invoke command for a given language and template +func GetInvokeSample(language, template string) string { + langSamples, ok := Commands[language] + if !ok { + return "" + } + + config, ok := langSamples[template] + if !ok { + return "" + } + + return config.InvokeCommand +} + +// GetRegisteredAppName returns the registered app name for a given language and template +func GetRegisteredAppName(language, template string) string { + langNames, ok := Commands[language] + if !ok { + return "" + } + + config, ok := langNames[template] + if !ok { + return "" + } + + return config.RegisteredAppName +} From 3048406eb28772c3d3ebd672a647f087c20795e3 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Tue, 9 Dec 2025 17:23:18 +0000 Subject: [PATCH 11/15] fix: remove registered app name --- pkg/create/templates.go | 106 +++++++++++++++------------------------- 1 file changed, 39 insertions(+), 67 deletions(-) diff --git a/pkg/create/templates.go b/pkg/create/templates.go index 54b7fdc..6233425 100644 --- a/pkg/create/templates.go +++ b/pkg/create/templates.go @@ -123,87 +123,74 @@ func (tkv TemplateKeyValues) ContainsKey(key string) bool { } type DeployConfig struct { - EntryPoint string - EnvVars []string - InvokeCommand string - RegisteredAppName string + EntryPoint string + EnvVars []string + InvokeCommand string } var Commands = map[string]map[string]DeployConfig{ LanguageTypeScript: { TemplateSampleApp: { - EntryPoint: "index.ts", - EnvVars: []string{}, - InvokeCommand: `kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}'`, - RegisteredAppName: "ts-basic", + EntryPoint: "index.ts", + EnvVars: []string{}, + InvokeCommand: `kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}'`, }, TemplateAdvancedSample: { - EntryPoint: "index.ts", - EnvVars: []string{}, - InvokeCommand: "kernel invoke ts-advanced test-captcha-solver", - RegisteredAppName: "ts-advanced", + EntryPoint: "index.ts", + EnvVars: []string{}, + InvokeCommand: "kernel invoke ts-advanced test-captcha-solver", }, TemplateStagehand: { - EntryPoint: "index.ts", - EnvVars: []string{"OPENAI_API_KEY=XXX"}, - InvokeCommand: `kernel invoke ts-stagehand teamsize-task --payload '{"company": "Kernel"}'`, - RegisteredAppName: "ts-stagehand", + EntryPoint: "index.ts", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-stagehand teamsize-task --payload '{"company": "Kernel"}'`, }, TemplateComputerUse: { - EntryPoint: "index.ts", - EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, - InvokeCommand: `kernel invoke ts-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, - RegisteredAppName: "ts-cu", + EntryPoint: "index.ts", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, }, TemplateMagnitude: { - EntryPoint: "index.ts", - EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, - InvokeCommand: `kernel invoke ts-magnitude mag-url-extract --payload '{"url": "https://en.wikipedia.org/wiki/Special:Random"}'`, - RegisteredAppName: "ts-magnitude", + EntryPoint: "index.ts", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-magnitude mag-url-extract --payload '{"url": "https://en.wikipedia.org/wiki/Special:Random"}'`, }, TemplateCUA: { - EntryPoint: "index.ts", - EnvVars: []string{"OPENAI_API_KEY=XXX"}, - InvokeCommand: `kernel invoke ts-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, - RegisteredAppName: "ts-cua", + EntryPoint: "index.ts", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke ts-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, }, TemplateGeminiCUA: { - EntryPoint: "index.ts", - EnvVars: []string{"GOOGLE_API_KEY=XXX", "OPENAI_API_KEY=XXX"}, - InvokeCommand: "kernel invoke ts-gemini-cua gemini-cua-task", - RegisteredAppName: "ts-gemini-cua", + EntryPoint: "index.ts", + EnvVars: []string{"GOOGLE_API_KEY=XXX", "OPENAI_API_KEY=XXX"}, + InvokeCommand: "kernel invoke ts-gemini-cua gemini-cua-task", }, }, LanguagePython: { TemplateSampleApp: { - EntryPoint: "main.py", - EnvVars: []string{}, - InvokeCommand: `kernel invoke python-basic get-page-title --payload '{"url": "https://www.google.com"}'`, - RegisteredAppName: "python-basic", + EntryPoint: "main.py", + EnvVars: []string{}, + InvokeCommand: `kernel invoke python-basic get-page-title --payload '{"url": "https://www.google.com"}'`, }, TemplateAdvancedSample: { - EntryPoint: "main.py", - EnvVars: []string{}, - InvokeCommand: "kernel invoke python-advanced test-captcha-solver", - RegisteredAppName: "python-advanced", + EntryPoint: "main.py", + EnvVars: []string{}, + InvokeCommand: "kernel invoke python-advanced test-captcha-solver", }, TemplateBrowserUse: { - EntryPoint: "main.py", - EnvVars: []string{"OPENAI_API_KEY=XXX"}, - InvokeCommand: `kernel invoke python-bu bu-task --payload '{"task": "Compare the price of gpt-4o and DeepSeek-V3"}'`, - RegisteredAppName: "python-bu", + EntryPoint: "main.py", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-bu bu-task --payload '{"task": "Compare the price of gpt-4o and DeepSeek-V3"}'`, }, TemplateComputerUse: { - EntryPoint: "main.py", - EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, - InvokeCommand: `kernel invoke python-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, - RegisteredAppName: "python-cu", + EntryPoint: "main.py", + EnvVars: []string{"ANTHROPIC_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-cu cu-task --payload '{"query": "Return the first url of a search result for NYC restaurant reviews Pete Wells"}'`, }, TemplateCUA: { - EntryPoint: "main.py", - EnvVars: []string{"OPENAI_API_KEY=XXX"}, - InvokeCommand: `kernel invoke python-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, - RegisteredAppName: "python-cua", + EntryPoint: "main.py", + EnvVars: []string{"OPENAI_API_KEY=XXX"}, + InvokeCommand: `kernel invoke python-cua cua-task --payload '{"task": "Go to https://news.ycombinator.com and get the top 5 articles"}'`, }, }, } @@ -242,18 +229,3 @@ func GetInvokeSample(language, template string) string { return config.InvokeCommand } - -// GetRegisteredAppName returns the registered app name for a given language and template -func GetRegisteredAppName(language, template string) string { - langNames, ok := Commands[language] - if !ok { - return "" - } - - config, ok := langNames[template] - if !ok { - return "" - } - - return config.RegisteredAppName -} From 6129873ed93957d9e5b86830fab1d7c052ad147c Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Tue, 9 Dec 2025 19:40:00 +0000 Subject: [PATCH 12/15] feat: add tests --- cmd/create_test.go | 212 +++++++++++++++++++++++++++++++++ pkg/create/copy_test.go | 257 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 pkg/create/copy_test.go diff --git a/cmd/create_test.go b/cmd/create_test.go index 835f135..9c9d232 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -412,6 +412,218 @@ func TestCreateCommand_DirectoryOverwrite(t *testing.T) { assert.FileExists(t, filepath.Join(appPath, "index.ts"), "new index.ts should exist") } +// TestCreateCommand_InvalidLanguageTemplateCombinations tests that invalid +// language/template combinations fail with appropriate error messages +func TestCreateCommand_InvalidLanguageTemplateCombinations(t *testing.T) { + tests := []struct { + name string + language string + template string + errContains string + }{ + { + name: "browser-use not available for typescript", + language: create.LanguageTypeScript, + template: create.TemplateBrowserUse, + errContains: "template not found: typescript/browser-use", + }, + { + name: "stagehand not available for python", + language: create.LanguagePython, + template: create.TemplateStagehand, + errContains: "template not found: python/stagehand", + }, + { + name: "magnitude not available for python", + language: create.LanguagePython, + template: create.TemplateMagnitude, + errContains: "template not found: python/magnitude", + }, + { + name: "gemini-cua not available for python", + language: create.LanguagePython, + template: create.TemplateGeminiCUA, + errContains: "template not found: python/gemini-cua", + }, + { + name: "invalid language", + language: "ruby", + template: create.TemplateSampleApp, + errContains: "template not found: ruby/sample-app", + }, + { + name: "invalid template", + language: create.LanguageTypeScript, + template: "nonexistent-template", + errContains: "template not found: typescript/nonexistent-template", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + + orgDir, err := os.Getwd() + require.NoError(t, err) + + err = os.Chdir(tmpDir) + require.NoError(t, err) + + t.Cleanup(func() { + os.Chdir(orgDir) + }) + + c := CreateCmd{} + err = c.Create(context.Background(), CreateInput{ + Name: "test-app", + Language: tt.language, + Template: tt.template, + }) + + require.Error(t, err, "should fail with invalid language/template combination") + assert.Contains(t, err.Error(), tt.errContains, "error message should contain expected text") + }) + } +} + +// TestCreateCommand_ValidateAllTemplateCombinations validates that only valid +// language/template combinations are defined in the Templates map +func TestCreateCommand_ValidateAllTemplateCombinations(t *testing.T) { + // This test ensures data consistency between Templates and actual template availability + for templateKey, templateInfo := range create.Templates { + for _, lang := range templateInfo.Languages { + t.Run(lang+"/"+templateKey, func(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, DIR_PERM) + require.NoError(t, err) + + // This should succeed for all combinations defined in Templates + err = create.CopyTemplateFiles(appPath, lang, templateKey) + require.NoError(t, err, "Template %s should be available for language %s as defined in Templates map", templateKey, lang) + }) + } + } +} + +// TestCreateCommand_InvalidLanguageShorthand tests that invalid language shorthands +// are handled appropriately +func TestCreateCommand_InvalidLanguageShorthand(t *testing.T) { + tests := []struct { + name string + languageInput string + expectedNormalized string + }{ + { + name: "ts shorthand normalizes to typescript", + languageInput: "ts", + expectedNormalized: create.LanguageTypeScript, + }, + { + name: "py shorthand normalizes to python", + languageInput: "py", + expectedNormalized: create.LanguagePython, + }, + { + name: "typescript remains typescript", + languageInput: "typescript", + expectedNormalized: create.LanguageTypeScript, + }, + { + name: "python remains python", + languageInput: "python", + expectedNormalized: create.LanguagePython, + }, + { + name: "invalid shorthand remains unchanged", + languageInput: "js", + expectedNormalized: "js", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + normalized := create.NormalizeLanguage(tt.languageInput) + assert.Equal(t, tt.expectedNormalized, normalized) + }) + } +} + +// TestCreateCommand_TemplateNotAvailableForLanguage tests specific cases where +// templates are not available for certain languages +func TestCreateCommand_TemplateNotAvailableForLanguage(t *testing.T) { + // Map of templates to languages they should NOT be available for + unavailableCombinations := map[string][]string{ + create.TemplateBrowserUse: {create.LanguageTypeScript}, + create.TemplateStagehand: {create.LanguagePython}, + create.TemplateMagnitude: {create.LanguagePython}, + create.TemplateGeminiCUA: {create.LanguagePython}, + } + + for template, unavailableLanguages := range unavailableCombinations { + for _, lang := range unavailableLanguages { + t.Run(template+"/"+lang, func(t *testing.T) { + // Verify the template info doesn't list this language + templateInfo, exists := create.Templates[template] + require.True(t, exists, "Template %s should exist in Templates map", template) + + assert.NotContains(t, templateInfo.Languages, lang, + "Template %s should not list %s as a supported language", template, lang) + + // Verify copying fails + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + err := os.MkdirAll(appPath, DIR_PERM) + require.NoError(t, err) + + err = create.CopyTemplateFiles(appPath, lang, template) + require.Error(t, err, "Should fail to copy %s template for %s", template, lang) + }) + } + } +} + +// TestCreateCommand_AllTemplatesHaveDeployCommands ensures that all templates +// have corresponding deploy commands defined +func TestCreateCommand_AllTemplatesHaveDeployCommands(t *testing.T) { + for templateKey, templateInfo := range create.Templates { + for _, lang := range templateInfo.Languages { + t.Run(lang+"/"+templateKey, func(t *testing.T) { + deployCmd := create.GetDeployCommand(lang, templateKey) + assert.NotEmpty(t, deployCmd, "Deploy command should exist for %s/%s", lang, templateKey) + + // Verify deploy command starts with "kernel deploy" + assert.Contains(t, deployCmd, "kernel deploy", "Deploy command should start with 'kernel deploy'") + + // Verify it contains the entry point + switch lang { + case create.LanguageTypeScript: + assert.Contains(t, deployCmd, "index.ts", "TypeScript deploy command should contain index.ts") + case create.LanguagePython: + assert.Contains(t, deployCmd, "main.py", "Python deploy command should contain main.py") + } + }) + } + } +} + +// TestCreateCommand_AllTemplatesHaveInvokeSamples ensures that all templates +// have corresponding invoke samples defined +func TestCreateCommand_AllTemplatesHaveInvokeSamples(t *testing.T) { + for templateKey, templateInfo := range create.Templates { + for _, lang := range templateInfo.Languages { + t.Run(lang+"/"+templateKey, func(t *testing.T) { + invokeCmd := create.GetInvokeSample(lang, templateKey) + assert.NotEmpty(t, invokeCmd, "Invoke sample should exist for %s/%s", lang, templateKey) + + // Verify invoke command starts with "kernel invoke" + assert.Contains(t, invokeCmd, "kernel invoke", "Invoke command should start with 'kernel invoke'") + }) + } + } +} + func getTemplateInfo() []struct { name string language string diff --git a/pkg/create/copy_test.go b/pkg/create/copy_test.go new file mode 100644 index 0000000..c8228e5 --- /dev/null +++ b/pkg/create/copy_test.go @@ -0,0 +1,257 @@ +package create + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testDirPerm = 0755 + testFilePerm = 0644 +) + +func TestCopyTemplateFiles_Success(t *testing.T) { + tests := []struct { + name string + language string + template string + validate func(t *testing.T, appPath string) + }{ + { + name: "typescript sample-app", + language: LanguageTypeScript, + template: TemplateSampleApp, + validate: func(t *testing.T, appPath string) { + assert.FileExists(t, filepath.Join(appPath, "index.ts")) + assert.FileExists(t, filepath.Join(appPath, "package.json")) + assert.FileExists(t, filepath.Join(appPath, "tsconfig.json")) + assert.FileExists(t, filepath.Join(appPath, ".gitignore")) + assert.NoFileExists(t, filepath.Join(appPath, "_gitignore")) + }, + }, + { + name: "python sample-app", + language: LanguagePython, + template: TemplateSampleApp, + validate: func(t *testing.T, appPath string) { + assert.FileExists(t, filepath.Join(appPath, "main.py")) + assert.FileExists(t, filepath.Join(appPath, "pyproject.toml")) + assert.FileExists(t, filepath.Join(appPath, ".gitignore")) + assert.NoFileExists(t, filepath.Join(appPath, "_gitignore")) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, tt.language, tt.template) + require.NoError(t, err, "CopyTemplateFiles should succeed") + + if tt.validate != nil { + tt.validate(t, appPath) + } + }) + } +} + +func TestCopyTemplateFiles_InvalidTemplate(t *testing.T) { + tests := []struct { + name string + language string + template string + errContains string + }{ + { + name: "nonexistent template", + language: LanguageTypeScript, + template: "nonexistent-template", + errContains: "template not found: typescript/nonexistent-template", + }, + { + name: "nonexistent language", + language: "ruby", + template: TemplateSampleApp, + errContains: "template not found: ruby/sample-app", + }, + { + name: "empty language", + language: "", + template: TemplateSampleApp, + errContains: "template not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, tt.language, tt.template) + require.Error(t, err, "CopyTemplateFiles should fail with invalid template") + assert.Contains(t, err.Error(), tt.errContains) + }) + } +} + +func TestCopyTemplateFiles_GitignoreRename(t *testing.T) { + // Test that _gitignore is properly renamed to .gitignore + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, LanguageTypeScript, TemplateSampleApp) + require.NoError(t, err) + + // Verify .gitignore exists + gitignorePath := filepath.Join(appPath, ".gitignore") + assert.FileExists(t, gitignorePath, ".gitignore should exist") + + // Verify _gitignore does not exist + underscoreGitignorePath := filepath.Join(appPath, "_gitignore") + assert.NoFileExists(t, underscoreGitignorePath, "_gitignore should not exist") + + // Verify .gitignore has content + content, err := os.ReadFile(gitignorePath) + require.NoError(t, err) + assert.NotEmpty(t, content, ".gitignore should have content") +} + +func TestCopyTemplateFiles_FilePermissions(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, LanguageTypeScript, TemplateSampleApp) + require.NoError(t, err) + + // Check file permissions + indexPath := filepath.Join(appPath, "index.ts") + info, err := os.Stat(indexPath) + require.NoError(t, err) + + // Verify file has correct permissions (0644) + mode := info.Mode() + assert.Equal(t, os.FileMode(FILE_PERM), mode.Perm(), "File should have 0644 permissions") +} + +func TestCopyTemplateFiles_DirectoryPermissions(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, LanguageTypeScript, TemplateSampleApp) + require.NoError(t, err) + + // Check directory permissions + info, err := os.Stat(appPath) + require.NoError(t, err) + + // Verify directory has correct permissions (0755) + mode := info.Mode() + assert.True(t, mode.IsDir(), "Should be a directory") + assert.Equal(t, os.FileMode(DIR_PERM), mode.Perm(), "Directory should have 0755 permissions") +} + +func TestCopyTemplateFiles_PreservesDirectoryStructure(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + // Use a template that has subdirectories + err = CopyTemplateFiles(appPath, LanguageTypeScript, TemplateComputerUse) + require.NoError(t, err) + + // Verify that subdirectories are created (computer-use has src/ directory) + srcDir := filepath.Join(appPath, "src") + if _, err := os.Stat(srcDir); err == nil { + assert.DirExists(t, srcDir, "Subdirectories should be preserved") + } +} + +func TestCopyTemplateFiles_OverwritesExistingFiles(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + // Create an existing file with different content + existingFile := filepath.Join(appPath, "index.ts") + existingContent := []byte("// old content") + err = os.WriteFile(existingFile, existingContent, testFilePerm) + require.NoError(t, err) + + // Copy template files (should overwrite) + err = CopyTemplateFiles(appPath, LanguageTypeScript, TemplateSampleApp) + require.NoError(t, err) + + // Verify file was overwritten + newContent, err := os.ReadFile(existingFile) + require.NoError(t, err) + assert.NotEqual(t, existingContent, newContent, "File should be overwritten with new content") +} + +func TestCopyTemplateFiles_InvalidDestinationPath(t *testing.T) { + // Test with a path that cannot be created (e.g., file exists where directory should be) + tmpDir := t.TempDir() + + // Create a file where we want to create a directory + blockingFile := filepath.Join(tmpDir, "test-app") + err := os.WriteFile(blockingFile, []byte("blocking"), testFilePerm) + require.NoError(t, err) + + // Try to copy template (should fail because test-app is a file, not a directory) + err = CopyTemplateFiles(blockingFile, LanguageTypeScript, TemplateSampleApp) + require.Error(t, err, "Should fail when destination path is invalid") +} + +func TestCopyTemplateFiles_AllTemplatesForAllLanguages(t *testing.T) { + // Comprehensive test that all template/language combinations work + for templateKey, templateInfo := range Templates { + for _, lang := range templateInfo.Languages { + t.Run(lang+"/"+templateKey, func(t *testing.T) { + tmpDir := t.TempDir() + appPath := filepath.Join(tmpDir, "test-app") + + err := os.MkdirAll(appPath, testDirPerm) + require.NoError(t, err) + + err = CopyTemplateFiles(appPath, lang, templateKey) + require.NoError(t, err, "Should successfully copy %s/%s template", lang, templateKey) + + // Verify at least some files were created + entries, err := os.ReadDir(appPath) + require.NoError(t, err) + assert.NotEmpty(t, entries, "Template directory should not be empty") + + // Verify .gitignore exists and _gitignore does not + gitignorePath := filepath.Join(appPath, ".gitignore") + assert.FileExists(t, gitignorePath, ".gitignore should exist") + + underscoreGitignorePath := filepath.Join(appPath, "_gitignore") + assert.NoFileExists(t, underscoreGitignorePath, "_gitignore should not exist") + }) + } + } +} From 600a65e1af75c5b3fc4ce7216c746d426326a570 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Wed, 10 Dec 2025 15:52:16 +0000 Subject: [PATCH 13/15] fix: re-add in dynamic commands --- cmd/create.go | 12 +++--------- cmd/create_test.go | 16 ++++++++-------- pkg/create/dependencies.go | 39 +++++++++++++++++++++++--------------- pkg/create/types.go | 6 ++++++ 4 files changed, 41 insertions(+), 32 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index d413442..1fd8ef7 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -11,17 +11,11 @@ import ( "github.com/spf13/cobra" ) -type CreateInput struct { - Name string - Language string - Template string -} - // CreateCmd is a cobra-independent command handler for create operations type CreateCmd struct{} // Create executes the creating a new Kernel app logic -func (c CreateCmd) Create(ctx context.Context, ci CreateInput) error { +func (c CreateCmd) Create(ctx context.Context, ci create.CreateInput) error { appPath, err := filepath.Abs(ci.Name) if err != nil { return fmt.Errorf("failed to resolve app path: %w", err) @@ -58,7 +52,7 @@ func (c CreateCmd) Create(ctx context.Context, ci CreateInput) error { return fmt.Errorf("failed to copy template files: %w", err) } - nextSteps, err := create.InstallDependencies(ci.Name, appPath, ci.Language) + nextSteps, err := create.InstallDependencies(appPath, ci) if err != nil { return fmt.Errorf("failed to install dependencies: %w", err) } @@ -103,7 +97,7 @@ func runCreateApp(cmd *cobra.Command, args []string) error { } c := CreateCmd{} - return c.Create(cmd.Context(), CreateInput{ + return c.Create(cmd.Context(), create.CreateInput{ Name: appName, Language: language, Template: template, diff --git a/cmd/create_test.go b/cmd/create_test.go index 9c9d232..614e758 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -22,14 +22,14 @@ const ( func TestCreateCommand(t *testing.T) { tests := []struct { name string - input CreateInput + input create.CreateInput wantErr bool errContains string validate func(t *testing.T, appPath string) }{ { name: "create typescript sample-app", - input: CreateInput{ + input: create.CreateInput{ Name: "test-app", Language: "typescript", Template: "sample-app", @@ -44,7 +44,7 @@ func TestCreateCommand(t *testing.T) { }, { name: "fail with invalid template", - input: CreateInput{ + input: create.CreateInput{ Name: "test-app", Language: "typescript", Template: "nonexistent", @@ -117,7 +117,7 @@ func TestAllTemplatesWithDependencies(t *testing.T) { // Create the app c := CreateCmd{} - err = c.Create(context.Background(), CreateInput{ + err = c.Create(context.Background(), create.CreateInput{ Name: appName, Language: tt.language, Template: tt.template, @@ -263,7 +263,7 @@ func TestCreateCommand_DependencyInstallationFails(t *testing.T) { // Create the app - should succeed even though dependency installation fails c := CreateCmd{} - err = c.Create(context.Background(), CreateInput{ + err = c.Create(context.Background(), create.CreateInput{ Name: appName, Language: create.LanguageTypeScript, Template: "sample-app", @@ -323,7 +323,7 @@ func TestCreateCommand_RequiredToolMissing(t *testing.T) { // Create the app - should succeed even though required tool is missing c := CreateCmd{} - err = c.Create(context.Background(), CreateInput{ + err = c.Create(context.Background(), create.CreateInput{ Name: appName, Language: tt.language, Template: tt.template, @@ -397,7 +397,7 @@ func TestCreateCommand_DirectoryOverwrite(t *testing.T) { require.NoError(t, err, "failed to remove existing directory") c := CreateCmd{} - err = c.Create(context.Background(), CreateInput{ + err = c.Create(context.Background(), create.CreateInput{ Name: appName, Language: create.LanguageTypeScript, Template: "sample-app", @@ -474,7 +474,7 @@ func TestCreateCommand_InvalidLanguageTemplateCombinations(t *testing.T) { }) c := CreateCmd{} - err = c.Create(context.Background(), CreateInput{ + err = c.Create(context.Background(), create.CreateInput{ Name: "test-app", Language: tt.language, Template: tt.template, diff --git a/pkg/create/dependencies.go b/pkg/create/dependencies.go index 6c31417..5e080d2 100644 --- a/pkg/create/dependencies.go +++ b/pkg/create/dependencies.go @@ -8,7 +8,11 @@ import ( ) // InstallDependencies sets up project dependencies based on language -func InstallDependencies(appName string, appPath string, language string) (string, error) { +func InstallDependencies(appPath string, ci CreateInput) (string, error) { + language := ci.Language + template := ci.Template + appName := ci.Name + installCommand, ok := InstallCommands[language] if !ok { return "", fmt.Errorf("unsupported language: %s", language) @@ -16,7 +20,7 @@ func InstallDependencies(appName string, appPath string, language string) (strin requiredTool := RequiredTools[language] if requiredTool != "" && !RequiredTools.CheckToolAvailable(language) { - return getNextStepsWithToolInstall(appName, language, requiredTool), nil + return getNextStepsWithToolInstall(appName, language, requiredTool, template), nil } spinner, _ := pterm.DefaultSpinner.Start(pterm.Sprintf("Setting up %s environment...", language)) @@ -36,16 +40,19 @@ func InstallDependencies(appName string, appPath string, language string) (strin pterm.Println(" uv venv && source .venv/bin/activate && uv sync") } pterm.Println() - return getNextStepsStandard(appName), nil + return getNextStepsStandard(appName, language, template), nil } spinner.Success(pterm.Sprintf("✔ %s environment set up successfully", language)) - return getNextStepsStandard(appName), nil + return getNextStepsStandard(appName, language, template), nil } // getNextStepsWithToolInstall returns next steps message including tool installation -func getNextStepsWithToolInstall(appName string, language string, requiredTool string) string { +func getNextStepsWithToolInstall(appName string, language string, requiredTool string, template string) string { + deployCommand := GetDeployCommand(language, template) + invokeCommand := GetInvokeSample(language, template) + pterm.Warning.Printfln(" %s is not installed or not in PATH", requiredTool) switch language { @@ -63,9 +70,9 @@ func getNextStepsWithToolInstall(appName string, language string, requiredTool s # Deploy your app: brew install onkernel/tap/kernel kernel login # or: export KERNEL_API_KEY= - kernel deploy index.ts - kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}' -`, appName) + %s + %s +`, appName, deployCommand, invokeCommand) case LanguagePython: return pterm.FgYellow.Sprintf(`Next steps: # Install uv (choose one): @@ -80,21 +87,23 @@ func getNextStepsWithToolInstall(appName string, language string, requiredTool s # Deploy your app: brew install onkernel/tap/kernel kernel login # or: export KERNEL_API_KEY= - kernel deploy index.py - kernel invoke py-basic get-page-title --payload '{"url": "https://www.google.com"}' -`, appName) + %s + %s +`, appName, deployCommand, invokeCommand) default: return "" } } // getNextStepsStandard returns standard next steps message -func getNextStepsStandard(appName string) string { +func getNextStepsStandard(appName string, language string, template string) string { + deployCommand := GetDeployCommand(language, template) + invokeCommand := GetInvokeSample(language, template) return pterm.FgYellow.Sprintf(`Next steps: brew install onkernel/tap/kernel cd %s kernel login # or: export KERNEL_API_KEY= - kernel deploy index.ts - kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}' -`, appName) + %s + %s +`, appName, deployCommand, invokeCommand) } diff --git a/pkg/create/types.go b/pkg/create/types.go index 67939b7..05e76bc 100644 --- a/pkg/create/types.go +++ b/pkg/create/types.go @@ -2,6 +2,12 @@ package create import "os/exec" +type CreateInput struct { + Name string + Language string + Template string +} + const ( DefaultAppName = "my-kernel-app" AppNamePrompt = "What is the name of your project?" From 803e9006bb388368cf28ad8afa7432c9301ba778 Mon Sep 17 00:00:00 2001 From: Archan Datta Date: Wed, 10 Dec 2025 16:08:19 +0000 Subject: [PATCH 14/15] feat: update README to include docs for command --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f50bb7..f2da257 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Kernel provides sandboxed, ready-to-use Chrome browsers for browser automations ### What you can do with the CLI +- Create new Kernel applications from templates - Deploy and version apps to Kernel - Invoke app actions (sync or async) and stream logs - Create, list, view, and delete managed browser sessions @@ -50,19 +51,25 @@ kernel --version ## Quick Start -1. **Authenticate with Kernel:** +1. **Create a new Kernel app:** + + ```bash + kernel create + ``` + +2. **Authenticate with Kernel:** ```bash kernel login ``` -2. **Deploy your first app:** +3. **Deploy your app:** ```bash kernel deploy index.ts ``` -3. **Invoke your app:** +4. **Invoke your app:** ```bash kernel invoke my-app action-name --payload '{"key": "value"}' ``` @@ -103,6 +110,20 @@ Create an API key from the [Kernel dashboard](https://dashboard.onkernel.com). - `kernel logout` - Clear stored credentials - `kernel auth` - Check authentication status +### App Creation + +- `--name `, `-n` - Name of the application +- `--language `, `-l` - Sepecify app language: `typescript`, or `python` +- `--template