kodu 1.1.21 → 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.
Files changed (129) hide show
  1. package/AGENTS.md +36 -68
  2. package/README.md +97 -96
  3. package/dist/package.json +1 -2
  4. package/dist/src/app.module.js +0 -8
  5. package/dist/src/app.module.js.map +1 -1
  6. package/dist/src/commands/init/init.command.d.ts +2 -9
  7. package/dist/src/commands/init/init.command.js +15 -241
  8. package/dist/src/commands/init/init.command.js.map +1 -1
  9. package/dist/src/commands/pack/pack.command.d.ts +9 -0
  10. package/dist/src/commands/pack/pack.command.js +72 -3
  11. package/dist/src/commands/pack/pack.command.js.map +1 -1
  12. package/dist/src/core/config/config.schema.d.ts +0 -46
  13. package/dist/src/core/config/config.schema.js +1 -51
  14. package/dist/src/core/config/config.schema.js.map +1 -1
  15. package/dist/src/core/config/config.service.js +2 -2
  16. package/dist/src/core/config/config.service.js.map +1 -1
  17. package/dist/src/core/config/prompt.service.d.ts +1 -4
  18. package/dist/src/core/config/prompt.service.js +4 -17
  19. package/dist/src/core/config/prompt.service.js.map +1 -1
  20. package/dist/src/core/file-system/fs.service.d.ts +1 -0
  21. package/dist/src/core/file-system/fs.service.js +4 -1
  22. package/dist/src/core/file-system/fs.service.js.map +1 -1
  23. package/dist/src/shared/constants.d.ts +0 -4
  24. package/dist/src/shared/constants.js +1 -5
  25. package/dist/src/shared/constants.js.map +1 -1
  26. package/dist/src/shared/git/git.module.js +0 -2
  27. package/dist/src/shared/git/git.module.js.map +1 -1
  28. package/dist/src/shared/git/git.service.d.ts +0 -8
  29. package/dist/src/shared/git/git.service.js +2 -34
  30. package/dist/src/shared/git/git.service.js.map +1 -1
  31. package/dist/src/shared/tokenizer/tokenizer.module.js +0 -2
  32. package/dist/src/shared/tokenizer/tokenizer.module.js.map +1 -1
  33. package/dist/src/shared/tokenizer/tokenizer.service.d.ts +0 -6
  34. package/dist/src/shared/tokenizer/tokenizer.service.js +8 -38
  35. package/dist/src/shared/tokenizer/tokenizer.service.js.map +1 -1
  36. package/dist/tsconfig.build.tsbuildinfo +1 -1
  37. package/kodu.schema.json +0 -139
  38. package/package.json +1 -2
  39. package/skills/kodu-ops/SKILL.md +184 -0
  40. package/src/app.module.ts +0 -8
  41. package/src/commands/init/init.command.ts +15 -310
  42. package/src/commands/pack/pack.command.ts +66 -3
  43. package/src/core/config/config.schema.ts +1 -68
  44. package/src/core/config/config.service.ts +2 -2
  45. package/src/core/config/prompt.service.ts +4 -26
  46. package/src/core/file-system/fs.service.ts +7 -1
  47. package/src/shared/constants.ts +0 -4
  48. package/src/shared/git/git.module.ts +0 -2
  49. package/src/shared/git/git.service.ts +1 -33
  50. package/src/shared/tokenizer/tokenizer.module.ts +0 -2
  51. package/src/shared/tokenizer/tokenizer.service.ts +9 -39
  52. package/.kodu/prompts/.keep +0 -0
  53. package/.kodu/prompts/commit.md +0 -9
  54. package/.kodu/prompts/pack.md +0 -7
  55. package/.kodu/prompts/review-bug.md +0 -6
  56. package/.kodu/prompts/review-security.md +0 -6
  57. package/.kodu/prompts/review-style.md +0 -6
  58. package/.opencode/command/openspec-apply.md +0 -24
  59. package/.opencode/command/openspec-archive.md +0 -27
  60. package/.opencode/command/openspec-proposal.md +0 -29
  61. package/.opencode/skills/kodu-ops/SKILL.md +0 -60
  62. package/dist/src/commands/commit/commit.command.d.ts +0 -18
  63. package/dist/src/commands/commit/commit.command.js +0 -149
  64. package/dist/src/commands/commit/commit.command.js.map +0 -1
  65. package/dist/src/commands/commit/commit.module.d.ts +0 -2
  66. package/dist/src/commands/commit/commit.module.js +0 -25
  67. package/dist/src/commands/commit/commit.module.js.map +0 -1
  68. package/dist/src/commands/ops/ops.command.d.ts +0 -4
  69. package/dist/src/commands/ops/ops.command.js +0 -39
  70. package/dist/src/commands/ops/ops.command.js.map +0 -1
  71. package/dist/src/commands/ops/ops.module.d.ts +0 -2
  72. package/dist/src/commands/ops/ops.module.js +0 -33
  73. package/dist/src/commands/ops/ops.module.js.map +0 -1
  74. package/dist/src/commands/ops/ops.types.d.ts +0 -13
  75. package/dist/src/commands/ops/ops.types.js +0 -12
  76. package/dist/src/commands/ops/ops.types.js.map +0 -1
  77. package/dist/src/commands/ops/ops.utils.d.ts +0 -13
  78. package/dist/src/commands/ops/ops.utils.js +0 -121
  79. package/dist/src/commands/ops/ops.utils.js.map +0 -1
  80. package/dist/src/commands/ops/subcommands/ops-env.command.d.ts +0 -24
  81. package/dist/src/commands/ops/subcommands/ops-env.command.js +0 -156
  82. package/dist/src/commands/ops/subcommands/ops-env.command.js.map +0 -1
  83. package/dist/src/commands/ops/subcommands/ops-routes.command.d.ts +0 -22
  84. package/dist/src/commands/ops/subcommands/ops-routes.command.js +0 -203
  85. package/dist/src/commands/ops/subcommands/ops-routes.command.js.map +0 -1
  86. package/dist/src/commands/ops/subcommands/ops-service.command.d.ts +0 -22
  87. package/dist/src/commands/ops/subcommands/ops-service.command.js +0 -169
  88. package/dist/src/commands/ops/subcommands/ops-service.command.js.map +0 -1
  89. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.d.ts +0 -14
  90. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js +0 -75
  91. package/dist/src/commands/ops/subcommands/ops-sysinfo.command.js.map +0 -1
  92. package/dist/src/commands/review/review.command.d.ts +0 -26
  93. package/dist/src/commands/review/review.command.js +0 -205
  94. package/dist/src/commands/review/review.command.js.map +0 -1
  95. package/dist/src/commands/review/review.module.d.ts +0 -2
  96. package/dist/src/commands/review/review.module.js +0 -26
  97. package/dist/src/commands/review/review.module.js.map +0 -1
  98. package/dist/src/core/config/default-prompts.d.ts +0 -9
  99. package/dist/src/core/config/default-prompts.js +0 -49
  100. package/dist/src/core/config/default-prompts.js.map +0 -1
  101. package/dist/src/shared/ai/ai.module.d.ts +0 -2
  102. package/dist/src/shared/ai/ai.module.js +0 -23
  103. package/dist/src/shared/ai/ai.module.js.map +0 -1
  104. package/dist/src/shared/ai/ai.service.d.ts +0 -22
  105. package/dist/src/shared/ai/ai.service.js +0 -164
  106. package/dist/src/shared/ai/ai.service.js.map +0 -1
  107. package/dist/src/shared/ssh/ssh.module.d.ts +0 -2
  108. package/dist/src/shared/ssh/ssh.module.js +0 -21
  109. package/dist/src/shared/ssh/ssh.module.js.map +0 -1
  110. package/dist/src/shared/ssh/ssh.service.d.ts +0 -11
  111. package/dist/src/shared/ssh/ssh.service.js +0 -53
  112. package/dist/src/shared/ssh/ssh.service.js.map +0 -1
  113. package/src/commands/commit/commit.command.ts +0 -139
  114. package/src/commands/commit/commit.module.ts +0 -12
  115. package/src/commands/ops/ops.command.ts +0 -30
  116. package/src/commands/ops/ops.module.ts +0 -20
  117. package/src/commands/ops/ops.types.ts +0 -24
  118. package/src/commands/ops/ops.utils.ts +0 -160
  119. package/src/commands/ops/subcommands/ops-env.command.ts +0 -165
  120. package/src/commands/ops/subcommands/ops-routes.command.ts +0 -221
  121. package/src/commands/ops/subcommands/ops-service.command.ts +0 -190
  122. package/src/commands/ops/subcommands/ops-sysinfo.command.ts +0 -77
  123. package/src/commands/review/review.command.ts +0 -199
  124. package/src/commands/review/review.module.ts +0 -13
  125. package/src/core/config/default-prompts.ts +0 -53
  126. package/src/shared/ai/ai.module.ts +0 -10
  127. package/src/shared/ai/ai.service.ts +0 -216
  128. package/src/shared/ssh/ssh.module.ts +0 -8
  129. package/src/shared/ssh/ssh.service.ts +0 -61
@@ -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
- }
@@ -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
- }