local-serverless-stack 0.17.2 → 1.0.1

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 (228) hide show
  1. package/CHANGELOG.md +327 -0
  2. package/README.md +1155 -221
  3. package/assets/icon.svg +37 -0
  4. package/assets/logo.svg +38 -0
  5. package/bin/cli.js +519 -228
  6. package/bin/i18n.js +525 -0
  7. package/dist/client/config-discovery.d.ts.map +1 -1
  8. package/dist/client/config-discovery.js +2 -1
  9. package/dist/client/config-discovery.js.map +1 -1
  10. package/dist/client/http.d.ts +7 -0
  11. package/dist/client/http.d.ts.map +1 -1
  12. package/dist/client/http.js +3 -2
  13. package/dist/client/http.js.map +1 -1
  14. package/dist/client/lifecycle.d.ts +1 -4
  15. package/dist/client/lifecycle.d.ts.map +1 -1
  16. package/dist/client/lifecycle.js +2 -8
  17. package/dist/client/lifecycle.js.map +1 -1
  18. package/dist/client/namespaces/health.d.ts +1 -1
  19. package/dist/client/namespaces/health.d.ts.map +1 -1
  20. package/dist/client/namespaces/queues.d.ts.map +1 -1
  21. package/dist/client/namespaces/queues.js +9 -0
  22. package/dist/client/namespaces/queues.js.map +1 -1
  23. package/dist/client/namespaces/seeds.d.ts +1 -1
  24. package/dist/client/namespaces/services.d.ts +34 -0
  25. package/dist/client/namespaces/services.d.ts.map +1 -1
  26. package/dist/client/namespaces/services.js +3 -0
  27. package/dist/client/namespaces/services.js.map +1 -1
  28. package/dist/client/types.d.ts +17 -5
  29. package/dist/client/types.d.ts.map +1 -1
  30. package/dist/mcp/http.d.ts +15 -0
  31. package/dist/mcp/http.d.ts.map +1 -0
  32. package/dist/mcp/http.js +86 -0
  33. package/dist/mcp/http.js.map +1 -0
  34. package/dist/mcp/protocol.d.ts +36 -0
  35. package/dist/mcp/protocol.d.ts.map +1 -0
  36. package/dist/mcp/protocol.js +96 -0
  37. package/dist/mcp/protocol.js.map +1 -0
  38. package/dist/mcp/server.d.ts +7 -0
  39. package/dist/mcp/server.d.ts.map +1 -0
  40. package/dist/mcp/server.js +41 -0
  41. package/dist/mcp/server.js.map +1 -0
  42. package/dist/mcp/tools.d.ts +18 -0
  43. package/dist/mcp/tools.d.ts.map +1 -0
  44. package/dist/mcp/tools.js +323 -0
  45. package/dist/mcp/tools.js.map +1 -0
  46. package/dist/server/dev/dynamo-proxy.d.ts.map +1 -1
  47. package/dist/server/dev/dynamo-proxy.js +16 -2
  48. package/dist/server/dev/dynamo-proxy.js.map +1 -1
  49. package/dist/server/engine/backends/self-backend.d.ts +11 -2
  50. package/dist/server/engine/backends/self-backend.d.ts.map +1 -1
  51. package/dist/server/engine/backends/self-backend.js +42 -1
  52. package/dist/server/engine/backends/self-backend.js.map +1 -1
  53. package/dist/server/engine/dispatch/dispatcher.js +2 -2
  54. package/dist/server/engine/dispatch/dispatcher.js.map +1 -1
  55. package/dist/server/engine/engine-manager.d.ts +6 -1
  56. package/dist/server/engine/engine-manager.d.ts.map +1 -1
  57. package/dist/server/engine/engine-manager.js +30 -27
  58. package/dist/server/engine/engine-manager.js.map +1 -1
  59. package/dist/server/engine/http/errors.js +1 -1
  60. package/dist/server/engine/http/errors.js.map +1 -1
  61. package/dist/server/engine/http/is-aws-request.d.ts +8 -0
  62. package/dist/server/engine/http/is-aws-request.d.ts.map +1 -0
  63. package/dist/server/engine/http/is-aws-request.js +53 -0
  64. package/dist/server/engine/http/is-aws-request.js.map +1 -0
  65. package/dist/server/engine/http/router.d.ts.map +1 -1
  66. package/dist/server/engine/http/router.js +7 -1
  67. package/dist/server/engine/http/router.js.map +1 -1
  68. package/dist/server/engine/store/engine-store.d.ts +1 -0
  69. package/dist/server/engine/store/engine-store.d.ts.map +1 -1
  70. package/dist/server/engine/store/engine-store.js +2 -1
  71. package/dist/server/engine/store/engine-store.js.map +1 -1
  72. package/dist/server/engine/store/memory-store.d.ts +16 -0
  73. package/dist/server/engine/store/memory-store.d.ts.map +1 -0
  74. package/dist/server/engine/store/memory-store.js +167 -0
  75. package/dist/server/engine/store/memory-store.js.map +1 -0
  76. package/dist/server/engine/store/wal.d.ts +3 -0
  77. package/dist/server/engine/store/wal.d.ts.map +1 -1
  78. package/dist/server/engine/store/wal.js +35 -1
  79. package/dist/server/engine/store/wal.js.map +1 -1
  80. package/dist/server/index.js +159 -44
  81. package/dist/server/index.js.map +1 -1
  82. package/dist/server/routes/config.d.ts.map +1 -1
  83. package/dist/server/routes/config.js +31 -52
  84. package/dist/server/routes/config.js.map +1 -1
  85. package/dist/server/routes/lambdas.d.ts.map +1 -1
  86. package/dist/server/routes/lambdas.js +77 -0
  87. package/dist/server/routes/lambdas.js.map +1 -1
  88. package/dist/server/routes/services.d.ts.map +1 -1
  89. package/dist/server/routes/services.js +288 -24
  90. package/dist/server/routes/services.js.map +1 -1
  91. package/dist/server/runtime/runtime-worker.js +7 -0
  92. package/dist/server/runtime/runtime-worker.js.map +1 -1
  93. package/dist/server/services/bind-host.d.ts +30 -0
  94. package/dist/server/services/bind-host.d.ts.map +1 -0
  95. package/dist/server/services/bind-host.js +182 -0
  96. package/dist/server/services/bind-host.js.map +1 -0
  97. package/dist/server/services/cache-manager.d.ts +7 -1
  98. package/dist/server/services/cache-manager.d.ts.map +1 -1
  99. package/dist/server/services/cache-manager.js +4 -14
  100. package/dist/server/services/cache-manager.js.map +1 -1
  101. package/dist/server/services/cloudformation-parser.js +3 -3
  102. package/dist/server/services/cloudformation-parser.js.map +1 -1
  103. package/dist/server/services/config-manager.d.ts +53 -35
  104. package/dist/server/services/config-manager.d.ts.map +1 -1
  105. package/dist/server/services/config-manager.js +600 -185
  106. package/dist/server/services/config-manager.js.map +1 -1
  107. package/dist/server/services/dynamo-explorer.d.ts +1 -0
  108. package/dist/server/services/dynamo-explorer.d.ts.map +1 -1
  109. package/dist/server/services/dynamo-explorer.js +28 -6
  110. package/dist/server/services/dynamo-explorer.js.map +1 -1
  111. package/dist/server/services/explorer-region.d.ts +2 -0
  112. package/dist/server/services/explorer-region.d.ts.map +1 -0
  113. package/dist/server/services/explorer-region.js +23 -0
  114. package/dist/server/services/explorer-region.js.map +1 -0
  115. package/dist/server/services/gateway-manager.d.ts.map +1 -1
  116. package/dist/server/services/gateway-manager.js +18 -5
  117. package/dist/server/services/gateway-manager.js.map +1 -1
  118. package/dist/server/services/invocation-activity.d.ts +45 -0
  119. package/dist/server/services/invocation-activity.d.ts.map +1 -0
  120. package/dist/server/services/invocation-activity.js +102 -0
  121. package/dist/server/services/invocation-activity.js.map +1 -0
  122. package/dist/server/services/lambda-runtime-manager.d.ts +8 -1
  123. package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
  124. package/dist/server/services/lambda-runtime-manager.js +131 -11
  125. package/dist/server/services/lambda-runtime-manager.js.map +1 -1
  126. package/dist/server/services/opensearch-explorer.d.ts.map +1 -1
  127. package/dist/server/services/opensearch-explorer.js +14 -10
  128. package/dist/server/services/opensearch-explorer.js.map +1 -1
  129. package/dist/server/services/project-scope.d.ts +2 -0
  130. package/dist/server/services/project-scope.d.ts.map +1 -0
  131. package/dist/server/services/project-scope.js +23 -0
  132. package/dist/server/services/project-scope.js.map +1 -0
  133. package/dist/server/services/queue-inspector.d.ts.map +1 -1
  134. package/dist/server/services/queue-inspector.js +15 -6
  135. package/dist/server/services/queue-inspector.js.map +1 -1
  136. package/dist/server/services/resource-provisioner.d.ts +19 -3
  137. package/dist/server/services/resource-provisioner.d.ts.map +1 -1
  138. package/dist/server/services/resource-provisioner.js +153 -26
  139. package/dist/server/services/resource-provisioner.js.map +1 -1
  140. package/dist/server/services/s3-explorer.js +4 -4
  141. package/dist/server/services/s3-explorer.js.map +1 -1
  142. package/dist/server/services/secrets-explorer.d.ts.map +1 -1
  143. package/dist/server/services/secrets-explorer.js +10 -3
  144. package/dist/server/services/secrets-explorer.js.map +1 -1
  145. package/dist/server/services/seed-manager.d.ts.map +1 -1
  146. package/dist/server/services/seed-manager.js +28 -14
  147. package/dist/server/services/seed-manager.js.map +1 -1
  148. package/dist/server/services/serverless-state-parser.d.ts +10 -0
  149. package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
  150. package/dist/server/services/serverless-state-parser.js +20 -1
  151. package/dist/server/services/serverless-state-parser.js.map +1 -1
  152. package/dist/server/services/service-registrar.d.ts +13 -1
  153. package/dist/server/services/service-registrar.d.ts.map +1 -1
  154. package/dist/server/services/service-registrar.js +80 -16
  155. package/dist/server/services/service-registrar.js.map +1 -1
  156. package/dist/server/services/service-scanner.d.ts +25 -0
  157. package/dist/server/services/service-scanner.d.ts.map +1 -0
  158. package/dist/server/services/service-scanner.js +219 -0
  159. package/dist/server/services/service-scanner.js.map +1 -0
  160. package/dist/server/services/source-watcher.d.ts.map +1 -1
  161. package/dist/server/services/source-watcher.js +31 -6
  162. package/dist/server/services/source-watcher.js.map +1 -1
  163. package/dist/ui/assets/ApisPage-CTOcNna-.js +1 -0
  164. package/dist/ui/assets/BucketDetailPage-BXW8-FrE.js +1 -0
  165. package/dist/ui/assets/BucketsPage-DY_ITepb.js +1 -0
  166. package/dist/ui/assets/DynamoPage-BnFFKnIU.js +1 -0
  167. package/dist/ui/assets/DynamoTablePage-BLCuyJHU.js +3 -0
  168. package/dist/ui/assets/LambdaDetailPage-D4rZNLRT.js +4 -0
  169. package/dist/ui/assets/LambdasPage-Bg-MWFBb.js +1 -0
  170. package/dist/ui/assets/OnboardingPage-nnl313Fm.js +1 -0
  171. package/dist/ui/assets/OpenSearchCollectionPage-Dn15lH_F.js +1 -0
  172. package/dist/ui/assets/OpenSearchPage-CgD5isok.js +1 -0
  173. package/dist/ui/assets/{OverviewPage-DS6XJ7kG.css → OverviewPage-CSWY71vZ.css} +1 -1
  174. package/dist/ui/assets/OverviewPage-m5sUr8IN.js +1 -0
  175. package/dist/ui/assets/QueueDetailPage-Ch_FDDoN.js +1 -0
  176. package/dist/ui/assets/QueuesPage-C_yDuGDu.js +1 -0
  177. package/dist/ui/assets/SecretsPage-BSGMSoqy.js +1 -0
  178. package/dist/ui/assets/ServiceDetailPage-6KyohyjI.js +2 -0
  179. package/dist/ui/assets/ServicesPage-BbSetOMo.js +2 -0
  180. package/dist/ui/assets/SettingsPage-BgF1jFr6.js +1 -0
  181. package/dist/ui/assets/engine-D0mFFNbL.js +1 -0
  182. package/dist/ui/assets/index-D627ZyYU.css +1 -0
  183. package/dist/ui/assets/index-DuSPnI19.js +37 -0
  184. package/dist/ui/assets/onboarding-DQmlvyWU.js +1 -0
  185. package/dist/ui/assets/resourceIcons-baecADXA.js +1 -0
  186. package/dist/ui/assets/triggerIcons-DIPWQkJ3.js +1 -0
  187. package/dist/ui/favicon.svg +37 -0
  188. package/dist/ui/index.html +5 -3
  189. package/package.json +11 -11
  190. package/dist/server/engine/aoss-sidecar.d.ts +0 -13
  191. package/dist/server/engine/aoss-sidecar.d.ts.map +0 -1
  192. package/dist/server/engine/aoss-sidecar.js +0 -93
  193. package/dist/server/engine/aoss-sidecar.js.map +0 -1
  194. package/dist/server/engine/backends/localstack-backend.d.ts +0 -25
  195. package/dist/server/engine/backends/localstack-backend.d.ts.map +0 -1
  196. package/dist/server/engine/backends/localstack-backend.js +0 -199
  197. package/dist/server/engine/backends/localstack-backend.js.map +0 -1
  198. package/dist/server/engine/engine-backend.d.ts +0 -17
  199. package/dist/server/engine/engine-backend.d.ts.map +0 -1
  200. package/dist/server/engine/engine-backend.js +0 -7
  201. package/dist/server/engine/engine-backend.js.map +0 -1
  202. package/dist/server/services/localstack-manager.d.ts +0 -17
  203. package/dist/server/services/localstack-manager.d.ts.map +0 -1
  204. package/dist/server/services/localstack-manager.js +0 -31
  205. package/dist/server/services/localstack-manager.js.map +0 -1
  206. package/dist/ui/assets/ApisPage-CXYcWDhm.js +0 -1
  207. package/dist/ui/assets/BucketDetailPage-BwS2--Pe.js +0 -1
  208. package/dist/ui/assets/BucketsPage-khuiYc9C.js +0 -1
  209. package/dist/ui/assets/DynamoPage-aYo0QC0N.js +0 -1
  210. package/dist/ui/assets/DynamoTablePage-rFuV2pgd.js +0 -3
  211. package/dist/ui/assets/LambdaDetailPage-CJmu8M_N.js +0 -4
  212. package/dist/ui/assets/LambdasPage-DniNyKMe.js +0 -1
  213. package/dist/ui/assets/OpenSearchCollectionPage-BBwjgbB9.js +0 -1
  214. package/dist/ui/assets/OpenSearchPage-D5RxJGRo.js +0 -1
  215. package/dist/ui/assets/OverviewPage-Cyt1Xdp8.js +0 -1
  216. package/dist/ui/assets/QueueDetailPage-3FPsowIp.js +0 -1
  217. package/dist/ui/assets/QueuesPage-CUoDDgHr.js +0 -1
  218. package/dist/ui/assets/SecretsPage-B_mTnUf3.js +0 -1
  219. package/dist/ui/assets/ServiceDetailPage-BcdSjA6e.js +0 -2
  220. package/dist/ui/assets/ServicesPage-CU6fb3f8.js +0 -2
  221. package/dist/ui/assets/SettingsPage-BG8cYxgC.js +0 -1
  222. package/dist/ui/assets/index-BxwA-UT0.css +0 -1
  223. package/dist/ui/assets/index-DGsXIPJ-.js +0 -37
  224. package/packages/serverless-plugin/README.md +0 -115
  225. package/packages/serverless-plugin/dist/index.d.ts +0 -2
  226. package/packages/serverless-plugin/dist/index.d.ts.map +0 -1
  227. package/packages/serverless-plugin/dist/index.js +0 -107
  228. package/packages/serverless-plugin/dist/index.js.map +0 -1
