@onkernel/cli 0.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +759 -0
- package/package.json +54 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import fs2 from 'fs';
|
|
3
|
+
import getPort from 'get-port';
|
|
4
|
+
import os2 from 'os';
|
|
5
|
+
import path3, { dirname } from 'path';
|
|
6
|
+
import { parse, stringify } from 'smol-toml';
|
|
7
|
+
import fsExtra from 'fs-extra';
|
|
8
|
+
import Parser from 'tree-sitter';
|
|
9
|
+
import Python from 'tree-sitter-python';
|
|
10
|
+
import TypeScript from 'tree-sitter-typescript';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { execa } from 'execa';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
// index.ts
|
|
16
|
+
|
|
17
|
+
// lib/constants.ts
|
|
18
|
+
var PYTHON_PACKAGE_NAME = "kernel";
|
|
19
|
+
var NODE_PACKAGE_NAME = "@onkernel/sdk";
|
|
20
|
+
async function runForExitCode(command, options = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const cwd = options.cwd ? path3.resolve(options.cwd) : process.cwd();
|
|
23
|
+
const { exitCode } = await execa(command, {
|
|
24
|
+
shell: true,
|
|
25
|
+
cwd,
|
|
26
|
+
stdio: "ignore",
|
|
27
|
+
// Don't show any output
|
|
28
|
+
reject: false
|
|
29
|
+
// Don't throw on non-zero exit code
|
|
30
|
+
});
|
|
31
|
+
return exitCode ?? 1;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function runInDirectory(command, cwd) {
|
|
37
|
+
const resolvedCwd = path3.resolve(cwd);
|
|
38
|
+
await execa(command, {
|
|
39
|
+
shell: true,
|
|
40
|
+
cwd: resolvedCwd,
|
|
41
|
+
stdio: "inherit"
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function getPackageVersion() {
|
|
45
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
46
|
+
const __dirname = dirname(__filename);
|
|
47
|
+
const pkgJsonPath = path3.join(__dirname, "..", "package.json");
|
|
48
|
+
const content = fsExtra.readJSONSync(pkgJsonPath);
|
|
49
|
+
if (!content.version) {
|
|
50
|
+
throw new Error("package.json does not contain a version");
|
|
51
|
+
}
|
|
52
|
+
return content.version;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// lib/package.ts
|
|
56
|
+
var KernelFunctionSchema = z.discriminatedUnion("type", [
|
|
57
|
+
z.object({
|
|
58
|
+
type: z.literal("schedule"),
|
|
59
|
+
name: z.string(),
|
|
60
|
+
cron: z.string(),
|
|
61
|
+
uses_browser: z.boolean().optional()
|
|
62
|
+
}),
|
|
63
|
+
z.object({
|
|
64
|
+
type: z.literal("http"),
|
|
65
|
+
name: z.string(),
|
|
66
|
+
path: z.string(),
|
|
67
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]),
|
|
68
|
+
uses_browser: z.boolean().optional()
|
|
69
|
+
}),
|
|
70
|
+
z.object({
|
|
71
|
+
type: z.literal("function"),
|
|
72
|
+
name: z.string(),
|
|
73
|
+
uses_browser: z.boolean().optional()
|
|
74
|
+
})
|
|
75
|
+
]);
|
|
76
|
+
z.object({
|
|
77
|
+
entrypoint: z.string(),
|
|
78
|
+
functions: z.array(KernelFunctionSchema)
|
|
79
|
+
});
|
|
80
|
+
function findPythonDecoratedFunctions(filePath) {
|
|
81
|
+
const parser = new Parser();
|
|
82
|
+
parser.setLanguage(Python);
|
|
83
|
+
const code = fs2.readFileSync(filePath, "utf8");
|
|
84
|
+
const tree = parser.parse(code);
|
|
85
|
+
const rootNode = tree.rootNode;
|
|
86
|
+
const result = [];
|
|
87
|
+
const functionNodes = [];
|
|
88
|
+
traverseTree(rootNode, (node) => {
|
|
89
|
+
if (node.type === "function_definition") {
|
|
90
|
+
functionNodes.push(node);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
for (const node of functionNodes) {
|
|
94
|
+
const nameNode = node.childForFieldName("name");
|
|
95
|
+
if (!nameNode) continue;
|
|
96
|
+
const functionName = nameNode.text;
|
|
97
|
+
let decoratorFound = false;
|
|
98
|
+
let curNode = node.previousSibling;
|
|
99
|
+
while (curNode && curNode.type === "decorator") {
|
|
100
|
+
const decoratorNameNode = curNode.child(1);
|
|
101
|
+
if (decoratorNameNode) {
|
|
102
|
+
const decoratorName = decoratorNameNode.text;
|
|
103
|
+
if (decoratorName === "func") {
|
|
104
|
+
decoratorFound = true;
|
|
105
|
+
result.push({
|
|
106
|
+
type: "function",
|
|
107
|
+
name: functionName,
|
|
108
|
+
uses_browser: code.includes("use_browser") && code.includes(`def ${functionName}`)
|
|
109
|
+
});
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
curNode = curNode.previousSibling;
|
|
114
|
+
}
|
|
115
|
+
if (!decoratorFound) {
|
|
116
|
+
const funcStartLine = node.startPosition.row;
|
|
117
|
+
const codeLines = code.split("\n");
|
|
118
|
+
for (let i = Math.max(0, funcStartLine - 3); i < funcStartLine; i++) {
|
|
119
|
+
const line = codeLines[i]?.trim() || "";
|
|
120
|
+
if (line.startsWith("@func")) {
|
|
121
|
+
result.push({
|
|
122
|
+
type: "function",
|
|
123
|
+
name: functionName,
|
|
124
|
+
uses_browser: code.includes("use_browser") && code.includes(`def ${functionName}`)
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function traverseTree(node, callback) {
|
|
134
|
+
callback(node);
|
|
135
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
136
|
+
const child = node.child(i);
|
|
137
|
+
if (child) {
|
|
138
|
+
traverseTree(child, callback);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function findTypeScriptDecoratedFunctions(filePath) {
|
|
143
|
+
const parser = new Parser();
|
|
144
|
+
parser.setLanguage(TypeScript.typescript);
|
|
145
|
+
const code = fs2.readFileSync(filePath, "utf8");
|
|
146
|
+
const tree = parser.parse(code);
|
|
147
|
+
const rootNode = tree.rootNode;
|
|
148
|
+
traverseTreeForDebug(rootNode);
|
|
149
|
+
const results = walkTreeForKernelExports(rootNode, code);
|
|
150
|
+
if (results.length === 0 && code.includes("export default Kernel.func")) {
|
|
151
|
+
results.push({
|
|
152
|
+
type: "function",
|
|
153
|
+
name: "default",
|
|
154
|
+
uses_browser: code.includes("useBrowser")
|
|
155
|
+
});
|
|
156
|
+
} else if (results.length === 0 && code.includes("export default Kernel.schedule")) {
|
|
157
|
+
results.push({
|
|
158
|
+
type: "schedule",
|
|
159
|
+
name: "default",
|
|
160
|
+
cron: "* * * * *",
|
|
161
|
+
// Default
|
|
162
|
+
uses_browser: code.includes("useBrowser")
|
|
163
|
+
});
|
|
164
|
+
} else if (results.length === 0 && code.includes("export default Kernel.endpoint")) {
|
|
165
|
+
results.push({
|
|
166
|
+
type: "http",
|
|
167
|
+
name: "default",
|
|
168
|
+
path: "/",
|
|
169
|
+
method: "GET",
|
|
170
|
+
uses_browser: code.includes("useBrowser")
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
function traverseTreeForDebug(node, level) {
|
|
176
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
177
|
+
const child = node.namedChild(i);
|
|
178
|
+
traverseTreeForDebug(child);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function walkTreeForKernelExports(rootNode, code) {
|
|
182
|
+
const results = [];
|
|
183
|
+
for (let i = 0; i < rootNode.namedChildCount; i++) {
|
|
184
|
+
const node = rootNode.namedChild(i);
|
|
185
|
+
if (!node) continue;
|
|
186
|
+
if (node.type === "export_statement") {
|
|
187
|
+
if (node.text.startsWith("export default")) {
|
|
188
|
+
const declaration = node.childForFieldName("declaration");
|
|
189
|
+
if (!declaration) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (declaration.type === "call_expression") {
|
|
193
|
+
const callExpr = declaration;
|
|
194
|
+
if (callExpr.childCount > 0 && callExpr.child(0)?.type === "member_expression") {
|
|
195
|
+
const memberExpr = callExpr.child(0);
|
|
196
|
+
const object = memberExpr.childForFieldName("object");
|
|
197
|
+
const property = memberExpr.childForFieldName("property");
|
|
198
|
+
if (object?.text === "Kernel") {
|
|
199
|
+
const method = property?.text;
|
|
200
|
+
const usesBrowser = code.includes("useBrowser");
|
|
201
|
+
if (method === "func") {
|
|
202
|
+
results.push({
|
|
203
|
+
type: "function",
|
|
204
|
+
name: "default",
|
|
205
|
+
uses_browser: usesBrowser
|
|
206
|
+
});
|
|
207
|
+
} else if (method === "schedule") {
|
|
208
|
+
results.push({
|
|
209
|
+
type: "schedule",
|
|
210
|
+
name: "default",
|
|
211
|
+
cron: "* * * * *",
|
|
212
|
+
// Default
|
|
213
|
+
uses_browser: usesBrowser
|
|
214
|
+
});
|
|
215
|
+
} else if (method === "endpoint") {
|
|
216
|
+
results.push({
|
|
217
|
+
type: "http",
|
|
218
|
+
name: "default",
|
|
219
|
+
path: "/",
|
|
220
|
+
method: "GET",
|
|
221
|
+
uses_browser: usesBrowser
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else if (node.type === "lexical_declaration") {
|
|
229
|
+
const decl = node.childForFieldName("declaration");
|
|
230
|
+
if (!decl) continue;
|
|
231
|
+
for (let j = 0; j < decl.namedChildCount; j++) {
|
|
232
|
+
const varDecl = decl.namedChild(j);
|
|
233
|
+
if (!varDecl) continue;
|
|
234
|
+
const varNameNode = varDecl.childForFieldName("name");
|
|
235
|
+
const valueNode = varDecl.childForFieldName("value");
|
|
236
|
+
if (!varNameNode || !valueNode) continue;
|
|
237
|
+
if (valueNode.type === "call_expression" && valueNode.child(0)?.type === "member_expression") {
|
|
238
|
+
const memberExpr = valueNode.child(0);
|
|
239
|
+
const object = memberExpr.childForFieldName("object");
|
|
240
|
+
const property = memberExpr.childForFieldName("property");
|
|
241
|
+
if (object?.text === "Kernel") {
|
|
242
|
+
const method = property?.text;
|
|
243
|
+
const name = varNameNode.text;
|
|
244
|
+
const usesBrowser = code.includes("useBrowser");
|
|
245
|
+
if (method === "func") {
|
|
246
|
+
results.push({
|
|
247
|
+
type: "function",
|
|
248
|
+
name,
|
|
249
|
+
uses_browser: usesBrowser
|
|
250
|
+
});
|
|
251
|
+
} else if (method === "schedule") {
|
|
252
|
+
let cronExpression = "* * * * *";
|
|
253
|
+
results.push({
|
|
254
|
+
type: "schedule",
|
|
255
|
+
name,
|
|
256
|
+
cron: cronExpression,
|
|
257
|
+
uses_browser: usesBrowser
|
|
258
|
+
});
|
|
259
|
+
} else if (method === "endpoint") {
|
|
260
|
+
let path5 = "/";
|
|
261
|
+
let method2 = "GET";
|
|
262
|
+
results.push({
|
|
263
|
+
type: "http",
|
|
264
|
+
name,
|
|
265
|
+
path: path5,
|
|
266
|
+
method: method2,
|
|
267
|
+
uses_browser: usesBrowser
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
async function packageApp(config) {
|
|
278
|
+
const { sourceDir, entrypoint, targetDir } = config;
|
|
279
|
+
if (fs2.existsSync(targetDir)) {
|
|
280
|
+
fs2.rmSync(targetDir, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
283
|
+
const entrypointRelative = path3.relative(sourceDir, entrypoint);
|
|
284
|
+
const extension = path3.extname(entrypoint);
|
|
285
|
+
let functions = [];
|
|
286
|
+
if (extension === ".py") {
|
|
287
|
+
functions = findPythonDecoratedFunctions(entrypoint);
|
|
288
|
+
} else if (extension === ".ts") {
|
|
289
|
+
functions = findTypeScriptDecoratedFunctions(entrypoint);
|
|
290
|
+
} else {
|
|
291
|
+
throw new Error(`Unsupported file extension: ${extension}`);
|
|
292
|
+
}
|
|
293
|
+
const kernelJson = {
|
|
294
|
+
entrypoint: entrypointRelative,
|
|
295
|
+
functions
|
|
296
|
+
};
|
|
297
|
+
const kernelJsonPath = path3.join(targetDir, "kernel.json");
|
|
298
|
+
fs2.writeFileSync(kernelJsonPath, JSON.stringify(kernelJson, null, 2));
|
|
299
|
+
copyDirectoryContents(sourceDir, targetDir);
|
|
300
|
+
if (extension === ".ts") {
|
|
301
|
+
if (config.tsOptions?.kernelDependencyOverride) {
|
|
302
|
+
const packageJsonPath = path3.join(targetDir, "package.json");
|
|
303
|
+
const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
|
|
304
|
+
packageJson.dependencies[NODE_PACKAGE_NAME] = config.tsOptions.kernelDependencyOverride;
|
|
305
|
+
fs2.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
306
|
+
}
|
|
307
|
+
await runInDirectory(`pnpm i`, targetDir);
|
|
308
|
+
}
|
|
309
|
+
return kernelJsonPath;
|
|
310
|
+
}
|
|
311
|
+
function copyDirectoryContents(sourceDir, targetDir) {
|
|
312
|
+
fsExtra.copySync(sourceDir, targetDir, {
|
|
313
|
+
filter: (src) => {
|
|
314
|
+
const basename = path3.basename(src);
|
|
315
|
+
return ![".build", "node_modules", ".git", ".mypy_cache", ".venv", "__pycache__"].includes(
|
|
316
|
+
basename
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
overwrite: true
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// lib/invoke.ts
|
|
324
|
+
var INVOKE_BASE_DIR = path3.join(
|
|
325
|
+
os2.homedir(),
|
|
326
|
+
".local",
|
|
327
|
+
"state",
|
|
328
|
+
"kernel",
|
|
329
|
+
"local",
|
|
330
|
+
"invoke"
|
|
331
|
+
);
|
|
332
|
+
function overwriteKernelDendencyInPyproject(pyprojectPath, kernelDependencyOverride) {
|
|
333
|
+
const pyproject = parse(fs2.readFileSync(pyprojectPath, "utf8"));
|
|
334
|
+
if (!pyproject.project) {
|
|
335
|
+
pyproject.project = { dependencies: [] };
|
|
336
|
+
} else if (!pyproject.project.dependencies) {
|
|
337
|
+
pyproject.project.dependencies = [];
|
|
338
|
+
}
|
|
339
|
+
pyproject.project.dependencies = pyproject.project.dependencies.filter((dep) => {
|
|
340
|
+
return !dep.startsWith(PYTHON_PACKAGE_NAME);
|
|
341
|
+
});
|
|
342
|
+
if (!kernelDependencyOverride.startsWith("/") && !kernelDependencyOverride.startsWith("./") && !kernelDependencyOverride.startsWith("../")) {
|
|
343
|
+
pyproject.project.dependencies.push(kernelDependencyOverride);
|
|
344
|
+
} else {
|
|
345
|
+
pyproject.project.dependencies.push(PYTHON_PACKAGE_NAME);
|
|
346
|
+
if (!pyproject.tool) {
|
|
347
|
+
pyproject.tool = {};
|
|
348
|
+
}
|
|
349
|
+
if (!pyproject.tool.uv) {
|
|
350
|
+
pyproject.tool.uv = {};
|
|
351
|
+
}
|
|
352
|
+
if (!pyproject.tool.uv.sources) {
|
|
353
|
+
pyproject.tool.uv.sources = {};
|
|
354
|
+
}
|
|
355
|
+
pyproject.tool.uv.sources.kernel = { path: kernelDependencyOverride };
|
|
356
|
+
}
|
|
357
|
+
fs2.writeFileSync(pyprojectPath, stringify(pyproject));
|
|
358
|
+
}
|
|
359
|
+
function overwriteKernelDendencyInRequirementsTxt(requirementsTxtPath, kernelDependencyOverride) {
|
|
360
|
+
const requirementsTxt = fs2.readFileSync(requirementsTxtPath, "utf8");
|
|
361
|
+
const requirementsTxtLines = requirementsTxt.split("\n");
|
|
362
|
+
const newRequirementsTxtLines = requirementsTxtLines.filter((line) => {
|
|
363
|
+
return !line.startsWith(PYTHON_PACKAGE_NAME);
|
|
364
|
+
});
|
|
365
|
+
if (!kernelDependencyOverride.startsWith("/") && !kernelDependencyOverride.startsWith("./") && !kernelDependencyOverride.startsWith("../")) {
|
|
366
|
+
newRequirementsTxtLines.push(`${PYTHON_PACKAGE_NAME} @ file:${kernelDependencyOverride}`);
|
|
367
|
+
} else {
|
|
368
|
+
newRequirementsTxtLines.push(kernelDependencyOverride);
|
|
369
|
+
}
|
|
370
|
+
fs2.writeFileSync(requirementsTxtPath, newRequirementsTxtLines.join("\n"));
|
|
371
|
+
}
|
|
372
|
+
async function preparePythonInvokeEnvironment(options) {
|
|
373
|
+
const { appName, appDir, bootLoaderDir} = options;
|
|
374
|
+
const invokeDir = path3.join(INVOKE_BASE_DIR, appName);
|
|
375
|
+
if (fs2.existsSync(invokeDir)) {
|
|
376
|
+
fs2.rmSync(invokeDir, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
fs2.mkdirSync(invokeDir, { recursive: true });
|
|
379
|
+
copyDirectoryContents(bootLoaderDir, invokeDir);
|
|
380
|
+
if (options.kernelDependencyOverride) {
|
|
381
|
+
if (fs2.existsSync(path3.join(invokeDir, "pyproject.toml"))) {
|
|
382
|
+
overwriteKernelDendencyInPyproject(
|
|
383
|
+
path3.join(invokeDir, "pyproject.toml"),
|
|
384
|
+
options.kernelDependencyOverride
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (fs2.existsSync(path3.join(appDir, "pyproject.toml"))) {
|
|
388
|
+
overwriteKernelDendencyInPyproject(
|
|
389
|
+
path3.join(appDir, "pyproject.toml"),
|
|
390
|
+
options.kernelDependencyOverride
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
if (fs2.existsSync(path3.join(appDir, "requirements.txt"))) {
|
|
394
|
+
overwriteKernelDendencyInRequirementsTxt(
|
|
395
|
+
path3.join(appDir, "requirements.txt"),
|
|
396
|
+
options.kernelDependencyOverride
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!fs2.existsSync(path3.join(appDir, "requirements.txt"))) {
|
|
401
|
+
await runInDirectory(
|
|
402
|
+
`/bin/bash -c 'uv venv && source .venv/bin/activate && uv sync --no-cache && uv pip freeze > requirements.txt'`,
|
|
403
|
+
appDir
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
await runInDirectory(
|
|
407
|
+
`/bin/bash -c 'uv venv && source .venv/bin/activate && uv add -r ${path3.join(appDir, "requirements.txt")}'`,
|
|
408
|
+
invokeDir
|
|
409
|
+
);
|
|
410
|
+
return invokeDir;
|
|
411
|
+
}
|
|
412
|
+
async function prepareTypeScriptInvokeEnvironment(options) {
|
|
413
|
+
const { appName, appDir, bootLoaderDir } = options;
|
|
414
|
+
const invokeDir = path3.join(INVOKE_BASE_DIR, appName);
|
|
415
|
+
if (fs2.existsSync(invokeDir)) {
|
|
416
|
+
fs2.rmSync(invokeDir, { recursive: true, force: true });
|
|
417
|
+
}
|
|
418
|
+
fs2.mkdirSync(invokeDir, { recursive: true });
|
|
419
|
+
copyDirectoryContents(bootLoaderDir, invokeDir);
|
|
420
|
+
if (options.kernelDependencyOverride) {
|
|
421
|
+
const packageJsonPath = path3.join(invokeDir, "package.json");
|
|
422
|
+
const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
|
|
423
|
+
packageJson.dependencies[NODE_PACKAGE_NAME] = options.kernelDependencyOverride;
|
|
424
|
+
fs2.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
425
|
+
}
|
|
426
|
+
const appPackageJsonPath = path3.join(appDir, "package.json");
|
|
427
|
+
const invokePackageJsonPath = path3.join(invokeDir, "package.json");
|
|
428
|
+
if (fs2.existsSync(appPackageJsonPath) && fs2.existsSync(invokePackageJsonPath)) {
|
|
429
|
+
const appPackageJson = JSON.parse(fs2.readFileSync(appPackageJsonPath, "utf8"));
|
|
430
|
+
const invokePackageJson = JSON.parse(fs2.readFileSync(invokePackageJsonPath, "utf8"));
|
|
431
|
+
if (appPackageJson.dependencies) {
|
|
432
|
+
if (!invokePackageJson.dependencies) {
|
|
433
|
+
invokePackageJson.dependencies = {};
|
|
434
|
+
}
|
|
435
|
+
for (const [depName, depVersion] of Object.entries(appPackageJson.dependencies)) {
|
|
436
|
+
if (!invokePackageJson.dependencies[depName]) {
|
|
437
|
+
invokePackageJson.dependencies[depName] = depVersion;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (appPackageJson.devDependencies) {
|
|
442
|
+
if (!invokePackageJson.devDependencies) {
|
|
443
|
+
invokePackageJson.devDependencies = {};
|
|
444
|
+
}
|
|
445
|
+
for (const [depName, depVersion] of Object.entries(appPackageJson.devDependencies)) {
|
|
446
|
+
if (!invokePackageJson.devDependencies[depName]) {
|
|
447
|
+
invokePackageJson.devDependencies[depName] = depVersion;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
fs2.writeFileSync(invokePackageJsonPath, JSON.stringify(invokePackageJson, null, 2));
|
|
452
|
+
console.log("Merged dependencies in package.json");
|
|
453
|
+
await runInDirectory("pnpm i", invokeDir);
|
|
454
|
+
}
|
|
455
|
+
return invokeDir;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// index.ts
|
|
459
|
+
var program = new Command();
|
|
460
|
+
var LOCAL_APPS_BASE_DIR = path3.join(os2.homedir(), ".local", "state", "kernel", "local", "apps");
|
|
461
|
+
var BOOT_PY_PATH = process.env.KERNEL_BOOT_PY_PATH || path3.join(os2.homedir(), "code", "onkernel", "kernel", "packages", "boot-py");
|
|
462
|
+
var BOOT_TS_PATH = process.env.KERNEL_BOOT_TS_PATH || path3.join(os2.homedir(), "code", "onkernel", "kernel", "packages", "boot-ts");
|
|
463
|
+
var KERNEL_TS_SDK_OVERRIDE = process.env.KERNEL_TS_SDK_OVERRIDE || void 0;
|
|
464
|
+
var KERNEL_PYTHON_SDK_OVERRIDE = process.env.KERNEL_PYTHON_SDK_OVERRIDE || void 0;
|
|
465
|
+
program.name("kernel").description("CLI for Kernel deployment and invocation").version(getPackageVersion());
|
|
466
|
+
program.command("deploy").description("Deploy a Kernel application").argument("<entrypoint>", "Path to entrypoint file (TypeScript or Python)").option(
|
|
467
|
+
"--local",
|
|
468
|
+
"Does not publish the app to Kernel, but installs it on disk for invoking locally"
|
|
469
|
+
).option("--name <name>", "Name for the deployed application").action(async (entrypoint, options) => {
|
|
470
|
+
if (!options.name) {
|
|
471
|
+
console.error("Error: --name option is required");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const resolvedEntrypoint = path3.resolve(entrypoint);
|
|
475
|
+
if (!fs2.existsSync(resolvedEntrypoint)) {
|
|
476
|
+
console.error(`Error: Entrypoint ${resolvedEntrypoint} doesn't exist`);
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
const sourceDir = path3.dirname(resolvedEntrypoint);
|
|
480
|
+
const localAppsDir = path3.join(LOCAL_APPS_BASE_DIR, options.name);
|
|
481
|
+
await packageApp({
|
|
482
|
+
sourceDir,
|
|
483
|
+
entrypoint: resolvedEntrypoint,
|
|
484
|
+
targetDir: localAppsDir,
|
|
485
|
+
tsOptions: {
|
|
486
|
+
kernelDependencyOverride: KERNEL_TS_SDK_OVERRIDE
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (options.local) {
|
|
490
|
+
console.log(`App "${options.name}" successfully installed to ${localAppsDir}`);
|
|
491
|
+
} else {
|
|
492
|
+
console.log(`Deploying ${resolvedEntrypoint} as "${options.name}"...`);
|
|
493
|
+
console.error("TODO: implement cloud :-p");
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
program.command("invoke").description("Invoke a deployed Kernel application").option("--local", "Invoke a locally deployed application").option("--name <name>", "Name of the application to invoke").option("--with-browser", "Provision browser infrastructure for the script").option("--func <name>", "Name of the function to invoke (default: the first function found)").argument("[payload]", "JSON payload to send to the application", "{}").action(async (payload, options) => {
|
|
498
|
+
if (!options.name) {
|
|
499
|
+
console.error("Error: --name option is required");
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
let parsedPayload;
|
|
503
|
+
try {
|
|
504
|
+
parsedPayload = JSON.parse(payload);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.error("Error: Invalid JSON payload");
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
if (!options.local) {
|
|
510
|
+
console.log(`Invoking "${options.name}" in the cloud is not implemented yet`);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
console.log(
|
|
514
|
+
`Invoking "${options.name}" with${options.withBrowser ? " browser and" : ""} payload:`
|
|
515
|
+
);
|
|
516
|
+
console.log(JSON.stringify(parsedPayload, null, 2));
|
|
517
|
+
const localAppsDir = path3.join(LOCAL_APPS_BASE_DIR, options.name);
|
|
518
|
+
if (!fs2.existsSync(localAppsDir)) {
|
|
519
|
+
console.error(`Error: App "${options.name}" not found in ${localAppsDir}`);
|
|
520
|
+
console.error("Did you deploy it with --local first?");
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
const kernelJsonPath = path3.join(localAppsDir, "kernel.json");
|
|
524
|
+
if (!fs2.existsSync(kernelJsonPath)) {
|
|
525
|
+
console.error(`Error: kernel.json not found in ${localAppsDir}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
const kernelJson = JSON.parse(fs2.readFileSync(kernelJsonPath, "utf8"));
|
|
529
|
+
const entrypointExt = path3.extname(kernelJson.entrypoint);
|
|
530
|
+
const isPythonApp = entrypointExt.toLowerCase() === ".py";
|
|
531
|
+
const isTypeScriptApp = entrypointExt.toLowerCase() === ".ts";
|
|
532
|
+
try {
|
|
533
|
+
if (isPythonApp) {
|
|
534
|
+
await invokePythonApp(localAppsDir, kernelJson, parsedPayload, options);
|
|
535
|
+
} else if (isTypeScriptApp) {
|
|
536
|
+
await invokeTypeScriptApp(localAppsDir, kernelJson, parsedPayload, options);
|
|
537
|
+
} else {
|
|
538
|
+
throw new Error(`Unsupported app type: ${entrypointExt}`);
|
|
539
|
+
}
|
|
540
|
+
} catch (error) {
|
|
541
|
+
console.error("Error invoking application:", error);
|
|
542
|
+
process.exit(1);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
async function waitForStartupMessage(childProcess, timeoutMs = 3e4) {
|
|
546
|
+
return new Promise(async (resolve, reject) => {
|
|
547
|
+
const timeout = setTimeout(() => {
|
|
548
|
+
reject(new Error("Timeout waiting for application startup."));
|
|
549
|
+
}, timeoutMs);
|
|
550
|
+
const reader = childProcess.stderr.getReader();
|
|
551
|
+
const decoder = new TextDecoder();
|
|
552
|
+
try {
|
|
553
|
+
while (true) {
|
|
554
|
+
const { done, value } = await reader.read();
|
|
555
|
+
if (done) break;
|
|
556
|
+
const text = decoder.decode(value);
|
|
557
|
+
process.stderr.write(text);
|
|
558
|
+
if (text.includes("Application startup complete.") || text.includes("Kernel application running")) {
|
|
559
|
+
clearTimeout(timeout);
|
|
560
|
+
resolve();
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} finally {
|
|
565
|
+
reader.releaseLock();
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
async function getBootPyPath() {
|
|
570
|
+
const bootPyDir = BOOT_PY_PATH;
|
|
571
|
+
if (!fs2.existsSync(bootPyDir)) {
|
|
572
|
+
console.error(`Error: Python boot loader not found at ${bootPyDir}`);
|
|
573
|
+
console.error("Please ensure the boot-py package is installed");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
return bootPyDir;
|
|
577
|
+
}
|
|
578
|
+
async function getBootTsPath() {
|
|
579
|
+
const bootTsDir = BOOT_TS_PATH;
|
|
580
|
+
if (!fs2.existsSync(bootTsDir)) {
|
|
581
|
+
console.error(`Error: TypeScript boot loader not found at ${bootTsDir}`);
|
|
582
|
+
console.error("Please ensure the boot-ts package is installed");
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
return bootTsDir;
|
|
586
|
+
}
|
|
587
|
+
async function isUvInstalled() {
|
|
588
|
+
const exitCode = await runForExitCode("uv --version");
|
|
589
|
+
return exitCode === 0;
|
|
590
|
+
}
|
|
591
|
+
async function invokePythonApp(appDir, kernelJson, payload, options) {
|
|
592
|
+
const uvInstalled = await isUvInstalled();
|
|
593
|
+
if (!uvInstalled) {
|
|
594
|
+
console.error("Error: uv is not installed. Please install it with:");
|
|
595
|
+
console.error(" curl -LsSf https://astral.sh/uv/install.sh | sh");
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
const bootPyDir = await getBootPyPath();
|
|
599
|
+
const invokeDir = await preparePythonInvokeEnvironment({
|
|
600
|
+
appName: options.name,
|
|
601
|
+
appDir,
|
|
602
|
+
bootLoaderDir: bootPyDir,
|
|
603
|
+
kernelDependencyOverride: KERNEL_PYTHON_SDK_OVERRIDE
|
|
604
|
+
});
|
|
605
|
+
const port = await getPort();
|
|
606
|
+
console.log(`Starting Python boot server for ${appDir} on port ${port}...`);
|
|
607
|
+
const pythonProcess = Bun.spawn(
|
|
608
|
+
["uv", "run", "--no-cache", "python", "main.py", appDir, "--port", port.toString()],
|
|
609
|
+
{
|
|
610
|
+
cwd: invokeDir,
|
|
611
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
612
|
+
env: {
|
|
613
|
+
...process.env,
|
|
614
|
+
// Pass through any browser-related environment variables
|
|
615
|
+
...options.withBrowser ? { KERNEL_WITH_BROWSER: "true" } : {}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
console.log(`Waiting for Python application to start...`);
|
|
620
|
+
try {
|
|
621
|
+
await waitForStartupMessage(pythonProcess);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
console.error("Error while waiting for application to start:", error);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
let serverReached = false;
|
|
627
|
+
try {
|
|
628
|
+
try {
|
|
629
|
+
const healthCheck = await fetch(`http://localhost:${port}/`, {
|
|
630
|
+
method: "GET"
|
|
631
|
+
}).catch(() => null);
|
|
632
|
+
if (!healthCheck) {
|
|
633
|
+
throw new Error(`Could not connect to boot server at http://localhost:${port}/`);
|
|
634
|
+
}
|
|
635
|
+
serverReached = true;
|
|
636
|
+
} catch (error) {
|
|
637
|
+
console.error("Error connecting to boot server:", error);
|
|
638
|
+
console.error("The boot server might not have started correctly.");
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
let funcName = options.func;
|
|
642
|
+
if (!funcName) {
|
|
643
|
+
const firstFunction = kernelJson.functions.find((f) => f.type === "function");
|
|
644
|
+
if (firstFunction) {
|
|
645
|
+
funcName = firstFunction.name;
|
|
646
|
+
} else {
|
|
647
|
+
throw new Error("No functions found in the application");
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
console.log(`Invoking function "${funcName}"...`);
|
|
651
|
+
const response = await fetch(`http://localhost:${port}/function/${funcName}`, {
|
|
652
|
+
method: "POST",
|
|
653
|
+
headers: {
|
|
654
|
+
"Content-Type": "application/json"
|
|
655
|
+
},
|
|
656
|
+
body: JSON.stringify(payload)
|
|
657
|
+
}).catch((error) => {
|
|
658
|
+
console.error(`Failed to connect to function endpoint: ${error.message}`);
|
|
659
|
+
throw new Error(
|
|
660
|
+
`Could not connect to function endpoint at http://localhost:${port}/function/${funcName}`
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
if (!response.ok) {
|
|
664
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
665
|
+
throw new Error(`HTTP error ${response.status}: ${errorText}`);
|
|
666
|
+
}
|
|
667
|
+
const result = await response.json();
|
|
668
|
+
console.log("Result:", JSON.stringify(result, null, 2));
|
|
669
|
+
return result;
|
|
670
|
+
} finally {
|
|
671
|
+
if (serverReached) {
|
|
672
|
+
console.log("Shutting down boot server...");
|
|
673
|
+
}
|
|
674
|
+
pythonProcess.kill();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function invokeTypeScriptApp(appDir, kernelJson, payload, options) {
|
|
678
|
+
const bootTsDir = await getBootTsPath();
|
|
679
|
+
const invokeDir = await prepareTypeScriptInvokeEnvironment({
|
|
680
|
+
appName: options.name,
|
|
681
|
+
appDir,
|
|
682
|
+
bootLoaderDir: bootTsDir,
|
|
683
|
+
kernelDependencyOverride: KERNEL_TS_SDK_OVERRIDE
|
|
684
|
+
});
|
|
685
|
+
const port = await getPort();
|
|
686
|
+
console.log(`Starting TypeScript boot server for ${appDir} on port ${port}...`);
|
|
687
|
+
const tsProcess = Bun.spawn(["./node_modules/.bin/tsx", "index.ts", appDir, port.toString()], {
|
|
688
|
+
cwd: invokeDir,
|
|
689
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
690
|
+
env: {
|
|
691
|
+
...process.env,
|
|
692
|
+
// Pass through any browser-related environment variables
|
|
693
|
+
...options.withBrowser ? { KERNEL_WITH_BROWSER: "true" } : {}
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
tsProcess.exited.then((exitCode) => {
|
|
697
|
+
if (exitCode !== 0) {
|
|
698
|
+
console.error(`Boot server exited with code ${exitCode}`);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
console.log(`Waiting for TypeScript application to start...`);
|
|
702
|
+
try {
|
|
703
|
+
await waitForStartupMessage(tsProcess);
|
|
704
|
+
} catch (error) {
|
|
705
|
+
console.error("Error while waiting for application to start:", error);
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
let serverReached = false;
|
|
709
|
+
try {
|
|
710
|
+
try {
|
|
711
|
+
const healthCheck = await fetch(`http://localhost:${port}/`, {
|
|
712
|
+
method: "GET"
|
|
713
|
+
}).catch(() => null);
|
|
714
|
+
if (!healthCheck) {
|
|
715
|
+
throw new Error(`Could not connect to boot server at http://localhost:${port}/`);
|
|
716
|
+
}
|
|
717
|
+
serverReached = true;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
console.error("Error connecting to boot server:", error);
|
|
720
|
+
console.error("The boot server might not have started correctly.");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
let funcName = options.func;
|
|
724
|
+
if (!funcName) {
|
|
725
|
+
const firstFunction = kernelJson.functions.find((f) => f.type === "function");
|
|
726
|
+
if (firstFunction) {
|
|
727
|
+
funcName = firstFunction.name;
|
|
728
|
+
} else {
|
|
729
|
+
throw new Error("No functions found in the application");
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
console.log(`Invoking function "${funcName}"...`);
|
|
733
|
+
const response = await fetch(`http://localhost:${port}/function/${funcName}`, {
|
|
734
|
+
method: "POST",
|
|
735
|
+
headers: {
|
|
736
|
+
"Content-Type": "application/json"
|
|
737
|
+
},
|
|
738
|
+
body: JSON.stringify(payload)
|
|
739
|
+
}).catch((error) => {
|
|
740
|
+
console.error(`Failed to connect to function endpoint: ${error.message}`);
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Could not connect to function endpoint at http://localhost:${port}/function/${funcName}`
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
if (!response.ok) {
|
|
746
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
747
|
+
throw new Error(`HTTP error ${response.status}: ${errorText}`);
|
|
748
|
+
}
|
|
749
|
+
const result = await response.json();
|
|
750
|
+
console.log("Result:", JSON.stringify(result, null, 2));
|
|
751
|
+
return result;
|
|
752
|
+
} finally {
|
|
753
|
+
if (serverReached) {
|
|
754
|
+
console.log("Shutting down boot server...");
|
|
755
|
+
}
|
|
756
|
+
tsProcess.kill();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onkernel/cli",
|
|
3
|
+
"version": "0.0.1-alpha.1",
|
|
4
|
+
"description": "Kernel CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "index.ts",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"kernel": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup --config tsup.config.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"kernel",
|
|
20
|
+
"cli",
|
|
21
|
+
"ai",
|
|
22
|
+
"llm",
|
|
23
|
+
"llms",
|
|
24
|
+
"agent",
|
|
25
|
+
"agents",
|
|
26
|
+
"dev",
|
|
27
|
+
"development",
|
|
28
|
+
"deploy",
|
|
29
|
+
"deployment",
|
|
30
|
+
"build",
|
|
31
|
+
"workflow",
|
|
32
|
+
"typescript",
|
|
33
|
+
"command-line",
|
|
34
|
+
"devtools"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@types/fs-extra": "^11.0.4",
|
|
39
|
+
"commander": "^13.1.0",
|
|
40
|
+
"execa": "^9.5.2",
|
|
41
|
+
"fs-extra": "^11.3.0",
|
|
42
|
+
"get-port": "^7.1.0",
|
|
43
|
+
"smol-toml": "^1.3.4",
|
|
44
|
+
"tree-sitter": "^0.22.4",
|
|
45
|
+
"tree-sitter-python": "^0.23.6",
|
|
46
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
47
|
+
"type-fest": "^4.40.1",
|
|
48
|
+
"zod": "^3.24.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/bun": "latest",
|
|
52
|
+
"tsup": "^8.4.0"
|
|
53
|
+
}
|
|
54
|
+
}
|