@napisani/scute 0.0.2 → 0.0.4
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 +79 -22
- package/bin/scute +2 -0
- package/package.json +84 -85
- package/src/commands/build.tsx +69 -0
- package/src/commands/config-debug.ts +33 -0
- package/src/commands/explain.ts +35 -0
- package/src/commands/generate.ts +47 -0
- package/src/commands/init.ts +29 -0
- package/src/commands/suggest.ts +26 -0
- package/src/components/Footer.tsx +41 -0
- package/src/components/Spinner.tsx +43 -0
- package/src/components/TokenAnnotatedView.tsx +55 -0
- package/src/components/TokenDisplay.tsx +34 -0
- package/src/components/TokenEditor.tsx +45 -0
- package/src/components/TokenListView.tsx +99 -0
- package/src/config/index.ts +255 -0
- package/src/config/schema.ts +202 -0
- package/src/core/cache.ts +94 -0
- package/src/core/constants.ts +13 -0
- package/src/core/environment.ts +39 -0
- package/src/core/index.ts +2 -0
- package/src/core/llm-context.ts +68 -0
- package/src/core/llm.ts +403 -0
- package/src/core/logger.ts +74 -0
- package/src/core/manpage/index.ts +62 -0
- package/src/core/manpage/parser.ts +96 -0
- package/src/core/manpage/retrieval-bm25.ts +258 -0
- package/src/core/output.ts +104 -0
- package/src/core/prompts.ts +81 -0
- package/src/core/shells/bash.ts +119 -0
- package/src/core/shells/common.ts +288 -0
- package/src/core/shells/index.ts +109 -0
- package/src/core/shells/keybindings.ts +95 -0
- package/src/core/shells/sh.ts +115 -0
- package/src/core/shells/zsh.ts +124 -0
- package/src/core/token-descriptions.ts +258 -0
- package/src/hooks/useColoredTokens.ts +35 -0
- package/src/hooks/useInsertMode.ts +142 -0
- package/src/hooks/useNormalMode.ts +221 -0
- package/src/hooks/useParsedCommand.ts +23 -0
- package/src/hooks/useTokenDescriptions.ts +95 -0
- package/src/hooks/useTokenWidth.ts +31 -0
- package/src/hooks/useVimMode.ts +205 -0
- package/src/index.ts +129 -0
- package/src/pages/build.tsx +364 -0
- package/src/utils/annotatedRenderer.tsx +406 -0
- package/src/utils/keyboard.ts +32 -0
- package/src/utils/prompt.ts +24 -0
- package/src/utils/tokenPositions.ts +28 -0
- package/dist/scute +0 -0
- package/scripts/install.sh +0 -73
- package/scripts/postinstall.ts +0 -112
- package/scripts/update-brew.ts +0 -145
package/README.md
CHANGED
|
@@ -10,13 +10,21 @@
|
|
|
10
10
|
- Integrating through lightweight keybindings and shell hooks
|
|
11
11
|
|
|
12
12
|
The name comes from the scute, the protective shell plate on a turtle, and the tool itself is meant to assist with shell commands.
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
Scute is built with Bun and can be installed via npm (requires Bun), Homebrew, or downloaded as a prebuilt binary.
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
17
|
-
Supported platforms: macOS and Linux (x86_64
|
|
18
|
+
Supported platforms: macOS and Linux (x86_64 and arm64).
|
|
19
|
+
|
|
20
|
+
### A. Homebrew (Recommended - Prebuilt Binary)
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
```sh
|
|
23
|
+
brew tap napisani/scute https://github.com/napisani/scute
|
|
24
|
+
brew install scute
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### B. Install via curl (install.sh)
|
|
20
28
|
|
|
21
29
|
Convenience installer (requires `curl` and `tar`):
|
|
22
30
|
|
|
@@ -26,14 +34,23 @@ curl -fsSL https://raw.githubusercontent.com/napisani/scute/main/scripts/install
|
|
|
26
34
|
|
|
27
35
|
By default it installs into `/usr/local/bin` and pulls the latest release. Pass `vX.Y.Z` and a custom directory to override.
|
|
28
36
|
|
|
29
|
-
###
|
|
37
|
+
### C. bunx / bun (Requires Bun)
|
|
38
|
+
|
|
39
|
+
Install globally:
|
|
30
40
|
|
|
31
41
|
```sh
|
|
32
|
-
|
|
33
|
-
brew install scute
|
|
42
|
+
bun install -g @napisani/scute
|
|
34
43
|
```
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
Or run once with:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
bunx @napisani/scute --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **Note:** The npm package requires Bun to be installed on your system. Install Bun from [bun.sh](https://bun.sh).
|
|
52
|
+
|
|
53
|
+
### D. npm / npx (Requires Bun)
|
|
37
54
|
|
|
38
55
|
Install globally:
|
|
39
56
|
|
|
@@ -47,9 +64,9 @@ Or run once with:
|
|
|
47
64
|
npx @napisani/scute --help
|
|
48
65
|
```
|
|
49
66
|
|
|
50
|
-
> The npm package
|
|
67
|
+
> **Note:** The npm package requires Bun to be installed on your system.
|
|
51
68
|
|
|
52
|
-
###
|
|
69
|
+
### E. Nix
|
|
53
70
|
|
|
54
71
|
Add the repo as an input and use it in your Home Manager flake:
|
|
55
72
|
|
|
@@ -66,12 +83,12 @@ outputs = { self, nixpkgs, scute, ... }: {
|
|
|
66
83
|
|
|
67
84
|
> The repository ships an intentionally minimal `flake.nix`. Run `nix flake lock --update-input scute` inside your own workspace to pin exact revisions.
|
|
68
85
|
|
|
69
|
-
###
|
|
86
|
+
### F. Prebuilt Binaries (Manual)
|
|
70
87
|
|
|
71
|
-
Every Git tag publishes
|
|
88
|
+
Every Git tag publishes macOS (x86_64, arm64) and Linux (x86_64) archives on the [GitHub Releases](https://github.com/napisani/scute/releases) page. Download the archive for your platform, unpack it, and move the `scute` binary onto your `PATH`:
|
|
72
89
|
|
|
73
90
|
```sh
|
|
74
|
-
curl -L -o scute.tar.gz "https://github.com/napisani/scute/releases/download/vX.Y.Z/scute-vX.Y.Z-macos-
|
|
91
|
+
curl -L -o scute.tar.gz "https://github.com/napisani/scute/releases/download/vX.Y.Z/scute-vX.Y.Z-macos-arm64.tar.gz"
|
|
75
92
|
tar -xzf scute.tar.gz
|
|
76
93
|
sudo mv scute /usr/local/bin/
|
|
77
94
|
```
|
|
@@ -80,10 +97,10 @@ Verify downloads with the checksums shipped alongside each release:
|
|
|
80
97
|
|
|
81
98
|
```sh
|
|
82
99
|
curl -LO https://github.com/napisani/scute/releases/download/vX.Y.Z/checksums.txt
|
|
83
|
-
grep scute-vX.Y.Z-macos-
|
|
100
|
+
grep scute-vX.Y.Z-macos-arm64.tar.gz checksums.txt | shasum -a 256 -c -
|
|
84
101
|
```
|
|
85
102
|
|
|
86
|
-
###
|
|
103
|
+
### G. Install from Source
|
|
87
104
|
|
|
88
105
|
```sh
|
|
89
106
|
git clone https://github.com/napisani/scute.git
|
|
@@ -93,7 +110,7 @@ bun run build:bin
|
|
|
93
110
|
sudo mv dist/scute /usr/local/bin/
|
|
94
111
|
```
|
|
95
112
|
|
|
96
|
-
|
|
113
|
+
Or use Make targets:
|
|
97
114
|
|
|
98
115
|
```sh
|
|
99
116
|
make build
|
|
@@ -315,13 +332,53 @@ Once installed and configured, you can use the following keyboard shortcuts in y
|
|
|
315
332
|
|
|
316
333
|
## Release Process (Maintainers)
|
|
317
334
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
335
|
+
### Creating a New Release
|
|
336
|
+
|
|
337
|
+
1. **Update the version** in `package.json` (e.g., `"version": "0.0.4"`)
|
|
338
|
+
2. **Commit the version change**: `git commit -am "Bump version to 0.0.4"`
|
|
339
|
+
3. **Create the release** (this creates the git tag and triggers CI):
|
|
340
|
+
```sh
|
|
341
|
+
make release-create
|
|
342
|
+
```
|
|
343
|
+
This will:
|
|
344
|
+
- Run lint and tests
|
|
345
|
+
- Create and push git tag `v0.0.4`
|
|
346
|
+
- Trigger GitHub Actions to build binaries
|
|
347
|
+
|
|
348
|
+
4. **Wait for CI** to complete (GitHub Actions builds and uploads binaries to the release)
|
|
349
|
+
|
|
350
|
+
5. **Publish to npm**:
|
|
351
|
+
```sh
|
|
352
|
+
make release-publish
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
6. **Update Homebrew formula** (after CI finishes):
|
|
356
|
+
```sh
|
|
357
|
+
make update-brew-latest
|
|
358
|
+
```
|
|
359
|
+
Or specify a version explicitly:
|
|
360
|
+
```sh
|
|
361
|
+
make update-brew VERSION=v0.0.4
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Full Release (Create + Publish)
|
|
365
|
+
|
|
366
|
+
To do it all in one command:
|
|
367
|
+
```sh
|
|
368
|
+
make release
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### What Gets Published
|
|
372
|
+
|
|
373
|
+
- **npm**: Source code (TypeScript/Bun) - users need Bun installed
|
|
374
|
+
- **GitHub Release**: Prebuilt binaries for macOS (x86_64, arm64) and Linux (x86_64)
|
|
375
|
+
- **Homebrew**: Points to the GitHub release binaries
|
|
376
|
+
|
|
377
|
+
### Pre-release Testing
|
|
324
378
|
|
|
379
|
+
For testing before publishing:
|
|
325
380
|
```sh
|
|
326
|
-
|
|
381
|
+
# Create a prerelease tag (contains '-')
|
|
382
|
+
make release-create
|
|
383
|
+
# CI will mark it as prerelease automatically
|
|
327
384
|
```
|
package/bin/scute
ADDED
package/package.json
CHANGED
|
@@ -1,87 +1,86 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
]
|
|
2
|
+
"name": "@napisani/scute",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "AI-powered shell assistant",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"shell",
|
|
9
|
+
"ai",
|
|
10
|
+
"explain",
|
|
11
|
+
"bash",
|
|
12
|
+
"zsh",
|
|
13
|
+
"suggestion",
|
|
14
|
+
"tui"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/napisani/scute",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/napisani/scute/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/napisani/scute.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "Nick Pisani",
|
|
26
|
+
"main": "src/index.ts",
|
|
27
|
+
"bin": {
|
|
28
|
+
"scute": "bin/scute"
|
|
29
|
+
},
|
|
30
|
+
"directories": {
|
|
31
|
+
"test": "tests"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"src/",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "bun run build:bin",
|
|
41
|
+
"build:bin": "bun build ./src/index.ts --compile --outfile dist/scute",
|
|
42
|
+
"clean": "rm -rf dist",
|
|
43
|
+
"lint": "bunx tsc --noEmit",
|
|
44
|
+
"test": "bun test tests/",
|
|
45
|
+
"coverage": "bun test tests/ --coverage",
|
|
46
|
+
"evals": "bun test evals --pattern \\\"\\.eval\\.test\\.ts$\\\"",
|
|
47
|
+
"prepublishOnly": "bun run lint && bun run test"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@opentui/core": "^0.1.75",
|
|
51
|
+
"@opentui/react": "^0.1.75",
|
|
52
|
+
"@tanstack/ai": "^0.2.2",
|
|
53
|
+
"@tanstack/ai-anthropic": "^0.2.0",
|
|
54
|
+
"@tanstack/ai-gemini": "^0.3.2",
|
|
55
|
+
"@tanstack/ai-ollama": "^0.3.0",
|
|
56
|
+
"@tanstack/ai-openai": "^0.2.1",
|
|
57
|
+
"chalk": "^5.6.2",
|
|
58
|
+
"commander": "^14.0.2",
|
|
59
|
+
"dotenv": "^17.2.3",
|
|
60
|
+
"js-yaml": "^4.1.1",
|
|
61
|
+
"react": "^19.2.4",
|
|
62
|
+
"shell-quote": "^1.8.3"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@biomejs/biome": "2.3.12",
|
|
66
|
+
"@testing-library/react": "^16.3.2",
|
|
67
|
+
"@types/bun": "latest",
|
|
68
|
+
"@types/js-yaml": "^4.0.9",
|
|
69
|
+
"@types/jsdom": "^27.0.0",
|
|
70
|
+
"@types/node": "^25.0.10",
|
|
71
|
+
"@types/react": "^19.2.9",
|
|
72
|
+
"@types/shell-quote": "^1.7.5",
|
|
73
|
+
"happy-dom": "^20.5.0",
|
|
74
|
+
"jsdom": "^28.0.0"
|
|
75
|
+
},
|
|
76
|
+
"peerDependencies": {
|
|
77
|
+
"typescript": "^5"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"bun": ">=1.0.0"
|
|
81
|
+
},
|
|
82
|
+
"os": [
|
|
83
|
+
"darwin",
|
|
84
|
+
"linux"
|
|
85
|
+
]
|
|
87
86
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createCliRenderer } from "@opentui/core";
|
|
2
|
+
import { createRoot } from "@opentui/react";
|
|
3
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
4
|
+
import { getReadlineLine, hasReadlineLine } from "../core/shells";
|
|
5
|
+
import { BuildApp } from "../pages/build";
|
|
6
|
+
import { promptForLine } from "../utils/prompt";
|
|
7
|
+
|
|
8
|
+
export interface BuildOptions {
|
|
9
|
+
output: OutputChannel;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveBuildCommand(
|
|
13
|
+
inputParts: string[],
|
|
14
|
+
options: { hasReadlineLine: boolean; readlineLine: string | null },
|
|
15
|
+
): string {
|
|
16
|
+
const positionalCommand = inputParts.join(" ").trim();
|
|
17
|
+
if (positionalCommand.length > 0) {
|
|
18
|
+
return positionalCommand;
|
|
19
|
+
}
|
|
20
|
+
if (options.hasReadlineLine && options.readlineLine) {
|
|
21
|
+
return options.readlineLine;
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function build(
|
|
27
|
+
inputParts: string[] = [],
|
|
28
|
+
{ output }: BuildOptions,
|
|
29
|
+
) {
|
|
30
|
+
const isLine = hasReadlineLine();
|
|
31
|
+
const readlineLine = getReadlineLine();
|
|
32
|
+
let command = resolveBuildCommand(inputParts, {
|
|
33
|
+
hasReadlineLine: isLine,
|
|
34
|
+
readlineLine,
|
|
35
|
+
});
|
|
36
|
+
if (command.length === 0) {
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
command = await promptForLine({
|
|
39
|
+
message: "Enter a command to start building: ",
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
command = await readAllStdin();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const renderer = await createCliRenderer();
|
|
46
|
+
let didSubmit = false;
|
|
47
|
+
const handleSubmit = (nextCommand: string) => {
|
|
48
|
+
if (didSubmit) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
didSubmit = true;
|
|
52
|
+
renderer.destroy();
|
|
53
|
+
emitOutput({
|
|
54
|
+
channel: output,
|
|
55
|
+
text: nextCommand,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
createRoot(renderer).render(
|
|
59
|
+
<BuildApp command={command} onSubmit={handleSubmit} />,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readAllStdin(): Promise<string> {
|
|
64
|
+
let input = "";
|
|
65
|
+
for await (const chunk of process.stdin) {
|
|
66
|
+
input += chunk;
|
|
67
|
+
}
|
|
68
|
+
return input.trim();
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getConfigSnapshot } from "../config";
|
|
2
|
+
import { getEnv, SUPPORTED_ENV_VARS } from "../core/environment";
|
|
3
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
4
|
+
|
|
5
|
+
type DebugOutput = {
|
|
6
|
+
config: ReturnType<typeof getConfigSnapshot>;
|
|
7
|
+
environment: Record<string, string | undefined>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface ConfigDebugOptions {
|
|
11
|
+
output: OutputChannel;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function configDebug({ output }: ConfigDebugOptions): void {
|
|
15
|
+
const resolvedConfig = getConfigSnapshot();
|
|
16
|
+
const environment: DebugOutput["environment"] = SUPPORTED_ENV_VARS.reduce(
|
|
17
|
+
(acc, varName) => {
|
|
18
|
+
acc[varName] = getEnv(varName);
|
|
19
|
+
return acc;
|
|
20
|
+
},
|
|
21
|
+
{} as Record<string, string | undefined>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const payload: DebugOutput = {
|
|
25
|
+
config: resolvedConfig,
|
|
26
|
+
environment,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
emitOutput({
|
|
30
|
+
channel: output,
|
|
31
|
+
text: JSON.stringify(payload, null, 2),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// src/commands/explain.ts
|
|
2
|
+
import { explain as explainCommand } from "../core";
|
|
3
|
+
import { logDebug } from "../core/logger";
|
|
4
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles the 'explain' command by calling the AI service.
|
|
8
|
+
* Renders a non-interfering hint at the bottom of the terminal.
|
|
9
|
+
* @param line The current READLINE_LINE content.
|
|
10
|
+
* @param point The current READLINE_POINT (cursor position).
|
|
11
|
+
*/
|
|
12
|
+
export interface ExplainOptions {
|
|
13
|
+
output: OutputChannel;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function explain(
|
|
17
|
+
line: string,
|
|
18
|
+
point: string,
|
|
19
|
+
{ output }: ExplainOptions,
|
|
20
|
+
) {
|
|
21
|
+
logDebug(`command:explain line="${line}" point=${point}`);
|
|
22
|
+
|
|
23
|
+
const explanation = await explainCommand(line);
|
|
24
|
+
if (explanation === null) {
|
|
25
|
+
logDebug("command:explain result=null");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
logDebug("command:explain hint ready");
|
|
29
|
+
emitOutput({
|
|
30
|
+
channel: output,
|
|
31
|
+
text: explanation,
|
|
32
|
+
promptPrefix: "[scute] ",
|
|
33
|
+
});
|
|
34
|
+
logDebug("command:explain output written");
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/commands/generate.ts
|
|
2
|
+
import { generateCommand } from "../core";
|
|
3
|
+
import { logDebug } from "../core/logger";
|
|
4
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
5
|
+
import { promptForLine } from "../utils/prompt";
|
|
6
|
+
|
|
7
|
+
export interface GenerateOptions {
|
|
8
|
+
output: OutputChannel;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function generate(
|
|
12
|
+
inputParts: string[] = [],
|
|
13
|
+
{ output }: GenerateOptions,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const prompt = await resolvePrompt(inputParts);
|
|
16
|
+
if (!prompt) {
|
|
17
|
+
logDebug("command:generate empty prompt");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
logDebug(`command:generate prompt="${prompt}"`);
|
|
21
|
+
const suggestion = await generateCommand(prompt);
|
|
22
|
+
if (suggestion === null) {
|
|
23
|
+
logDebug("command:generate result=null");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
emitOutput({
|
|
27
|
+
channel: output,
|
|
28
|
+
text: suggestion,
|
|
29
|
+
});
|
|
30
|
+
logDebug("command:generate output written");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function resolvePrompt(inputParts: string[]): Promise<string> {
|
|
34
|
+
if (inputParts.length) {
|
|
35
|
+
return inputParts.join(" ").trim();
|
|
36
|
+
}
|
|
37
|
+
if (process.stdin.isTTY) {
|
|
38
|
+
return await promptForLine({
|
|
39
|
+
message: "Enter a command request: ",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
let input = "";
|
|
43
|
+
for await (const chunk of process.stdin) {
|
|
44
|
+
input += chunk;
|
|
45
|
+
}
|
|
46
|
+
return input.trim();
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/commands/init.ts
|
|
2
|
+
|
|
3
|
+
import { getShellKeybindings } from "../config";
|
|
4
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
5
|
+
import { getShellHelperByName, supportedShells } from "../core/shells";
|
|
6
|
+
import type { ShellName } from "../core/shells/common";
|
|
7
|
+
|
|
8
|
+
export interface InitOptions {
|
|
9
|
+
output: OutputChannel;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function init(shell: string, { output }: InitOptions) {
|
|
13
|
+
if (!supportedShells.includes(shell as ShellName)) {
|
|
14
|
+
console.error(
|
|
15
|
+
`Error: Unsupported shell '${shell}'. Supported shells: ${supportedShells.join(
|
|
16
|
+
", ",
|
|
17
|
+
)}.`,
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const shellHelper = getShellHelperByName(shell as ShellName);
|
|
23
|
+
const initScript = shellHelper.getInitScript(getShellKeybindings());
|
|
24
|
+
|
|
25
|
+
emitOutput({
|
|
26
|
+
channel: output,
|
|
27
|
+
text: initScript,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/commands/suggest.ts
|
|
2
|
+
import { suggest as suggestCommand } from "../core";
|
|
3
|
+
import { logDebug } from "../core/logger";
|
|
4
|
+
import { emitOutput, type OutputChannel } from "../core/output";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles the 'suggest' command by calling the AI service.
|
|
8
|
+
* @param line The current text from the READLINE_LINE environment variable.
|
|
9
|
+
*/
|
|
10
|
+
export interface SuggestOptions {
|
|
11
|
+
output: OutputChannel;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function suggest(line: string, { output }: SuggestOptions) {
|
|
15
|
+
logDebug(`command:suggest line="${line}"`);
|
|
16
|
+
const suggestion = await suggestCommand(line);
|
|
17
|
+
if (suggestion === null) {
|
|
18
|
+
logDebug("command:suggest result=null");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
logDebug(`command:suggest result="${suggestion}"`);
|
|
22
|
+
emitOutput({
|
|
23
|
+
channel: output,
|
|
24
|
+
text: suggestion,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getKeybindings } from "../config";
|
|
2
|
+
import type { ViewMode, VimMode } from "../hooks/useVimMode";
|
|
3
|
+
import { Spinner } from "./Spinner";
|
|
4
|
+
|
|
5
|
+
interface FooterProps {
|
|
6
|
+
mode: VimMode;
|
|
7
|
+
viewMode: ViewMode;
|
|
8
|
+
isLoading?: boolean;
|
|
9
|
+
error?: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Footer({
|
|
13
|
+
mode,
|
|
14
|
+
viewMode,
|
|
15
|
+
isLoading = false,
|
|
16
|
+
error = null,
|
|
17
|
+
}: FooterProps) {
|
|
18
|
+
const modeColor = mode === "insert" ? "#00FF00" : "#888";
|
|
19
|
+
const modeText = mode === "insert" ? "-- INSERT --" : "-- NORMAL --";
|
|
20
|
+
const toggleViewKey = getKeybindings("toggleView")[0] ?? "m";
|
|
21
|
+
|
|
22
|
+
const viewModeHelp =
|
|
23
|
+
viewMode === "annotated"
|
|
24
|
+
? "h/l: move, w/b: word, 0/$: line"
|
|
25
|
+
: "j/k: move, gg/G: jump";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<box height={1} flexDirection="row" gap={2}>
|
|
29
|
+
<text fg={modeColor}>{modeText}</text>
|
|
30
|
+
{error ? (
|
|
31
|
+
<text fg="#F38BA8">{error}</text>
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<text fg="#888">Press '{toggleViewKey}' to toggle view</text>
|
|
35
|
+
<text fg="#888">{viewModeHelp}</text>
|
|
36
|
+
<Spinner isActive={isLoading} />
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</box>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { getThemeColorFor } from "../config";
|
|
3
|
+
|
|
4
|
+
type SpinnerProps = {
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function getSpinnerFrame(index: number): string {
|
|
9
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
10
|
+
return frames[index % frames.length]!;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Spinner({ isActive }: SpinnerProps) {
|
|
14
|
+
const [spinnerIndex, setSpinnerIndex] = useState(0);
|
|
15
|
+
const markerColor = getThemeColorFor("markerColor");
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isActive) {
|
|
19
|
+
setSpinnerIndex(0);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const timer = setInterval(() => {
|
|
23
|
+
setSpinnerIndex((index) => index + 1);
|
|
24
|
+
}, 120);
|
|
25
|
+
return () => clearInterval(timer);
|
|
26
|
+
}, [isActive]);
|
|
27
|
+
|
|
28
|
+
if (!isActive) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<box
|
|
34
|
+
style={{
|
|
35
|
+
flexDirection: "row",
|
|
36
|
+
justifyContent: "flex-end",
|
|
37
|
+
width: "100%",
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<text fg={markerColor}>{getSpinnerFrame(spinnerIndex)}</text>
|
|
41
|
+
</box>
|
|
42
|
+
);
|
|
43
|
+
}
|