package/bin/cli.js CHANGED
@@ -4,10 +4,17 @@ const { spawn } = require('child_process');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
+ const { pathToFileURL } = require('url');
8
+ // Every user-facing string goes through t(). The locale is resolved once, at
9
+ // require time, from LSS_LANG / LC_ALL / LC_MESSAGES / LANG — English by
10
+ // default. See bin/i18n.js for what is deliberately left untranslated.
11
+ const { t } = require('./i18n');
7
12
 
8
13
  // Default paths used when no config is present. The actual paths are derived
9
14
  // per-invocation by runtimePaths() below, scoped to the configured serverPort
10
15
  // so multiple examples (each with their own lss.config.json) can run in parallel.
16
+ // The dashboard, the REST API and the AWS wire all answer here by default.
17
+ const DEFAULT_PORT = 14566;
11
18
  const DEFAULT_PID_FILE = path.join(os.tmpdir(), 'lss-orchestrator.pid');
12
19
  const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'lss-orchestrator.log');
13
20
 
@@ -32,7 +39,7 @@ function runtimePaths() {
32
39
  const port = cfg.serverPort;
33
40
  // Keep the legacy global path when the default port is in use so existing
34
41
  // installations don't lose their running process across an upgrade.
35
- if (!port || port === 3100) {
42
+ if (!port || port === DEFAULT_PORT) {
36
43
  return { pidFile: DEFAULT_PID_FILE, logFile: DEFAULT_LOG_FILE };
37
44
  }
38
45
  return {
@@ -53,10 +60,10 @@ function loadConfig() {
53
60
  try {
54
61
  return JSON.parse(fs.readFileSync(EXPLICIT_CONFIG, 'utf-8'));
55
62
  } catch (error) {
56
- console.warn(`⚠️ Failed to parse config file ${EXPLICIT_CONFIG}`);
63
+ console.warn(t('config.parseFailed', { path: EXPLICIT_CONFIG }));
57
64
  }
58
65
  } else {
59
- console.warn(`⚠️ Config file not found: ${EXPLICIT_CONFIG}`);
66
+ console.warn(t('config.notFound', { path: EXPLICIT_CONFIG }));
60
67
  }
61
68
  }
62
69
 
@@ -73,7 +80,7 @@ function loadConfig() {
73
80
  const content = fs.readFileSync(candidate, 'utf-8');
74
81
  return JSON.parse(content);
75
82
  } catch (error) {
76
- console.warn(`⚠️ Failed to parse config file ${candidate}`);
83
+ console.warn(t('config.parseFailed', { path: candidate }));
77
84
  }
78
85
  }
79
86
  }
