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,139 +0,0 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
|
-
import { Command, CommandRunner, Option } from 'nest-commander';
|
|
3
|
-
import { UiService } from '../../core/ui/ui.service';
|
|
4
|
-
import { AiService } from '../../shared/ai/ai.service';
|
|
5
|
-
import { GitService } from '../../shared/git/git.service';
|
|
6
|
-
|
|
7
|
-
type CommitOptions = {
|
|
8
|
-
ci?: boolean;
|
|
9
|
-
output?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
@Command({
|
|
13
|
-
name: 'commit',
|
|
14
|
-
description: 'Generate and apply commit message',
|
|
15
|
-
})
|
|
16
|
-
export class CommitCommand extends CommandRunner {
|
|
17
|
-
constructor(
|
|
18
|
-
private readonly ui: UiService,
|
|
19
|
-
private readonly git: GitService,
|
|
20
|
-
private readonly ai: AiService,
|
|
21
|
-
) {
|
|
22
|
-
super();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Option({ flags: '--ci', description: 'CI mode: no spinners and dialogs' })
|
|
26
|
-
parseCi(): boolean {
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@Option({
|
|
31
|
-
flags: '-o, --output <path>',
|
|
32
|
-
description: 'Save message to file',
|
|
33
|
-
})
|
|
34
|
-
parseOutput(value: string): string {
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async run(_inputs: string[], options: CommitOptions = {}): Promise<void> {
|
|
39
|
-
const ciMode = Boolean(options.ci);
|
|
40
|
-
const spinner = ciMode
|
|
41
|
-
? undefined
|
|
42
|
-
: this.ui.createSpinner({ text: 'Collecting diff...' }).start();
|
|
43
|
-
|
|
44
|
-
const logProgress = (text: string): void => {
|
|
45
|
-
if (ciMode) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (spinner) {
|
|
49
|
-
spinner.text = text;
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
this.ui.log.info(text);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const finishProgress = (text: string): void => {
|
|
56
|
-
if (ciMode) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
if (spinner) {
|
|
60
|
-
spinner.success(text);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
this.ui.log.success(text);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
if (!this.ai.hasApiKey()) {
|
|
68
|
-
const envName = this.ai.getApiKeyEnvName();
|
|
69
|
-
if (spinner) {
|
|
70
|
-
spinner.stop('AI key not found');
|
|
71
|
-
} else {
|
|
72
|
-
this.ui.log.error('AI key not found');
|
|
73
|
-
}
|
|
74
|
-
this.ui.log.warn(`'commit' command requires AI key to work.`);
|
|
75
|
-
this.ui.log.info(`Set key: export ${envName}=<your_key>`);
|
|
76
|
-
this.ui.log.info(
|
|
77
|
-
`Environment variable name is configured via llm.apiKeyEnv in kodu.json`,
|
|
78
|
-
);
|
|
79
|
-
process.exitCode = 1;
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
await this.git.ensureRepo();
|
|
84
|
-
|
|
85
|
-
const hasStaged = await this.git.hasStagedChanges();
|
|
86
|
-
if (!hasStaged) {
|
|
87
|
-
if (spinner) {
|
|
88
|
-
spinner.stop('No staged changes');
|
|
89
|
-
} else {
|
|
90
|
-
this.ui.log.info('No staged changes');
|
|
91
|
-
}
|
|
92
|
-
this.ui.log.warn('First run git add for the required files.');
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const diff = await this.git.getStagedDiff();
|
|
97
|
-
if (!diff.trim()) {
|
|
98
|
-
if (spinner) {
|
|
99
|
-
spinner.stop(
|
|
100
|
-
'Diff is empty - possibly everything excluded by packer.ignore',
|
|
101
|
-
);
|
|
102
|
-
} else {
|
|
103
|
-
this.ui.log.info(
|
|
104
|
-
'Diff is empty - possibly everything excluded by packer.ignore',
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
this.ui.log.warn(
|
|
108
|
-
'Diff is empty: all changes fell into packer.ignore exclusions.',
|
|
109
|
-
);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
logProgress('Generating commit message...');
|
|
114
|
-
const commitMessage = await this.ai.generateCommitMessage(diff);
|
|
115
|
-
|
|
116
|
-
finishProgress('Message ready');
|
|
117
|
-
if (!ciMode) {
|
|
118
|
-
this.ui.log.info(`Suggestion: ${commitMessage}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
console.log(commitMessage);
|
|
122
|
-
if (options.output) {
|
|
123
|
-
await writeFile(options.output, commitMessage, { encoding: 'utf8' });
|
|
124
|
-
if (!ciMode) {
|
|
125
|
-
this.ui.log.success(`Message saved to ${options.output}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch (error) {
|
|
129
|
-
if (spinner) {
|
|
130
|
-
spinner.error('Error creating commit');
|
|
131
|
-
} else {
|
|
132
|
-
this.ui.log.error('Error creating commit');
|
|
133
|
-
}
|
|
134
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
135
|
-
this.ui.log.error(message);
|
|
136
|
-
process.exitCode = 1;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
@@ -1,12 +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 { CommitCommand } from './commit.command';
|
|
7
|
-
|
|
8
|
-
@Module({
|
|
9
|
-
imports: [ConfigModule, UiModule, GitModule, AiModule],
|
|
10
|
-
providers: [CommitCommand],
|
|
11
|
-
})
|
|
12
|
-
export class CommitModule {}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Command, CommandRunner } from 'nest-commander';
|
|
2
|
-
import { printJson } from './ops.utils';
|
|
3
|
-
import { OpsEnvCommand } from './subcommands/ops-env.command';
|
|
4
|
-
import { OpsRoutesCommand } from './subcommands/ops-routes.command';
|
|
5
|
-
import { OpsServiceCommand } from './subcommands/ops-service.command';
|
|
6
|
-
import { OpsSysinfoCommand } from './subcommands/ops-sysinfo.command';
|
|
7
|
-
|
|
8
|
-
@Command({
|
|
9
|
-
name: 'ops',
|
|
10
|
-
description: 'Remote server operations for agents',
|
|
11
|
-
subCommands: [
|
|
12
|
-
OpsSysinfoCommand,
|
|
13
|
-
OpsEnvCommand,
|
|
14
|
-
OpsRoutesCommand,
|
|
15
|
-
OpsServiceCommand,
|
|
16
|
-
],
|
|
17
|
-
})
|
|
18
|
-
export class OpsCommand extends CommandRunner {
|
|
19
|
-
async run(): Promise<void> {
|
|
20
|
-
printJson(
|
|
21
|
-
{
|
|
22
|
-
status: 'error',
|
|
23
|
-
code: 'VALIDATION_ERROR',
|
|
24
|
-
error: 'Missing ops subcommand',
|
|
25
|
-
},
|
|
26
|
-
true,
|
|
27
|
-
);
|
|
28
|
-
process.exitCode = 1;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Module } from '@nestjs/common';
|
|
2
|
-
import { ConfigModule } from '../../core/config/config.module';
|
|
3
|
-
import { SshModule } from '../../shared/ssh/ssh.module';
|
|
4
|
-
import { OpsCommand } from './ops.command';
|
|
5
|
-
import { OpsEnvCommand } from './subcommands/ops-env.command';
|
|
6
|
-
import { OpsRoutesCommand } from './subcommands/ops-routes.command';
|
|
7
|
-
import { OpsServiceCommand } from './subcommands/ops-service.command';
|
|
8
|
-
import { OpsSysinfoCommand } from './subcommands/ops-sysinfo.command';
|
|
9
|
-
|
|
10
|
-
@Module({
|
|
11
|
-
imports: [ConfigModule, SshModule],
|
|
12
|
-
providers: [
|
|
13
|
-
OpsCommand,
|
|
14
|
-
OpsSysinfoCommand,
|
|
15
|
-
OpsEnvCommand,
|
|
16
|
-
OpsRoutesCommand,
|
|
17
|
-
OpsServiceCommand,
|
|
18
|
-
],
|
|
19
|
-
})
|
|
20
|
-
export class OpsModule {}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ServerConfig } from '../../core/config/config.schema';
|
|
2
|
-
|
|
3
|
-
export type OpsErrorCode =
|
|
4
|
-
| 'CLI_ERROR'
|
|
5
|
-
| 'CONFIG_ERROR'
|
|
6
|
-
| 'NOT_FOUND'
|
|
7
|
-
| 'VALIDATION_ERROR';
|
|
8
|
-
|
|
9
|
-
export type ResolvedServerConfig = ServerConfig & {
|
|
10
|
-
sshKeyPath: string;
|
|
11
|
-
paths?: {
|
|
12
|
-
apps: string;
|
|
13
|
-
caddy?: string;
|
|
14
|
-
};
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class OpsCliError extends Error {
|
|
18
|
-
constructor(
|
|
19
|
-
public readonly code: OpsErrorCode,
|
|
20
|
-
message: string,
|
|
21
|
-
) {
|
|
22
|
-
super(message);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { access } from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import type { KoduConfig, ServerConfig } from '../../core/config/config.schema';
|
|
5
|
-
import type { SshResult } from '../../shared/ssh/ssh.service';
|
|
6
|
-
import {
|
|
7
|
-
OpsCliError,
|
|
8
|
-
type OpsErrorCode,
|
|
9
|
-
type ResolvedServerConfig,
|
|
10
|
-
} from './ops.types';
|
|
11
|
-
|
|
12
|
-
const DEFAULT_APPS_PATH = '/var/agent-apps';
|
|
13
|
-
|
|
14
|
-
export function printJson(payload: unknown, isError = false): void {
|
|
15
|
-
const line = JSON.stringify(payload);
|
|
16
|
-
if (isError) {
|
|
17
|
-
console.error(line);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
console.log(line);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function printCliError(error: unknown): void {
|
|
24
|
-
const cliError = toCliError(error);
|
|
25
|
-
printJson(
|
|
26
|
-
{
|
|
27
|
-
status: 'error',
|
|
28
|
-
code: cliError.code,
|
|
29
|
-
error: cliError.message,
|
|
30
|
-
},
|
|
31
|
-
true,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function printSshError(result: SshResult, command: string): void {
|
|
36
|
-
printJson({
|
|
37
|
-
status: 'error',
|
|
38
|
-
code: result.exitCode,
|
|
39
|
-
stderr: result.stderr || result.error || 'Unknown SSH error',
|
|
40
|
-
command,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function resolveServerOrThrow(
|
|
45
|
-
config: KoduConfig,
|
|
46
|
-
alias: string,
|
|
47
|
-
): Promise<ResolvedServerConfig> {
|
|
48
|
-
const servers = config.ops?.servers;
|
|
49
|
-
if (!servers) {
|
|
50
|
-
throw new OpsCliError(
|
|
51
|
-
'CONFIG_ERROR',
|
|
52
|
-
'ops.servers not configured in kodu.json',
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const server = servers[alias];
|
|
57
|
-
if (!server) {
|
|
58
|
-
throw new OpsCliError(
|
|
59
|
-
'VALIDATION_ERROR',
|
|
60
|
-
`Server alias '${alias}' not found in kodu.json`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const resolved = normalizeServer(server);
|
|
65
|
-
await assertSshKeyExists(resolved.sshKeyPath);
|
|
66
|
-
return resolved;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function resolveAppsPath(server: ResolvedServerConfig): string {
|
|
70
|
-
return server.paths?.apps ?? DEFAULT_APPS_PATH;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function resolveCaddyPath(server: ResolvedServerConfig): string {
|
|
74
|
-
return (
|
|
75
|
-
server.paths?.caddy ?? path.posix.join(resolveAppsPath(server), 'caddy')
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function shellQuote(value: string): string {
|
|
80
|
-
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function ensureEnvKey(key: string | undefined): string {
|
|
84
|
-
if (!key) {
|
|
85
|
-
throw new OpsCliError('VALIDATION_ERROR', 'Option --key is required');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
89
|
-
throw new OpsCliError(
|
|
90
|
-
'VALIDATION_ERROR',
|
|
91
|
-
'Invalid env key format. Allowed: [A-Za-z_][A-Za-z0-9_]*',
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return key;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function ensureRequired(
|
|
99
|
-
value: string | undefined,
|
|
100
|
-
name: string,
|
|
101
|
-
): string {
|
|
102
|
-
if (!value) {
|
|
103
|
-
throw new OpsCliError('VALIDATION_ERROR', `Option --${name} is required`);
|
|
104
|
-
}
|
|
105
|
-
return value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function ensureAction<T extends string>(
|
|
109
|
-
value: string,
|
|
110
|
-
allowed: readonly T[],
|
|
111
|
-
context: string,
|
|
112
|
-
): T {
|
|
113
|
-
if (!allowed.includes(value as T)) {
|
|
114
|
-
throw new OpsCliError(
|
|
115
|
-
'VALIDATION_ERROR',
|
|
116
|
-
`Unsupported ${context}: '${value}'. Allowed: ${allowed.join(', ')}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return value as T;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function normalizeServer(server: ServerConfig): ResolvedServerConfig {
|
|
123
|
-
const sshKeyPath = server.sshKeyPath.startsWith('~')
|
|
124
|
-
? path.join(os.homedir(), server.sshKeyPath.slice(1))
|
|
125
|
-
: server.sshKeyPath;
|
|
126
|
-
const apps = server.paths?.apps ?? DEFAULT_APPS_PATH;
|
|
127
|
-
return {
|
|
128
|
-
...server,
|
|
129
|
-
sshKeyPath: path.isAbsolute(sshKeyPath)
|
|
130
|
-
? sshKeyPath
|
|
131
|
-
: path.resolve(process.cwd(), sshKeyPath),
|
|
132
|
-
paths: {
|
|
133
|
-
apps,
|
|
134
|
-
caddy: server.paths?.caddy,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function assertSshKeyExists(sshKeyPath: string): Promise<void> {
|
|
140
|
-
try {
|
|
141
|
-
await access(sshKeyPath);
|
|
142
|
-
} catch {
|
|
143
|
-
throw new OpsCliError(
|
|
144
|
-
'VALIDATION_ERROR',
|
|
145
|
-
`SSH key file not found or inaccessible: ${sshKeyPath}`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function toCliError(error: unknown): { code: OpsErrorCode; message: string } {
|
|
151
|
-
if (error instanceof OpsCliError) {
|
|
152
|
-
return { code: error.code, message: error.message };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (error instanceof Error) {
|
|
156
|
-
return { code: 'CLI_ERROR', message: error.message };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { code: 'CLI_ERROR', message: 'Unknown CLI error' };
|
|
160
|
-
}
|
|
@@ -1,165 +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
|
-
ensureEnvKey,
|
|
9
|
-
ensureRequired,
|
|
10
|
-
printCliError,
|
|
11
|
-
printJson,
|
|
12
|
-
printSshError,
|
|
13
|
-
resolveAppsPath,
|
|
14
|
-
resolveServerOrThrow,
|
|
15
|
-
shellQuote,
|
|
16
|
-
} from '../ops.utils';
|
|
17
|
-
|
|
18
|
-
type OpsEnvAction = 'get' | 'set' | 'unset';
|
|
19
|
-
|
|
20
|
-
type OpsEnvOptions = {
|
|
21
|
-
server?: string;
|
|
22
|
-
action?: string;
|
|
23
|
-
project?: string;
|
|
24
|
-
key?: string;
|
|
25
|
-
val?: string;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
@SubCommand({
|
|
29
|
-
name: 'env',
|
|
30
|
-
description:
|
|
31
|
-
'Manage remote .env files for a specific project.\nExamples:\n kodu ops env --server dev --action get --project my-app\n kodu ops env --server dev --action set --project my-app --key PORT --val 3000\n kodu ops env --server dev --action unset --project my-app --key PORT',
|
|
32
|
-
})
|
|
33
|
-
export class OpsEnvCommand extends CommandRunner {
|
|
34
|
-
constructor(
|
|
35
|
-
private readonly configService: ConfigService,
|
|
36
|
-
private readonly sshService: SshService,
|
|
37
|
-
) {
|
|
38
|
-
super();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@Option({
|
|
42
|
-
flags: '-s, --server <name>',
|
|
43
|
-
description: 'Server alias defined in kodu.json (e.g., dev)',
|
|
44
|
-
})
|
|
45
|
-
parseServer(value: string): string {
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
@Option({
|
|
50
|
-
flags: '-a, --action <type>',
|
|
51
|
-
description: 'Action to perform: get | set | unset',
|
|
52
|
-
})
|
|
53
|
-
parseAction(value: string): string {
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
@Option({
|
|
58
|
-
flags: '-p, --project <name>',
|
|
59
|
-
description: 'Target project directory name',
|
|
60
|
-
})
|
|
61
|
-
parseProject(value: string): string {
|
|
62
|
-
return value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
@Option({ flags: '--key <key>', description: 'Environment key' })
|
|
66
|
-
parseKey(value: string): string {
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
@Option({ flags: '--val <value>', description: 'Environment value' })
|
|
71
|
-
parseVal(value: string): string {
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async run(
|
|
76
|
-
passedParams: string[],
|
|
77
|
-
options: OpsEnvOptions = {},
|
|
78
|
-
): Promise<void> {
|
|
79
|
-
try {
|
|
80
|
-
if (passedParams.length > 0) {
|
|
81
|
-
throw new OpsCliError(
|
|
82
|
-
'VALIDATION_ERROR',
|
|
83
|
-
'Positional arguments are not supported. Use named flags (e.g., --server, --action). Run with --help for examples.',
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const serverAlias = ensureRequired(options.server, 'server');
|
|
88
|
-
const rawAction = ensureRequired(options.action, 'action');
|
|
89
|
-
const project = ensureRequired(options.project, 'project');
|
|
90
|
-
|
|
91
|
-
const action = ensureAction<OpsEnvAction>(
|
|
92
|
-
rawAction,
|
|
93
|
-
['get', 'set', 'unset'],
|
|
94
|
-
'env action',
|
|
95
|
-
);
|
|
96
|
-
const server = await resolveServerOrThrow(
|
|
97
|
-
this.configService.getConfig(),
|
|
98
|
-
serverAlias,
|
|
99
|
-
);
|
|
100
|
-
const envPath = path.posix.join(resolveAppsPath(server), project, '.env');
|
|
101
|
-
|
|
102
|
-
const command = this.buildCommand(action, envPath, options);
|
|
103
|
-
const result = await this.sshService.execute(server, command);
|
|
104
|
-
|
|
105
|
-
if (!result.success) {
|
|
106
|
-
printSshError(result, command);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (action === 'get') {
|
|
111
|
-
printJson({ status: 'ok', data: { content: result.stdout } });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
printJson({ status: 'ok', message: 'Env updated' });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
printCliError(error);
|
|
118
|
-
process.exitCode = 1;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private buildCommand(
|
|
123
|
-
action: OpsEnvAction,
|
|
124
|
-
envPath: string,
|
|
125
|
-
options: OpsEnvOptions,
|
|
126
|
-
): string {
|
|
127
|
-
const quotedPath = shellQuote(envPath);
|
|
128
|
-
|
|
129
|
-
if (action === 'get') {
|
|
130
|
-
return `cat ${quotedPath}`;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const key = ensureEnvKey(options.key);
|
|
134
|
-
const quotedKey = shellQuote(key);
|
|
135
|
-
|
|
136
|
-
if (action === 'set') {
|
|
137
|
-
const val = ensureRequired(options.val, 'val');
|
|
138
|
-
const quotedVal = shellQuote(val);
|
|
139
|
-
const scriptLines = [
|
|
140
|
-
`ENV_FILE=${quotedPath}`,
|
|
141
|
-
`KEY=${quotedKey}`,
|
|
142
|
-
`VAL=${quotedVal}`,
|
|
143
|
-
'mkdir -p "$(dirname "$ENV_FILE")"',
|
|
144
|
-
'touch "$ENV_FILE"',
|
|
145
|
-
'awk -v k="$KEY" -v v="$VAL" \'BEGIN{found=0} $0 ~ "^" k "=" { print k "=" v; found=1; next } { print } END { if (!found) print k "=" v }\' "$ENV_FILE" > "$ENV_FILE.tmp"',
|
|
146
|
-
'mv "$ENV_FILE.tmp" "$ENV_FILE"',
|
|
147
|
-
];
|
|
148
|
-
return this.buildBashScript(scriptLines);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const scriptLines = [
|
|
152
|
-
`ENV_FILE=${quotedPath}`,
|
|
153
|
-
`KEY=${quotedKey}`,
|
|
154
|
-
'if [ ! -f "$ENV_FILE" ]; then exit 0; fi',
|
|
155
|
-
'grep -v "^$KEY=" "$ENV_FILE" > "$ENV_FILE.tmp"',
|
|
156
|
-
'mv "$ENV_FILE.tmp" "$ENV_FILE"',
|
|
157
|
-
];
|
|
158
|
-
return this.buildBashScript(scriptLines);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private buildBashScript(scriptLines: string[]): string {
|
|
162
|
-
const script = scriptLines.join(' && ');
|
|
163
|
-
return `bash -lc ${shellQuote(script)}`;
|
|
164
|
-
}
|
|
165
|
-
}
|