Skip to content

Commit dbf33f8

Browse files
authored
feat: tnf chat (#90)
1 parent 5dbd0e5 commit dbf33f8

File tree

7 files changed

+140
-46
lines changed

7 files changed

+140
-46
lines changed

.changeset/quiet-glasses-give.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@umijs/tnf': patch
3+
---
4+
5+
feat: tnf chat

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ $ pnpm preview
5454
## Commands
5555

5656
- `tnf build`: Build the project.
57+
- `tnf chat --message=<message> --model=<model> --verbose`: Chat with AI assistant.
5758
- `tnf config list/get/set/remove [name] [value]`: Manage the config.
5859
- `tnf dev`: Start the development server.
5960
- `tnf doctor`: Check the project for potential issues.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@
5050
"@babel/core": "^7.26.0",
5151
"@babel/preset-react": "^7.26.3",
5252
"@babel/preset-typescript": "^7.26.0",
53-
"@clack/prompts": "^0.9.0",
5453
"@tanstack/react-router": "^1.92.3",
5554
"@tanstack/router-devtools": "^1.92.3",
5655
"@tanstack/router-generator": "^1.87.7",
@@ -67,6 +66,7 @@
6766
"@types/react-dom": "^19.0.2",
6867
"@types/spdy": "^3.4.9",
6968
"@types/yargs-parser": "^21.0.3",
69+
"@umijs/clack-prompts": "^0.0.4",
7070
"@umijs/mako": "^0.11.0",
7171
"babel-plugin-react-compiler": "19.0.0-beta-b2e8e9c-20241220",
7272
"body-parser": "^1.20.3",
@@ -98,6 +98,7 @@
9898
"random-color": "^1.0.1",
9999
"react": "^19.0.0",
100100
"react-dom": "^19.0.0",
101+
"resolve": "^1.22.10",
101102
"sirv": "^3.0.0",
102103
"spdy": "^4.0.2",
103104
"tiny-invariant": "^1.3.3",

pnpm-lock.yaml

Lines changed: 22 additions & 42 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/chat.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import * as p from '@umijs/clack-prompts';
2+
import assert from 'assert';
3+
import fs from 'fs';
4+
import path from 'path';
5+
import { tools } from './ai/ai.js';
6+
import * as logger from './fishkit/logger.js';
7+
import { getNpmClient, installWithNpmClient } from './fishkit/npm.js';
8+
import { type Context } from './types/index.js';
9+
10+
const CANCEL_TEXT = 'Operation cancelled.';
11+
12+
export async function chat({ context }: { context: Context }) {
13+
const { cwd } = context;
14+
p.intro('Welcome to TNF Chat!');
15+
try {
16+
// Check if @umijs/ai is installed
17+
while (true) {
18+
if (isUmijsAiExists(cwd)) {
19+
break;
20+
} else {
21+
p.log.error('@umijs/ai is not installed');
22+
const result = await p.confirm({
23+
message: 'Do you want to install @umijs/ai?',
24+
});
25+
if (p.isCancel(result)) {
26+
throw new Error(CANCEL_TEXT);
27+
}
28+
if (result) {
29+
// install @umijs/ai
30+
logger.debug('Installing @umijs/ai');
31+
addUmijsAiToPackageJson(cwd);
32+
const npmClient = await getNpmClient({ cwd });
33+
logger.debug(`Using npm client: ${npmClient}`);
34+
installWithNpmClient({
35+
npmClient,
36+
cwd,
37+
});
38+
} else {
39+
throw new Error('Process cancelled, please install @umijs/ai first');
40+
}
41+
}
42+
}
43+
44+
// use @umijs/ai
45+
const docsDir = path.join(context.paths.tmpPath, 'docs');
46+
assert(
47+
fs.existsSync(docsDir),
48+
'.tnf/docs directory not found, please run tnf build/sync/dev first',
49+
);
50+
const tnfPath = path.join(docsDir, 'tnf.md');
51+
const generalPath = path.join(docsDir, 'general.md');
52+
const systemPrompts = [
53+
fs.readFileSync(tnfPath, 'utf-8'),
54+
fs.readFileSync(generalPath, 'utf-8'),
55+
];
56+
const umijsAiPath = path.join(
57+
cwd,
58+
'node_modules',
59+
'@umijs',
60+
'ai',
61+
'dist',
62+
'index.js',
63+
);
64+
const ai = await import(umijsAiPath);
65+
await ai.chat({
66+
tools,
67+
verbose: context.argv.verbose,
68+
message: context.argv.message,
69+
systemPrompts,
70+
model: context.argv.model,
71+
});
72+
p.outro('Bye!');
73+
} catch (e: any) {
74+
p.cancel(e.message);
75+
}
76+
}
77+
78+
function addUmijsAiToPackageJson(cwd: string) {
79+
const pkg = readPackageJson(cwd);
80+
pkg.dependencies['@umijs/ai'] = '^0.1.0';
81+
fs.writeFileSync(
82+
path.join(cwd, 'package.json'),
83+
JSON.stringify(pkg, null, 2),
84+
);
85+
}
86+
87+
function readPackageJson(cwd: string) {
88+
const pkgPath = path.join(cwd, 'package.json');
89+
logger.debug(`Reading package.json from ${pkgPath}`);
90+
assert(fs.existsSync(pkgPath), 'package.json not found');
91+
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
92+
}
93+
94+
function isUmijsAiExists(cwd: string) {
95+
const pkg = readPackageJson(cwd);
96+
return pkg.dependencies['@umijs/ai'] || pkg.devDependencies['@umijs/ai'];
97+
}

src/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ async function run(cwd: string) {
131131
case 'build':
132132
const { build } = await import('./build.js');
133133
return build({ context });
134+
case 'chat':
135+
const { chat } = await import('./chat.js');
136+
return chat({ context });
134137
case 'config':
135138
const { config } = await import('./config/config.js');
136139
return config({ context });

src/fishkit/npm.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ import { spawnSync } from 'child_process';
22
import { existsSync, readFileSync } from 'fs';
33
import fs from 'fs-extra';
44
import path from 'pathe';
5+
import { fileURLToPath } from 'url';
56

67
export type NpmClient = 'npm' | 'cnpm' | 'tnpm' | 'yarn' | 'pnpm';
78

8-
export const getNpmClient = (opts: { cwd: string }): NpmClient => {
9+
async function resolveModule(id: string) {
10+
return fileURLToPath(await import.meta.resolve(id));
11+
}
12+
13+
export const getNpmClient = async (opts: {
14+
cwd: string;
15+
}): Promise<NpmClient> => {
916
const tnpmRegistries = ['.alibaba-inc.', '.antgroup-inc.'];
1017
const tcnpmLockPath = path.join(
1118
opts.cwd,
@@ -20,7 +27,7 @@ export const getNpmClient = (opts: { cwd: string }): NpmClient => {
2027
}
2128

2229
// 检查 pnpm
23-
const chokidarPath = require.resolve('chokidar');
30+
const chokidarPath = await resolveModule('chokidar');
2431
if (
2532
chokidarPath.includes('.pnpm') ||
2633
existsSync(path.join(opts.cwd, 'node_modules', '.pnpm'))
@@ -123,7 +130,7 @@ export class PackageManager {
123130
try {
124131
await this.writePackageJson();
125132

126-
const npmClient = getNpmClient({ cwd: this.cwd });
133+
const npmClient = await getNpmClient({ cwd: this.cwd });
127134
await installWithNpmClient({
128135
npmClient,
129136
});

0 commit comments

Comments
 (0)