@@ -81,23 +88,34 @@ function loadConfig() {
81
88
  return {};
82
89
  }
83
90
 
91
+ // Positive integer from an env var, or undefined when unset/garbage — so a
92
+ // typo falls back to the file value instead of yielding NaN.
93
+ function envPort(name) {
94
+ const parsed = parseInt(process.env[name] || '', 10);
95
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
96
+ }
97
+
84
98
  /**
85
- * Get configuration values with defaults
99
+ * Get configuration values with defaults.
100
+ *
101
+ * Applies the same environment overrides the server does. Without this the CLI
102
+ * printed the file's ports while the server bound the env's — and, worse,
103
+ * runtimePaths() derives the PID/log path from serverPort when no stateDir is
104
+ * set, so `LSS_DASHBOARD_PORT=… lss start` followed by `lss stop` addressed two
105
+ * different files. Running a second instance purely from the environment is a
106
+ * documented workflow (docs/CONFIGURATION.md), so it has to agree end to end.
86
107
  */
87
108
  function getConfig(config) {
88
109
  return {
89
- serverPort: config.serverPort || 3100,
90
- localstackPort: config.localstackPort || 4566,
91
- enableDynamoProxy: config.enableDynamoProxy || false,
92
- dynamoProxyPort: config.dynamoProxyPort || 8000,
110
+ serverPort: envPort('LSS_DASHBOARD_PORT') || envPort('PORT') || config.serverPort || DEFAULT_PORT,
111
+ enableDynamoProxy: process.env.LSS_ENABLE_DYNAMO_PROXY
112
+ ? process.env.LSS_ENABLE_DYNAMO_PROXY === 'true' || process.env.LSS_ENABLE_DYNAMO_PROXY === '1'
113
+ : config.enableDynamoProxy || false,
114
+ dynamoProxyPort: envPort('LSS_DYNAMO_PROXY_PORT') || config.dynamoProxyPort || 8000,
93
115
  mode: config.mode || 'managed',
94
- localstackEdition: config.localstackEdition || 'community',
95
- localstackVersion: config.localstackVersion || 'latest',
96
- localstackImage: config.localstackImage,
97
- localstackAuthToken: config.localstackAuthToken,
98
116
  stateDir: config.stateDir,
99
- engine: config.engine || 'localstack',
100
- selfEnginePort: (config.selfEngine && config.selfEngine.port) || 14566,
117
+ engine: config.engine,
118
+ selfEnginePort: envPort('LSS_ENGINE_PORT') || (config.selfEngine && config.selfEngine.port) || DEFAULT_PORT,
101
119
  };
102
120
  }
103
121
 
@@ -141,7 +159,51 @@ function getOrchestratorPath() {
141
159
  return null;
142
160
  }
143
161
 
