diff --git a/cmd/create.go b/cmd/create.go new file mode 100644 index 0000000..9d50c5d --- /dev/null +++ b/cmd/create.go @@ -0,0 +1,104 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/onkernel/cli/pkg/create" + "github.com/pterm/pterm" + "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 { + appPath, err := filepath.Abs(ci.Name) + if err != nil { + return fmt.Errorf("failed to resolve app path: %w", err) + } + + // TODO: handle overwrite gracefully (prompt user) + // Check if directory already exists + if _, err := os.Stat(appPath); err == nil { + return fmt.Errorf("directory %s already exists", ci.Name) + } + + if err := os.MkdirAll(appPath, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + pterm.Println(fmt.Sprintf("\nCreating a new %s %s\n", ci.Language, ci.Template)) + + spinner, _ := pterm.DefaultSpinner.Start("Copying template files...") + + if err := create.CopyTemplateFiles(appPath, ci.Language, ci.Template); err != nil { + spinner.Fail("Failed to copy template files") + return fmt.Errorf("failed to copy template files: %w", err) + } + spinner.Success(fmt.Sprintf("✔ %s environment set up successfully", ci.Language)) + + nextSteps := fmt.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"}' +`, ci.Name) + + pterm.Success.Println("🎉 Kernel app created successfully!") + pterm.Println() + pterm.FgYellow.Println(nextSteps) + + return nil +} + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create a new application", + Long: "Commands for creating new Kernel applications", + RunE: runCreateApp, +} + +func init() { + createCmd.Flags().StringP("name", "n", "", "Name of the application") + createCmd.Flags().StringP("language", "l", "", "Language of the application") + createCmd.Flags().StringP("template", "t", "", "Template to use for the application") +} + +func runCreateApp(cmd *cobra.Command, args []string) error { + appName, _ := cmd.Flags().GetString("name") + language, _ := cmd.Flags().GetString("language") + template, _ := cmd.Flags().GetString("template") + + appName, err := create.PromptForAppName(appName) + if err != nil { + return fmt.Errorf("failed to get app name: %w", err) + } + + language, err = create.PromptForLanguage(language) + if err != nil { + return fmt.Errorf("failed to get language: %w", err) + } + + template, err = create.PromptForTemplate(template) + if err != nil { + return fmt.Errorf("failed to get template: %w", err) + } + + c := CreateCmd{} + return c.Create(cmd.Context(), CreateInput{ + Name: appName, + Language: language, + Template: template, + }) +} diff --git a/cmd/create_test.go b/cmd/create_test.go new file mode 100644 index 0000000..29c8358 --- /dev/null +++ b/cmd/create_test.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateCommand(t *testing.T) { + tests := []struct { + name string + input CreateInput + wantErr bool + errContains string + validate func(t *testing.T, appPath string) + }{ + { + name: "create typescript sample-app", + input: CreateInput{ + Name: "test-app", + Language: "typescript", + Template: "sample-app", + }, + validate: func(t *testing.T, appPath string) { + // Verify files were created + assert.FileExists(t, filepath.Join(appPath, "index.ts")) + assert.FileExists(t, filepath.Join(appPath, "package.json")) + assert.FileExists(t, filepath.Join(appPath, ".gitignore")) + assert.NoFileExists(t, filepath.Join(appPath, "_gitignore")) + }, + }, + { + name: "fail with invalid template", + input: CreateInput{ + Name: "test-app", + Language: "typescript", + Template: "nonexistent", + }, + wantErr: true, + errContains: "template not found: typescript/nonexistent", + }, + } + + 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(), tt.input) + + // Check if error is expected + if tt.wantErr { + require.Error(t, err, "expected command to fail but it succeeded") + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains, "error message should contain expected text") + } + return + } + + require.NoError(t, err, "failed to execute create command") + + // Validate the created app + appPath := filepath.Join(tmpDir, tt.input.Name) + assert.DirExists(t, appPath, "app directory should be created") + + if tt.validate != nil { + tt.validate(t, appPath) + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 06cce7c..61f0fc5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -79,7 +79,8 @@ func isAuthExempt(cmd *cobra.Command) bool { } for c := cmd; c != nil; c = c.Parent() { switch c.Name() { - case "login", "logout", "auth", "help", "completion": + case "login", "logout", "auth", "help", "completion", + "create": return true } } @@ -128,6 +129,7 @@ func init() { rootCmd.AddCommand(profilesCmd) rootCmd.AddCommand(proxies.ProxiesCmd) rootCmd.AddCommand(extensionsCmd) + rootCmd.AddCommand(createCmd) rootCmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) error { // running synchronously so we never slow the command diff --git a/pkg/create/copy.go b/pkg/create/copy.go new file mode 100644 index 0000000..c70be9f --- /dev/null +++ b/pkg/create/copy.go @@ -0,0 +1,84 @@ +package create + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/onkernel/cli/pkg/templates" +) + +const ( + DIR_PERM = 0755 // rwxr-xr-x + FILE_PERM = 0644 // rw-r--r-- +) + +// CopyTemplateFiles copies all files and directories from the specified embedded template +// into the target application path. It uses the given language and template names +// to locate the template inside the embedded filesystem. +// +// - appPath: filesystem path where the files should be written (the project directory) +// - language: language subdirectory (e.g., "typescript") +// - template: template subdirectory (e.g., "sample-app") +// +// The function will recursively walk through the embedded template directory and +// replicate all files and folders in appPath. If a file named "_gitignore" is encountered, +// it is renamed to ".gitignore" in the output, to work around file embedding limitations. +// +// Returns an error if the template path is invalid, empty, or if any file operations fail. +func CopyTemplateFiles(appPath, language, template string) error { + // Build the template path within the embedded FS (e.g., "typescript/sample-app") + templatePath := filepath.Join(language, template) + + // Check if the template exists and is non-empty + entries, err := fs.ReadDir(templates.FS, templatePath) + if err != nil { + return fmt.Errorf("template not found: %s/%s", language, template) + } + if len(entries) == 0 { + return fmt.Errorf("template directory is empty: %s/%s", language, template) + } + + // Walk through the embedded template directory and copy contents + return fs.WalkDir(templates.FS, templatePath, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Determine the path relative to the root of the template + relPath, err := filepath.Rel(templatePath, path) + if err != nil { + return err + } + + // Skip the template root directory itself + if relPath == "." { + return nil + } + + destPath := filepath.Join(appPath, relPath) + + if d.IsDir() { + return os.MkdirAll(destPath, DIR_PERM) + } + + // Read the file content from the embedded filesystem + content, err := fs.ReadFile(templates.FS, path) + if err != nil { + return fmt.Errorf("failed to read template file %s: %w", path, err) + } + + // Rename _gitignore to .gitignore in the destination + if filepath.Base(destPath) == "_gitignore" { + destPath = filepath.Join(filepath.Dir(destPath), ".gitignore") + } + + // Write the file to disk in the target project directory + if err := os.WriteFile(destPath, content, FILE_PERM); err != nil { + return fmt.Errorf("failed to write file %s: %w", destPath, err) + } + + return nil + }) +} diff --git a/pkg/create/prompts.go b/pkg/create/prompts.go new file mode 100644 index 0000000..1d656a3 --- /dev/null +++ b/pkg/create/prompts.go @@ -0,0 +1,112 @@ +package create + +import ( + "fmt" + "regexp" + "slices" + + "github.com/pterm/pterm" +) + +// validateAppName validates that an app name follows the required format. +// Returns an error if the name is invalid. +func validateAppName(val any) error { + str, ok := val.(string) + if !ok { + return fmt.Errorf("invalid input type") + } + + if len(str) == 0 { + return fmt.Errorf("project name cannot be empty") + } + + // Validate project name: only letters, numbers, underscores, and hyphens + matched, err := regexp.MatchString(`^[A-Za-z\-_\d]+$`, str) + if err != nil { + return err + } + if !matched { + return fmt.Errorf("project name may only include letters, numbers, underscores, and hyphens") + } + return nil +} + +// handleAppNamePrompt prompts the user for an app name interactively. +func handleAppNamePrompt() (string, error) { + promptText := fmt.Sprintf("%s (%s)", AppNamePrompt, DefaultAppName) + appName, err := pterm.DefaultInteractiveTextInput. + WithDefaultText(promptText). + Show() + if err != nil { + return "", err + } + + if appName == "" { + appName = DefaultAppName + } + + if err := validateAppName(appName); err != nil { + pterm.Warning.Printf("Invalid app name '%s': %v\n", appName, err) + pterm.Info.Println("Please provide a valid app name.") + return handleAppNamePrompt() + } + + return appName, nil +} + +// PromptForAppName validates the provided app name or prompts the user for one. +// If the provided name is invalid, it shows a warning and prompts the user. +func PromptForAppName(providedAppName string) (string, error) { + // If no app name was provided, prompt the user + if providedAppName == "" { + return handleAppNamePrompt() + } + + if err := validateAppName(providedAppName); err != nil { + pterm.Warning.Printf("Invalid app name '%s': %v\n", providedAppName, err) + pterm.Info.Println("Please provide a valid app name.") + return handleAppNamePrompt() + } + + return providedAppName, nil +} + +func handleLanguagePrompt() (string, error) { + l, err := pterm.DefaultInteractiveSelect. + WithOptions(SupportedLanguages). + WithDefaultText(LanguagePrompt). + Show() + if err != nil { + return "", err + } + return l, nil +} + +func PromptForLanguage(providedLanguage string) (string, error) { + if providedLanguage == "" { + return handleLanguagePrompt() + } + + l := NormalizeLanguage(providedLanguage) + if slices.Contains(SupportedLanguages, l) { + return l, nil + } + + return handleLanguagePrompt() +} + +// TODO: add validation for template +func PromptForTemplate(providedTemplate string) (string, error) { + if providedTemplate != "" { + return providedTemplate, nil + } + + template, err := pterm.DefaultInteractiveSelect. + WithOptions(GetSupportedTemplates()). + WithDefaultText(TemplatePrompt). + Show() + if err != nil { + return "", err + } + return template, nil +} diff --git a/pkg/create/types.go b/pkg/create/types.go new file mode 100644 index 0000000..a09ddfb --- /dev/null +++ b/pkg/create/types.go @@ -0,0 +1,56 @@ +package create + +const ( + DefaultAppName = "my-kernel-app" + AppNamePrompt = "What is the name of your project?" + LanguagePrompt = "Choose a programming language:" + TemplatePrompt = "Select a template:" +) + +const ( + LanguageTypeScript = "typescript" + LanguagePython = "python" + LanguageShorthandTypeScript = "ts" + LanguageShorthandPython = "py" +) + +type TemplateInfo struct { + Name string + Description string + Languages []string +} + +var Templates = map[string]TemplateInfo{ + "sample-app": { + Name: "Sample App", + Description: "Implements basic Kernel apps", + Languages: []string{LanguageTypeScript, LanguagePython}, + }, +} + +// SupportedLanguages returns a list of all supported languages +var SupportedLanguages = []string{ + LanguageTypeScript, + LanguagePython, +} + +// GetSupportedTemplates returns a list of all supported template names +func GetSupportedTemplates() []string { + templates := make([]string, 0, len(Templates)) + for tn := range Templates { + templates = append(templates, tn) + } + return templates +} + +// Helper to normalize language input (handle shorthand) +func NormalizeLanguage(language string) string { + switch language { + case LanguageShorthandTypeScript: + return LanguageTypeScript + case LanguageShorthandPython: + return LanguagePython + default: + return language + } +} diff --git a/pkg/create/types_test.go b/pkg/create/types_test.go new file mode 100644 index 0000000..7e84c74 --- /dev/null +++ b/pkg/create/types_test.go @@ -0,0 +1,44 @@ +package create + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeLanguage(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"ts", "typescript"}, + {"py", "python"}, + {"typescript", "typescript"}, + {"invalid", "invalid"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got := NormalizeLanguage(tt.input) + assert.Equal(t, tt.expected, got, "NormalizeLanguage(%q) should return %q, got %q", tt.input, tt.expected, got) + }) + } +} + +func TestTemplates(t *testing.T) { + // Should have at least one template + assert.NotEmpty(t, Templates, "Templates map should not be empty") + + // Sample app should exist + sampleApp, exists := Templates["sample-app"] + assert.True(t, exists, "sample-app template should exist") + + // Sample app should have required fields + assert.NotEmpty(t, sampleApp.Name, "Template should have a name") + assert.NotEmpty(t, sampleApp.Description, "Template should have a description") + assert.NotEmpty(t, sampleApp.Languages, "Template should support at least one language") + + // Should support both typescript and python + assert.Contains(t, sampleApp.Languages, string(LanguageTypeScript), "sample-app should support typescript") + assert.Contains(t, sampleApp.Languages, string(LanguagePython), "sample-app should support python") +} diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go new file mode 100644 index 0000000..c487ddb --- /dev/null +++ b/pkg/templates/templates.go @@ -0,0 +1,6 @@ +package templates + +import "embed" + +//go:embed all:typescript +var FS embed.FS diff --git a/pkg/templates/typescript/sample-app/README.md b/pkg/templates/typescript/sample-app/README.md new file mode 100644 index 0000000..1d85657 --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/_gitignore b/pkg/templates/typescript/sample-app/_gitignore new file mode 100644 index 0000000..9325515 --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/index.ts b/pkg/templates/typescript/sample-app/index.ts new file mode 100644 index 0000000..ea3a94c --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/package-lock.json b/pkg/templates/typescript/sample-app/package-lock.json new file mode 100644 index 0000000..104353f --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/package.json b/pkg/templates/typescript/sample-app/package.json new file mode 100644 index 0000000..527437d --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/pnpm-lock.yaml b/pkg/templates/typescript/sample-app/pnpm-lock.yaml new file mode 100644 index 0000000..a69bdd0 --- /dev/null +++ b/pkg/templates/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/pkg/templates/typescript/sample-app/tsconfig.json b/pkg/templates/typescript/sample-app/tsconfig.json new file mode 100644 index 0000000..39959d0 --- /dev/null +++ b/pkg/templates/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