kodu 1.2.0 → 2.0.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/AGENTS.md +36 -68
- package/README.md +97 -96
- package/dist/package.json +1 -2
- package/dist/src/app.module.js +0 -8
- package/dist/src/app.module.js.map +1 -1
- package/dist/src/commands/init/init.command.d.ts +2 -9
- package/dist/src/commands/init/init.command.js +15 -241
- package/dist/src/commands/init/init.command.js.map +1 -1
- package/dist/src/commands/pack/pack.command.d.ts +7 -0
- package/dist/src/commands/pack/pack.command.js +59 -3
- package/dist/src/commands/pack/pack.command.js.map +1 -1
- package/dist/src/core/config/config.schema.d.ts +0 -46
- package/dist/src/core/config/config.schema.js +1 -51
- package/dist/src/core/config/config.schema.js.map +1 -1
- package/dist/src/core/config/config.service.js +2 -2
- package/dist/src/core/config/config.service.js.map +1 -1
- package/dist/src/core/config/prompt.service.d.ts +1 -4
- package/dist/src/core/config/prompt.service.js +4 -17
- package/dist/src/core/config/prompt.service.js.map +1 -1
- package/dist/src/shared/constants.d.ts +0 -4
- package/dist/src/shared/constants.js +1 -5
- package/dist/src/shared/constants.js.map +1 -1
- package/dist/src/shared/git/git.module.js +0 -2
- package/dist/src/shared/git/git.module.js.map +1 -1
- package/dist/src/shared/git/git.service.d.ts +0 -8
- package/dist/src/shared/git/git.service.js +2 -34
- package/dist/src/shared/git/git.service.js.map +1 -1
- package/dist/src/shared/tokenizer/tokenizer.module.js +0 -2
- package/dist/src/shared/tokenizer/tokenizer.module.js.map +1 -1
- package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -6
- package/dist/src/shared/tokenizer/tokenizer.service.js +8 -38
- package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/kodu.schema.json +0 -139
- package/package.json +1 -2
- package/src/app.module.ts +0 -8
- package/src/commands/init/init.command.ts +15 -310
- package/src/commands/pack/pack.command.ts +56 -3
- package/src/core/config/config.schema.ts +1 -68
- package/src/core/config/config.service.ts +2 -2
- package/src/core/config/prompt.service.ts +4 -26
- package/src/shared/constants.ts +0 -4
- package/src/shared/git/git.module.ts +0 -2
- package/src/shared/git/git.service.ts +1 -33
- package/src/shared/tokenizer/tokenizer.module.ts +0 -2
- package/src/shared/tokenizer/tokenizer.service.ts +9 -39
- package/.kodu/prompts/.keep +0 -0
- package/.kodu/prompts/commit.md +0 -9
- package/.kodu/prompts/pack.md +0 -7
- package/.kodu/prompts/review-bug.md +0 -6
- package/.kodu/prompts/review-security.md +0 -6
- package/.kodu/prompts/review-style.md +0 -6
- package/.opencode/command/openspec-apply.md +0 -24
- package/.opencode/command/openspec-archive.md +0 -27
- package/.opencode/command/openspec-proposal.md +0 -29
- package/.opencode/skills/kodu-ops/SKILL.md +0 -184
- package/dist/src/commands/commit/commit.command.d.ts +0 -18
- package/dist/src/commands/commit/commit.command.js +0 -149
- package/dist/src/commands/commit/commit.command.js.map +0 -1
- package/dist/src/commands/commit/commit.module.d.ts +0 -2
- package/dist/src/commands/commit/commit.module.js +0 -25
- package/dist/src/commands/commit/commit.module.js.map +0 -1
- package/dist/src/commands/ops/ops.command.d.ts +0 -4
- package/dist/src/commands/ops/ops.command.js +0 -39
- package/dist/src/commands/ops/ops.command.js.map +0 -1
- package/dist/src/commands/ops/ops.module.d.ts +0 -2
- package/dist/src/commands/ops/ops.module.js +0 -33
- package/dist/src/commands/ops/ops.module.js.map +0 -1
- package/dist/src/commands/ops/ops.types.d.ts +0 -13
- package/dist/src/commands/ops/ops.types.js +0 -12
- package/dist/src/commands/ops/ops.types.js.map +0 -1
- package/dist/src/commands/ops/ops.utils.d.ts +0 -13
- package/dist/src/commands/ops/ops.utils.js +0 -121
- package/dist/src/commands/ops/ops.utils.js.map +0 -1
- package/dist/src/commands/ops/subcommands/ops-env.command.d.ts +0 -24
- package/dist/src/commands/ops/subcommands/ops-env.command.js +0 -156
- package/dist/src/commands/ops/subcommands/ops-env.command.js.map +0 -1
- package/dist/src/commands/ops/subcommands/ops-routes.command.d.ts +0 -22
- package/dist/src/commands/ops/subcommands/ops-routes.command.js +0 -203
- package/dist/src/commands/ops/subcommands/ops-routes.command.js.map +0 -1
- package/dist/src/commands/ops/subcommands/ops-service.command.d.ts +0 -22
- package/dist/src/commands/ops/subcommands/ops-service.command.js +0 -169
- package/dist/src/commands/ops/subcommands/ops-service.command.js.map +0 -1
- package/dist/src/commands/ops/subcommands/ops-sysinfo.command.d.ts +0 -14
- package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js +0 -75
- package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js.map +0 -1
- package/dist/src/commands/review/review.command.d.ts +0 -26
- package/dist/src/commands/review/review.command.js +0 -205
- package/dist/src/commands/review/review.command.js.map +0 -1
- package/dist/src/commands/review/review.module.d.ts +0 -2
- package/dist/src/commands/review/review.module.js +0 -26
- package/dist/src/commands/review/review.module.js.map +0 -1
- package/dist/src/core/config/default-prompts.d.ts +0 -9
- package/dist/src/core/config/default-prompts.js +0 -49
- package/dist/src/core/config/default-prompts.js.map +0 -1
- package/dist/src/shared/ai/ai.module.d.ts +0 -2
- package/dist/src/shared/ai/ai.module.js +0 -23
- package/dist/src/shared/ai/ai.module.js.map +0 -1
- package/dist/src/shared/ai/ai.service.d.ts +0 -22
- package/dist/src/shared/ai/ai.service.js +0 -164
- package/dist/src/shared/ai/ai.service.js.map +0 -1
- package/dist/src/shared/ssh/ssh.module.d.ts +0 -2
- package/dist/src/shared/ssh/ssh.module.js +0 -21
- package/dist/src/shared/ssh/ssh.module.js.map +0 -1
- package/dist/src/shared/ssh/ssh.service.d.ts +0 -11
- package/dist/src/shared/ssh/ssh.service.js +0 -53
- package/dist/src/shared/ssh/ssh.service.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/src/commands/commit/commit.command.ts +0 -139
- package/src/commands/commit/commit.module.ts +0 -12
- package/src/commands/ops/ops.command.ts +0 -30
- package/src/commands/ops/ops.module.ts +0 -20
- package/src/commands/ops/ops.types.ts +0 -24
- package/src/commands/ops/ops.utils.ts +0 -160
- package/src/commands/ops/subcommands/ops-env.command.ts +0 -165
- package/src/commands/ops/subcommands/ops-routes.command.ts +0 -221
- package/src/commands/ops/subcommands/ops-service.command.ts +0 -190
- package/src/commands/ops/subcommands/ops-sysinfo.command.ts +0 -77
- package/src/commands/review/review.command.ts +0 -199
- package/src/commands/review/review.module.ts +0 -13
- package/src/core/config/default-prompts.ts +0 -53
- package/src/shared/ai/ai.module.ts +0 -10
- package/src/shared/ai/ai.service.ts +0 -216
- package/src/shared/ssh/ssh.module.ts +0 -8
- package/src/shared/ssh/ssh.service.ts +0 -61
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { CommandRunner, Option, SubCommand } from 'nest-commander';
|
|
3
|
-
import { ConfigService } from '../../../core/config/config.service';
|
|
4
|
-
import { SshService } from '../../../shared/ssh/ssh.service';
|
|
5
|
-
import { OpsCliError } from '../ops.types';
|
|
6
|
-
import {
|
|
7
|
-
ensureAction,
|
|
8
|
-
ensureRequired,
|
|
9
|
-
printCliError,
|
|
10
|
-
printJson,
|
|
11
|
-
printSshError,
|
|
12
|
-
resolveCaddyPath,
|
|
13
|
-
resolveServerOrThrow,
|
|
14
|
-
shellQuote,
|
|
15
|
-
} from '../ops.utils';
|
|
16
|
-
|
|
17
|
-
type OpsRoutesAction = 'list' | 'add' | 'remove' | 'update';
|
|
18
|
-
|
|
19
|
-
type OpsRoutesOptions = {
|
|
20
|
-
server?: string;
|
|
21
|
-
action?: string;
|
|
22
|
-
domain?: string;
|
|
23
|
-
upstream?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
@SubCommand({
|
|
27
|
-
name: 'routes',
|
|
28
|
-
description:
|
|
29
|
-
'Manage remote Caddy reverse proxy routes.\nExamples:\n kodu ops routes --server dev --action list\n kodu ops routes --server dev --action add --domain api.example.com --upstream 127.0.0.1:3000\n kodu ops routes --server dev --action update --domain api.example.com --upstream 127.0.0.1:4000\n kodu ops routes --server dev --action remove --domain api.example.com',
|
|
30
|
-
})
|
|
31
|
-
export class OpsRoutesCommand extends CommandRunner {
|
|
32
|
-
constructor(
|
|
33
|
-
private readonly configService: ConfigService,
|
|
34
|
-
private readonly sshService: SshService,
|
|
35
|
-
) {
|
|
36
|
-
super();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@Option({
|
|
40
|
-
flags: '-s, --server <name>',
|
|
41
|
-
description: 'Server alias defined in kodu.json (e.g., dev)',
|
|
42
|
-
})
|
|
43
|
-
parseServer(value: string): string {
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@Option({
|
|
48
|
-
flags: '-a, --action <type>',
|
|
49
|
-
description: 'Action to perform: list | add | remove | update',
|
|
50
|
-
})
|
|
51
|
-
parseAction(value: string): string {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@Option({
|
|
56
|
-
flags: '--domain <domain>',
|
|
57
|
-
description: 'Domain name (required for add, update, remove)',
|
|
58
|
-
})
|
|
59
|
-
parseDomain(value: string): string {
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Option({
|
|
64
|
-
flags: '--upstream <upstream>',
|
|
65
|
-
description: 'Upstream host:port (required for add, update)',
|
|
66
|
-
})
|
|
67
|
-
parseUpstream(value: string): string {
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async run(
|
|
72
|
-
passedParams: string[],
|
|
73
|
-
options: OpsRoutesOptions = {},
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
try {
|
|
76
|
-
if (passedParams.length > 0) {
|
|
77
|
-
throw new OpsCliError(
|
|
78
|
-
'VALIDATION_ERROR',
|
|
79
|
-
'Positional arguments are not supported. Use named flags (e.g., --server, --action). Run with --help for examples.',
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const serverAlias = ensureRequired(options.server, 'server');
|
|
84
|
-
const rawAction = ensureRequired(options.action, 'action');
|
|
85
|
-
const action = ensureAction<OpsRoutesAction>(
|
|
86
|
-
rawAction,
|
|
87
|
-
['list', 'add', 'remove', 'update'],
|
|
88
|
-
'routes action',
|
|
89
|
-
);
|
|
90
|
-
const server = await resolveServerOrThrow(
|
|
91
|
-
this.configService.getConfig(),
|
|
92
|
-
serverAlias,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const caddyRoot = resolveCaddyPath(server);
|
|
96
|
-
const caddyfilePath = path.posix.join(caddyRoot, 'data', 'Caddyfile');
|
|
97
|
-
|
|
98
|
-
if (action === 'list') {
|
|
99
|
-
const command = `cat ${shellQuote(caddyfilePath)}`;
|
|
100
|
-
const result = await this.sshService.execute(server, command);
|
|
101
|
-
if (!result.success) {
|
|
102
|
-
printSshError(result, command);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
printJson({ status: 'ok', data: { caddyfile: result.stdout } });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const domain = ensureRequired(options.domain, 'domain');
|
|
111
|
-
const upstream =
|
|
112
|
-
action === 'add' || action === 'update'
|
|
113
|
-
? ensureRequired(options.upstream, 'upstream')
|
|
114
|
-
: '';
|
|
115
|
-
|
|
116
|
-
const editCommand = this.buildEditCommand({
|
|
117
|
-
action,
|
|
118
|
-
caddyfilePath,
|
|
119
|
-
domain,
|
|
120
|
-
upstream,
|
|
121
|
-
});
|
|
122
|
-
const editResult = await this.sshService.execute(server, editCommand);
|
|
123
|
-
|
|
124
|
-
if (!editResult.success) {
|
|
125
|
-
if (editResult.exitCode === 4) {
|
|
126
|
-
printJson({
|
|
127
|
-
status: 'error',
|
|
128
|
-
code: 'NOT_FOUND',
|
|
129
|
-
stderr: editResult.stderr || editResult.stdout || 'Route not found',
|
|
130
|
-
command: editCommand,
|
|
131
|
-
});
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
printSshError(editResult, editCommand);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const applyCommand = `cd ${shellQuote(caddyRoot)} && ./caddy.sh`;
|
|
140
|
-
const applyResult = await this.sshService.execute(server, applyCommand);
|
|
141
|
-
if (!applyResult.success) {
|
|
142
|
-
printSshError(applyResult, applyCommand);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
printJson({
|
|
147
|
-
status: 'ok',
|
|
148
|
-
message: 'Routes updated',
|
|
149
|
-
data: {
|
|
150
|
-
action,
|
|
151
|
-
domain,
|
|
152
|
-
upstream: upstream || undefined,
|
|
153
|
-
caddyOutput: applyResult.stdout,
|
|
154
|
-
},
|
|
155
|
-
});
|
|
156
|
-
} catch (error) {
|
|
157
|
-
printCliError(error);
|
|
158
|
-
process.exitCode = 1;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
private buildEditCommand(params: {
|
|
163
|
-
action: Exclude<OpsRoutesAction, 'list'>;
|
|
164
|
-
caddyfilePath: string;
|
|
165
|
-
domain: string;
|
|
166
|
-
upstream: string;
|
|
167
|
-
}): string {
|
|
168
|
-
const script = this.buildPythonScript(params);
|
|
169
|
-
return `python3 -c ${shellQuote(script)}`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private buildPythonScript(params: {
|
|
173
|
-
action: Exclude<OpsRoutesAction, 'list'>;
|
|
174
|
-
caddyfilePath: string;
|
|
175
|
-
domain: string;
|
|
176
|
-
upstream: string;
|
|
177
|
-
}): string {
|
|
178
|
-
return [
|
|
179
|
-
'import json',
|
|
180
|
-
'import os',
|
|
181
|
-
'import random',
|
|
182
|
-
'import re',
|
|
183
|
-
'import sys',
|
|
184
|
-
'import time',
|
|
185
|
-
'from pathlib import Path',
|
|
186
|
-
`file_path = ${JSON.stringify(params.caddyfilePath)}`,
|
|
187
|
-
`action = ${JSON.stringify(params.action)}`,
|
|
188
|
-
`domain = ${JSON.stringify(params.domain)}`,
|
|
189
|
-
`upstream = ${JSON.stringify(params.upstream)}`,
|
|
190
|
-
'with Path(file_path).open("r", encoding="utf-8") as handle:',
|
|
191
|
-
' text = handle.read()',
|
|
192
|
-
'domain_re = re.compile(r"(^|\\n)\\s*" + re.escape(domain) + r"\\s*\\{[\\s\\S]*?\\n\\}", re.MULTILINE)',
|
|
193
|
-
'if action == "add":',
|
|
194
|
-
' if domain_re.search(text):',
|
|
195
|
-
' sys.stderr.write("Route already exists for domain")',
|
|
196
|
-
' sys.exit(3)',
|
|
197
|
-
' block = "\\n" + domain + " {\\n reverse_proxy " + upstream + "\\n}\\n"',
|
|
198
|
-
' text = re.sub(r"\\s*$", "", text) + block',
|
|
199
|
-
'elif action == "remove":',
|
|
200
|
-
' if not domain_re.search(text):',
|
|
201
|
-
' sys.stderr.write("Route not found")',
|
|
202
|
-
' sys.exit(4)',
|
|
203
|
-
' text = domain_re.sub("\\n", text, count=1)',
|
|
204
|
-
' text = re.sub(r"\\n{3,}", "\\n\\n", text)',
|
|
205
|
-
' text = re.sub(r"^\\n+", "", text)',
|
|
206
|
-
'elif action == "update":',
|
|
207
|
-
' match = domain_re.search(text)',
|
|
208
|
-
' if not match:',
|
|
209
|
-
' sys.stderr.write("Route not found")',
|
|
210
|
-
' sys.exit(4)',
|
|
211
|
-
' block = match.group(0)',
|
|
212
|
-
' updated = re.sub(r"reverse_proxy\\s+[^\\n]+", "reverse_proxy " + upstream, block, count=1)',
|
|
213
|
-
' text = domain_re.sub(updated, text, count=1)',
|
|
214
|
-
'tmp = os.path.join(os.path.dirname(file_path), ".tmp-" + str(int(time.time() * 1000)) + "-" + format(random.getrandbits(48), "x"))',
|
|
215
|
-
'with open(tmp, "w", encoding="utf-8") as handle:',
|
|
216
|
-
' handle.write(text)',
|
|
217
|
-
'os.replace(tmp, file_path)',
|
|
218
|
-
'print(json.dumps({"status": "ok"}))',
|
|
219
|
-
].join('\n');
|
|
220
|
-
}
|
|
221
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { CommandRunner, Option, SubCommand } from 'nest-commander';
|
|
3
|
-
import { ConfigService } from '../../../core/config/config.service';
|
|
4
|
-
import { SshService } from '../../../shared/ssh/ssh.service';
|
|
5
|
-
import { OpsCliError, type ResolvedServerConfig } from '../ops.types';
|
|
6
|
-
import {
|
|
7
|
-
ensureAction,
|
|
8
|
-
ensureRequired,
|
|
9
|
-
printCliError,
|
|
10
|
-
printJson,
|
|
11
|
-
printSshError,
|
|
12
|
-
resolveAppsPath,
|
|
13
|
-
resolveServerOrThrow,
|
|
14
|
-
shellQuote,
|
|
15
|
-
} from '../ops.utils';
|
|
16
|
-
|
|
17
|
-
type OpsServiceAction = 'clone' | 'pull' | 'up' | 'down' | 'logs' | 'status';
|
|
18
|
-
|
|
19
|
-
type OpsServiceOptions = {
|
|
20
|
-
server?: string;
|
|
21
|
-
action?: string;
|
|
22
|
-
project?: string;
|
|
23
|
-
repo?: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
@SubCommand({
|
|
27
|
-
name: 'service',
|
|
28
|
-
description:
|
|
29
|
-
'Manage project lifecycle using Docker Compose.\nExamples:\n kodu ops service --server dev --action clone --project temp --repo https://github.com/org/repo.git\n kodu ops service --server dev --action up --project temp\n kodu ops service --server dev --action status --project temp\n kodu ops service --server dev --action logs --project temp\n kodu ops service --server dev --action pull --project temp\n kodu ops service --server dev --action down --project temp',
|
|
30
|
-
})
|
|
31
|
-
export class OpsServiceCommand extends CommandRunner {
|
|
32
|
-
constructor(
|
|
33
|
-
private readonly configService: ConfigService,
|
|
34
|
-
private readonly sshService: SshService,
|
|
35
|
-
) {
|
|
36
|
-
super();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@Option({
|
|
40
|
-
flags: '-s, --server <name>',
|
|
41
|
-
description: 'Server alias defined in kodu.json (e.g., dev)',
|
|
42
|
-
})
|
|
43
|
-
parseServer(value: string): string {
|
|
44
|
-
return value;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
@Option({
|
|
48
|
-
flags: '-a, --action <type>',
|
|
49
|
-
description: 'Action to perform: clone | pull | up | down | logs | status',
|
|
50
|
-
})
|
|
51
|
-
parseAction(value: string): string {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@Option({
|
|
56
|
-
flags: '-p, --project <name>',
|
|
57
|
-
description: 'Target project directory name',
|
|
58
|
-
})
|
|
59
|
-
parseProject(value: string): string {
|
|
60
|
-
return value;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Option({ flags: '--repo <url>', description: 'Repository URL for clone' })
|
|
64
|
-
parseRepo(value: string): string {
|
|
65
|
-
return value;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async run(
|
|
69
|
-
passedParams: string[],
|
|
70
|
-
options: OpsServiceOptions = {},
|
|
71
|
-
): Promise<void> {
|
|
72
|
-
try {
|
|
73
|
-
if (passedParams.length > 0) {
|
|
74
|
-
throw new OpsCliError(
|
|
75
|
-
'VALIDATION_ERROR',
|
|
76
|
-
'Positional arguments are not supported. Use named flags (e.g., --server, --action). Run with --help for examples.',
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const serverAlias = ensureRequired(options.server, 'server');
|
|
81
|
-
const rawAction = ensureRequired(options.action, 'action');
|
|
82
|
-
const project = ensureRequired(options.project, 'project');
|
|
83
|
-
|
|
84
|
-
const action = ensureAction<OpsServiceAction>(
|
|
85
|
-
rawAction,
|
|
86
|
-
['clone', 'pull', 'up', 'down', 'logs', 'status'],
|
|
87
|
-
'service action',
|
|
88
|
-
);
|
|
89
|
-
const server = await resolveServerOrThrow(
|
|
90
|
-
this.configService.getConfig(),
|
|
91
|
-
serverAlias,
|
|
92
|
-
);
|
|
93
|
-
const projectPath = path.posix.join(resolveAppsPath(server), project);
|
|
94
|
-
|
|
95
|
-
if (action === 'status') {
|
|
96
|
-
await this.runStatus(server, projectPath);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const command = this.buildActionCommand(action, projectPath, options);
|
|
101
|
-
const result = await this.sshService.execute(server, command);
|
|
102
|
-
if (!result.success) {
|
|
103
|
-
printSshError(result, command);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
printJson({
|
|
108
|
-
status: 'ok',
|
|
109
|
-
data: {
|
|
110
|
-
action,
|
|
111
|
-
project,
|
|
112
|
-
stdout: result.stdout,
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
} catch (error) {
|
|
116
|
-
printCliError(error);
|
|
117
|
-
process.exitCode = 1;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private buildActionCommand(
|
|
122
|
-
action: Exclude<OpsServiceAction, 'status'>,
|
|
123
|
-
projectPath: string,
|
|
124
|
-
options: OpsServiceOptions,
|
|
125
|
-
): string {
|
|
126
|
-
const quotedProjectPath = shellQuote(projectPath);
|
|
127
|
-
|
|
128
|
-
if (action === 'clone') {
|
|
129
|
-
const repo = ensureRequired(options.repo, 'repo');
|
|
130
|
-
const quotedRepo = shellQuote(repo);
|
|
131
|
-
return [
|
|
132
|
-
`if [ -d ${quotedProjectPath} ]; then echo 'Project already exists' >&2; exit 3; fi`,
|
|
133
|
-
`git clone ${quotedRepo} ${quotedProjectPath}`,
|
|
134
|
-
].join(' && ');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (action === 'pull') {
|
|
138
|
-
return `cd ${quotedProjectPath} && git pull`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (action === 'up') {
|
|
142
|
-
return `cd ${quotedProjectPath} && docker compose up -d`;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (action === 'down') {
|
|
146
|
-
return `cd ${quotedProjectPath} && docker compose down`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return `cd ${quotedProjectPath} && docker compose logs --no-color --tail=200`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private async runStatus(
|
|
153
|
-
server: ResolvedServerConfig,
|
|
154
|
-
projectPath: string,
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
const quotedProjectPath = shellQuote(projectPath);
|
|
157
|
-
const jsonCommand = `cd ${quotedProjectPath} && docker compose ps --format json`;
|
|
158
|
-
const jsonResult = await this.sshService.execute(server, jsonCommand);
|
|
159
|
-
|
|
160
|
-
if (jsonResult.success) {
|
|
161
|
-
printJson({
|
|
162
|
-
status: 'ok',
|
|
163
|
-
data: {
|
|
164
|
-
action: 'status',
|
|
165
|
-
stdout: jsonResult.stdout,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const fallbackCommand = `cd ${quotedProjectPath} && docker compose ps`;
|
|
172
|
-
const fallbackResult = await this.sshService.execute(
|
|
173
|
-
server,
|
|
174
|
-
fallbackCommand,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
if (!fallbackResult.success) {
|
|
178
|
-
printSshError(fallbackResult, fallbackCommand);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
printJson({
|
|
183
|
-
status: 'ok',
|
|
184
|
-
data: {
|
|
185
|
-
action: 'status',
|
|
186
|
-
raw: fallbackResult.stdout,
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { CommandRunner, Option, SubCommand } from 'nest-commander';
|
|
2
|
-
import { ConfigService } from '../../../core/config/config.service';
|
|
3
|
-
import { SshService } from '../../../shared/ssh/ssh.service';
|
|
4
|
-
import { OpsCliError } from '../ops.types';
|
|
5
|
-
import {
|
|
6
|
-
ensureRequired,
|
|
7
|
-
printCliError,
|
|
8
|
-
printJson,
|
|
9
|
-
printSshError,
|
|
10
|
-
resolveServerOrThrow,
|
|
11
|
-
} from '../ops.utils';
|
|
12
|
-
|
|
13
|
-
type OpsSysinfoOptions = {
|
|
14
|
-
server?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
@SubCommand({
|
|
18
|
-
name: 'sysinfo',
|
|
19
|
-
description:
|
|
20
|
-
'Collect remote server diagnostics.\nExample: kodu ops sysinfo --server dev',
|
|
21
|
-
})
|
|
22
|
-
export class OpsSysinfoCommand extends CommandRunner {
|
|
23
|
-
constructor(
|
|
24
|
-
private readonly configService: ConfigService,
|
|
25
|
-
private readonly sshService: SshService,
|
|
26
|
-
) {
|
|
27
|
-
super();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@Option({
|
|
31
|
-
flags: '-s, --server <name>',
|
|
32
|
-
description: 'Server alias defined in kodu.json (e.g., dev)',
|
|
33
|
-
})
|
|
34
|
-
parseServer(value: string): string {
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async run(
|
|
39
|
-
passedParams: string[],
|
|
40
|
-
options: OpsSysinfoOptions = {},
|
|
41
|
-
): Promise<void> {
|
|
42
|
-
try {
|
|
43
|
-
if (passedParams.length > 0) {
|
|
44
|
-
throw new OpsCliError(
|
|
45
|
-
'VALIDATION_ERROR',
|
|
46
|
-
'Positional arguments are not supported. Use named flags (e.g., --server, --action). Run with --help for examples.',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const serverAlias = ensureRequired(options.server, 'server');
|
|
51
|
-
|
|
52
|
-
const server = await resolveServerOrThrow(
|
|
53
|
-
this.configService.getConfig(),
|
|
54
|
-
serverAlias,
|
|
55
|
-
);
|
|
56
|
-
const payload = `echo "{"uptime": "$(uptime -p)", "disk_usage": "$(df -h / | tail -1 | awk '{print $5}')", "mem_free": "$(free -m | grep Mem | awk '{print $4}')MB"}`;
|
|
57
|
-
|
|
58
|
-
const result = await this.sshService.execute(server, payload);
|
|
59
|
-
if (!result.success) {
|
|
60
|
-
printSshError(result, payload);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
let data: Record<string, string> = {};
|
|
65
|
-
try {
|
|
66
|
-
data = JSON.parse(result.stdout) as Record<string, string>;
|
|
67
|
-
} catch {
|
|
68
|
-
data = { raw: result.stdout.trim() };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
printJson({ status: 'ok', data });
|
|
72
|
-
} catch (error) {
|
|
73
|
-
printCliError(error);
|
|
74
|
-
process.exitCode = 1;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import clipboard from 'clipboardy';
|
|
3
|
-
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
4
|
-
import { UiService } from '../../core/ui/ui.service';
|
|
5
|
-
import { AiService, type ReviewMode } from '../../shared/ai/ai.service';
|
|
6
|
-
import { WARNING_TOKEN_THRESHOLD } from '../../shared/constants';
|
|
7
|
-
import { GitService } from '../../shared/git/git.service';
|
|
8
|
-
import { TokenizerService } from '../../shared/tokenizer/tokenizer.service';
|
|
9
|
-
|
|
10
|
-
type ReviewOptions = {
|
|
11
|
-
mode?: ReviewMode;
|
|
12
|
-
copy?: boolean;
|
|
13
|
-
ci?: boolean;
|
|
14
|
-
output?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const DEFAULT_MODE: ReviewMode = 'bug';
|
|
18
|
-
|
|
19
|
-
@Command({
|
|
20
|
-
name: 'review',
|
|
21
|
-
description: 'AI review for staged changes',
|
|
22
|
-
})
|
|
23
|
-
export class ReviewCommand extends CommandRunner {
|
|
24
|
-
constructor(
|
|
25
|
-
private readonly ui: UiService,
|
|
26
|
-
private readonly git: GitService,
|
|
27
|
-
private readonly tokenizer: TokenizerService,
|
|
28
|
-
private readonly ai: AiService,
|
|
29
|
-
) {
|
|
30
|
-
super();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Option({
|
|
34
|
-
flags: '-m, --mode <mode>',
|
|
35
|
-
description: 'Review mode: bug | style | security | <custom-mode>',
|
|
36
|
-
})
|
|
37
|
-
parseMode(value: string): ReviewMode {
|
|
38
|
-
const availableModes = this.ai.getAvailableReviewModes();
|
|
39
|
-
|
|
40
|
-
if (availableModes.includes(value)) {
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.ui.log.warn(
|
|
45
|
-
`Mode "${value}" not found. Available modes: ${availableModes.join(', ')}. Using default mode: ${DEFAULT_MODE}`,
|
|
46
|
-
);
|
|
47
|
-
return DEFAULT_MODE;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
@Option({ flags: '-c, --copy', description: 'Copy result to clipboard' })
|
|
51
|
-
parseCopy(): boolean {
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
@Option({
|
|
56
|
-
flags: '--ci',
|
|
57
|
-
description: 'CI mode: no spinner and no buffering',
|
|
58
|
-
})
|
|
59
|
-
parseCi(): boolean {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
@Option({
|
|
64
|
-
flags: '-o, --output <path>',
|
|
65
|
-
description: 'Save final review to file',
|
|
66
|
-
})
|
|
67
|
-
parseOutput(value: string): string {
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async run(_inputs: string[], options: ReviewOptions = {}): Promise<void> {
|
|
72
|
-
const ciMode = Boolean(options.ci);
|
|
73
|
-
const spinner = ciMode
|
|
74
|
-
? undefined
|
|
75
|
-
: this.ui.createSpinner({ text: 'Collecting diff from git...' }).start();
|
|
76
|
-
|
|
77
|
-
const logProgress = (text: string): void => {
|
|
78
|
-
if (ciMode) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (spinner) {
|
|
82
|
-
spinner.text = text;
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
this.ui.log.info(text);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const finishProgress = (text: string): void => {
|
|
89
|
-
if (ciMode) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (spinner) {
|
|
93
|
-
spinner.success(text);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.ui.log.success(text);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
if (!this.ai.hasApiKey()) {
|
|
101
|
-
const envName = this.ai.getApiKeyEnvName();
|
|
102
|
-
if (spinner) {
|
|
103
|
-
spinner.stop('AI key not found');
|
|
104
|
-
} else {
|
|
105
|
-
this.ui.log.error('AI key not found');
|
|
106
|
-
}
|
|
107
|
-
this.ui.log.warn(`'review' command requires AI key to work.`);
|
|
108
|
-
this.ui.log.info(`Set key: export ${envName}=<your_key>`);
|
|
109
|
-
this.ui.log.info(
|
|
110
|
-
`Environment variable name is configured via llm.apiKeyEnv in kodu.json`,
|
|
111
|
-
);
|
|
112
|
-
process.exitCode = 1;
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
await this.git.ensureRepo();
|
|
117
|
-
|
|
118
|
-
const hasStaged = await this.git.hasStagedChanges();
|
|
119
|
-
if (!hasStaged) {
|
|
120
|
-
if (spinner) {
|
|
121
|
-
spinner.stop('No staged changes');
|
|
122
|
-
} else {
|
|
123
|
-
this.ui.log.info('No staged changes');
|
|
124
|
-
}
|
|
125
|
-
this.ui.log.warn('First run git add for the required files.');
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const diff = await this.git.getStagedDiff();
|
|
130
|
-
if (!diff.trim()) {
|
|
131
|
-
if (spinner) {
|
|
132
|
-
spinner.stop(
|
|
133
|
-
'Diff is empty - possibly everything excluded by packer.ignore',
|
|
134
|
-
);
|
|
135
|
-
} else {
|
|
136
|
-
this.ui.log.info(
|
|
137
|
-
'Diff is empty - possibly everything excluded by packer.ignore',
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
this.ui.log.warn(
|
|
141
|
-
'Diff is empty: all changes fell into packer.ignore exclusions.',
|
|
142
|
-
);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const tokens = this.tokenizer.count(diff);
|
|
147
|
-
if (tokens.tokens > WARNING_TOKEN_THRESHOLD) {
|
|
148
|
-
this.ui.log.warn(
|
|
149
|
-
`Large context (${tokens.tokens} tokens, ~$${tokens.usdEstimate.toFixed(2)}). Review may cost more.`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
logProgress('Requesting AI...');
|
|
154
|
-
const mode = options.mode ?? DEFAULT_MODE;
|
|
155
|
-
const result = await this.ai.reviewDiff(diff, mode);
|
|
156
|
-
|
|
157
|
-
finishProgress('Review ready');
|
|
158
|
-
|
|
159
|
-
console.log(result.text);
|
|
160
|
-
await this.writeOutput(options.output, result.text, ciMode);
|
|
161
|
-
|
|
162
|
-
if (options.copy) {
|
|
163
|
-
await this.copyText(result.text, ciMode);
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
if (spinner) {
|
|
167
|
-
spinner.error('Review error');
|
|
168
|
-
} else {
|
|
169
|
-
this.ui.log.error('Review error');
|
|
170
|
-
}
|
|
171
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
172
|
-
this.ui.log.error(message);
|
|
173
|
-
process.exitCode = 1;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private async writeOutput(
|
|
178
|
-
target: string | undefined,
|
|
179
|
-
payload: string,
|
|
180
|
-
ciMode?: boolean,
|
|
181
|
-
): Promise<void> {
|
|
182
|
-
if (!target) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
await writeFile(target, payload, { encoding: 'utf8' });
|
|
186
|
-
if (!ciMode) {
|
|
187
|
-
this.ui.log.success(`Result saved to ${target}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private async copyText(text: string, ciMode: boolean): Promise<void> {
|
|
192
|
-
if (ciMode) {
|
|
193
|
-
this.ui.log.warn('--copy ignored in CI mode');
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
await clipboard.write(text);
|
|
197
|
-
this.ui.log.success('Result copied to clipboard');
|
|
198
|
-
}
|
|
199
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { ConfigModule } from '../../core/config/config.module';
|
|
3
|
-
import { UiModule } from '../../core/ui/ui.module';
|
|
4
|
-
import { AiModule } from '../../shared/ai/ai.module';
|
|
5
|
-
import { GitModule } from '../../shared/git/git.module';
|
|
6
|
-
import { TokenizerModule } from '../../shared/tokenizer/tokenizer.module';
|
|
7
|
-
import { ReviewCommand } from './review.command';
|
|
8
|
-
|
|
9
|
-
@Module({
|
|
10
|
-
imports: [ConfigModule, UiModule, GitModule, TokenizerModule, AiModule],
|
|
11
|
-
providers: [ReviewCommand],
|
|
12
|
-
})
|
|
13
|
-
export class ReviewModule {}
|