144
- function startOrchestrator() {
162
+ /**
163
+ * Who is answering on our serverPort, if anyone: the `projectRoot` reported by
164
+ * `GET /api/config`. Under `--net=host` the port belongs to the whole machine,
165
+ * so "someone answers but it is not us" is a routine outcome — and the one the
166
+ * old `NOT RUNNING` turned into a wild goose chase. Returns null when nothing
167
+ * answers (or answers something else), because this only ever adds detail to a
168
+ * message; it must never become a reason to fail.
169
+ */
170
+ async function probePortOwner() {
171
+ try {
172
+ const config = await getJson('/api/config');
173
+ return typeof config?.projectRoot === 'string' ? config.projectRoot : null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ // The last `lines` lines of the orchestrator log — where the reason a boot died
180
+ // (EADDRINUSE, a bad config) is written, seconds before the CLI notices.
181
+ function tailLog(logFile, lines = 15) {
182
+ try {
183
+ const content = fs.readFileSync(logFile, 'utf8');
184
+ return content.split('\n').filter(Boolean).slice(-lines).join('\n');
185
+ } catch {
186
+ return '';
187
+ }
188
+ }
189
+
190
+ // Report a port held by a different project's orchestrator. Returns true when
191
+ // it printed something, so callers can skip their own vaguer message.
192
+ function reportPortOwner(owner, port, log = console.log) {
193
+ if (!owner) return false;
194
+ const here = process.cwd();
195
+ const mine = owner === here || here.startsWith(`${owner}${path.sep}`);
196
+ log(mine
197
+ ? t('status.portAnsweringHere', { port })
198
+ : t('status.portOwnedByOther', { port, root: owner }));
199
+ return true;
200
+ }
201
+
202
+ async function startOrchestrator() {
203
+ // Before anything else — including the already-running short-circuit — so a
204
+ // stale 0.x flag or config value always fails loudly instead of looking like
205
+ // a successful no-op.
206
+
145
207
  const { pidFile, logFile } = runtimePaths();
146
208
 
147
209
  if (fs.existsSync(pidFile)) {
@@ -150,15 +212,11 @@ function startOrchestrator() {
150
212
  process.kill(pid, 0);
151
213
  const config = loadConfig();
152
214
  const cfg = getConfig(config);
153
- console.log('✅ LSS Orchestrator already running (PID:', pid + ')');
154
- console.log(`📊 Server: http://localhost:${cfg.serverPort}`);
155
- if (cfg.engine === 'self') {
156
- console.log(`🔧 Self Engine: http://localhost:${cfg.selfEnginePort}`);
157
- } else {
158
- console.log(`🔧 LocalStack: http://localhost:${cfg.localstackPort}`);
159
- }
215
+ console.log(t('start.already', { pid }));
216
+ console.log(t('start.server', { url: `http://localhost:${cfg.serverPort}` }));
217
+ console.log(t('start.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
160
218
  if (cfg.enableDynamoProxy) {
161
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
219
+ console.log(t('start.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
162
220
  }
163
221
  return;
164
222
  } catch (e) {
@@ -169,12 +227,12 @@ function startOrchestrator() {
169
227
  const orchestratorPath = getOrchestratorPath();
170
228
 
171
229
  if (!orchestratorPath) {
172
- console.error('❌ Orchestrator not found or not built.');
230
+ console.error(t('start.notBuilt'));
173
231
  console.error('');
174
- console.error('If you are developing LSS, run:');
232
+ console.error(t('start.notBuiltDev'));
175
233
  console.error(' cd /path/to/local-serverless-stack && npm run build');
176
234
  console.error('');
177
- console.error('If you installed via npm, please report this as a bug.');
235
+ console.error(t('start.notBuiltBug'));
178
236
  process.exit(1);
179
237
  }
180
238
 
@@ -184,33 +242,14 @@ function startOrchestrator() {
184
242
  const config = loadConfig();
185
243
  const cfg = getConfig(config);
186
244
 
187
- // Check for flags
188
245
  const enableDynamoProxy = process.argv.includes('--enable-dynamo-proxy') || cfg.enableDynamoProxy;
189
- const useExternal = process.argv.includes('--external');
190
- const usePro = process.argv.includes('--pro');
191
- const cliToken = getArgValue('--localstack-token');
192
- const useSelfEngine = process.argv.includes('--self-engine');
193
-
194
- const engine = useSelfEngine ? 'self' : cfg.engine;
195
- if (engine === 'self' && (useExternal || usePro || cliToken)) {
196
- console.error('❌ --self-engine cannot be combined with --external, --pro or --localstack-token (those are LocalStack options).');
197
- process.exit(1);
198
- }
199
-
200
- const mode = useExternal ? 'external' : cfg.mode;
201
- const edition = usePro ? 'pro' : cfg.localstackEdition;
202
- const authToken = cliToken || cfg.localstackAuthToken || process.env.LOCALSTACK_AUTH_TOKEN;
203
246
 
204
247
  // Build environment variables from config
205
248
  const env = { ...process.env };
206
- /* istanbul ignore else: getConfig() always defaults serverPort to 3100, so the else is unreachable */
249
+ /* istanbul ignore else: getConfig() always defaults serverPort, so the else is unreachable */
207
250
  if (cfg.serverPort) {
208
251
  env.PORT = cfg.serverPort;
209
252
  }
210
- /* istanbul ignore else: getConfig() always defaults localstackPort to 4566, so the else is unreachable */
211
- if (cfg.localstackPort) {
212
- env.LSS_LOCALSTACK_PORT = cfg.localstackPort;
213
- }
214
253
  if (enableDynamoProxy) {
215
254
  env.LSS_ENABLE_DYNAMO_PROXY = 'true';
216
255
  }
@@ -218,30 +257,9 @@ function startOrchestrator() {
218
257
  if (cfg.dynamoProxyPort) {
219
258
  env.LSS_DYNAMO_PROXY_PORT = cfg.dynamoProxyPort;
220
259
  }
221
- /* istanbul ignore else: mode resolves to cfg.mode which getConfig() defaults to 'managed', so the else is unreachable */
222
- if (mode) {
223
- env.LSS_LOCALSTACK_MODE = mode;
224
- }
225
- /* istanbul ignore else: edition resolves to localstackEdition which getConfig() defaults to 'community', so the else is unreachable */
226
- if (edition) {
227
- env.LSS_LOCALSTACK_EDITION = edition;
228
- }
229
- /* istanbul ignore else: getConfig() always defaults localstackVersion to 'latest', so the else is unreachable */
230
- if (cfg.localstackVersion) {
231
- env.LSS_LOCALSTACK_VERSION = cfg.localstackVersion;
232
- }
233
- if (cfg.localstackImage) {
234
- env.LSS_LOCALSTACK_IMAGE = cfg.localstackImage;
235
- }
236
- if (authToken) {
237
- env.LOCALSTACK_AUTH_TOKEN = authToken;
238
- }
239
- if (engine === 'self') {
240
- env.LSS_ENGINE = 'self';
241
- }
242
260
  // Hand the same config file to the server so its ConfigManager reads the
243
- // identical serverPort/localstackPort/seedsDir/region/mode (not just the
244
- // hand-translated subset above). Keeps the two config loaders in agreement.
261
+ // identical serverPort/seedsDir/region (not just the hand-translated subset
262
+ // above). Keeps the two config loaders in agreement.
245
263
  if (EXPLICIT_CONFIG) {
246
264
  env.LSS_CONFIG_PATH = EXPLICIT_CONFIG;
247
265
  }
@@ -257,29 +275,96 @@ function startOrchestrator() {
257
275
  fs.closeSync(logFd);
258
276
  fs.writeFileSync(pidFile, child.pid.toString());
259
277
 
260
- console.log('🚀 LSS Orchestrator started (PID:', child.pid + ')');
261
- console.log(`📊 Server: http://localhost:${cfg.serverPort}`);
262
- if (engine === 'self') {
263
- console.log(`🔧 Self Engine: http://localhost:${cfg.selfEnginePort} (no Docker)`);
264
- } else {
265
- console.log(`🔧 LocalStack: http://localhost:${cfg.localstackPort} (mode: ${mode}, edition: ${edition})`);
266
- }
278
+ console.log(t('start.started', { pid: child.pid }));
279
+ console.log(t('start.server', { url: `http://localhost:${cfg.serverPort}` }));
280
+ console.log(t('start.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
267
281
  if (enableDynamoProxy) {
268
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
282
+ console.log(t('start.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
283
+ }
284
+ console.log(t('start.logs', { path: logFile }));
285
+
286
+ // `--wait` blocks until the orchestrator actually answers, which is what a
287
+ // sequential chain (a VS Code task, a CI step, a boot script) needs: without
288
+ // it the next step's `register` fails fifteen times for a reason that is not
289
+ // the cause — the cause is in the first line of the log, minutes earlier.
290
+ if (process.argv.includes('--wait') || getArgValue('--wait') !== undefined) {
291
+ const seconds = Number.parseInt(getArgValue('--wait') || '', 10);
292
+ return waitForHealthy(child.pid, pidFile, logFile, Number.isInteger(seconds) && seconds > 0 ? seconds : 30);
269
293
  }
270
- console.log('📝 Logs:', logFile);
271
294
 
272
- setTimeout(() => {
273
- try {
274
- process.kill(child.pid, 0);
275
- console.log('✅ Service is running');
276
- } catch (e) {
277
- console.error('❌ Service failed to start. Check logs:', logFile);
278
- if (fs.existsSync(pidFile)) {
279
- fs.unlinkSync(pidFile);
295
+ // Without --wait the CLI still has to answer honestly. Daemonizing means the
296
+ // child discovers a busy port AFTER this process has printed a PID, so the
297
+ // liveness check below is the only place a non-zero exit can come from — and
298
+ // it used to print the failure and exit 0 anyway, making a dead orchestrator
299
+ // count as a successful step.
300
+ return scheduleLivenessCheck(child.pid, pidFile, logFile);
301
+ }
302
+
303
+ // Two seconds later: is the child still there? Resolves when it is, exits
304
+ // non-zero when it is not. Returned as a promise so the caller (and the tests)
305
+ // can observe the outcome instead of it happening in a detached timer.
306
+ function scheduleLivenessCheck(pid, pidFile, logFile) {
307
+ return new Promise((resolve, reject) => {
308
+ setTimeout(() => {
309
+ try {
310
+ process.kill(pid, 0);
311
+ } catch (e) {
312
+ // process.exit never returns in production; under test it throws, and
313
+ // the rejection is how a test asserts the exit code.
314
+ reportStartFailure(pidFile, logFile).then(() => process.exit(1)).then(resolve, reject);
315
+ return;
280
316
  }
317
+ console.log(t('start.running'));
318
+ resolve();
319
+ }, 2000);
320
+ });
321
+ }
322
+
323
+ // Everything known about a boot that did not survive: the log tail, and whether
324
+ // the port it wanted is held by another project's orchestrator.
325
+ async function reportStartFailure(pidFile, logFile) {
326
+ console.error(t('start.failed', { path: logFile }));
327
+ if (fs.existsSync(pidFile)) {
328
+ fs.unlinkSync(pidFile);
329
+ }
330
+ const tail = tailLog(logFile);
331
+ if (tail) {
332
+ console.error(t('start.logTail'));
333
+ console.error(tail);
334
+ }
335
+ reportPortOwner(await probePortOwner(), getServerPort(), console.error);
336
+ }
337
+
338
+ /**
339
+ * Poll `GET /api/health` until the orchestrator answers, the child dies, or the
340
+ * deadline passes. Resolves on success; exits non-zero (after printing the log
341
+ * tail) on either failure — a `lss start --wait` that returns 0 means the stack
342
+ * is up and can be registered against.
343
+ */
344
+ async function waitForHealthy(pid, pidFile, logFile, timeoutSeconds) {
345
+ const deadline = Date.now() + timeoutSeconds * 1000;
346
+ console.log(t('start.waiting', { seconds: timeoutSeconds }));
347
+ for (;;) {
348
+ try {
349
+ process.kill(pid, 0);
350
+ } catch {
351
+ await reportStartFailure(pidFile, logFile);
352
+ process.exit(1);
281
353
  }
282
- }, 2000);
354
+ try {
355
+ await getJson('/api/health');
356
+ console.log(t('start.running'));
357
+ return;
358
+ } catch {
359
+ // Not up yet — the connection is refused until the listener binds.
360
+ }
361
+ if (Date.now() >= deadline) {
362
+ console.error(t('start.waitTimeout', { seconds: timeoutSeconds }));
363
+ await reportStartFailure(pidFile, logFile);
364
+ process.exit(1);
365
+ }
366
+ await new Promise(resolve => setTimeout(resolve, 250));
367
+ }
283
368
  }
284
369
 
285
370
  // Poll kill(pid, 0) until the process is gone. SIGTERM alone isn't enough to
@@ -318,7 +403,7 @@ function waitForExit(pid, timeoutMs = 10000, intervalMs = 200) {
318
403
  async function stopOrchestrator() {
319
404
  const { pidFile } = runtimePaths();
320
405
  if (!fs.existsSync(pidFile)) {
321
- console.log('⚠️ LSS Orchestrator is not running');
406
+ console.log(t('stop.notRunning'));
322
407
  return;
323
408
  }
324
409
 
@@ -327,7 +412,7 @@ async function stopOrchestrator() {
327
412
  try {
328
413
  process.kill(pid, 'SIGTERM');
329
414
  } catch (e) {
330
- console.error('❌ Failed to stop process:', e.message);
415
+ console.error(t('stop.failed', { error: e.message }));
331
416
  if (fs.existsSync(pidFile)) {
332
417
  fs.unlinkSync(pidFile);
333
418
  }
@@ -337,16 +422,29 @@ async function stopOrchestrator() {
337
422
  const exited = await waitForExit(pid);
338
423
  fs.unlinkSync(pidFile);
339
424
  if (exited) {
340
- console.log('🛑 LSS Orchestrator stopped (PID:', pid + ')');
425
+ console.log(t('stop.stopped', { pid }));
341
426
  } else {
342
- console.warn(`⚠️ Process ${pid} did not exit within 10s of SIGTERM — the server port may still be busy. Check \`lss status\` before starting again.`);
427
+ console.warn(t('stop.timeout', { pid }));
343
428
  }
344
429
  }
345
430
 
346
- function showStatus() {
431
+ /**
432
+ * `lss status` — is THIS project's orchestrator running?
433
+ *
434
+ * The pid file answers that, and it used to answer nothing else: with
435
+ * `--net=host` the port belongs to the whole machine, so another project's
436
+ * orchestrator holding it produced `NOT RUNNING` from a port that answers 200 —
437
+ * a status that is not merely unhelpful but points the operator at the wrong
438
+ * place. Every "no" is now followed by asking WHO is on the port, which
439
+ * `GET /api/config` answers exactly (`projectRoot`).
440
+ */
441
+ async function showStatus() {
347
442
  const { pidFile, logFile } = runtimePaths();
443
+ const cfg = getConfig(loadConfig());
348
444
  if (!fs.existsSync(pidFile)) {
349
- console.log('⚪ LSS Orchestrator: NOT RUNNING');
445
+ if (!reportPortOwner(await probePortOwner(), cfg.serverPort)) {
446
+ console.log(t('status.notRunning'));
447
+ }
350
448
  return;
351
449
  }
352
450
 
@@ -354,18 +452,17 @@ function showStatus() {
354
452
 
355
453
  try {
356
454
  process.kill(pid, 0);
357
- const config = loadConfig();
358
- const cfg = getConfig(config);
359
- console.log('🟢 LSS Orchestrator: RUNNING (PID:', pid + ')');
360
- console.log(`📊 Server: http://localhost:${cfg.serverPort}`);
361
- console.log(`🔧 LocalStack: http://localhost:${cfg.localstackPort}`);
455
+ console.log(t('status.running', { pid }));
456
+ console.log(t('status.server', { url: `http://localhost:${cfg.serverPort}` }));
457
+ console.log(t('status.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
362
458
  if (cfg.enableDynamoProxy) {
363
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
459
+ console.log(t('status.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
364
460
  }
365
- console.log('📝 Logs:', logFile);
461
+ console.log(t('status.logs', { path: logFile }));
366
462
  } catch (e) {
367
- console.log('⚪ LSS Orchestrator: NOT RUNNING (stale PID file)');
463
+ console.log(t('status.stale'));
368
464
  fs.unlinkSync(pidFile);
465
+ reportPortOwner(await probePortOwner(), cfg.serverPort);
369
466
  }
370
467
  }
371
468
 
@@ -379,13 +476,13 @@ function getServerPort() {
379
476
  // orchestrator startup window) — fall back through every signal we have so
380
477
  // `formatError` is guaranteed not to return an empty string.
381
478
  function formatError(e) {
382
- if (!e) return 'erro desconhecido (sem detalhes)';
383
- if (typeof e === 'string') return e.trim() || 'erro desconhecido (sem detalhes)';
479
+ if (!e) return t('error.unknown');
480
+ if (typeof e === 'string') return e.trim() || t('error.unknown');
384
481
  if (e.message && String(e.message).trim()) return String(e.message).trim();
385
- if (e.code) return `erro de I/O (${e.code})`;
482
+ if (e.code) return t('error.io', { code: e.code });
386
483
  if (e.name) return e.name;
387
484
  const s = String(e);
388
- return s && s !== '[object Object]' ? s : 'erro desconhecido (sem detalhes)';
485
+ return s && s !== '[object Object]' ? s : t('error.unknown');
389
486
  }
390
487
 
391
488
  function buildHttpError(res, data) {
@@ -395,7 +492,9 @@ function buildHttpError(res, data) {
395
492
  if (fromBody && String(fromBody).trim()) return new Error(String(fromBody).trim());
396
493
  const snippet = data && data.length < 300 ? data.trim() : '';
397
494
  const statusText = res.statusMessage ? `${res.statusCode} ${res.statusMessage}` : `${res.statusCode}`;
398
- return new Error(snippet ? `HTTP ${statusText}: ${snippet}` : `HTTP ${statusText} (sem corpo de erro)`);
495
+ // `HTTP <status>: <snippet>` is pure wire detail, so only the "no body at
496
+ // all" variant carries translatable prose.
497
+ return new Error(snippet ? `HTTP ${statusText}: ${snippet}` : t('error.httpNoBody', { status: statusText }));
399
498
  }
400
499
 
401
500
  function postJson(path, body) {
@@ -427,7 +526,7 @@ function postJson(path, body) {
427
526
  });
428
527
  },
429
528
  );
430
- req.on('error', err => reject(err && err.message ? err : new Error(`falha na conexão HTTP com o orchestrator: ${formatError(err)}`)));
529
+ req.on('error', err => reject(err && err.message ? err : new Error(t('error.httpConnection', { error: formatError(err) }))));
431
530
  req.write(payload);
432
531
  req.end();
433
532
  });
@@ -436,7 +535,7 @@ function postJson(path, body) {
436
535
  function ensureRunningOrExit() {
437
536
  const { pidFile } = runtimePaths();
438
537
  if (!fs.existsSync(pidFile)) {
439
- console.error('❌ LSS Orchestrator is not running. Start it with: npx lss start');
538
+ console.error(t('error.notRunning'));
440
539
  process.exit(1);
441
540
  }
442
541
  }
@@ -445,19 +544,19 @@ function printSeedRunResults(results) {
445
544
  let missingTables = 0;
446
545
  for (const r of results) {
447
546
  if (r.skipped) {
448
- console.log(` ⚠ ${r.tableName}: skipped (${r.reason})`);
449
- if (r.reason && r.reason.includes('does not exist in LocalStack')) {
547
+ console.log(t('seed.skipped', { table: r.tableName, reason: r.reason }));
548
+ if (r.reason && r.reason.includes('does not exist in the engine')) {
450
549
  missingTables++;
451
550
  }
452
551
  } else {
453
- console.log(` ✓ ${r.tableName}: ${r.inserted} item(s) inserted`);
552
+ console.log(t('seed.inserted', { table: r.tableName, count: r.inserted }));
454
553
  }
455
554
  }
456
555
  return { missingTables };
457
556
  }
458
557
 
459
558
  // Show the user both sides of the comparison: the seed files we found, and
460
- // the DynamoDB tables actually living in LocalStack. This is the difference
559
+ // the DynamoDB tables actually living in the engine. This is the difference
461
560
  // between "I didn't deploy yet" (no live tables at all) and "my seed file
462
561
  // name doesn't match the CFN TableName" (live tables exist, just not those).
463
562
  function printSeedMismatchDiagnostic({ entries, liveTables, focusTable }) {
@@ -466,37 +565,39 @@ function printSeedMismatchDiagnostic({ entries, liveTables, focusTable }) {
466
565
 
467
566
  console.log('');
468
567
  if (focusTable) {
469
- console.log(`📂 Arquivo de seed inspecionado: ${focusTable}.json`);
568
+ console.log(t('seed.diagFile', { table: focusTable }));
470
569
  } else if (focusList.length > 0) {
471
- console.log('📂 Arquivos de seed inspecionados (tabela esperada):');
570
+ console.log(t('seed.diagFiles'));
472
571
  for (const name of focusList) console.log(` - ${name}`);
473
572
  } else {
474
- console.log('📂 Nenhum arquivo *.json encontrado no seedsDir.');
573
+ console.log(t('seed.diagNoFiles'));
475
574
  }
476
575
 
477
576
  if (liveTables && liveTables.length > 0) {
478
577
  console.log('');
479
- console.log(`🗂️ Tabelas vivas no LocalStack (${liveTables.length}):`);
578
+ console.log(t('seed.diagLiveTables', { count: liveTables.length }));
480
579
  for (const name of liveTables) console.log(` - ${name}`);
481
580
  console.log('');
482
- console.log('💡 Os nomes dos arquivos de seed precisam bater EXATAMENTE com o `TableName` no CloudFormation.');
483
- console.log(' Confira se há prefixo/sufixo divergente entre o arquivo e a tabela.');
581
+ console.log(t('seed.diagMatchHint'));
582
+ console.log(t('seed.diagPrefixHint'));
484
583
  } else {
485
584
  console.log('');
486
- console.log('🗂️ Nenhuma tabela viva no LocalStack ainda.');
487
- console.log('💡 Provavelmente o stack ainda não foi provisionado. Tente:');
488
- console.log(' npx lss start # garante LocalStack rodando');
489
- console.log(' npx serverless deploy # cria as tabelas');
490
- console.log(' E rode `npx lss seed` novamente.');
585
+ console.log(t('seed.diagNoLiveTables'));
586
+ console.log(t('seed.diagNotProvisioned'));
587
+ // The commands themselves are never translated — only the comment that
588
+ // explains what each one buys you.
589
+ console.log(` npx lss start # ${t('seed.diagStepStart')}`);
590
+ console.log(` npx serverless deploy # ${t('seed.diagStepDeploy')}`);
591
+ console.log(t('seed.diagRetry'));
491
592
  }
492
593
  }
493
594
 
494
595
  function printSeedClearResults(results) {
495
596
  for (const r of results) {
496
597
  if (r.skipped) {
497
- console.log(` ⚠ ${r.tableName}: skipped (${r.reason})`);
598
+ console.log(t('seed.skipped', { table: r.tableName, reason: r.reason }));
498
599
  } else {
499
- console.log(` ✓ ${r.tableName}: ${r.deleted} item(s) deleted`);
600
+ console.log(t('seed.deleted', { table: r.tableName, count: r.deleted }));
500
601
  }
501
602
  }
502
603
  }
@@ -504,7 +605,7 @@ function printSeedClearResults(results) {
504
605
  async function runSeed(tableName) {
505
606
  ensureRunningOrExit();
506
607
  try {
507
- console.log(tableName ? `🌱 Seeding ${tableName}...` : '🌱 Seeding all tables with seed files...');
608
+ console.log(tableName ? t('seed.runningTable', { table: tableName }) : t('seed.running'));
508
609
  const res = await postJson('/api/seeds/run', tableName ? { tableName } : {});
509
610
  const { missingTables } = printSeedRunResults(res.results || []);
510
611
  if (missingTables > 0) {
@@ -517,11 +618,11 @@ async function runSeed(tableName) {
517
618
  });
518
619
  } catch (diagErr) {
519
620
  // Diagnostic is best-effort; don't fail the seed because the hint failed.
520
- console.log(`(não consegui detalhar tabelas vivas: ${formatError(diagErr)})`);
621
+ console.log(t('seed.diagFailed', { error: formatError(diagErr) }));
521
622
  }
522
623
  }
523
624
  } catch (e) {
524
- console.error('❌ Seed failed:', formatError(e));
625
+ console.error(t('seed.failed'), formatError(e));
525
626
  process.exit(1);
526
627
  }
527
628
  }
@@ -550,7 +651,7 @@ function getJson(path) {
550
651
  });
551
652
  },
552
653
  );
553
- req.on('error', err => reject(err && err.message ? err : new Error(`falha na conexão HTTP com o orchestrator: ${formatError(err)}`)));
654
+ req.on('error', err => reject(err && err.message ? err : new Error(t('error.httpConnection', { error: formatError(err) }))));
554
655
  req.end();
555
656
  });
556
657
  }
@@ -562,7 +663,7 @@ function promptConfirmation(expectedWord) {
562
663
  input: process.stdin,
563
664
  output: process.stdout,
564
665
  });
565
- rl.question(`Digite "${expectedWord}" para prosseguir (ou qualquer outra coisa para cancelar): `, answer => {
666
+ rl.question(t('seed.confirmPrompt', { word: expectedWord }), answer => {
566
667
  rl.close();
567
668
  resolve(answer.trim() === expectedWord);
568
669
  });
@@ -577,8 +678,8 @@ async function clearSeed(tableName) {
577
678
 
578
679
  // Show the user exactly what's about to be wiped before they confirm.
579
680
  // Hitting the orchestrator's GET /api/seeds also implicitly proves we're
580
- // talking to LocalStack and not AWS — the orchestrator only ever connects
581
- // to the configured local LocalStack endpoint.
681
+ // talking to the local engine and not AWS — the orchestrator only ever
682
+ // connects to the configured local engine endpoint.
582
683
  let scopeDescription;
583
684
  try {
584
685
  const list = await getJson('/api/seeds');
@@ -589,138 +690,301 @@ async function clearSeed(tableName) {
589
690
 
590
691
  if (liveTargets.length === 0) {
591
692
  if (tableName) {
592
- console.log(`⚠️ Nenhuma tabela "${tableName}" existente no LocalStack para limpar.`);
693
+ console.log(t('clear.noSuchTable', { table: tableName }));
593
694
  } else if (entries.length === 0) {
594
- console.log('⚠️ Nenhum arquivo de seed (*.json) encontrado no seedsDir — nada para limpar.');
695
+ console.log(t('clear.noSeedFiles'));
595
696
  } else {
596
- console.log(`⚠️ ${entries.length} arquivo(s) de seed encontrados, mas NENHUMA das tabelas correspondentes existe no LocalStack.`);
697
+ console.log(t('clear.noneLive', { count: entries.length }));
597
698
  }
598
699
  printSeedMismatchDiagnostic({ entries, liveTables, focusTable: tableName });
599
700
  return;
600
701
  }
601
702
 
602
703
  scopeDescription = tableName
603
- ? `a tabela "${tableName}"`
604
- : `${liveTargets.length} tabela(s): ${liveTargets.map(e => e.tableName).join(', ')}`;
704
+ ? t('clear.scopeTable', { table: tableName })
705
+ : t('clear.scopeTables', {
706
+ count: liveTargets.length,
707
+ tables: liveTargets.map(e => e.tableName).join(', '),
708
+ });
605
709
 
606
710
  console.log('');
607
- console.log('⚠️ ATENÇÃO: operação destrutiva');
608
- console.log(` Alvo: ${scopeDescription}`);
609
- console.log(` LocalStack: http://localhost:${cfg.localstackPort}`);
610
- console.log(' Esta ação NÃO toca em nenhuma conta AWS — apenas o LocalStack acima.');
711
+ console.log(t('clear.warning'));
712
+ console.log(t('clear.target', { scope: scopeDescription }));
713
+ console.log(t('clear.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
714
+ console.log(t('clear.noAws'));
611
715
  console.log('');
612
716
  } catch (e) {
613
- console.error('❌ Não consegui listar as tabelas antes de limpar:', formatError(e));
717
+ console.error(t('clear.listFailed'), formatError(e));
614
718
  process.exit(1);
615
719
  }
616
720
 
617
721
  if (!skipConfirm) {
722
+ // The word itself is NOT translated: it is a magic token CI scripts and the
723
+ // docs already depend on, so it stays `confirmar` in every locale.
618
724
  const ok = await promptConfirmation('confirmar');
619
725
  if (!ok) {
620
- console.log('🚫 Cancelado — nenhuma alteração feita.');
726
+ console.log(t('seed.aborted'));
621
727
  return;
622
728
  }
623
729
  } else {
624
- console.log('↳ --yes informado, pulando confirmação interativa.');
730
+ console.log(t('clear.skipPrompt'));
625
731
  }
626
732
 
627
733
  try {
628
- console.log(tableName ? `🧹 Clearing ${tableName}...` : '🧹 Clearing all seeded tables...');
734
+ console.log(tableName ? t('seed.clearingTable', { table: tableName }) : t('seed.clearing'));
629
735
  const res = await postJson('/api/seeds/clear', tableName ? { tableName } : {});
630
736
  printSeedClearResults(res.results || []);
631
737
  } catch (e) {
632
- console.error('❌ Clear failed:', formatError(e));
738
+ console.error(t('clear.failed'), formatError(e));
633
739
  process.exit(1);
634
740
  }
635
741
  }
636
742
 
743
+ /**
744
+ * `lss mcp` — run the Model Context Protocol server on stdio.
745
+ *
746
+ * Started by an MCP client (Claude Code reads .mcp.json), never by a human at a
747
+ * prompt: stdout is the JSON-RPC frame stream, so nothing else may be written
748
+ * there. This process only talks HTTP to an already-running orchestrator — it
749
+ * never boots one — so `lss start` has to have happened first.
750
+ */
751
+ /**
752
+ * `lss register [path...]` — register services with the running orchestrator.
753
+ *
754
+ * This replaces the retired serverless-lss plugin: registration is a plain
755
+ * POST of { servicePath }, and the orchestrator resolves everything else
756
+ * (packaging via autoPackage when needed, then name/region/custom.lss ports
757
+ * from the packaged serverless-state.json).
758
+ */
759
+ async function registerServices(paths) {
760
+ ensureRunningOrExit();
761
+ const targets = paths.length > 0 ? paths : ['.'];
762
+ let failed = 0;
763
+ for (const target of targets) {
764
+ const servicePath = path.resolve(target);
765
+ if (!fs.existsSync(servicePath)) {
766
+ console.error(t('register.notFound', { target }));
767
+ failed++;
768
+ continue;
769
+ }
770
+ try {
771
+ const result = await postJson('/api/services/register', { servicePath });
772
+ console.log(t('register.ok', {
773
+ name: result.serviceName,
774
+ resources: result.resourcesCount,
775
+ functions: result.functionsCount,
776
+ routes: result.routesCount,
777
+ }));
778
+ // Warnings come from the orchestrator already worded; only the bullet is ours.
779
+ for (const warning of result.warnings || []) {
780
+ console.log(` ⚠ ${warning}`);
781
+ }
782
+ } catch (e) {
783
+ console.error(t('register.failed', { target, error: formatError(e) }));
784
+ failed++;
785
+ }
786
+ }
787
+ if (failed > 0) {
788
+ process.exit(1);
789
+ }
790
+ }
791
+
792
+ /**
793
+ * `lss scan` — list every Serverless/osls service found under the project
794
+ * root, with the same flags onboarding shows.
795
+ *
796
+ * The three flags mirror the checklist: `registered` (known to this
797
+ * orchestrator), `installed` (node_modules resolvable, so packaging can run
798
+ * without an install first) and `packaged` (a template already exists). The
799
+ * effective package command gets its own line because it is per-service
800
+ * configurable (`servicePackaging`) and is what the install→package→register
801
+ * buttons would run.
802
+ */
803
+ /**
804
+ * Render one scan warning. The orchestrator sends a stable `code` plus the
805
+ * English `message` it produced; the code is what gets localised, and the
806
+ * message is the fallback for a code this CLI has not been taught yet — a
807
+ * newer server's warning still prints instead of disappearing.
808
+ */
809
+ function scanWarningText(warning) {
810
+ if (!warning || typeof warning !== 'object') return String(warning);
811
+ const key = `scan.warning.${warning.code}`;
812
+ const translated = t(key, warning.params);
813
+ return translated === key ? warning.message : translated;
814
+ }
815
+
816
+ async function scanServices() {
817
+ ensureRunningOrExit();
818
+ try {
819
+ const result = await getJson('/api/services/scan');
820
+ const services = result.services || [];
821
+ if (services.length === 0) {
822
+ console.log(t('scan.none', { root: result.projectRoot }));
823
+ return;
824
+ }
825
+ console.log(`${t('scan.header', { count: services.length, root: result.projectRoot })}\n`);
826
+ for (const svc of services) {
827
+ const flags = [
828
+ svc.registered ? t('scan.registered') : t('scan.notRegistered'),
829
+ svc.installed ? t('scan.installed') : t('scan.notInstalled'),
830
+ svc.packaged ? t('scan.packaged') : t('scan.notPackaged'),
831
+ ].join(', ');
832
+ const ports = svc.apiPort ? ` api:${svc.apiPort}${svc.invokePort ? ` invoke:${svc.invokePort}` : ''}` : '';
833
+ console.log(` ${svc.registered ? '✓' : '·'} ${svc.name} (${svc.relPath}) — ${flags}${ports}`);
834
+ if (svc.packageCommand) {
835
+ console.log(` ${t('scan.packageCommand', { command: svc.packageCommand })}`);
836
+ }
837
+ for (const warning of svc.warnings || []) {
838
+ console.log(` ⚠ ${scanWarningText(warning)}`);
839
+ }
840
+ }
841
+ console.log(t('scan.hint'));
842
+ } catch (e) {
843
+ console.error(t('scan.failed'), formatError(e));
844
+ process.exit(1);
845
+ }
846
+ }
847
+
848
+ function getMcpServerPath() {
849
+ const candidates = [
850
+ path.join(__dirname, '../dist/mcp/server.js'),
851
+ path.join(__dirname, '..', 'dist', 'mcp', 'server.js'),
852
+ ];
853
+ return candidates.find(candidate => fs.existsSync(candidate)) || null;
854
+ }
855
+
856
+ function getPackageVersion() {
857
+ try {
858
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8')).version || '0.0.0';
859
+ } catch {
860
+ return '0.0.0';
861
+ }
862
+ }
863
+
864
+ function runMcpServer() {
865
+ const serverPath = getMcpServerPath();
866
+ const version = getPackageVersion();
867
+ /* istanbul ignore else: the "found" path falls through to the dynamic import below, which is the process entry point (it binds this process's stdio) and so is not unit-testable */
868
+ if (!serverPath) {
869
+ console.error(t('mcp.missing'));
870
+ process.exit(1); // never returns
871
+ }
872
+ // Dynamic import: the MCP build is ESM, and loading it only for this command
873
+ // keeps every other CLI invocation free of it.
874
+ /* istanbul ignore next: process entry point — loads the ESM build and binds this process's stdio */
875
+ import(pathToFileURL(serverPath).href)
876
+ .then(mod => mod.main(version))
877
+ .catch(error => {
878
+ console.error(t('mcp.startFailed'), error && error.message ? error.message : error);
879
+ process.exit(1);
880
+ });
881
+ }
882
+
883
+ // One ` <name> <description>` row of the help screen. The description is
884
+ // wrapped and hanging-indented under itself so a translation can be longer than
885
+ // the English original without shredding the column — the reason the help text
886
+ // is assembled here instead of living in the catalogue as one pre-formatted blob.
887
+ function helpRow(name, description, nameWidth, textWidth) {
888
+ const gutter = ' '.repeat(2 + nameWidth);
889
+ const wrapped = [];
890
+ let current = '';
891
+ for (const word of description.split(' ')) {
892
+ if (current && current.length + 1 + word.length > textWidth) {
893
+ wrapped.push(current);
894
+ current = word;
895
+ } else {
896
+ current = current ? `${current} ${word}` : word;
897
+ }
898
+ }
899
+ wrapped.push(current);
900
+ return [` ${name.padEnd(nameWidth)}${wrapped[0]}`, ...wrapped.slice(1).map(line => gutter + line)]
901
+ .join('\n');
902
+ }
903
+
904
+ // A ` npx lss … # what it does` example line. The command is never translated.
905
+ function helpExample(command, comment) {
906
+ return ` ${command.padEnd(43)}# ${comment}`;
907
+ }
908
+
637
909
  function showHelp() {
638
- console.log(`
639
- Local Serverless Stack (LSS) CLI
640
-
641
- Usage: npx lss <command> [options]
642
-
643
- Commands:
644
- start Start the LSS Orchestrator in background
645
- stop Stop the LSS Orchestrator
646
- status Check if the orchestrator is running
647
- logs Show the logs
648
- seed [table] Apply seed file(s) from seedsDir into DynamoDB
649
- (no args = all matching tables)
650
- seed:clear [table] Delete all items from the given table (or all
651
- tables with a seed file when no arg is given).
652
- Pede confirmação interativa (digitar "confirmar")
653
- antes de qualquer escrita.
654
- help Show this help message
655
-
656
- Options:
657
- --config <path> Load config from this file (precedes the cwd/home search).
658
- Applies to start/stop/status/logs so an isolated instance
659
- can be addressed without cd-ing into its folder.
660
- --enable-dynamo-proxy Enable DynamoDB proxy on port 8000 (for start command)
661
- --self-engine Use the in-process AWS engine instead of LocalStack (no Docker)
662
- --external Connect to a LocalStack already running, do not spawn a container
663
- --pro Use the LocalStack Pro image (requires LOCALSTACK_AUTH_TOKEN)
664
- --localstack-token <token> Pass a LOCALSTACK_AUTH_TOKEN to the container
665
- --yes, -y Skip interactive confirmation on seed:clear (use only in CI)
666
-
667
- Environment:
668
- LOCALSTACK_AUTH_TOKEN Token forwarded to LocalStack (Pro and >=2026.5 community)
669
-
670
- Configuration:
671
- Create a lss.config.json or .lssrc file in your project root to customize:
672
-
673
- Example lss.config.json:
674
- {
675
- "serverPort": 3100,
676
- "localstackPort": 4566,
677
- "mode": "managed",
678
- "localstackEdition": "community",
679
- "localstackVersion": "latest",
680
- "enableDynamoProxy": false,
681
- "dynamoProxyPort": 8000,
682
- "region": "us-east-1",
683
- "services": ["dynamodb", "sqs", "sns", "lambda"],
684
- "persistence": true,
685
- "debug": false,
686
- "stateDir": ".lss"
687
- }
688
-
689
- stateDir (optional): directory for this instance's PID/log files. Set it (e.g.
690
- ".lss-e2e") together with --config to run a fully isolated instance alongside
691
- the dev one. When omitted, PID/log live in the OS temp dir scoped by serverPort.
692
-
693
- For the Serverless Plugin, add to serverless.yml:
694
- custom:
695
- orchestrator:
696
- enabled: true
697
- orchestratorUrl: http://localhost:3100
698
-
699
- Examples:
700
- npx lss start # Start the orchestrator (managed LocalStack)
701
- npx lss start --self-engine # Use the in-process engine (no Docker, port 14566)
702
- npx lss start --enable-dynamo-proxy # Start with DynamoDB proxy enabled
703
- npx lss start --external # Connect to an external LocalStack
704
- npx lss start --pro # Use LocalStack Pro (token required)
705
- LOCALSTACK_AUTH_TOKEN=xxx npx lss start # Inject a token via env var
706
- npx lss stop # Stop the orchestrator
707
- npx lss status # Check status
708
- npx lss logs # View logs
709
- npx lss seed # Seed every table that has a {name}.json file
710
- npx lss seed users # Seed only the "users" table
711
- npx lss seed:clear users # Delete all items from "users" (com confirmação)
712
- npx lss seed:clear users --yes # Mesma coisa, sem prompt (CI)
713
- `);
910
+ // Command names, flags, env var names, paths and the JSON template are
911
+ // identifiers — they stay here, untranslated. Only prose goes through t().
912
+ const cmd = (name, key) => helpRow(name, t(key), 19, 56);
913
+ const opt = (name, key) => helpRow(name, t(key), 29, 46);
914
+
915
+ console.log([
916
+ '',
917
+ 'Local Serverless Stack (LSS) CLI',
918
+ '',
919
+ t('help.usage'),
920
+ '',
921
+ t('help.commands'),
922
+ cmd('start', 'help.cmd.start'),
923
+ cmd('stop', 'help.cmd.stop'),
924
+ cmd('status', 'help.cmd.status'),
925
+ cmd('logs', 'help.cmd.logs'),
926
+ cmd('seed [table]', 'help.cmd.seed'),
927
+ cmd('scan', 'help.cmd.scan'),
928
+ cmd('register [path...]', 'help.cmd.register'),
929
+ cmd('seed:clear [table]', 'help.cmd.seedClear'),
930
+ cmd('mcp', 'help.cmd.mcp'),
931
+ cmd('help', 'help.cmd.help'),
932
+ '',
933
+ t('help.options'),
934
+ opt('--config <path>', 'help.opt.config'),
935
+ opt('--enable-dynamo-proxy', 'help.opt.dynamoProxy'),
936
+ opt('--wait[=<seconds>]', 'help.opt.wait'),
937
+ opt('--yes, -y', 'help.opt.yes'),
938
+ '',
939
+ t('help.environment'),
940
+ opt('LSS_DASHBOARD_PORT', 'help.env.dashboardPort'),
941
+ opt('LSS_ENGINE_PORT', 'help.env.enginePort'),
942
+ opt('LSS_ENGINE_DATA_DIR', 'help.env.engineDataDir'),
943
+ opt('AWS_REGION', 'help.env.awsRegion'),
944
+ opt('LSS_LANG', 'help.env.lang'),
945
+ '',
946
+ t('help.configuration'),
947
+ ` ${t('help.config.intro')}`,
948
+ '',
949
+ ` ${t('help.config.example')}`,
950
+ ' {',
951
+ ' "serverPort": 3100,',
952
+ ' "selfEngine": { "port": 14566 },',
953
+ ' "enableDynamoProxy": false,',
954
+ ' "dynamoProxyPort": 8000,',
955
+ ' "region": "us-east-1",',
956
+ ' "persistence": true,',
957
+ ' "debug": false,',
958
+ ' "stateDir": ".lss"',
959
+ ' }',
960
+ '',
961
+ helpRow('', t('help.config.stateDir'), 0, 76),
962
+ '',
963
+ t('help.examples'),
964
+ helpExample('npx lss start', t('help.ex.start')),
965
+ helpExample('npx lss start --enable-dynamo-proxy', t('help.ex.startProxy')),
966
+ helpExample('npx lss mcp', t('help.ex.mcp')),
967
+ helpExample('npx lss stop', t('help.ex.stop')),
968
+ helpExample('npx lss status', t('help.ex.status')),
969
+ helpExample('npx lss logs', t('help.ex.logs')),
970
+ helpExample('npx lss scan', t('help.ex.scan')),
971
+ helpExample('npx lss register', t('help.ex.register')),
972
+ helpExample('npx lss seed', t('help.ex.seed')),
973
+ helpExample('npx lss seed users', t('help.ex.seedTable')),
974
+ helpExample('npx lss seed:clear users', t('help.ex.seedClear')),
975
+ helpExample('npx lss seed:clear users --yes', t('help.ex.seedClearYes')),
976
+ '',
977
+ ].join('\n'));
714
978
  }
715
979
 
716
980
  function showLogs() {
717
981
  const { logFile } = runtimePaths();
718
982
  if (!fs.existsSync(logFile)) {
719
- console.log('⚠️ No logs found');
983
+ console.log(t('logs.missing', { path: logFile }));
720
984
  return;
721
985
  }
722
986
 
723
- console.log('📝 Logs from:', logFile);
987
+ console.log(t('logs.from', { path: logFile }));
724
988
  console.log('---');
725
989
  const logs = fs.readFileSync(logFile, 'utf8');
726
990
  const lines = logs.split('\n');
@@ -732,11 +996,23 @@ function showLogs() {
732
996
  // Skip anything that looks like a flag so `seed:clear --yes` doesn't pass
733
997
  // "--yes" as the table name.
734
998
  function firstPositional() {
999
+ return allPositionals()[0];
1000
+ }
1001
+
1002
+ // Every non-flag argument after the command, skipping flag VALUES too
1003
+ // (`--config <path>` consumes the next token).
1004
+ function allPositionals() {
1005
+ const flagsWithValue = new Set(['--config']);
1006
+ const out = [];
735
1007
  for (let i = 3; i < process.argv.length; i++) {
736
1008
  const arg = process.argv[i];
737
- if (!arg.startsWith('-')) return arg;
1009
+ if (arg.startsWith('-')) {
1010
+ if (flagsWithValue.has(arg)) i++;
1011
+ continue;
1012
+ }
1013
+ out.push(arg);
738
1014
  }
739
- return undefined;
1015
+ return out;
740
1016
  }
741
1017
 
742
1018
  // Exported so the pure/in-process helpers can be unit-tested by requiring this
@@ -746,6 +1022,12 @@ function firstPositional() {
746
1022
  module.exports = {
747
1023
  loadConfig,
748
1024
  getConfig,
1025
+ allPositionals,
1026
+ registerServices,
1027
+ scanServices,
1028
+ getMcpServerPath,
1029
+ getPackageVersion,
1030
+ runMcpServer,
749
1031
  getArgValue,
750
1032
  runtimePaths,
751
1033
  getOrchestratorPath,
@@ -792,13 +1074,22 @@ if (require.main === module) {
792
1074
  case 'seed:clear':
793
1075
  clearSeed(firstPositional());
794
1076
  break;
1077
+ case 'register':
1078
+ registerServices(allPositionals());
1079
+ break;
1080
+ case 'scan':
1081
+ scanServices();
1082
+ break;
1083
+ case 'mcp':
1084
+ runMcpServer();
1085
+ break;
795
1086
  case 'help':
796
1087
  case '--help':
797
1088
  case '-h':
798
1089
  showHelp();
799
1090
  break;
800
1091
  default:
801
- console.log('❌ Unknown command:', command);
1092
+ console.log(t('help.unknown', { command }));
802
1093
  showHelp();
803
1094
  process.exit(1);
804
1095
  }