@onkernel/cli 0.0.1-alpha.1 → 0.0.2

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.js +106 -715
  3. package/package.json +11 -11
package/dist/index.d.ts CHANGED
@@ -1,2 +1 @@
1
-
2
- export { }
1
+ #!/usr/bin/env bun
package/dist/index.js CHANGED
@@ -1,504 +1,122 @@
1
+ #!/usr/bin/env bun
2
+ import { Kernel } from '@onkernel/sdk';
3
+ import chalk from 'chalk';
1
4
  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';
5
+ import fs, { createReadStream } from 'fs';
6
+ import path2 from 'path';
7
+ import * as tmp from 'tmp';
8
+ import archiver from 'archiver';
7
9
  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';
10
+ import walk from 'ignore-walk';
14
11
 
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
12
  function getPackageVersion() {
45
- const __filename = fileURLToPath(import.meta.url);
46
- const __dirname = dirname(__filename);
47
- const pkgJsonPath = path3.join(__dirname, "..", "package.json");
13
+ const pkgJsonPath = path2.join(__dirname, "..", "..", "..", "package.json");
48
14
  const content = fsExtra.readJSONSync(pkgJsonPath);
49
15
  if (!content.version) {
50
16
  throw new Error("package.json does not contain a version");
51
17
  }
52
18
  return content.version;
53
19
  }
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
20
+ async function zipDirectory(inputDir, outputZip) {
21
+ const entries = await walk({
22
+ path: inputDir,
23
+ ignoreFiles: [".gitignore", ".dockerignore"],
24
+ includeEmpty: true,
25
+ follow: false
320
26
  });
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);
27
+ const output = fs.createWriteStream(outputZip);
28
+ const archive = archiver("zip", { zlib: { level: 9 } });
29
+ const finalizePromise = new Promise((resolve, reject) => {
30
+ output.on("close", resolve);
31
+ archive.on("error", reject);
341
32
  });
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
- );
33
+ archive.pipe(output);
34
+ for (const entry of entries) {
35
+ const fullPath = path2.join(inputDir, entry);
36
+ const stat = fs.statSync(fullPath);
37
+ const archivePath = entry.split(path2.sep).join("/");
38
+ if (stat.isFile()) {
39
+ archive.file(fullPath, { name: archivePath });
40
+ } else if (stat.isDirectory()) {
41
+ archive.append("", { name: archivePath.endsWith("/") ? archivePath : archivePath + "/" });
398
42
  }
399
43
  }
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;
44
+ await archive.finalize();
45
+ await finalizePromise;
456
46
  }
457
47
 
458
48
  // index.ts
459
49
  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);
