@heysalad/cheri-cli 0.1.0 → 0.3.0
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 +59 -31
- package/bin/cheri.js +7 -4
- package/package.json +7 -8
- package/src/commands/config.js +42 -26
- package/src/commands/init.js +82 -74
- package/src/commands/login.js +47 -40
- package/src/commands/memory.js +84 -60
- package/src/commands/status.js +50 -40
- package/src/commands/usage.js +64 -0
- package/src/commands/workspace.js +103 -69
- package/src/lib/api-client.js +5 -0
- package/src/lib/logger.js +10 -0
- package/src/repl.js +217 -0
package/README.md
CHANGED
|
@@ -1,60 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cheri-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for [Cheri](https://cheri.heysalad.app) — the AI-powered cloud IDE that never forgets.
|
|
4
|
+
|
|
5
|
+
Manage workspaces, track API usage, and access your AI memory from the terminal.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install -g
|
|
10
|
+
npm install -g cheri-cli
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Requires Node.js 18+.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
12
16
|
|
|
13
17
|
```bash
|
|
14
|
-
#
|
|
18
|
+
# Authenticate with your Cheri account
|
|
15
19
|
cheri login
|
|
16
20
|
|
|
21
|
+
# Launch a cloud workspace
|
|
22
|
+
cheri workspace launch owner/my-repo
|
|
23
|
+
|
|
17
24
|
# Check account status
|
|
18
25
|
cheri status
|
|
19
26
|
|
|
20
|
-
#
|
|
21
|
-
cheri
|
|
22
|
-
|
|
23
|
-
# List your workspaces
|
|
24
|
-
cheri workspace list
|
|
27
|
+
# View API usage and rate limits
|
|
28
|
+
cheri usage
|
|
29
|
+
```
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
## Commands
|
|
32
|
+
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `cheri login` | Authenticate with GitHub |
|
|
36
|
+
| `cheri status` | Show account and workspace status |
|
|
37
|
+
| `cheri usage` | Show API usage and rate limit status |
|
|
38
|
+
| `cheri workspace launch <repo>` | Launch a new cloud workspace |
|
|
39
|
+
| `cheri workspace list` | List all workspaces |
|
|
40
|
+
| `cheri workspace stop <id>` | Stop a running workspace |
|
|
41
|
+
| `cheri workspace status <id>` | Get workspace status |
|
|
42
|
+
| `cheri memory show` | Show current memory entries |
|
|
43
|
+
| `cheri memory add <text>` | Add a memory entry |
|
|
44
|
+
| `cheri memory clear` | Clear all memory |
|
|
45
|
+
| `cheri memory export` | Export memory to JSON |
|
|
46
|
+
| `cheri config list` | Show all configuration |
|
|
47
|
+
| `cheri config get <key>` | Get a config value |
|
|
48
|
+
| `cheri config set <key> <value>` | Set a config value |
|
|
49
|
+
| `cheri init` | Initialize a project |
|
|
50
|
+
|
|
51
|
+
## Interactive REPL
|
|
52
|
+
|
|
53
|
+
Run `cheri` with no arguments to enter the interactive REPL:
|
|
28
54
|
|
|
29
|
-
|
|
30
|
-
cheri
|
|
55
|
+
```
|
|
56
|
+
$ cheri
|
|
57
|
+
🍒 cheri > help
|
|
58
|
+
🍒 cheri > workspace list
|
|
59
|
+
🍒 cheri > usage
|
|
60
|
+
🍒 cheri > exit
|
|
61
|
+
```
|
|
31
62
|
|
|
32
|
-
|
|
33
|
-
cheri memory show
|
|
34
|
-
cheri memory add "Always use TypeScript strict mode"
|
|
35
|
-
cheri memory clear
|
|
63
|
+
## Rate Limits
|
|
36
64
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
| Plan | Limit |
|
|
66
|
+
|---|---|
|
|
67
|
+
| Free | 100 requests/hour |
|
|
68
|
+
| Pro | 1,000 requests/hour |
|
|
41
69
|
|
|
42
|
-
|
|
70
|
+
Use `cheri usage` to check your current rate limit status.
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
2. **`cheri workspace launch`** spins up a cloud workspace with code-server (VS Code in browser)
|
|
46
|
-
3. **`cheri memory`** stores persistent context that follows you across sessions
|
|
47
|
-
4. **`cheri init`** creates a local `.ai/` directory with project constitution files
|
|
72
|
+
## Configuration
|
|
48
73
|
|
|
49
|
-
|
|
74
|
+
Config is stored in `~/.cheri/`. Set the API URL if self-hosting:
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
```bash
|
|
77
|
+
cheri config set apiUrl https://your-instance.example.com
|
|
78
|
+
```
|
|
52
79
|
|
|
53
80
|
## Links
|
|
54
81
|
|
|
55
82
|
- [Cheri Cloud IDE](https://cheri.heysalad.app)
|
|
56
|
-
- [
|
|
83
|
+
- [Dashboard](https://cheri.heysalad.app/dashboard)
|
|
84
|
+
- [GitHub](https://github.com/chilu18/cloud-ide)
|
|
57
85
|
|
|
58
86
|
## License
|
|
59
87
|
|
|
60
|
-
MIT
|
|
88
|
+
MIT
|
package/bin/cheri.js
CHANGED
|
@@ -7,6 +7,7 @@ import { registerStatusCommand } from "../src/commands/status.js";
|
|
|
7
7
|
import { registerMemoryCommand } from "../src/commands/memory.js";
|
|
8
8
|
import { registerConfigCommand } from "../src/commands/config.js";
|
|
9
9
|
import { registerWorkspaceCommand } from "../src/commands/workspace.js";
|
|
10
|
+
import { registerUsageCommand } from "../src/commands/usage.js";
|
|
10
11
|
|
|
11
12
|
program
|
|
12
13
|
.name("cheri")
|
|
@@ -19,10 +20,12 @@ registerStatusCommand(program);
|
|
|
19
20
|
registerMemoryCommand(program);
|
|
20
21
|
registerConfigCommand(program);
|
|
21
22
|
registerWorkspaceCommand(program);
|
|
23
|
+
registerUsageCommand(program);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Show help if no command provided
|
|
25
|
+
// If no args, launch interactive command REPL
|
|
26
26
|
if (!process.argv.slice(2).length) {
|
|
27
|
-
|
|
27
|
+
const { startCommandRepl } = await import("../src/repl.js");
|
|
28
|
+
await startCommandRepl();
|
|
29
|
+
} else {
|
|
30
|
+
program.parse(process.argv);
|
|
28
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heysalad/cheri-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,15 +8,14 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/",
|
|
11
|
-
"src/"
|
|
12
|
-
"README.md"
|
|
11
|
+
"src/"
|
|
13
12
|
],
|
|
14
13
|
"scripts": {
|
|
15
14
|
"start": "node bin/cheri.js",
|
|
16
15
|
"dev": "node bin/cheri.js",
|
|
17
|
-
"release:patch": "npm version patch && npm publish
|
|
18
|
-
"release:minor": "npm version minor && npm publish
|
|
19
|
-
"release:major": "npm version major && npm publish
|
|
16
|
+
"release:patch": "npm version patch && npm publish && git push && git push --tags",
|
|
17
|
+
"release:minor": "npm version minor && npm publish && git push && git push --tags",
|
|
18
|
+
"release:major": "npm version major && npm publish && git push && git push --tags"
|
|
20
19
|
},
|
|
21
20
|
"keywords": [
|
|
22
21
|
"cloud-ide",
|
|
@@ -29,9 +28,9 @@
|
|
|
29
28
|
],
|
|
30
29
|
"repository": {
|
|
31
30
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "https://github.com/chilu18/cloud-ide.git",
|
|
32
|
+
"directory": "cli"
|
|
33
33
|
},
|
|
34
|
-
"homepage": "https://cheri.heysalad.app",
|
|
35
34
|
"author": "HeySalad",
|
|
36
35
|
"license": "MIT",
|
|
37
36
|
"engines": {
|
package/src/commands/config.js
CHANGED
|
@@ -2,6 +2,42 @@ import chalk from "chalk";
|
|
|
2
2
|
import { getConfig, getConfigValue, setConfigValue } from "../lib/config-store.js";
|
|
3
3
|
import { log } from "../lib/logger.js";
|
|
4
4
|
|
|
5
|
+
export function listConfig() {
|
|
6
|
+
const cfg = getConfig();
|
|
7
|
+
|
|
8
|
+
log.blank();
|
|
9
|
+
log.brand("Configuration");
|
|
10
|
+
log.blank();
|
|
11
|
+
|
|
12
|
+
printObject(cfg, "");
|
|
13
|
+
log.blank();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getConfigKey(key) {
|
|
17
|
+
const value = getConfigValue(key);
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
throw new Error(`Key '${key}' not found.`);
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "object") {
|
|
22
|
+
console.log(JSON.stringify(value, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setConfigKey(key, value) {
|
|
29
|
+
// Try to parse as JSON (for arrays, numbers, booleans)
|
|
30
|
+
let parsed;
|
|
31
|
+
try {
|
|
32
|
+
parsed = JSON.parse(value);
|
|
33
|
+
} catch {
|
|
34
|
+
parsed = value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setConfigValue(key, parsed);
|
|
38
|
+
log.success(`Set ${chalk.cyan(key)} = ${chalk.white(value)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
5
41
|
export function registerConfigCommand(program) {
|
|
6
42
|
const config = program
|
|
7
43
|
.command("config")
|
|
@@ -11,46 +47,26 @@ export function registerConfigCommand(program) {
|
|
|
11
47
|
.command("list")
|
|
12
48
|
.description("Show all configuration values")
|
|
13
49
|
.action(() => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
log.blank();
|
|
17
|
-
log.brand("Configuration");
|
|
18
|
-
log.blank();
|
|
19
|
-
|
|
20
|
-
printObject(cfg, "");
|
|
21
|
-
log.blank();
|
|
50
|
+
listConfig();
|
|
22
51
|
});
|
|
23
52
|
|
|
24
53
|
config
|
|
25
54
|
.command("get <key>")
|
|
26
55
|
.description("Get a configuration value")
|
|
27
56
|
.action((key) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
try {
|
|
58
|
+
getConfigKey(key);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
log.error(err.message);
|
|
31
61
|
process.exit(1);
|
|
32
62
|
}
|
|
33
|
-
if (typeof value === "object") {
|
|
34
|
-
console.log(JSON.stringify(value, null, 2));
|
|
35
|
-
} else {
|
|
36
|
-
console.log(value);
|
|
37
|
-
}
|
|
38
63
|
});
|
|
39
64
|
|
|
40
65
|
config
|
|
41
66
|
.command("set <key> <value>")
|
|
42
67
|
.description("Set a configuration value")
|
|
43
68
|
.action((key, value) => {
|
|
44
|
-
|
|
45
|
-
let parsed;
|
|
46
|
-
try {
|
|
47
|
-
parsed = JSON.parse(value);
|
|
48
|
-
} catch {
|
|
49
|
-
parsed = value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
setConfigValue(key, parsed);
|
|
53
|
-
log.success(`Set ${chalk.cyan(key)} = ${chalk.white(value)}`);
|
|
69
|
+
setConfigKey(key, value);
|
|
54
70
|
});
|
|
55
71
|
}
|
|
56
72
|
|
package/src/commands/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
2
|
import inquirer from "inquirer";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { existsSync, mkdirSync, writeFileSync
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { setConfigValue } from "../lib/config-store.js";
|
|
7
7
|
import { log } from "../lib/logger.js";
|
|
@@ -79,6 +79,86 @@ const TEMPLATES = {
|
|
|
79
79
|
`,
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
export async function initProject(options = {}) {
|
|
83
|
+
log.blank();
|
|
84
|
+
log.brand("Initializing project...");
|
|
85
|
+
log.blank();
|
|
86
|
+
|
|
87
|
+
const aiDir = join(process.cwd(), AI_DIR);
|
|
88
|
+
|
|
89
|
+
if (existsSync(aiDir) && !options.yes) {
|
|
90
|
+
const { overwrite } = await inquirer.prompt([
|
|
91
|
+
{
|
|
92
|
+
type: "confirm",
|
|
93
|
+
name: "overwrite",
|
|
94
|
+
message: "A .ai/ directory already exists. Add missing files?",
|
|
95
|
+
default: true,
|
|
96
|
+
},
|
|
97
|
+
]);
|
|
98
|
+
if (!overwrite) {
|
|
99
|
+
log.info("Initialization cancelled.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let projectName = options.name;
|
|
105
|
+
if (!projectName && !options.yes) {
|
|
106
|
+
const answers = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: "input",
|
|
109
|
+
name: "name",
|
|
110
|
+
message: "Project name:",
|
|
111
|
+
default: process.cwd().split("/").pop(),
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
projectName = answers.name;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
projectName = projectName || process.cwd().split("/").pop();
|
|
118
|
+
|
|
119
|
+
const spinner = ora("Creating project constitution...").start();
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (!existsSync(aiDir)) {
|
|
123
|
+
mkdirSync(aiDir, { recursive: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const created = [];
|
|
127
|
+
for (const [filename, content] of Object.entries(TEMPLATES)) {
|
|
128
|
+
const filePath = join(aiDir, filename);
|
|
129
|
+
if (!existsSync(filePath)) {
|
|
130
|
+
writeFileSync(filePath, content);
|
|
131
|
+
created.push(filename);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
spinner.succeed("Project constitution created");
|
|
136
|
+
log.blank();
|
|
137
|
+
|
|
138
|
+
if (created.length > 0) {
|
|
139
|
+
created.forEach((file) => {
|
|
140
|
+
console.log(` ${chalk.green("+")} .ai/${file}`);
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
log.info("All constitution files already exist.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Save project config
|
|
147
|
+
setConfigValue("project.name", projectName);
|
|
148
|
+
setConfigValue("project.initializedAt", new Date().toISOString());
|
|
149
|
+
|
|
150
|
+
log.blank();
|
|
151
|
+
log.success(`Project '${chalk.bold(projectName)}' initialized.`);
|
|
152
|
+
log.dim(
|
|
153
|
+
`Run ${chalk.cyan("cheri workspace launch owner/repo")} to start coding in the cloud.`
|
|
154
|
+
);
|
|
155
|
+
log.blank();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
spinner.fail("Failed to initialize project");
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
82
162
|
export function registerInitCommand(program) {
|
|
83
163
|
program
|
|
84
164
|
.command("init")
|
|
@@ -86,81 +166,9 @@ export function registerInitCommand(program) {
|
|
|
86
166
|
.option("-n, --name <name>", "Project name")
|
|
87
167
|
.option("-y, --yes", "Skip prompts and use defaults")
|
|
88
168
|
.action(async (options) => {
|
|
89
|
-
log.blank();
|
|
90
|
-
log.brand("Initializing project...");
|
|
91
|
-
log.blank();
|
|
92
|
-
|
|
93
|
-
const aiDir = join(process.cwd(), AI_DIR);
|
|
94
|
-
|
|
95
|
-
if (existsSync(aiDir) && !options.yes) {
|
|
96
|
-
const { overwrite } = await inquirer.prompt([
|
|
97
|
-
{
|
|
98
|
-
type: "confirm",
|
|
99
|
-
name: "overwrite",
|
|
100
|
-
message: "A .ai/ directory already exists. Add missing files?",
|
|
101
|
-
default: true,
|
|
102
|
-
},
|
|
103
|
-
]);
|
|
104
|
-
if (!overwrite) {
|
|
105
|
-
log.info("Initialization cancelled.");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
let projectName = options.name;
|
|
111
|
-
if (!projectName && !options.yes) {
|
|
112
|
-
const answers = await inquirer.prompt([
|
|
113
|
-
{
|
|
114
|
-
type: "input",
|
|
115
|
-
name: "name",
|
|
116
|
-
message: "Project name:",
|
|
117
|
-
default: process.cwd().split("/").pop(),
|
|
118
|
-
},
|
|
119
|
-
]);
|
|
120
|
-
projectName = answers.name;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
projectName = projectName || process.cwd().split("/").pop();
|
|
124
|
-
|
|
125
|
-
const spinner = ora("Creating project constitution...").start();
|
|
126
|
-
|
|
127
169
|
try {
|
|
128
|
-
|
|
129
|
-
mkdirSync(aiDir, { recursive: true });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const created = [];
|
|
133
|
-
for (const [filename, content] of Object.entries(TEMPLATES)) {
|
|
134
|
-
const filePath = join(aiDir, filename);
|
|
135
|
-
if (!existsSync(filePath)) {
|
|
136
|
-
writeFileSync(filePath, content);
|
|
137
|
-
created.push(filename);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
spinner.succeed("Project constitution created");
|
|
142
|
-
log.blank();
|
|
143
|
-
|
|
144
|
-
if (created.length > 0) {
|
|
145
|
-
created.forEach((file) => {
|
|
146
|
-
console.log(` ${chalk.green("+")} .ai/${file}`);
|
|
147
|
-
});
|
|
148
|
-
} else {
|
|
149
|
-
log.info("All constitution files already exist.");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Save project config
|
|
153
|
-
setConfigValue("project.name", projectName);
|
|
154
|
-
setConfigValue("project.initializedAt", new Date().toISOString());
|
|
155
|
-
|
|
156
|
-
log.blank();
|
|
157
|
-
log.success(`Project '${chalk.bold(projectName)}' initialized.`);
|
|
158
|
-
log.dim(
|
|
159
|
-
`Run ${chalk.cyan("cheri workspace launch owner/repo")} to start coding in the cloud.`
|
|
160
|
-
);
|
|
161
|
-
log.blank();
|
|
170
|
+
await initProject(options);
|
|
162
171
|
} catch (err) {
|
|
163
|
-
spinner.fail("Failed to initialize project");
|
|
164
172
|
log.error(err.message);
|
|
165
173
|
process.exit(1);
|
|
166
174
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -4,53 +4,60 @@ import { setConfigValue, getConfigValue } from "../lib/config-store.js";
|
|
|
4
4
|
import { apiClient } from "../lib/api-client.js";
|
|
5
5
|
import { log } from "../lib/logger.js";
|
|
6
6
|
|
|
7
|
+
export async function loginFlow() {
|
|
8
|
+
const apiUrl = getConfigValue("apiUrl") || "https://cheri.heysalad.app";
|
|
9
|
+
|
|
10
|
+
log.blank();
|
|
11
|
+
log.brand("Login to Cheri");
|
|
12
|
+
log.blank();
|
|
13
|
+
|
|
14
|
+
log.info("Step 1: Open this URL in your browser to authenticate:");
|
|
15
|
+
log.blank();
|
|
16
|
+
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github`)}`);
|
|
17
|
+
log.blank();
|
|
18
|
+
log.info("Step 2: After login, visit the token page:");
|
|
19
|
+
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/token?user=YOUR_USERNAME`)}`);
|
|
20
|
+
log.blank();
|
|
21
|
+
log.info("Step 3: Copy your API token and paste it below.");
|
|
22
|
+
log.blank();
|
|
23
|
+
|
|
24
|
+
const { token } = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: "password",
|
|
27
|
+
name: "token",
|
|
28
|
+
message: "Paste your API token:",
|
|
29
|
+
mask: "*",
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (!token || !token.trim()) {
|
|
34
|
+
throw new Error("No token provided.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setConfigValue("token", token.trim());
|
|
38
|
+
|
|
39
|
+
// Verify token works
|
|
40
|
+
try {
|
|
41
|
+
const me = await apiClient.getMe();
|
|
42
|
+
log.blank();
|
|
43
|
+
log.success(`Logged in as ${chalk.cyan(me.ghLogin || me.userId)}`);
|
|
44
|
+
log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
45
|
+
log.blank();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setConfigValue("token", "");
|
|
48
|
+
throw new Error(`Token verification failed: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
7
52
|
export function registerLoginCommand(program) {
|
|
8
53
|
program
|
|
9
54
|
.command("login")
|
|
10
55
|
.description("Authenticate with Cheri cloud IDE")
|
|
11
56
|
.action(async () => {
|
|
12
|
-
const apiUrl = getConfigValue("apiUrl") || "https://cheri.heysalad.app";
|
|
13
|
-
|
|
14
|
-
log.blank();
|
|
15
|
-
log.brand("Login to Cheri");
|
|
16
|
-
log.blank();
|
|
17
|
-
|
|
18
|
-
log.info("Step 1: Open this URL in your browser to authenticate:");
|
|
19
|
-
log.blank();
|
|
20
|
-
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/github`)}`);
|
|
21
|
-
log.blank();
|
|
22
|
-
log.info("Step 2: After login, visit the token page:");
|
|
23
|
-
console.log(` ${chalk.cyan.underline(`${apiUrl}/auth/token?user=YOUR_USERNAME`)}`);
|
|
24
|
-
log.blank();
|
|
25
|
-
log.info("Step 3: Copy your API token and paste it below.");
|
|
26
|
-
log.blank();
|
|
27
|
-
|
|
28
|
-
const { token } = await inquirer.prompt([
|
|
29
|
-
{
|
|
30
|
-
type: "password",
|
|
31
|
-
name: "token",
|
|
32
|
-
message: "Paste your API token:",
|
|
33
|
-
mask: "*",
|
|
34
|
-
},
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
if (!token || !token.trim()) {
|
|
38
|
-
log.error("No token provided.");
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
setConfigValue("token", token.trim());
|
|
43
|
-
|
|
44
|
-
// Verify token works
|
|
45
57
|
try {
|
|
46
|
-
|
|
47
|
-
log.blank();
|
|
48
|
-
log.success(`Logged in as ${chalk.cyan(me.ghLogin || me.userId)}`);
|
|
49
|
-
log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
50
|
-
log.blank();
|
|
58
|
+
await loginFlow();
|
|
51
59
|
} catch (err) {
|
|
52
|
-
log.error(
|
|
53
|
-
setConfigValue("token", "");
|
|
60
|
+
log.error(err.message);
|
|
54
61
|
process.exit(1);
|
|
55
62
|
}
|
|
56
63
|
});
|
package/src/commands/memory.js
CHANGED
|
@@ -4,6 +4,86 @@ import { writeFileSync } from "fs";
|
|
|
4
4
|
import { apiClient } from "../lib/api-client.js";
|
|
5
5
|
import { log } from "../lib/logger.js";
|
|
6
6
|
|
|
7
|
+
export async function showMemory(options = {}) {
|
|
8
|
+
const spinner = ora("Fetching memories...").start();
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const { memories } = await apiClient.getMemory();
|
|
12
|
+
spinner.stop();
|
|
13
|
+
|
|
14
|
+
log.blank();
|
|
15
|
+
log.brand("AI Memory");
|
|
16
|
+
log.blank();
|
|
17
|
+
log.keyValue("Entries", `${memories.length}`);
|
|
18
|
+
|
|
19
|
+
if (memories.length === 0) {
|
|
20
|
+
log.blank();
|
|
21
|
+
log.dim(" No memories yet. Use 'memory add <content>' to store context.");
|
|
22
|
+
log.blank();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const limit = parseInt(options.limit || "10");
|
|
27
|
+
const recent = memories.slice(-limit);
|
|
28
|
+
|
|
29
|
+
log.keyValue(
|
|
30
|
+
"Last updated",
|
|
31
|
+
memories.length > 0
|
|
32
|
+
? timeSince(new Date(memories[memories.length - 1].timestamp))
|
|
33
|
+
: "never"
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
log.blank();
|
|
37
|
+
log.dim(" Recent memories:");
|
|
38
|
+
|
|
39
|
+
recent.forEach((entry) => {
|
|
40
|
+
const category = chalk.dim(`(${entry.category})`);
|
|
41
|
+
const content =
|
|
42
|
+
entry.content.length > 60
|
|
43
|
+
? entry.content.slice(0, 60) + "..."
|
|
44
|
+
: entry.content;
|
|
45
|
+
console.log(` ${chalk.dim("-")} "${content}" ${category}`);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
log.blank();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spinner.fail("Failed to fetch memories");
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function addMemory(content, category = "general") {
|
|
56
|
+
const { entry, count } = await apiClient.addMemory(content, category);
|
|
57
|
+
log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function clearMemory() {
|
|
61
|
+
await apiClient.clearMemory();
|
|
62
|
+
log.success("All memories cleared.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function exportMemory(options = {}) {
|
|
66
|
+
const spinner = ora("Exporting memories...").start();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { memories } = await apiClient.getMemory();
|
|
70
|
+
|
|
71
|
+
const exportData = {
|
|
72
|
+
version: "0.1.0",
|
|
73
|
+
exportedAt: new Date().toISOString(),
|
|
74
|
+
entryCount: memories.length,
|
|
75
|
+
entries: memories,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const outputFile = options.output || `cheri-memory-export.json`;
|
|
79
|
+
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
|
|
80
|
+
spinner.succeed(`Exported ${memories.length} entries to ${chalk.cyan(outputFile)}`);
|
|
81
|
+
} catch (err) {
|
|
82
|
+
spinner.fail("Failed to export memories");
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
7
87
|
export function registerMemoryCommand(program) {
|
|
8
88
|
const memory = program
|
|
9
89
|
.command("memory")
|
|
@@ -14,49 +94,9 @@ export function registerMemoryCommand(program) {
|
|
|
14
94
|
.description("Show current memory entries")
|
|
15
95
|
.option("-l, --limit <n>", "Number of entries to show", "10")
|
|
16
96
|
.action(async (options) => {
|
|
17
|
-
const spinner = ora("Fetching memories...").start();
|
|
18
|
-
|
|
19
97
|
try {
|
|
20
|
-
|
|
21
|
-
spinner.stop();
|
|
22
|
-
|
|
23
|
-
log.blank();
|
|
24
|
-
log.brand("AI Memory");
|
|
25
|
-
log.blank();
|
|
26
|
-
log.keyValue("Entries", `${memories.length}`);
|
|
27
|
-
|
|
28
|
-
if (memories.length === 0) {
|
|
29
|
-
log.blank();
|
|
30
|
-
log.dim(" No memories yet. Use 'cheri memory add' to store context.");
|
|
31
|
-
log.blank();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const limit = parseInt(options.limit);
|
|
36
|
-
const recent = memories.slice(-limit);
|
|
37
|
-
|
|
38
|
-
log.keyValue(
|
|
39
|
-
"Last updated",
|
|
40
|
-
memories.length > 0
|
|
41
|
-
? timeSince(new Date(memories[memories.length - 1].timestamp))
|
|
42
|
-
: "never"
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
log.blank();
|
|
46
|
-
log.dim(" Recent memories:");
|
|
47
|
-
|
|
48
|
-
recent.forEach((entry) => {
|
|
49
|
-
const category = chalk.dim(`(${entry.category})`);
|
|
50
|
-
const content =
|
|
51
|
-
entry.content.length > 60
|
|
52
|
-
? entry.content.slice(0, 60) + "..."
|
|
53
|
-
: entry.content;
|
|
54
|
-
console.log(` ${chalk.dim("-")} "${content}" ${category}`);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
log.blank();
|
|
98
|
+
await showMemory(options);
|
|
58
99
|
} catch (err) {
|
|
59
|
-
spinner.fail("Failed to fetch memories");
|
|
60
100
|
log.error(err.message);
|
|
61
101
|
process.exit(1);
|
|
62
102
|
}
|
|
@@ -69,8 +109,7 @@ export function registerMemoryCommand(program) {
|
|
|
69
109
|
.option("-c, --category <category>", "Category", "general")
|
|
70
110
|
.action(async (content, options) => {
|
|
71
111
|
try {
|
|
72
|
-
|
|
73
|
-
log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
|
|
112
|
+
await addMemory(content, options.category);
|
|
74
113
|
} catch (err) {
|
|
75
114
|
log.error(err.message);
|
|
76
115
|
process.exit(1);
|
|
@@ -82,8 +121,7 @@ export function registerMemoryCommand(program) {
|
|
|
82
121
|
.description("Clear all memory")
|
|
83
122
|
.action(async () => {
|
|
84
123
|
try {
|
|
85
|
-
await
|
|
86
|
-
log.success("All memories cleared.");
|
|
124
|
+
await clearMemory();
|
|
87
125
|
} catch (err) {
|
|
88
126
|
log.error(err.message);
|
|
89
127
|
process.exit(1);
|
|
@@ -95,23 +133,9 @@ export function registerMemoryCommand(program) {
|
|
|
95
133
|
.description("Export memory to JSON file")
|
|
96
134
|
.option("-o, --output <file>", "Output file path")
|
|
97
135
|
.action(async (options) => {
|
|
98
|
-
const spinner = ora("Exporting memories...").start();
|
|
99
|
-
|
|
100
136
|
try {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const exportData = {
|
|
104
|
-
version: "0.1.0",
|
|
105
|
-
exportedAt: new Date().toISOString(),
|
|
106
|
-
entryCount: memories.length,
|
|
107
|
-
entries: memories,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
const outputFile = options.output || `cheri-memory-export.json`;
|
|
111
|
-
writeFileSync(outputFile, JSON.stringify(exportData, null, 2));
|
|
112
|
-
spinner.succeed(`Exported ${memories.length} entries to ${chalk.cyan(outputFile)}`);
|
|
137
|
+
await exportMemory(options);
|
|
113
138
|
} catch (err) {
|
|
114
|
-
spinner.fail("Failed to export memories");
|
|
115
139
|
log.error(err.message);
|
|
116
140
|
process.exit(1);
|
|
117
141
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -4,53 +4,63 @@ import { getConfigValue } from "../lib/config-store.js";
|
|
|
4
4
|
import { apiClient } from "../lib/api-client.js";
|
|
5
5
|
import { log } from "../lib/logger.js";
|
|
6
6
|
|
|
7
|
+
export async function showStatus() {
|
|
8
|
+
log.blank();
|
|
9
|
+
log.brand("Status");
|
|
10
|
+
|
|
11
|
+
// Account info
|
|
12
|
+
log.header("Account");
|
|
13
|
+
const spinner = ora("Fetching account info...").start();
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const me = await apiClient.getMe();
|
|
17
|
+
spinner.stop();
|
|
18
|
+
|
|
19
|
+
log.keyValue("User", chalk.cyan(me.ghLogin || me.userId));
|
|
20
|
+
log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
21
|
+
} catch (err) {
|
|
22
|
+
spinner.stop();
|
|
23
|
+
log.keyValue("User", chalk.yellow("Not logged in"));
|
|
24
|
+
log.dim(` Run ${chalk.cyan("cheri login")} to authenticate.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Workspaces
|
|
28
|
+
log.header("Workspaces");
|
|
29
|
+
try {
|
|
30
|
+
const { workspaces } = await apiClient.listWorkspaces();
|
|
31
|
+
if (workspaces.length === 0) {
|
|
32
|
+
log.keyValue("Count", "0");
|
|
33
|
+
log.dim(` Run ${chalk.cyan("cheri workspace launch owner/repo")} to create one.`);
|
|
34
|
+
} else {
|
|
35
|
+
log.keyValue("Count", `${workspaces.length}`);
|
|
36
|
+
workspaces.forEach((ws) => {
|
|
37
|
+
const statusIcon = ws.status === "running" ? chalk.green("●") : chalk.dim("○");
|
|
38
|
+
const statusColor = ws.status === "running" ? chalk.green : chalk.dim;
|
|
39
|
+
console.log(` ${statusIcon} ${ws.id} (${ws.repo || ws.id}) — ${statusColor(ws.status)}`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
log.dim(" Could not fetch workspaces.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Config info
|
|
47
|
+
log.header("Configuration");
|
|
48
|
+
log.keyValue("API URL", getConfigValue("apiUrl") || "https://cheri.heysalad.app");
|
|
49
|
+
log.keyValue("Config dir", chalk.dim("~/.cheri/"));
|
|
50
|
+
|
|
51
|
+
log.blank();
|
|
52
|
+
}
|
|
53
|
+
|
|
7
54
|
export function registerStatusCommand(program) {
|
|
8
55
|
program
|
|
9
56
|
.command("status")
|
|
10
57
|
.description("Show account and workspace status")
|
|
11
58
|
.action(async () => {
|
|
12
|
-
log.blank();
|
|
13
|
-
log.brand("Status");
|
|
14
|
-
|
|
15
|
-
// Account info
|
|
16
|
-
log.header("Account");
|
|
17
|
-
const spinner = ora("Fetching account info...").start();
|
|
18
|
-
|
|
19
59
|
try {
|
|
20
|
-
|
|
21
|
-
spinner.stop();
|
|
22
|
-
|
|
23
|
-
log.keyValue("User", chalk.cyan(me.ghLogin || me.userId));
|
|
24
|
-
log.keyValue("Plan", me.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
60
|
+
await showStatus();
|
|
25
61
|
} catch (err) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
log.dim(` Run ${chalk.cyan("cheri login")} to authenticate.`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Workspaces
|
|
32
|
-
log.header("Workspaces");
|
|
33
|
-
try {
|
|
34
|
-
const { workspaces } = await apiClient.listWorkspaces();
|
|
35
|
-
if (workspaces.length === 0) {
|
|
36
|
-
log.keyValue("Count", "0");
|
|
37
|
-
log.dim(` Run ${chalk.cyan("cheri workspace launch owner/repo")} to create one.`);
|
|
38
|
-
} else {
|
|
39
|
-
log.keyValue("Count", `${workspaces.length}`);
|
|
40
|
-
workspaces.forEach((ws) => {
|
|
41
|
-
const statusColor = ws.status === "running" ? chalk.green : chalk.dim;
|
|
42
|
-
console.log(` ${statusColor("●")} ${ws.id} (${ws.repo || ws.id}) — ${statusColor(ws.status)}`);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
log.dim(" Could not fetch workspaces.");
|
|
62
|
+
log.error(err.message);
|
|
63
|
+
process.exit(1);
|
|
47
64
|
}
|
|
48
|
-
|
|
49
|
-
// Config info
|
|
50
|
-
log.header("Configuration");
|
|
51
|
-
log.keyValue("API URL", getConfigValue("apiUrl") || "https://cheri.heysalad.app");
|
|
52
|
-
log.keyValue("Config dir", chalk.dim("~/.cheri/"));
|
|
53
|
-
|
|
54
|
-
log.blank();
|
|
55
65
|
});
|
|
56
66
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import { apiClient } from "../lib/api-client.js";
|
|
4
|
+
import { log } from "../lib/logger.js";
|
|
5
|
+
|
|
6
|
+
export async function showUsage() {
|
|
7
|
+
log.blank();
|
|
8
|
+
log.brand("Usage");
|
|
9
|
+
|
|
10
|
+
const spinner = ora("Fetching usage data...").start();
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const data = await apiClient.getUsage();
|
|
14
|
+
spinner.stop();
|
|
15
|
+
|
|
16
|
+
// Rate limit
|
|
17
|
+
log.header("Rate Limit");
|
|
18
|
+
log.keyValue("Plan", data.plan === "pro" ? chalk.green("Pro") : "Free");
|
|
19
|
+
log.keyValue("Limit", `${data.rateLimit.limit} requests/hour`);
|
|
20
|
+
const remaining = data.rateLimit.remaining;
|
|
21
|
+
const limit = data.rateLimit.limit;
|
|
22
|
+
const remainColor = remaining > limit * 0.5 ? chalk.green : remaining > limit * 0.1 ? chalk.yellow : chalk.red;
|
|
23
|
+
log.keyValue("Remaining", remainColor(`${remaining}`));
|
|
24
|
+
log.keyValue("Resets at", data.rateLimit.resetsAt);
|
|
25
|
+
|
|
26
|
+
// Today's usage
|
|
27
|
+
log.header("Today");
|
|
28
|
+
log.keyValue("Requests", `${data.usage.today.requests}`);
|
|
29
|
+
const endpoints = data.usage.today.endpoints || {};
|
|
30
|
+
if (Object.keys(endpoints).length > 0) {
|
|
31
|
+
for (const [ep, count] of Object.entries(endpoints)) {
|
|
32
|
+
console.log(` ${chalk.dim(ep)} ${chalk.cyan(count)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Summary
|
|
37
|
+
log.header("Summary");
|
|
38
|
+
log.keyValue("Last 7 days", `${data.usage.last7d.requests} requests`);
|
|
39
|
+
log.keyValue("Last 30 days", `${data.usage.last30d.requests} requests`);
|
|
40
|
+
log.keyValue("All time", `${data.summary.totalRequests} requests`);
|
|
41
|
+
if (data.summary.memberSince) {
|
|
42
|
+
log.keyValue("Member since", new Date(data.summary.memberSince).toLocaleDateString());
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
spinner.stop();
|
|
46
|
+
log.error(err.message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.blank();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function registerUsageCommand(program) {
|
|
53
|
+
program
|
|
54
|
+
.command("usage")
|
|
55
|
+
.description("Show API usage and rate limit status")
|
|
56
|
+
.action(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await showUsage();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
log.error(err.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -3,6 +3,105 @@ import ora from "ora";
|
|
|
3
3
|
import { apiClient } from "../lib/api-client.js";
|
|
4
4
|
import { log } from "../lib/logger.js";
|
|
5
5
|
|
|
6
|
+
export async function launchWorkspace(repo) {
|
|
7
|
+
log.blank();
|
|
8
|
+
log.brand("Launching workspace");
|
|
9
|
+
log.blank();
|
|
10
|
+
|
|
11
|
+
const spinner = ora("Creating workspace...").start();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const { workspace: ws } = await apiClient.createWorkspace(repo);
|
|
15
|
+
spinner.succeed("Workspace is live!");
|
|
16
|
+
|
|
17
|
+
log.blank();
|
|
18
|
+
log.keyValue("ID", chalk.cyan(ws.id));
|
|
19
|
+
log.keyValue("Repo", chalk.cyan(ws.repo));
|
|
20
|
+
log.keyValue("Status", chalk.green(ws.status));
|
|
21
|
+
log.keyValue("URL", chalk.cyan.underline(ws.url));
|
|
22
|
+
log.blank();
|
|
23
|
+
log.dim("Open the URL above in your browser to start coding.");
|
|
24
|
+
log.blank();
|
|
25
|
+
} catch (err) {
|
|
26
|
+
spinner.fail("Failed to launch workspace");
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function stopWorkspace(id) {
|
|
32
|
+
const spinner = ora(`Stopping workspace: ${id}...`).start();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await apiClient.deleteWorkspace(id);
|
|
36
|
+
spinner.succeed(`Workspace '${chalk.cyan(id)}' stopped.`);
|
|
37
|
+
log.dim(" Data preserved. Launch again to resume.");
|
|
38
|
+
} catch (err) {
|
|
39
|
+
spinner.fail("Failed to stop workspace");
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function listWorkspaces() {
|
|
45
|
+
const spinner = ora("Fetching workspaces...").start();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const { workspaces } = await apiClient.listWorkspaces();
|
|
49
|
+
spinner.stop();
|
|
50
|
+
|
|
51
|
+
log.blank();
|
|
52
|
+
log.brand("Workspaces");
|
|
53
|
+
log.blank();
|
|
54
|
+
|
|
55
|
+
if (workspaces.length === 0) {
|
|
56
|
+
log.dim(" No workspaces found. Run 'cheri workspace launch owner/repo' to create one.");
|
|
57
|
+
log.blank();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Table header
|
|
62
|
+
console.log(
|
|
63
|
+
` ${chalk.dim("ID".padEnd(24))} ${chalk.dim("REPO".padEnd(24))} ${chalk.dim("STATUS".padEnd(10))} ${chalk.dim("URL")}`
|
|
64
|
+
);
|
|
65
|
+
console.log(chalk.dim(" " + "─".repeat(90)));
|
|
66
|
+
|
|
67
|
+
workspaces.forEach((ws) => {
|
|
68
|
+
const statusIcon =
|
|
69
|
+
ws.status === "running" ? chalk.green("●") : chalk.dim("○");
|
|
70
|
+
const statusColor =
|
|
71
|
+
ws.status === "running" ? chalk.green : chalk.dim;
|
|
72
|
+
console.log(
|
|
73
|
+
` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusIcon} ${statusColor(ws.status.padEnd(9))} ${chalk.cyan(ws.url || "")}`
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
log.blank();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
spinner.fail("Failed to fetch workspaces");
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getWorkspaceStatus(id) {
|
|
85
|
+
const spinner = ora("Fetching workspace status...").start();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const { workspace: ws } = await apiClient.getWorkspaceStatus(id);
|
|
89
|
+
spinner.stop();
|
|
90
|
+
|
|
91
|
+
log.blank();
|
|
92
|
+
log.brand(`Workspace: ${ws.id}`);
|
|
93
|
+
log.blank();
|
|
94
|
+
log.keyValue("Status", ws.status === "running" ? chalk.green("● " + ws.status) : chalk.dim("○ " + ws.status));
|
|
95
|
+
log.keyValue("Repo", chalk.cyan(ws.repo || ""));
|
|
96
|
+
log.keyValue("URL", chalk.cyan.underline(ws.url || ""));
|
|
97
|
+
log.keyValue("Created", new Date(ws.createdAt).toLocaleString());
|
|
98
|
+
log.blank();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
spinner.fail("Failed to fetch workspace status");
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
6
105
|
export function registerWorkspaceCommand(program) {
|
|
7
106
|
const workspace = program
|
|
8
107
|
.command("workspace")
|
|
@@ -13,26 +112,9 @@ export function registerWorkspaceCommand(program) {
|
|
|
13
112
|
.description("Launch a new cloud workspace")
|
|
14
113
|
.argument("<repo>", "GitHub repository (owner/repo)")
|
|
15
114
|
.action(async (repo) => {
|
|
16
|
-
log.blank();
|
|
17
|
-
log.brand("Launching workspace");
|
|
18
|
-
log.blank();
|
|
19
|
-
|
|
20
|
-
const spinner = ora("Creating workspace...").start();
|
|
21
|
-
|
|
22
115
|
try {
|
|
23
|
-
|
|
24
|
-
spinner.succeed("Workspace is live!");
|
|
25
|
-
|
|
26
|
-
log.blank();
|
|
27
|
-
log.keyValue("ID", chalk.cyan(ws.id));
|
|
28
|
-
log.keyValue("Repo", chalk.cyan(ws.repo));
|
|
29
|
-
log.keyValue("Status", chalk.green(ws.status));
|
|
30
|
-
log.keyValue("URL", chalk.cyan.underline(ws.url));
|
|
31
|
-
log.blank();
|
|
32
|
-
log.dim("Open the URL above in your browser to start coding.");
|
|
33
|
-
log.blank();
|
|
116
|
+
await launchWorkspace(repo);
|
|
34
117
|
} catch (err) {
|
|
35
|
-
spinner.fail("Failed to launch workspace");
|
|
36
118
|
log.error(err.message);
|
|
37
119
|
process.exit(1);
|
|
38
120
|
}
|
|
@@ -43,14 +125,9 @@ export function registerWorkspaceCommand(program) {
|
|
|
43
125
|
.description("Stop a running workspace")
|
|
44
126
|
.argument("<id>", "Workspace ID")
|
|
45
127
|
.action(async (id) => {
|
|
46
|
-
const spinner = ora(`Stopping workspace: ${id}...`).start();
|
|
47
|
-
|
|
48
128
|
try {
|
|
49
|
-
await
|
|
50
|
-
spinner.succeed(`Workspace '${chalk.cyan(id)}' stopped.`);
|
|
51
|
-
log.dim(" Data preserved. Launch again to resume.");
|
|
129
|
+
await stopWorkspace(id);
|
|
52
130
|
} catch (err) {
|
|
53
|
-
spinner.fail("Failed to stop workspace");
|
|
54
131
|
log.error(err.message);
|
|
55
132
|
process.exit(1);
|
|
56
133
|
}
|
|
@@ -60,39 +137,9 @@ export function registerWorkspaceCommand(program) {
|
|
|
60
137
|
.command("list")
|
|
61
138
|
.description("List all workspaces")
|
|
62
139
|
.action(async () => {
|
|
63
|
-
const spinner = ora("Fetching workspaces...").start();
|
|
64
|
-
|
|
65
140
|
try {
|
|
66
|
-
|
|
67
|
-
spinner.stop();
|
|
68
|
-
|
|
69
|
-
log.blank();
|
|
70
|
-
log.brand("Workspaces");
|
|
71
|
-
log.blank();
|
|
72
|
-
|
|
73
|
-
if (workspaces.length === 0) {
|
|
74
|
-
log.dim(" No workspaces found. Run 'cheri workspace launch owner/repo' to create one.");
|
|
75
|
-
log.blank();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Table header
|
|
80
|
-
console.log(
|
|
81
|
-
` ${chalk.dim("ID".padEnd(24))} ${chalk.dim("REPO".padEnd(24))} ${chalk.dim("STATUS".padEnd(10))} ${chalk.dim("URL")}`
|
|
82
|
-
);
|
|
83
|
-
console.log(chalk.dim(" " + "-".repeat(90)));
|
|
84
|
-
|
|
85
|
-
workspaces.forEach((ws) => {
|
|
86
|
-
const statusColor =
|
|
87
|
-
ws.status === "running" ? chalk.green : chalk.dim;
|
|
88
|
-
console.log(
|
|
89
|
-
` ${ws.id.padEnd(24)} ${(ws.repo || "").padEnd(24)} ${statusColor(ws.status.padEnd(10))} ${chalk.cyan(ws.url || "")}`
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
log.blank();
|
|
141
|
+
await listWorkspaces();
|
|
94
142
|
} catch (err) {
|
|
95
|
-
spinner.fail("Failed to fetch workspaces");
|
|
96
143
|
log.error(err.message);
|
|
97
144
|
process.exit(1);
|
|
98
145
|
}
|
|
@@ -103,22 +150,9 @@ export function registerWorkspaceCommand(program) {
|
|
|
103
150
|
.description("Get workspace status")
|
|
104
151
|
.argument("<id>", "Workspace ID")
|
|
105
152
|
.action(async (id) => {
|
|
106
|
-
const spinner = ora("Fetching workspace status...").start();
|
|
107
|
-
|
|
108
153
|
try {
|
|
109
|
-
|
|
110
|
-
spinner.stop();
|
|
111
|
-
|
|
112
|
-
log.blank();
|
|
113
|
-
log.brand(`Workspace: ${ws.id}`);
|
|
114
|
-
log.blank();
|
|
115
|
-
log.keyValue("Status", ws.status === "running" ? chalk.green(ws.status) : chalk.dim(ws.status));
|
|
116
|
-
log.keyValue("Repo", chalk.cyan(ws.repo || ""));
|
|
117
|
-
log.keyValue("URL", chalk.cyan.underline(ws.url || ""));
|
|
118
|
-
log.keyValue("Created", new Date(ws.createdAt).toLocaleString());
|
|
119
|
-
log.blank();
|
|
154
|
+
await getWorkspaceStatus(id);
|
|
120
155
|
} catch (err) {
|
|
121
|
-
spinner.fail("Failed to fetch workspace status");
|
|
122
156
|
log.error(err.message);
|
|
123
157
|
process.exit(1);
|
|
124
158
|
}
|
package/src/lib/api-client.js
CHANGED
package/src/lib/logger.js
CHANGED
|
@@ -36,4 +36,14 @@ export const log = {
|
|
|
36
36
|
console.log(chalk.dim(prefix) + " " + item);
|
|
37
37
|
});
|
|
38
38
|
},
|
|
39
|
+
banner(version = "0.1.0") {
|
|
40
|
+
console.log();
|
|
41
|
+
console.log(` ${chalk.red("🍒")} ${chalk.red.bold("Cheri")}`);
|
|
42
|
+
console.log(` ${chalk.dim("AI-powered cloud IDE by HeySalad")}`);
|
|
43
|
+
console.log(` ${chalk.dim("v" + version)}`);
|
|
44
|
+
console.log();
|
|
45
|
+
},
|
|
46
|
+
tip(msg) {
|
|
47
|
+
console.log(` ${chalk.blue("tip")} ${chalk.dim(msg)}`);
|
|
48
|
+
},
|
|
39
49
|
};
|
package/src/repl.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import readline from "readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { log } from "./lib/logger.js";
|
|
4
|
+
|
|
5
|
+
// Import standalone action handlers
|
|
6
|
+
import { launchWorkspace, stopWorkspace, listWorkspaces, getWorkspaceStatus } from "./commands/workspace.js";
|
|
7
|
+
import { showMemory, addMemory, clearMemory, exportMemory } from "./commands/memory.js";
|
|
8
|
+
import { showStatus } from "./commands/status.js";
|
|
9
|
+
import { listConfig, getConfigKey, setConfigKey } from "./commands/config.js";
|
|
10
|
+
import { loginFlow } from "./commands/login.js";
|
|
11
|
+
import { initProject } from "./commands/init.js";
|
|
12
|
+
import { showUsage } from "./commands/usage.js";
|
|
13
|
+
|
|
14
|
+
const COMMANDS = {
|
|
15
|
+
"workspace launch": { args: "<repo>", desc: "Launch a new cloud workspace" },
|
|
16
|
+
"workspace list": { args: "", desc: "List all workspaces" },
|
|
17
|
+
"workspace stop": { args: "<id>", desc: "Stop a running workspace" },
|
|
18
|
+
"workspace status": { args: "<id>", desc: "Get workspace status" },
|
|
19
|
+
"memory show": { args: "", desc: "Show current memory entries" },
|
|
20
|
+
"memory add": { args: "<text>", desc: "Add a memory entry" },
|
|
21
|
+
"memory clear": { args: "", desc: "Clear all memory" },
|
|
22
|
+
"memory export": { args: "", desc: "Export memory to JSON" },
|
|
23
|
+
"status": { args: "", desc: "Show account & workspace status" },
|
|
24
|
+
"usage": { args: "", desc: "Show API usage & rate limits" },
|
|
25
|
+
"config list": { args: "", desc: "Show all configuration" },
|
|
26
|
+
"config get": { args: "<key>", desc: "Get a config value" },
|
|
27
|
+
"config set": { args: "<k> <v>",desc: "Set a config value" },
|
|
28
|
+
"login": { args: "", desc: "Authenticate with Cheri" },
|
|
29
|
+
"init": { args: "", desc: "Initialize a project" },
|
|
30
|
+
"help": { args: "", desc: "Show this help" },
|
|
31
|
+
"clear": { args: "", desc: "Clear the terminal" },
|
|
32
|
+
"exit": { args: "", desc: "Exit Cheri" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function showHelp() {
|
|
36
|
+
log.blank();
|
|
37
|
+
log.brand("Available Commands");
|
|
38
|
+
log.blank();
|
|
39
|
+
|
|
40
|
+
const maxCmd = 20;
|
|
41
|
+
const maxArgs = 10;
|
|
42
|
+
|
|
43
|
+
for (const [cmd, { args, desc }] of Object.entries(COMMANDS)) {
|
|
44
|
+
const cmdStr = chalk.cyan(cmd.padEnd(maxCmd));
|
|
45
|
+
const argsStr = chalk.dim(args.padEnd(maxArgs));
|
|
46
|
+
console.log(` ${cmdStr} ${argsStr} ${desc}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.blank();
|
|
50
|
+
log.tip("Type a command above, or 'exit' to quit. Use arrow keys for history.");
|
|
51
|
+
log.blank();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function dispatch(input) {
|
|
55
|
+
const parts = input.split(/\s+/);
|
|
56
|
+
const cmd = parts[0];
|
|
57
|
+
const sub = parts[1];
|
|
58
|
+
const rest = parts.slice(2).join(" ");
|
|
59
|
+
|
|
60
|
+
switch (cmd) {
|
|
61
|
+
case "help":
|
|
62
|
+
showHelp();
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
case "clear":
|
|
66
|
+
console.clear();
|
|
67
|
+
return;
|
|
68
|
+
|
|
69
|
+
case "exit":
|
|
70
|
+
case "quit":
|
|
71
|
+
console.log(chalk.dim("\nGoodbye! 🍒\n"));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
|
|
74
|
+
case "status":
|
|
75
|
+
await showStatus();
|
|
76
|
+
return;
|
|
77
|
+
|
|
78
|
+
case "usage":
|
|
79
|
+
await showUsage();
|
|
80
|
+
return;
|
|
81
|
+
|
|
82
|
+
case "login":
|
|
83
|
+
await loginFlow();
|
|
84
|
+
return;
|
|
85
|
+
|
|
86
|
+
case "init":
|
|
87
|
+
await initProject();
|
|
88
|
+
return;
|
|
89
|
+
|
|
90
|
+
case "workspace":
|
|
91
|
+
switch (sub) {
|
|
92
|
+
case "launch":
|
|
93
|
+
if (!rest) {
|
|
94
|
+
log.error("Usage: workspace launch <owner/repo>");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
await launchWorkspace(rest);
|
|
98
|
+
return;
|
|
99
|
+
case "list":
|
|
100
|
+
await listWorkspaces();
|
|
101
|
+
return;
|
|
102
|
+
case "stop":
|
|
103
|
+
if (!rest) {
|
|
104
|
+
log.error("Usage: workspace stop <id>");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await stopWorkspace(rest);
|
|
108
|
+
return;
|
|
109
|
+
case "status":
|
|
110
|
+
if (!rest) {
|
|
111
|
+
log.error("Usage: workspace status <id>");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await getWorkspaceStatus(rest);
|
|
115
|
+
return;
|
|
116
|
+
default:
|
|
117
|
+
log.error("Usage: workspace [launch|list|stop|status]");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "memory":
|
|
122
|
+
switch (sub) {
|
|
123
|
+
case "show":
|
|
124
|
+
await showMemory();
|
|
125
|
+
return;
|
|
126
|
+
case "add":
|
|
127
|
+
if (!rest) {
|
|
128
|
+
log.error("Usage: memory add <content>");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
await addMemory(rest);
|
|
132
|
+
return;
|
|
133
|
+
case "clear":
|
|
134
|
+
await clearMemory();
|
|
135
|
+
return;
|
|
136
|
+
case "export":
|
|
137
|
+
await exportMemory();
|
|
138
|
+
return;
|
|
139
|
+
default:
|
|
140
|
+
log.error("Usage: memory [show|add|clear|export]");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "config":
|
|
145
|
+
switch (sub) {
|
|
146
|
+
case "list":
|
|
147
|
+
listConfig();
|
|
148
|
+
return;
|
|
149
|
+
case "get":
|
|
150
|
+
if (!rest) {
|
|
151
|
+
log.error("Usage: config get <key>");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
getConfigKey(rest);
|
|
155
|
+
return;
|
|
156
|
+
case "set": {
|
|
157
|
+
const setParts = rest.split(/\s+/);
|
|
158
|
+
if (setParts.length < 2) {
|
|
159
|
+
log.error("Usage: config set <key> <value>");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setConfigKey(setParts[0], setParts.slice(1).join(" "));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
log.error("Usage: config [list|get|set]");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
log.warn(`Unknown command: '${cmd}'. Type ${chalk.cyan("help")} for available commands.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function startCommandRepl() {
|
|
176
|
+
log.banner();
|
|
177
|
+
console.log(chalk.dim(" " + "─".repeat(40)));
|
|
178
|
+
log.tip(`Type ${chalk.cyan("help")} for commands, ${chalk.cyan("exit")} to quit.`);
|
|
179
|
+
log.blank();
|
|
180
|
+
|
|
181
|
+
const rl = readline.createInterface({
|
|
182
|
+
input: process.stdin,
|
|
183
|
+
output: process.stdout,
|
|
184
|
+
prompt: chalk.red("🍒 cheri") + chalk.dim(" > "),
|
|
185
|
+
terminal: true,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
rl.prompt();
|
|
189
|
+
|
|
190
|
+
rl.on("line", async (line) => {
|
|
191
|
+
const input = line.trim();
|
|
192
|
+
|
|
193
|
+
if (!input) {
|
|
194
|
+
rl.prompt();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await dispatch(input);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
log.error(err.message);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
rl.prompt();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
rl.on("close", () => {
|
|
208
|
+
console.log(chalk.dim("\nGoodbye! 🍒\n"));
|
|
209
|
+
process.exit(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Handle Ctrl+C gracefully
|
|
213
|
+
rl.on("SIGINT", () => {
|
|
214
|
+
console.log(chalk.dim("\nGoodbye! 🍒\n"));
|
|
215
|
+
process.exit(0);
|
|
216
|
+
});
|
|
217
|
+
}
|