Skip to content

Commit 575ef83

Browse files
committed
update template init for new templates SDK
1 parent 5c0ea6d commit 575ef83

File tree

12 files changed

+1064
-298
lines changed

12 files changed

+1064
-298
lines changed

.changeset/silent-coins-brush.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@e2b/cli': patch
3+
---
4+
5+
add init command for the new template format

packages/cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@types/inquirer": "^9.0.7",
5454
"@types/json2md": "^1.5.4",
5555
"@types/node": "^18.18.6",
56+
"@types/npmcli__package-json": "^4.0.4",
5657
"@types/statuses": "^2.0.5",
5758
"@types/update-notifier": "6.0.5",
5859
"@vitest/coverage-v8": "^3.2.4",
@@ -75,6 +76,7 @@
7576
"dependencies": {
7677
"@iarna/toml": "^2.2.5",
7778
"@inquirer/prompts": "^5.5.0",
79+
"@npmcli/package-json": "^5.2.1",
7880
"async-listen": "^3.0.1",
7981
"boxen": "^7.1.1",
8082
"chalk": "^5.3.0",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
4+
/**
5+
* Generate unique file names to avoid overwriting existing files
6+
*/
7+
export function getUniqueFileName(
8+
directory: string,
9+
baseName: string,
10+
extension: string
11+
): string {
12+
let fileName = `${baseName}${extension}`
13+
let counter = 1
14+
15+
while (fs.existsSync(path.join(directory, fileName))) {
16+
fileName = `${baseName}-${counter}${extension}`
17+
counter++
18+
}
19+
20+
return fileName
21+
}
22+
23+
/**
24+
* Write content to a file, creating directories if needed
25+
*/
26+
export async function writeFileContent(
27+
filePath: string,
28+
content: string
29+
): Promise<void> {
30+
const dir = path.dirname(filePath)
31+
32+
// Ensure directory exists
33+
if (!fs.existsSync(dir)) {
34+
await fs.promises.mkdir(dir, { recursive: true })
35+
}
36+
37+
await fs.promises.writeFile(filePath, content)
38+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { Template, TemplateClass } from 'e2b'
2+
import * as fs from 'fs'
3+
import Handlebars from 'handlebars'
4+
import * as path from 'path'
5+
import { TemplateJSON } from './types'
6+
7+
// Track if helpers are registered to avoid duplicate registration
8+
let helpersRegistered = false
9+
10+
export function registerHandlebarsHelpers() {
11+
if (helpersRegistered) return
12+
13+
Handlebars.registerHelper('eq', function (a: any, b: any, options: any) {
14+
if (a === b) {
15+
// @ts-ignore - this context is provided by Handlebars
16+
return options.fn(this)
17+
}
18+
return ''
19+
})
20+
21+
Handlebars.registerHelper('escapeQuotes', function (str) {
22+
return str ? str.replace(/'/g, "\\'") : str
23+
})
24+
25+
Handlebars.registerHelper('escapeDoubleQuotes', function (str) {
26+
return str ? str.replace(/"/g, '\\"') : str
27+
})
28+
29+
helpersRegistered = true
30+
}
31+
32+
/**
33+
* Transform template data for Handlebars
34+
*/
35+
export async function transformTemplateData(template: TemplateClass) {
36+
// Extract JSON structure from parsed template
37+
const jsonString = await Template.toJSON(template, false)
38+
const json = JSON.parse(jsonString) as TemplateJSON
39+
40+
const transformedSteps: any[] = []
41+
42+
for (const step of json.steps) {
43+
switch (step.type) {
44+
case 'ENV': {
45+
// Keep all environment variables from one ENV instruction together
46+
const envVars: Record<string, string> = {}
47+
for (let i = 0; i < step.args.length; i += 2) {
48+
if (i + 1 < step.args.length) {
49+
envVars[step.args[i]] = step.args[i + 1]
50+
}
51+
}
52+
transformedSteps.push({
53+
type: 'ENV',
54+
envVars,
55+
})
56+
break
57+
}
58+
case 'COPY': {
59+
if (step.args.length >= 2) {
60+
const src = step.args[0]
61+
let dest = step.args[step.args.length - 1]
62+
if (!dest || dest === '') {
63+
dest = '.'
64+
}
65+
transformedSteps.push({
66+
type: 'COPY',
67+
src,
68+
dest,
69+
})
70+
}
71+
break
72+
}
73+
default:
74+
transformedSteps.push({
75+
type: step.type,
76+
args: step.args,
77+
})
78+
}
79+
}
80+
81+
return {
82+
...json,
83+
steps: transformedSteps,
84+
}
85+
}
86+
87+
/**
88+
* Convert the template to TypeScript code using Handlebars
89+
*/
90+
export async function generateTypeScriptCode(
91+
template: TemplateClass,
92+
alias: string,
93+
cpuCount?: number,
94+
memoryMB?: number
95+
): Promise<{ templateContent: string; buildContent: string }> {
96+
registerHandlebarsHelpers()
97+
const transformedData = await transformTemplateData(template)
98+
99+
// Load and compile templates
100+
// In dist, templates are at dist/templates/, __dirname is dist/
101+
const templatesDir = path.join(__dirname, 'templates')
102+
const templateSource = fs.readFileSync(
103+
path.join(templatesDir, 'typescript-template.hbs'),
104+
'utf8'
105+
)
106+
const buildSource = fs.readFileSync(
107+
path.join(templatesDir, 'typescript-build.hbs'),
108+
'utf8'
109+
)
110+
111+
const templateTemplate = Handlebars.compile(templateSource)
112+
const buildTemplate = Handlebars.compile(buildSource)
113+
114+
// Generate content
115+
const templateData = {
116+
...transformedData,
117+
}
118+
119+
const templateContent = templateTemplate(templateData)
120+
121+
const buildContent = buildTemplate({
122+
alias,
123+
cpuCount,
124+
memoryMB,
125+
})
126+
127+
return {
128+
templateContent: templateContent.trim(),
129+
buildContent: buildContent.trim(),
130+
}
131+
}
132+
133+
/**
134+
* Convert the template to Python code using Handlebars
135+
*/
136+
export async function generatePythonCode(
137+
template: TemplateClass,
138+
alias: string,
139+
cpuCount?: number,
140+
memoryMB?: number,
141+
isAsync: boolean = false
142+
): Promise<{ templateContent: string; buildContent: string }> {
143+
registerHandlebarsHelpers()
144+
const transformedData = await transformTemplateData(template)
145+
146+
// Load and compile templates
147+
// In dist, templates are at dist/templates/, __dirname is dist/
148+
const templatesDir = path.join(__dirname, 'templates')
149+
const templateSource = fs.readFileSync(
150+
path.join(templatesDir, 'python-template.hbs'),
151+
'utf8'
152+
)
153+
const buildSource = fs.readFileSync(
154+
path.join(templatesDir, `python-build-${isAsync ? 'async' : 'sync'}.hbs`),
155+
'utf8'
156+
)
157+
158+
const templateTemplate = Handlebars.compile(templateSource)
159+
const buildTemplate = Handlebars.compile(buildSource)
160+
161+
// Generate content
162+
const templateContent = templateTemplate({
163+
...transformedData,
164+
isAsync,
165+
})
166+
167+
const buildContent = buildTemplate({
168+
alias,
169+
cpuCount,
170+
memoryMB,
171+
})
172+
173+
return {
174+
templateContent: templateContent.trim(),
175+
buildContent: buildContent.trim(),
176+
}
177+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Re-export all the public APIs from the lib modules
2+
export * from './types'
3+
export * from './template-generator'
4+
export * from './file-utils'
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import * as path from 'path'
2+
import { asLocalRelative, asPrimary } from 'src/utils/format'
3+
import { GeneratedFiles, Language, languageDisplay } from './types'
4+
import { generatePythonCode, generateTypeScriptCode } from './handlebars'
5+
import { getUniqueFileName, writeFileContent } from './file-utils'
6+
import { TemplateClass } from 'e2b'
7+
8+
/**
9+
* Generate and write template files for a given language
10+
*/
11+
export async function generateAndWriteTemplateFiles(
12+
root: string,
13+
alias: string,
14+
language: Language,
15+
template: TemplateClass,
16+
cpuCount?: number,
17+
memoryMB?: number
18+
): Promise<GeneratedFiles> {
19+
if (language === Language.TypeScript) {
20+
const { templateContent, buildContent: buildDevContent } =
21+
await generateTypeScriptCode(template, `${alias}-dev`, cpuCount, memoryMB)
22+
const { buildContent: buildProdContent } = await generateTypeScriptCode(
23+
template,
24+
alias,
25+
cpuCount,
26+
memoryMB
27+
)
28+
29+
const extension = '.ts'
30+
const templateFile = getUniqueFileName(root, 'template', extension)
31+
const buildDevFile = getUniqueFileName(root, 'build.dev', extension)
32+
const buildProdFile = getUniqueFileName(root, 'build.prod', extension)
33+
34+
await writeFileContent(path.join(root, templateFile), templateContent)
35+
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
36+
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
37+
38+
console.log(
39+
`\n✅ Generated ${asPrimary(
40+
languageDisplay[Language.TypeScript]
41+
)} template files:`
42+
)
43+
console.log(` ${asLocalRelative(templateFile)}`)
44+
console.log(` ${asLocalRelative(buildDevFile)}`)
45+
console.log(` ${asLocalRelative(buildProdFile)}`)
46+
47+
return { templateFile, buildDevFile, buildProdFile, language }
48+
} else {
49+
const isAsync = language === Language.PythonAsync
50+
const { templateContent, buildContent: buildDevContent } =
51+
await generatePythonCode(
52+
template,
53+
`${alias}-dev`,
54+
cpuCount,
55+
memoryMB,
56+
isAsync
57+
)
58+
const { buildContent: buildProdContent } = await generatePythonCode(
59+
template,
60+
alias,
61+
cpuCount,
62+
memoryMB,
63+
isAsync
64+
)
65+
66+
const extension = '.py'
67+
const templateFile = getUniqueFileName(root, 'template', extension)
68+
const buildDevFile = getUniqueFileName(root, 'build_dev', extension)
69+
const buildProdFile = getUniqueFileName(root, 'build_prod', extension)
70+
71+
await writeFileContent(path.join(root, templateFile), templateContent)
72+
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
73+
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
74+
75+
console.log(
76+
`\n✅ Generated ${asPrimary(languageDisplay[language])} template files:`
77+
)
78+
console.log(` ${asLocalRelative(templateFile)}`)
79+
console.log(` ${asLocalRelative(buildDevFile)}`)
80+
console.log(` ${asLocalRelative(buildProdFile)}`)
81+
82+
return { templateFile, buildDevFile, buildProdFile, language }
83+
}
84+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export enum Language {
2+
TypeScript = 'typescript',
3+
PythonSync = 'python-sync',
4+
PythonAsync = 'python-async',
5+
}
6+
7+
export const validLanguages: Language[] = [
8+
Language.TypeScript,
9+
Language.PythonSync,
10+
Language.PythonAsync,
11+
]
12+
13+
export const languageDisplay = {
14+
[Language.TypeScript]: 'TypeScript',
15+
[Language.PythonSync]: 'Python (sync)',
16+
[Language.PythonAsync]: 'Python (async)',
17+
}
18+
19+
export interface TemplateJSON {
20+
fromImage?: string
21+
fromTemplate?: string
22+
startCmd?: string
23+
readyCmd?: string
24+
force: boolean
25+
steps: Array<{
26+
type: string
27+
args: string[]
28+
filesHash?: string
29+
force?: boolean
30+
}>
31+
}
32+
33+
export interface GeneratedFiles {
34+
templateFile: string
35+
buildDevFile: string
36+
buildProdFile: string
37+
language: Language
38+
}

packages/cli/src/commands/template/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { initCommand } from './init'
66
import { deleteCommand } from './delete'
77
import { publishCommand, unPublishCommand } from './publish'
88
import { migrateCommand } from './migrate'
9+
import { initV2Command } from './init-v2'
910

1011
export const templateCommand = new commander.Command('template')
1112
.description('manage sandbox templates')
1213
.alias('tpl')
1314
.addCommand(buildCommand)
1415
.addCommand(listCommand)
1516
.addCommand(initCommand)
17+
.addCommand(initV2Command)
1618
.addCommand(deleteCommand)
1719
.addCommand(publishCommand)
1820
.addCommand(unPublishCommand)

0 commit comments

Comments
 (0)