@tdk-landscape/tdk-cli-core 1.3.126 → 1.3.127
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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ Notable changes to the `tdk` CLI. Versions match [npm](https://www.npmjs.com/pac
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 1.3.127 (2026-10-08)
|
|
8
|
+
|
|
9
|
+
- Security hardening flagged by Socket (#826). The npm package no longer includes `ext/ide-components/code_executor/server.py`, a browser terminal that ran client-supplied commands through a shell. `tdk` and the Tilt engine never started it. The other IDE components are unchanged. `tdk doctor` now single-quotes the registry URL and container name it passes to `curl` and `docker logs`, so a value such as `$(...)` can no longer run a command.
|
|
10
|
+
|
|
7
11
|
## 1.3.126 (2026-10-08)
|
|
8
12
|
|
|
9
13
|
- Security: fixes [GHSA-phgf-pww4-7jxc](https://github.com/tdk-landscape/tdk-cli-core/security/advisories/GHSA-phgf-pww4-7jxc) (all earlier versions). A line break in a `service.json` value was written into the generated Docker Compose file and could add keys such as `privileged: true` to a service when you ran `tdk up` or `tilt up` on a repository you did not write. `tdk` and the Tilt engine now refuse any `service.json` string key or value that contains a line break (including U+0085, U+2028 and U+2029) or another control character, and `healthCheckPath`, `traefik.host`, `traefik.pathPrefix`, `traefik.healthCheck`, `nats.queueGroup`, `databaseName`, `stack` and `image` must also match a pattern. `smoke` is exempt. `tdk up` and `tdk config regenerate` stop with an error that names the field and does not print the value, and `tilt up` stops the same way. Upgrade, and check `service.json` files from repositories you did not write.
|
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/quickstart-e2e.yml)
|
|
8
8
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/erp-scale-e2e.yml)
|
|
9
9
|
[](https://github.com/tdk-landscape/tdk-cli-core/actions/workflows/examples-e2e.yml)
|
|
10
|
+
[](https://github.com/tdk-landscape/tdk-cli-core/security/code-scanning)
|
|
11
|
+
[](https://scorecard.dev/viewer/?uri=github.com/tdk-landscape/tdk-cli-core)
|
|
10
12
|
[](https://snyk.io/test/github/tdk-landscape/tdk-cli-core)
|
|
11
13
|
[](https://socket.dev/npm/package/@tdk-landscape/tdk-cli-core/overview)
|
|
12
14
|
[](LICENSE)
|
|
@@ -17,6 +17,13 @@ export const HOST_PORT_FIXES = {
|
|
|
17
17
|
port80: "Stop the process bound to port 80, or stop local nginx/caddy. Then: tdk doctor",
|
|
18
18
|
port5432: "Stop local Postgres or change the host port. Then: tdk doctor",
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Quote a value for a POSIX shell command line. JSON.stringify is not enough:
|
|
22
|
+
* inside double quotes `$(...)`, backticks and `$VAR` are still expanded.
|
|
23
|
+
*/
|
|
24
|
+
export function shellQuote(value) {
|
|
25
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
26
|
+
}
|
|
20
27
|
function runHostCommand(exec, command, args, shellCommand, options) {
|
|
21
28
|
if (process.platform === "win32") {
|
|
22
29
|
return execFileSync(findOnPath(command) ?? command, args, {
|
|
@@ -269,7 +276,7 @@ export function checkPrivateNpmRegistry(exec = execSync, projectRoot = findProje
|
|
|
269
276
|
timeout: REGISTRY_PROBE_TIMEOUT_MS + 1_000,
|
|
270
277
|
windowsHide: true,
|
|
271
278
|
}).trim()
|
|
272
|
-
: exec(`curl -fsS -o /dev/null -w '%{http_code}' --max-time 2 ${
|
|
279
|
+
: exec(`curl -fsS -o /dev/null -w '%{http_code}' --max-time 2 ${shellQuote(registryUrl)}`, {
|
|
273
280
|
stdio: "pipe",
|
|
274
281
|
encoding: "utf-8",
|
|
275
282
|
timeout: REGISTRY_PROBE_TIMEOUT_MS + 1_000,
|
|
@@ -648,7 +655,7 @@ export function probeContainerRuntimeError(resourceName, exec = execSync) {
|
|
|
648
655
|
names.find((name) => name.includes(resourceName));
|
|
649
656
|
if (!match)
|
|
650
657
|
return null;
|
|
651
|
-
const logs = runHostCommand(exec, "docker", ["logs", "--tail", "120", match], `docker logs --tail 120 ${
|
|
658
|
+
const logs = runHostCommand(exec, "docker", ["logs", "--tail", "120", match], `docker logs --tail 120 ${shellQuote(match)} 2>&1`, {
|
|
652
659
|
stdio: "pipe",
|
|
653
660
|
encoding: "utf-8",
|
|
654
661
|
timeout: EXEC_TIMEOUT_MS,
|
package/cli/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tdk-cli-internal",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.127",
|
|
5
5
|
"description": "TDK CLI — start services on your laptop.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"tdk",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"discovery",
|
|
39
39
|
"specs",
|
|
40
40
|
"ext",
|
|
41
|
+
"!ext/ide-components/code_executor",
|
|
41
42
|
"shared-platform-engineering/docker-templates"
|
|
42
43
|
],
|
|
43
44
|
"scripts": {
|
|
@@ -13,6 +13,8 @@ The Tilt IDE provides browser-based tools for browsing, viewing, and executing c
|
|
|
13
13
|
| **Config Inspector** | 9767 | http://configs.localhost | http://localhost:9767 |
|
|
14
14
|
| **Code Executor** | 9768 | http://terminal.localhost | http://localhost:9768 |
|
|
15
15
|
|
|
16
|
+
> **Note:** `code_executor` is not shipped in the npm package or started by `tilt up` (see #826). Its source stays in the repository.
|
|
17
|
+
|
|
16
18
|
## Quick Start
|
|
17
19
|
|
|
18
20
|
All components start automatically when you run `tilt up`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tdk-landscape/tdk-cli-core",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.127",
|
|
4
4
|
"description": "TDK CLI — start services on your laptop.",
|
|
5
5
|
"homepage": "https://tdk-landscape.github.io/tdk-website",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"discovery",
|
|
26
26
|
"specs",
|
|
27
27
|
"ext",
|
|
28
|
+
"!ext/ide-components/code_executor",
|
|
28
29
|
"shared-platform-engineering/docker-templates",
|
|
29
30
|
"examples/tdk-example",
|
|
30
31
|
"cli/bin",
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Tilt IDE Code Executor
|
|
4
|
-
Execute commands from web UI at http://localhost:9768
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import subprocess
|
|
10
|
-
import sys
|
|
11
|
-
import urllib.parse
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
# Add shared modules
|
|
15
|
-
sys.path.insert(0, str(Path(__file__).parent.parent / 'shared'))
|
|
16
|
-
from file_utils import PROJECT_ROOT
|
|
17
|
-
from http_utils import BaseIDEHandler, run_server
|
|
18
|
-
|
|
19
|
-
PORT = 9768
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class CodeExecutorHandler(BaseIDEHandler):
|
|
23
|
-
"""HTTP request handler for code executor"""
|
|
24
|
-
|
|
25
|
-
# Command validation constants
|
|
26
|
-
DANGEROUS_COMMANDS = [
|
|
27
|
-
'rm -rf /', 'rm -rf /*', ':(){ :|:& };:', '> /dev/sda',
|
|
28
|
-
'mv / /dev/null', 'dd if=/dev/zero', 'mkfs.', 'chmod -R 777 /',
|
|
29
|
-
'sudo', 'su -', 'passwd', 'deluser', 'userdel'
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
ALLOWED_PREFIXES = [
|
|
33
|
-
'bun ', 'ls ', 'pwd', 'echo ', 'cat ', 'head ', 'tail ',
|
|
34
|
-
'grep ', 'find ', 'cd ', 'mkdir ', 'touch ', 'clear', 'exit',
|
|
35
|
-
'npm ', 'yarn ', 'npx ', 'pnpm ', 'git ', 'curl ', 'wget '
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
def validate_command(self, cmd: str) -> tuple:
|
|
39
|
-
"""
|
|
40
|
-
Validate command for safety.
|
|
41
|
-
Returns (is_valid, error_message)
|
|
42
|
-
"""
|
|
43
|
-
# Block dangerous commands
|
|
44
|
-
for dangerous in self.DANGEROUS_COMMANDS:
|
|
45
|
-
if dangerous in cmd.lower():
|
|
46
|
-
return False, f"Command blocked for safety: {dangerous}"
|
|
47
|
-
|
|
48
|
-
# Only allow bun commands and safe system commands
|
|
49
|
-
cmd_stripped = cmd.strip()
|
|
50
|
-
is_allowed = any(cmd_stripped.startswith(prefix) for prefix in self.ALLOWED_PREFIXES)
|
|
51
|
-
|
|
52
|
-
if not is_allowed:
|
|
53
|
-
return False, "Only bun, npm, git, and safe system commands are allowed"
|
|
54
|
-
|
|
55
|
-
return True, None
|
|
56
|
-
|
|
57
|
-
def execute_command(self, cmd: str, cwd: str = None) -> dict:
|
|
58
|
-
"""Execute command and return result"""
|
|
59
|
-
# Validate
|
|
60
|
-
is_valid, error = self.validate_command(cmd)
|
|
61
|
-
if not is_valid:
|
|
62
|
-
return {'success': False, 'stdout': '', 'stderr': error, 'exit_code': 1}
|
|
63
|
-
|
|
64
|
-
# Determine working directory
|
|
65
|
-
work_dir = cwd or str(PROJECT_ROOT)
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
# Execute with timeout
|
|
69
|
-
result = subprocess.run(
|
|
70
|
-
cmd,
|
|
71
|
-
shell=True,
|
|
72
|
-
cwd=work_dir,
|
|
73
|
-
capture_output=True,
|
|
74
|
-
text=True,
|
|
75
|
-
timeout=60, # 60 second timeout
|
|
76
|
-
env={**os.environ, 'TERM': 'xterm-color'}
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
'success': result.returncode == 0,
|
|
81
|
-
'stdout': result.stdout,
|
|
82
|
-
'stderr': result.stderr,
|
|
83
|
-
'exit_code': result.returncode
|
|
84
|
-
}
|
|
85
|
-
except subprocess.TimeoutExpired:
|
|
86
|
-
return {'success': False, 'stdout': '', 'stderr': 'Command timed out (60s limit)', 'exit_code': -1}
|
|
87
|
-
except Exception as e:
|
|
88
|
-
return {'success': False, 'stdout': '', 'stderr': str(e), 'exit_code': -1}
|
|
89
|
-
|
|
90
|
-
def _get_template_path(self) -> Path:
|
|
91
|
-
"""Override to use shared template"""
|
|
92
|
-
return Path(__file__).parent.parent / 'shared' / 'templates' / 'base.html'
|
|
93
|
-
|
|
94
|
-
def do_GET(self):
|
|
95
|
-
"""Handle GET requests"""
|
|
96
|
-
parsed = urllib.parse.urlparse(self.path)
|
|
97
|
-
path = parsed.path
|
|
98
|
-
query = urllib.parse.parse_qs(parsed.query)
|
|
99
|
-
|
|
100
|
-
# Health check
|
|
101
|
-
if path == '/health':
|
|
102
|
-
self.handle_health_check('code-executor')
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
# Static files
|
|
106
|
-
if path.startswith('/static/'):
|
|
107
|
-
static_path = Path(__file__).parent.parent / 'shared' / path[1:]
|
|
108
|
-
if static_path.exists():
|
|
109
|
-
with open(static_path, 'r') as f:
|
|
110
|
-
content = f.read()
|
|
111
|
-
self.send_css_response(content)
|
|
112
|
-
else:
|
|
113
|
-
self.send_error_response(404, 'Not found')
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
if path in ('/', '/terminal'):
|
|
117
|
-
cwd = query.get('cwd', [str(PROJECT_ROOT)])[0]
|
|
118
|
-
cwd_display = self.escape_html(cwd.replace(str(PROJECT_ROOT), '~'))
|
|
119
|
-
|
|
120
|
-
content = f'''
|
|
121
|
-
<div class="terminal" style="height: calc(100vh - 200px);">
|
|
122
|
-
<div class="terminal-header">
|
|
123
|
-
<div>
|
|
124
|
-
<span class="panel-title">💻 Terminal</span>
|
|
125
|
-
<span class="text-muted" style="margin-left: 1rem; font-size: 0.85rem;">Working directory: {cwd_display}</span>
|
|
126
|
-
</div>
|
|
127
|
-
<div>
|
|
128
|
-
<span class="text-muted" style="font-size: 0.8rem;">Supports: bun, npm, git, basic shell</span>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="terminal-content" id="terminal-output" style="white-space: pre-wrap; font-family: var(--font-mono);">
|
|
132
|
-
<div class="text-muted">TDK Landscape Tilt IDE Terminal</div>
|
|
133
|
-
<div class="text-muted">Type a command and press Enter to execute</div>
|
|
134
|
-
<div class="text-muted">─────────────────────────────────────────</div>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="terminal-input-line">
|
|
137
|
-
<span class="terminal-prompt">$</span>
|
|
138
|
-
<input type="text" id="terminal-input" class="terminal-input"
|
|
139
|
-
placeholder="Enter command (e.g., bun test, ls -la)..."
|
|
140
|
-
autocomplete="off" spellcheck="false">
|
|
141
|
-
</div>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
<script>
|
|
145
|
-
const terminalOutput = document.getElementById('terminal-output');
|
|
146
|
-
const terminalInput = document.getElementById('terminal-input');
|
|
147
|
-
const cwd = {json.dumps(cwd)};
|
|
148
|
-
let commandHistory = [];
|
|
149
|
-
let historyIndex = -1;
|
|
150
|
-
|
|
151
|
-
terminalInput.addEventListener('keydown', async function(e) {{
|
|
152
|
-
if (e.key === 'Enter') {{
|
|
153
|
-
const cmd = this.value.trim();
|
|
154
|
-
if (!cmd) return;
|
|
155
|
-
|
|
156
|
-
// Add to history
|
|
157
|
-
commandHistory.push(cmd);
|
|
158
|
-
historyIndex = commandHistory.length;
|
|
159
|
-
|
|
160
|
-
// Display command
|
|
161
|
-
terminalOutput.innerHTML += `<div><span style="color: var(--accent-primary);">$</span> ${{self.escape_html(cmd)}}</div>`;
|
|
162
|
-
terminalInput.value = '';
|
|
163
|
-
|
|
164
|
-
// Execute
|
|
165
|
-
try {{
|
|
166
|
-
const response = await fetch('/execute', {{
|
|
167
|
-
method: 'POST',
|
|
168
|
-
headers: {{'Content-Type': 'application/json'}},
|
|
169
|
-
body: JSON.stringify({{command: cmd, cwd: cwd}})
|
|
170
|
-
}});
|
|
171
|
-
const result = await response.json();
|
|
172
|
-
|
|
173
|
-
// Display output
|
|
174
|
-
if (result.stdout) {{
|
|
175
|
-
terminalOutput.innerHTML += `<div style="color: var(--text-primary);">${{self.escape_html(result.stdout)}}</div>`;
|
|
176
|
-
}}
|
|
177
|
-
if (result.stderr) {{
|
|
178
|
-
terminalOutput.innerHTML += `<div style="color: var(--error);">${{self.escape_html(result.stderr)}}</div>`;
|
|
179
|
-
}}
|
|
180
|
-
|
|
181
|
-
// Status
|
|
182
|
-
const statusColor = result.success ? 'var(--success)' : 'var(--error)';
|
|
183
|
-
terminalOutput.innerHTML += `<div style="color: ${{statusColor}}; font-size: 0.8rem;">Exit code: ${{result.exit_code}}</div>`;
|
|
184
|
-
|
|
185
|
-
}} catch (err) {{
|
|
186
|
-
terminalOutput.innerHTML += `<div style="color: var(--error);">Error: ${{err.message}}</div>`;
|
|
187
|
-
}}
|
|
188
|
-
|
|
189
|
-
// Scroll to bottom
|
|
190
|
-
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
|
191
|
-
}}
|
|
192
|
-
|
|
193
|
-
// History navigation
|
|
194
|
-
if (e.key === 'ArrowUp') {{
|
|
195
|
-
e.preventDefault();
|
|
196
|
-
if (historyIndex > 0) {{
|
|
197
|
-
historyIndex--;
|
|
198
|
-
terminalInput.value = commandHistory[historyIndex];
|
|
199
|
-
}}
|
|
200
|
-
}}
|
|
201
|
-
if (e.key === 'ArrowDown') {{
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
if (historyIndex < commandHistory.length - 1) {{
|
|
204
|
-
historyIndex++;
|
|
205
|
-
terminalInput.value = commandHistory[historyIndex];
|
|
206
|
-
}} else {{
|
|
207
|
-
historyIndex = commandHistory.length;
|
|
208
|
-
terminalInput.value = '';
|
|
209
|
-
}}
|
|
210
|
-
}}
|
|
211
|
-
}});
|
|
212
|
-
|
|
213
|
-
terminalInput.focus();
|
|
214
|
-
</script>
|
|
215
|
-
'''
|
|
216
|
-
|
|
217
|
-
self.send_html_response(self.render_page('Terminal', content, icon='💻'))
|
|
218
|
-
return
|
|
219
|
-
|
|
220
|
-
# 404
|
|
221
|
-
self.send_html_response(self.render_page(
|
|
222
|
-
'Not Found',
|
|
223
|
-
'<div class="empty-state"><div class="empty-state-icon">❓</div>Page not found</div>',
|
|
224
|
-
icon='❓'
|
|
225
|
-
), 404)
|
|
226
|
-
|
|
227
|
-
def do_POST(self):
|
|
228
|
-
"""Handle POST requests"""
|
|
229
|
-
parsed = urllib.parse.urlparse(self.path)
|
|
230
|
-
|
|
231
|
-
if parsed.path == '/execute':
|
|
232
|
-
# Read request body
|
|
233
|
-
content_length = int(self.headers.get('Content-Length', 0))
|
|
234
|
-
body = self.rfile.read(content_length).decode()
|
|
235
|
-
|
|
236
|
-
try:
|
|
237
|
-
data = json.loads(body)
|
|
238
|
-
cmd = data.get('command', '').strip()
|
|
239
|
-
cwd = data.get('cwd', str(PROJECT_ROOT))
|
|
240
|
-
|
|
241
|
-
if not cmd:
|
|
242
|
-
self.send_json_response({'success': False, 'error': 'No command provided'}, 400)
|
|
243
|
-
return
|
|
244
|
-
|
|
245
|
-
result = self.execute_command(cmd, cwd)
|
|
246
|
-
self.send_json_response(result)
|
|
247
|
-
|
|
248
|
-
except json.JSONDecodeError:
|
|
249
|
-
self.send_json_response({'success': False, 'error': 'Invalid JSON'}, 400)
|
|
250
|
-
except Exception as e:
|
|
251
|
-
self.send_json_response({'success': False, 'error': str(e)}, 500)
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
self.send_json_response({'error': 'Not found'}, 404)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
def main():
|
|
258
|
-
"""Run the code executor server"""
|
|
259
|
-
run_server(
|
|
260
|
-
CodeExecutorHandler,
|
|
261
|
-
PORT,
|
|
262
|
-
'💻 Tilt IDE Code Executor',
|
|
263
|
-
'🚀 Execute bun commands from the browser\n ⚠️ Safety: Only bun, npm, git, and safe system commands allowed'
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if __name__ == '__main__':
|
|
268
|
-
main()
|