@tonbo/cli 0.0.4 → 0.0.5

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 CHANGED
@@ -7,25 +7,32 @@ npm install --global @tonbo/cli
7
7
  tonbo --version
8
8
  ```
9
9
 
10
- The V1 CLI deploys a PI-based Agent directly from a local directory without requiring Git or Tonbo calls in the Agent source. Add one JSON declaration at the directory root:
11
-
12
- ```json
13
- {
14
- "version": 1,
15
- "execution": {
16
- "mode": "managed",
17
- "runtime": "pi"
18
- },
19
- "inference": {
20
- "model": "claude-sonnet-4-5"
21
- },
22
- "session_capture": {
23
- "adapter": "pi-jsonl-v3"
24
- }
25
- }
10
+ The V1 CLI deploys a PI-based Agent directly from a local directory without requiring Git or Tonbo calls in the Agent source. Initialize the current directory interactively:
11
+
12
+ ```console
13
+ tonbo init
14
+ ? Inference model [claude-sonnet-4-5]:
15
+ Created .tonbo.
16
+ Next: tonbo project create <slug>
17
+ ```
18
+
19
+ The generated `.tonbo` is TOML:
20
+
21
+ ```toml
22
+ version = 1
23
+
24
+ [execution]
25
+ mode = "managed"
26
+ runtime = "pi"
27
+
28
+ [inference]
29
+ model = "claude-sonnet-4-5"
30
+
31
+ [session_capture]
32
+ adapter = "pi-jsonl-v3"
26
33
  ```
27
34
 
28
- Then authenticate, bind the directory and deploy its current contents:
35
+ For automation, use `tonbo init --model <model>`; an existing declaration requires interactive confirmation or `--force`. Then authenticate, bind the directory and deploy its current contents:
29
36
 