50
+ if (process.argv.length === 3 && ["-v", "--version"].includes(process.argv[2])) {
51
+ console.log(getPackageVersion());
52
+ process.exit(0);
53
+ }
54
+ program.name("kernel").description("CLI for Kernel deployment and invocation");
55
+ program.command("deploy").description("Deploy a Kernel application").argument("<entrypoint>", "Path to entrypoint file (TypeScript or Python)").option("--version <version>", "Specify a version for the app (default: latest)").option("--force", "Allow overwrite of an existing version with the same name").action(async (entrypoint, options) => {
56
+ let { version: versionArg, force } = options;
57
+ if (!versionArg) {
58
+ versionArg = "latest";
59
+ if (force && force !== "true") {
60
+ console.error("Error: --force must be used when version is latest");
61
+ process.exit(1);
62
+ } else if (!force) {
63
+ force = "true";
64
+ }
473
65
  }
474
- const resolvedEntrypoint = path3.resolve(entrypoint);
475
- if (!fs2.existsSync(resolvedEntrypoint)) {
66
+ const resolvedEntrypoint = path2.resolve(entrypoint);
67
+ if (!fs.existsSync(resolvedEntrypoint)) {
476
68
  console.error(`Error: Entrypoint ${resolvedEntrypoint} doesn't exist`);
477
69
  process.exit(1);
478
70
  }
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
71
+ const sourceDir = path2.dirname(resolvedEntrypoint);
72
+ if (!process.env["KERNEL_API_KEY"]) {
73
+ console.error("Error: KERNEL_API_KEY environment variable is not set");
74
+ console.error("Please set your Kernel API key using: export KERNEL_API_KEY=your_api_key");
75
+ process.exit(1);
76
+ }
77
+ const client = new Kernel();
78
+ console.log(chalk.green(`Compressing files...`));
79
+ const tmpZipFile = tmp.fileSync({ postfix: ".zip" });
80
+ try {
81
+ await zipDirectory(path2.join(sourceDir), tmpZipFile.name);
82
+ console.log(chalk.green(`Deploying app (version: ${versionArg})...`));
83
+ const response = await client.apps.deploy(
84
+ {
85
+ file: createReadStream(tmpZipFile.name),
86
+ version: versionArg,
87
+ force,
88
+ entrypointRelPath: path2.relative(sourceDir, resolvedEntrypoint)
89
+ },
90
+ { maxRetries: 0 }
91
+ );
92
+ if (!response.success) {
93
+ console.error("Error deploying to Kernel:", response.message);
94
+ process.exit(1);
487
95
  }
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");
96
+ for (const app of response.apps) {
97
+ console.log(
98
+ chalk.green(
99
+ `App "${app.name}" successfully deployed to Kernel with action${app.actions.length > 1 ? "s" : ""}: ${app.actions.map((a) => a.name).join(", ")}`
100
+ )
101
+ );
102
+ console.log(
103
+ `You can invoke it with: kernel invoke${versionArg !== "latest" ? ` --version ${versionArg}` : ""} ${quoteIfNeeded(app.name)} ${quoteIfNeeded(app.actions[0].name)} '{ ... JSON payload ... }'`
104
+ );
105
+ }
106
+ } catch (error) {
107
+ console.error("Error deploying to Kernel:", error);
494
108
  process.exit(1);
109
+ } finally {
110
+ tmpZipFile.removeCallback();
495
111
  }
496
112
  });
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);
113
+ function quoteIfNeeded(str) {
114
+ if (str.includes(" ")) {
115
+ return `"${str}"`;
501
116
  }
117
+ return str;
118
+ }
119
+ program.command("invoke").description("Invoke a deployed Kernel application").option("--version <version>", "Specify a version of the app to invoke").argument("<app_name>", "Name of the application to invoke").argument("<action_name>", "Name of the action to invoke").argument("<payload>", "JSON payload to send to the application").action(async (appName, actionName, payload, options) => {
502
120
  let parsedPayload;
503
121
  try {
504
122
  parsedPayload = JSON.parse(payload);
@@ -506,254 +124,27 @@ program.command("invoke").description("Invoke a deployed Kernel application").op
506
124
  console.error("Error: Invalid JSON payload");
507
125
  process.exit(1);
508
126
  }
509
- if (!options.local) {
510
- console.log(`Invoking "${options.name}" in the cloud is not implemented yet`);
127
+ if (!process.env["KERNEL_API_KEY"]) {
128
+ console.error("Error: KERNEL_API_KEY environment variable is not set");
129
+ console.error("Please set your Kernel API key using: export KERNEL_API_KEY=your_api_key");
511
130
  process.exit(1);
512
131
  }
513
- console.log(
514
- `Invoking "${options.name}" with${options.withBrowser ? " browser and" : ""} payload:`
515
- );
132
+ const client = new Kernel();
133
+ console.log(`Invoking "${appName}" with action "${actionName}" and payload:`);
516
134
  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
135
  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
- }
136
+ const response = await client.apps.invoke({
137
+ appName,
138
+ actionName,
139
+ payload,
140
+ ...options.version && { version: options.version }
141
+ });
142
+ console.log("Result:");
143
+ console.log(JSON.stringify(JSON.parse(response.output || "{}"), null, 2));
540
144
  } catch (error) {
541
145
  console.error("Error invoking application:", error);
542
146
  process.exit(1);
543
147
  }
148
+ return;
544
149
  });
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
150
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onkernel/cli",
3
- "version": "0.0.1-alpha.1",
3
+ "version": "0.0.2",
4
4
  "description": "Kernel CLI",
5
5
  "type": "module",
6
6
  "module": "index.ts",
@@ -35,20 +35,20 @@
35
35
  ],
36
36
  "author": "",
37
37
  "dependencies": {
38
- "@types/fs-extra": "^11.0.4",
38
+ "@onkernel/sdk": "0.1.0-alpha.9",
39
+ "@types/archiver": "^6.0.3",
40
+ "@types/ignore-walk": "^4.0.3",
41
+ "archiver": "^7.0.1",
42
+ "chalk": "^5.4.1",
39
43
  "commander": "^13.1.0",
40
- "execa": "^9.5.2",
41
44
  "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"
45
+ "ignore-walk": "^7.0.0",
46
+ "tmp": "^0.2.3",
47
+ "type-fest": "^4.40.1"
49
48
  },
50
49
  "devDependencies": {
51
- "@types/bun": "latest",
50
+ "@types/fs-extra": "^11.0.4",
51
+ "@types/tmp": "^0.2.6",
52
52
  "tsup": "^8.4.0"
53
53
  }
54
54
  }