Skip to content

Commit 1bc8095

Browse files
authored
Add dependecies.go to install packages (#46)
This PR adds methods to install dependencies after copying templates into a new directory. ## Testing - run `make build && ./bin/kernel create` - select name, language, template - cd into directory and check if `node_modules` or `.venv` exist <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds language-specific dependency installation to the create flow with tool availability checks, fallback messaging, and comprehensive tests. > > - **CLI Create Flow (`cmd/create.go`)**: > - Invoke `create.InstallDependencies` after copying templates; minor output tweak with `pterm.Printfln`. > - **Dependency Setup (`pkg/create/dependencies.go`)**: > - Install deps per language via `InstallCommands` with spinner. > - Handle missing required tools using `RequiredTools` and print installation/next steps. > - Gracefully handle install failures with warnings and manual steps. > - **Types and Config (`pkg/create/types.go`)**: > - Add `RequiredTools`, `InstallCommands`, and `Tools.CheckToolAvailable`. > - **Tests (`cmd/create_test.go`)**: > - Add tests for all templates with/without dependency installation. > - Cover failure scenarios (install command fails) and missing required tools. > - Validate created files, environment artifacts (`node_modules`, `.venv`), and next-step output. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 66d4e7b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4ada771 commit 1bc8095

File tree

4 files changed

+420
-10
lines changed

4 files changed

+420
-10
lines changed

cmd/create.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,24 +37,19 @@ func (c CreateCmd) Create(ctx context.Context, ci CreateInput) error {
3737
return fmt.Errorf("failed to create directory: %w", err)
3838
}
3939

40-
pterm.Println(fmt.Sprintf("\nCreating a new %s %s\n", ci.Language, ci.Template))
40+
pterm.Printfln("\nCreating a new %s %s", ci.Language, ci.Template)
4141

4242
spinner, _ := pterm.DefaultSpinner.Start("Copying template files...")
4343

4444
if err := create.CopyTemplateFiles(appPath, ci.Language, ci.Template); err != nil {
4545
spinner.Fail("Failed to copy template files")
4646
return fmt.Errorf("failed to copy template files: %w", err)
4747
}
48-
spinner.Success(fmt.Sprintf("✔ %s environment set up successfully", ci.Language))
49-
50-
nextSteps := fmt.Sprintf(`Next steps:
51-
brew install onkernel/tap/kernel
52-
cd %s
53-
kernel login # or: export KERNEL_API_KEY=<YOUR_API_KEY>
54-
kernel deploy index.ts
55-
kernel invoke ts-basic get-page-title --payload '{"url": "https://www.google.com"}'
56-
`, ci.Name)
5748

49+
nextSteps, err := create.InstallDependencies(ci.Name, appPath, ci.Language)
50+
if err != nil {
51+
return fmt.Errorf("failed to install dependencies: %w", err)
52+
}
5853
pterm.Success.Println("🎉 Kernel app created successfully!")
5954
pterm.Println()
6055
pterm.FgYellow.Println(nextSteps)

cmd/create_test.go

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"context"
6+
"io"
57
"os"
68
"path/filepath"
79
"testing"
810

11+
"github.com/onkernel/cli/pkg/create"
12+
"github.com/pterm/pterm"
913
"github.com/stretchr/testify/assert"
1014
"github.com/stretchr/testify/require"
1115
)
@@ -83,3 +87,295 @@ func TestCreateCommand(t *testing.T) {
8387
})
8488
}
8589
}
90+
91+
// TestAllTemplatesWithDependencies tests all available templates and verifies dependencies are installed
92+
func TestAllTemplatesWithDependencies(t *testing.T) {
93+
if testing.Short() {
94+
t.Skip("Skipping dependency installation tests in short mode")
95+
}
96+
97+
tests := getTemplateInfo()
98+
for _, tt := range tests {
99+
t.Run(tt.name, func(t *testing.T) {
100+
tmpDir := t.TempDir()
101+
appName := "test-app"
102+
103+
orgDir, err := os.Getwd()
104+
require.NoError(t, err)
105+
106+
err = os.Chdir(tmpDir)
107+
require.NoError(t, err)
108+
109+
t.Cleanup(func() {
110+
os.Chdir(orgDir)
111+
})
112+
113+
// Create the app
114+
c := CreateCmd{}
115+
err = c.Create(context.Background(), CreateInput{
116+
Name: appName,
117+
Language: tt.language,
118+
Template: tt.template,
119+
})
120+
require.NoError(t, err, "failed to create app")
121+
122+
appPath := filepath.Join(tmpDir, appName)
123+
124+
// Verify app directory exists
125+
assert.DirExists(t, appPath, "app directory should exist")
126+
127+
// Language-specific validations
128+
switch tt.language {
129+
case create.LanguageTypeScript:
130+
validateTypeScriptTemplate(t, appPath, true)
131+
case create.LanguagePython:
132+
validatePythonTemplate(t, appPath, true)
133+
}
134+
})
135+
}
136+
}
137+
138+
// TestAllTemplatesCreation tests that all templates can be created without installing dependencies
139+
func TestAllTemplatesCreation(t *testing.T) {
140+
tests := getTemplateInfo()
141+
for _, tt := range tests {
142+
t.Run(tt.name, func(t *testing.T) {
143+
tmpDir := t.TempDir()
144+
appName := "test-app"
145+
appPath := filepath.Join(tmpDir, appName)
146+
147+
// Create app directory
148+
err := os.MkdirAll(appPath, 0755)
149+
require.NoError(t, err, "failed to create app directory")
150+
151+
// Copy template files without installing dependencies
152+
err = create.CopyTemplateFiles(appPath, tt.language, tt.template)
153+
require.NoError(t, err, "failed to copy template files")
154+
155+
// Verify app directory exists
156+
assert.DirExists(t, appPath, "app directory should exist")
157+
158+
// Language-specific validations (without dependency checks)
159+
switch tt.language {
160+
case create.LanguageTypeScript:
161+
validateTypeScriptTemplate(t, appPath, false)
162+
case create.LanguagePython:
163+
validatePythonTemplate(t, appPath, false)
164+
}
165+
})
166+
}
167+
}
168+
169+
// validateTypeScriptTemplate verifies TypeScript template structure and optionally dependencies
170+
func validateTypeScriptTemplate(t *testing.T, appPath string, checkDependencies bool) {
171+
t.Helper()
172+
173+
// Verify essential files exist
174+
assert.FileExists(t, filepath.Join(appPath, "package.json"), "package.json should exist")
175+
assert.FileExists(t, filepath.Join(appPath, "tsconfig.json"), "tsconfig.json should exist")
176+
assert.FileExists(t, filepath.Join(appPath, "index.ts"), "index.ts should exist")
177+
assert.FileExists(t, filepath.Join(appPath, ".gitignore"), ".gitignore should exist")
178+
179+
// Verify _gitignore was renamed
180+
assert.NoFileExists(t, filepath.Join(appPath, "_gitignore"), "_gitignore should not exist")
181+
182+
if checkDependencies {
183+
// Verify node_modules exists (dependencies were installed)
184+
nodeModulesPath := filepath.Join(appPath, "node_modules")
185+
if _, err := os.Stat(nodeModulesPath); err == nil {
186+
// Only check contents if node_modules exists
187+
entries, err := os.ReadDir(nodeModulesPath)
188+
require.NoError(t, err, "should be able to read node_modules directory")
189+
assert.NotEmpty(t, entries, "node_modules should contain installed packages")
190+
} else {
191+
t.Logf("Warning: node_modules not found at %s (npm install may have failed)", nodeModulesPath)
192+
}
193+
}
194+
}
195+
196+
// validatePythonTemplate verifies Python template structure and optionally dependencies
197+
func validatePythonTemplate(t *testing.T, appPath string, checkDependencies bool) {
198+
t.Helper()
199+
200+
// Verify essential files exist
201+
assert.FileExists(t, filepath.Join(appPath, "pyproject.toml"), "pyproject.toml should exist")
202+
assert.FileExists(t, filepath.Join(appPath, "main.py"), "main.py should exist")
203+
assert.FileExists(t, filepath.Join(appPath, ".gitignore"), ".gitignore should exist")
204+
205+
// Verify _gitignore was renamed
206+
assert.NoFileExists(t, filepath.Join(appPath, "_gitignore"), "_gitignore should not exist")
207+
208+
if checkDependencies {
209+
// Verify .venv exists (virtual environment was created)
210+
venvPath := filepath.Join(appPath, ".venv")
211+
if _, err := os.Stat(venvPath); err == nil {
212+
// Only check contents if .venv exists
213+
binPath := filepath.Join(venvPath, "bin")
214+
assert.DirExists(t, binPath, ".venv/bin directory should exist")
215+
216+
pythonPath := filepath.Join(binPath, "python")
217+
assert.FileExists(t, pythonPath, ".venv/bin/python should exist")
218+
} else {
219+
t.Logf("Warning: .venv not found at %s (uv venv may have failed)", venvPath)
220+
}
221+
}
222+
}
223+
224+
// TestCreateCommand_DependencyInstallationFails tests that the app is still created
225+
// even when dependency installation fails, with appropriate warning message
226+
func TestCreateCommand_DependencyInstallationFails(t *testing.T) {
227+
tmpDir := t.TempDir()
228+
appName := "test-app"
229+
230+
orgDir, err := os.Getwd()
231+
require.NoError(t, err)
232+
233+
err = os.Chdir(tmpDir)
234+
require.NoError(t, err)
235+
236+
t.Cleanup(func() {
237+
os.Chdir(orgDir)
238+
})
239+
240+
var outputBuf bytes.Buffer
241+
multiWriter := io.MultiWriter(&outputBuf, os.Stdout)
242+
pterm.SetDefaultOutput(multiWriter)
243+
244+
t.Cleanup(func() {
245+
pterm.SetDefaultOutput(os.Stdout)
246+
})
247+
248+
// Override the install command to use a command that will fail
249+
originalInstallCommands := create.InstallCommands
250+
create.InstallCommands = map[string]string{
251+
create.LanguageTypeScript: "exit 1", // Command that always fails
252+
}
253+
254+
// Restore original install commands after test
255+
t.Cleanup(func() {
256+
create.InstallCommands = originalInstallCommands
257+
})
258+
259+
// Create the app - should succeed even though dependency installation fails
260+
c := CreateCmd{}
261+
err = c.Create(context.Background(), CreateInput{
262+
Name: appName,
263+
Language: create.LanguageTypeScript,
264+
Template: "sample-app",
265+
})
266+
267+
output := outputBuf.String()
268+
269+
assert.Contains(t, output, "cd test-app", "should print cd command")
270+
assert.Contains(t, output, "pnpm install", "should print pnpm install command")
271+
}
272+
273+
// TestCreateCommand_RequiredToolMissing tests that the app is created
274+
func TestCreateCommand_RequiredToolMissing(t *testing.T) {
275+
tests := []struct {
276+
name string
277+
language string
278+
template string
279+
}{
280+
{
281+
name: "typescript with missing pnpm",
282+
language: create.LanguageTypeScript,
283+
template: "sample-app",
284+
},
285+
{
286+
name: "python with missing uv",
287+
language: create.LanguagePython,
288+
template: "sample-app",
289+
},
290+
}
291+
292+
for _, tt := range tests {
293+
t.Run(tt.name, func(t *testing.T) {
294+
tmpDir := t.TempDir()
295+
appName := "test-app"
296+
297+
orgDir, err := os.Getwd()
298+
require.NoError(t, err)
299+
300+
err = os.Chdir(tmpDir)
301+
require.NoError(t, err)
302+
303+
t.Cleanup(func() {
304+
os.Chdir(orgDir)
305+
})
306+
307+
// Override the required tool to point to a non-existent command
308+
originalRequiredTools := create.RequiredTools
309+
create.RequiredTools = map[string]string{
310+
create.LanguageTypeScript: "nonexistent-pnpm-tool",
311+
create.LanguagePython: "nonexistent-uv-tool",
312+
}
313+
314+
// Restore original required tools after test
315+
t.Cleanup(func() {
316+
create.RequiredTools = originalRequiredTools
317+
})
318+
319+
// Create the app - should succeed even though required tool is missing
320+
c := CreateCmd{}
321+
err = c.Create(context.Background(), CreateInput{
322+
Name: appName,
323+
Language: tt.language,
324+
Template: tt.template,
325+
})
326+
327+
// Should not return an error - the command should complete successfully
328+
// but skip dependency installation
329+
require.NoError(t, err, "app creation should succeed even when required tool is missing")
330+
331+
// Verify the app directory and files were created
332+
appPath := filepath.Join(tmpDir, appName)
333+
assert.DirExists(t, appPath, "app directory should exist")
334+
335+
// Language-specific file checks
336+
switch tt.language {
337+
case create.LanguageTypeScript:
338+
assert.FileExists(t, filepath.Join(appPath, "package.json"), "package.json should exist")
339+
assert.FileExists(t, filepath.Join(appPath, "index.ts"), "index.ts should exist")
340+
assert.FileExists(t, filepath.Join(appPath, "tsconfig.json"), "tsconfig.json should exist")
341+
342+
// node_modules should NOT exist since pnpm was not available
343+
assert.NoDirExists(t, filepath.Join(appPath, "node_modules"), "node_modules should not exist when pnpm is missing")
344+
case create.LanguagePython:
345+
assert.FileExists(t, filepath.Join(appPath, "pyproject.toml"), "pyproject.toml should exist")
346+
assert.FileExists(t, filepath.Join(appPath, "main.py"), "main.py should exist")
347+
348+
// .venv should NOT exist since uv was not available
349+
assert.NoDirExists(t, filepath.Join(appPath, ".venv"), ".venv should not exist when uv is missing")
350+
}
351+
})
352+
}
353+
}
354+
355+
func getTemplateInfo() []struct {
356+
name string
357+
language string
358+
template string
359+
} {
360+
tests := make([]struct {
361+
name string
362+
language string
363+
template string
364+
}, 0)
365+
366+
for templateKey, templateInfo := range create.Templates {
367+
for _, lang := range templateInfo.Languages {
368+
tests = append(tests, struct {
369+
name string
370+
language string
371+
template string
372+
}{
373+
name: lang + "/" + templateKey,
374+
language: lang,
375+
template: templateKey,
376+
})
377+
}
378+
}
379+
380+
return tests
381+
}

0 commit comments

Comments
 (0)