30
37
  ```console
31
38
  tonbo login
package/dist/src/app.js CHANGED
@@ -2,10 +2,12 @@ import { Command } from "commander";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { TonboApi } from "./api.js";
4
4
  import { AuthClient } from "./auth.js";
5
- import { deployCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
5
+ import { deployCommand, initCommand, runCommand, loginCommand, projectCreateCommand, projectUseCommand, sshCommand, sshKeyAddCommand, sshKeyRemoveCommand, secretListCommand, secretRemoveCommand, secretSetCommand, } from "./commands.js";
6
6
  import { FileConfigStore } from "./config.js";
7
7
  import { FileCredentialStore } from "./credentials.js";
8
+ import { DECLARATION_FILENAME } from "./declaration.js";
8
9
  import { silentProgress, TerminalProgress } from "./progress.js";
10
+ import { terminalPrompt } from "./prompt.js";
9
11
  import { readDefaultSshPublicKeys } from "./ssh-key.js";
10
12
  const packageVersion = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
11
13
  export function createDependencies(json = false) {
@@ -17,6 +19,7 @@ export function createDependencies(json = false) {
17
19
  config: new FileConfigStore(),
18
20
  cwd: () => process.cwd(),
19
21
  defaultSshPublicKeys: readDefaultSshPublicKeys,
22
+ interactive: () => !json && process.stdin.isTTY === true && process.stderr.isTTY === true,
20
23
  output: (value) => {
21
24
  if (json)
22
25
  console.log(JSON.stringify(value));
@@ -24,6 +27,7 @@ export function createDependencies(json = false) {
24
27
  console.log(value.message ?? value);
25
28
  },
26
29
  progress: json ? silentProgress : new TerminalProgress(process.stderr),
30
+ prompt: terminalPrompt,
27
31
  secretValue: async (name, fromEnvironment) => {
28
32
  const environmentName = fromEnvironment ?? name;
29
33
  const value = process.env[environmentName];
@@ -39,6 +43,12 @@ export function createProgram(dependencies = createDependencies) {
39
43
  .description("Deploy a persistent Project Agent to Tonbo.")
40
44
  .version(packageVersion.version)
41
45
  .option("--json", "print machine-readable JSON");
46
+ program
47
+ .command("init")
48
+ .description("interactively create a Tonbo Agent declaration in this directory")
49
+ .option("--model <model>", "inference model")
50
+ .option("--force", `replace an existing ${DECLARATION_FILENAME}`)
51
+ .action(async (options) => initCommand(dependencies(program.opts().json), options));
42
52
  program
43
53
  .command("login")
44
54
  .description("sign in through the browser and store the session in the user config")
@@ -10,8 +10,10 @@ export interface CommandDependencies {
10
10
  config: ConfigStore;
11
11
  cwd: () => string;
12
12
  defaultSshPublicKeys: typeof readDefaultSshPublicKeys;
13
+ interactive: () => boolean;
13
14
  output: (value: unknown) => void;
14
15
  progress: ProgressReporter;
16
+ prompt: (question: string) => Promise<string>;
15
17
  executable?: () => string;
16
18
  secretValue: (name: string, fromEnvironment?: string) => Promise<string>;
17
19
  }
@@ -20,6 +22,10 @@ export declare function resolveProject(deps: CommandDependencies, selector?: str
20
22
  project: ProjectSummary;
21
23
  }>;
22
24
  export declare function selectProject(projects: ProjectSummary[], selector: string): ProjectSummary;
25
+ export declare function initCommand(deps: CommandDependencies, options: {
26
+ force?: boolean;
27
+ model?: string;
28
+ }): Promise<void>;
23
29
  export declare function loginCommand(deps: CommandDependencies): Promise<void>;
24
30
  export declare function sshKeyAddCommand(deps: CommandDependencies, path: string): Promise<void>;
25
31
  export declare function sshKeyRemoveCommand(deps: CommandDependencies, fingerprint: string): Promise<void>;
@@ -1,4 +1,5 @@
1
- import { buildRevision, loadDeclaration } from "./declaration.js";
1
+ import path from "node:path";
2
+ import { buildRevision, createDeclaration, declarationExists, DECLARATION_FILENAME, DEFAULT_INFERENCE_MODEL, loadDeclaration, saveDeclaration, } from "./declaration.js";
2
3
  import { buildSourceBundle, findDeclarationRoot } from "./source.js";
3
4
  import { readSshPublicKey } from "./ssh-key.js";
4
5
  import { launchProjectSsh } from "./ssh.js";
@@ -36,6 +37,35 @@ export function selectProject(projects, selector) {
36
37
  throw new Error(`Project ${selector} is not active.`);
37
38
  return matches[0];
38
39
  }
40
+ export async function initCommand(deps, options) {
41
+ const root = deps.cwd();
42
+ const exists = await declarationExists(root);
43
+ let overwrite = options.force === true;
44
+ if (exists && !overwrite) {
45
+ if (!deps.interactive())
46
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
47
+ const answer = (await deps.prompt(`Replace existing ${DECLARATION_FILENAME}? [y/N] `))
48
+ .trim()
49
+ .toLowerCase();
50
+ if (answer !== "y" && answer !== "yes") {
51
+ deps.output({ message: `Kept existing ${DECLARATION_FILENAME}.` });
52
+ return;
53
+ }
54
+ overwrite = true;
55
+ }
56
+ let model = options.model?.trim();
57
+ if (!model && deps.interactive()) {
58
+ model = (await deps.prompt(`Inference model [${DEFAULT_INFERENCE_MODEL}]: `)).trim();
59
+ }
60
+ model ||= DEFAULT_INFERENCE_MODEL;
61
+ const declaration = createDeclaration(model);
62
+ await saveDeclaration(root, declaration, overwrite);
63
+ deps.output({
64
+ message: `${exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.\nNext: tonbo project create <slug>`,
65
+ declaration,
66
+ path: path.join(root, DECLARATION_FILENAME),
67
+ });
68
+ }
39
69
  export async function loginCommand(deps) {
40
70
  try {
41
71
  const tokens = await deps.auth.login((event) => reportLoginProgress(deps.progress, event));
@@ -13,7 +13,7 @@ function validationMessage(label, errors) {
13
13
  export function parseDeclaration(value) {
14
14
  const candidate = structuredClone(value);
15
15
  if (!validateDeclaration(candidate)) {
16
- throw new Error(validationMessage(".tonbo", validateDeclaration.errors));
16
+ throw new Error(validationMessage(".tonbo TOML", validateDeclaration.errors));
17
17
  }
18
18
  return candidate;
19
19
  }
@@ -1,4 +1,10 @@
1
1
  import { parseDeclaration } from "./contracts.js";
2
- import type { ManagedRevisionSpec, SourceBundle } from "./types.js";
3
- export declare function loadDeclaration(declarationRoot: string): Promise<import("./types.js").TonboDeclaration>;
2
+ import type { ManagedRevisionSpec, SourceBundle, TonboDeclaration } from "./types.js";
3
+ export declare const DECLARATION_FILENAME = ".tonbo";
4
+ export declare const DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
5
+ export declare function createDeclaration(model?: string): TonboDeclaration;
6
+ export declare function renderDeclaration(declaration: TonboDeclaration): string;
7
+ export declare function declarationExists(root: string): Promise<boolean>;
8
+ export declare function saveDeclaration(root: string, declaration: TonboDeclaration, overwrite: boolean): Promise<void>;
9
+ export declare function loadDeclaration(declarationRoot: string): Promise<TonboDeclaration>;
4
10
  export declare function buildRevision(declaration: ReturnType<typeof parseDeclaration>, source: SourceBundle): ManagedRevisionSpec;
@@ -1,16 +1,82 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, open, readFile, rename, rm } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { parse, stringify } from "smol-toml";
3
5
  import { assertManagedRevision, parseDeclaration } from "./contracts.js";
6
+ export const DECLARATION_FILENAME = ".tonbo";
7
+ export const DEFAULT_INFERENCE_MODEL = "claude-sonnet-4-5";
8
+ export function createDeclaration(model = DEFAULT_INFERENCE_MODEL) {
9
+ return parseDeclaration({
10
+ version: 1,
11
+ execution: { mode: "managed", runtime: "pi" },
12
+ inference: { model: model.trim() },
13
+ session_capture: { adapter: "pi-jsonl-v3" },
14
+ });
15
+ }
16
+ export function renderDeclaration(declaration) {
17
+ return `# Tonbo Project Agent configuration.\n${stringify(declaration)}`;
18
+ }
19
+ export async function declarationExists(root) {
20
+ const filename = path.join(root, DECLARATION_FILENAME);
21
+ try {
22
+ const metadata = await lstat(filename);
23
+ if (metadata.isSymbolicLink() || !metadata.isFile())
24
+ throw new Error(`${filename} must be a regular file.`);
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ if (error.code === "ENOENT")
29
+ return false;
30
+ throw error;
31
+ }
32
+ }
33
+ export async function saveDeclaration(root, declaration, overwrite) {
34
+ const filename = path.join(root, DECLARATION_FILENAME);
35
+ const exists = await declarationExists(root);
36
+ if (exists && !overwrite)
37
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
38
+ const contents = renderDeclaration(declaration);
39
+ if (!overwrite) {
40
+ const handle = await open(filename, "wx", 0o644).catch((error) => {
41
+ if (error.code === "EEXIST")
42
+ throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
43
+ throw error;
44
+ });
45
+ try {
46
+ await handle.writeFile(contents, "utf8");
47
+ await handle.sync();
48
+ }
49
+ finally {
50
+ await handle.close();
51
+ }
52
+ return;
53
+ }
54
+ const temporary = path.join(root, `.${DECLARATION_FILENAME}.${process.pid}.${randomUUID()}.tmp`);
55
+ let handle;
56
+ try {
57
+ handle = await open(temporary, "wx", 0o644);
58
+ await handle.writeFile(contents, "utf8");
59
+ await handle.sync();
60
+ await handle.close();
61
+ handle = undefined;
62
+ await rename(temporary, filename);
63
+ }
64
+ catch (error) {
65
+ await handle?.close().catch(() => undefined);
66
+ await rm(temporary, { force: true }).catch(() => undefined);
67
+ throw error;
68
+ }
69
+ }
4
70
  export async function loadDeclaration(declarationRoot) {
5
- const filename = path.join(declarationRoot, ".tonbo");
71
+ const filename = path.join(declarationRoot, DECLARATION_FILENAME);
6
72
  let parsed;
7
73
  try {
8
- parsed = JSON.parse(await readFile(filename, "utf8"));
74
+ parsed = parse(await readFile(filename, "utf8"));
9
75
  }
10
76
  catch (error) {
11
77
  if (error.code === "ENOENT")
12
- throw new Error(`No .tonbo declaration found at ${filename}.`);
13
- throw new Error(`Could not read ${filename} as JSON.`, { cause: error });
78
+ throw new Error(`No ${DECLARATION_FILENAME} declaration found at ${filename}.`);
79
+ throw new Error(`Could not read ${filename} as TOML.`, { cause: error });
14
80
  }
15
81
  return parseDeclaration(parsed);
16
82
  }
@@ -0,0 +1 @@
1
+ export declare function terminalPrompt(question: string): Promise<string>;
@@ -0,0 +1,10 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ export async function terminalPrompt(question) {
3
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
4
+ try {
5
+ return await prompt.question(`? ${question}`);
6
+ }
7
+ finally {
8
+ prompt.close();
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonbo/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Deploy one persistent Agent per Project from the command line.",
5
5
  "homepage": "https://tonbo.dev",
6
6
  "bugs": {
@@ -33,6 +33,7 @@
33
33
  "ajv": "8.20.0",
34
34
  "commander": "^14.0.3",
35
35
  "ignore": "^7.0.5",
36
+ "smol-toml": "1.6.1",
36
37
  "tar-stream": "^3.1.7"
37
38
  },
38
39
  "devDependencies": {