local-serverless-stack 0.17.2 → 1.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 (221) hide show
  1. package/CHANGELOG.md +327 -0
  2. package/README.md +1150 -221
  3. package/assets/icon.svg +37 -0
  4. package/assets/logo.svg +38 -0
  5. package/bin/cli.js +384 -217
  6. package/bin/i18n.js +498 -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/is-aws-request.d.ts +8 -0
  60. package/dist/server/engine/http/is-aws-request.d.ts.map +1 -0
  61. package/dist/server/engine/http/is-aws-request.js +53 -0
  62. package/dist/server/engine/http/is-aws-request.js.map +1 -0
  63. package/dist/server/engine/http/router.d.ts.map +1 -1
  64. package/dist/server/engine/http/router.js +7 -1
  65. package/dist/server/engine/http/router.js.map +1 -1
  66. package/dist/server/engine/store/engine-store.d.ts +1 -0
  67. package/dist/server/engine/store/engine-store.d.ts.map +1 -1
  68. package/dist/server/engine/store/engine-store.js +2 -1
  69. package/dist/server/engine/store/engine-store.js.map +1 -1
  70. package/dist/server/engine/store/memory-store.d.ts +16 -0
  71. package/dist/server/engine/store/memory-store.d.ts.map +1 -0
  72. package/dist/server/engine/store/memory-store.js +167 -0
  73. package/dist/server/engine/store/memory-store.js.map +1 -0
  74. package/dist/server/engine/store/wal.d.ts +3 -0
  75. package/dist/server/engine/store/wal.d.ts.map +1 -1
  76. package/dist/server/engine/store/wal.js +35 -1
  77. package/dist/server/engine/store/wal.js.map +1 -1
  78. package/dist/server/index.js +159 -44
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/routes/config.d.ts.map +1 -1
  81. package/dist/server/routes/config.js +25 -52
  82. package/dist/server/routes/config.js.map +1 -1
  83. package/dist/server/routes/lambdas.d.ts.map +1 -1
  84. package/dist/server/routes/lambdas.js +65 -0
  85. package/dist/server/routes/lambdas.js.map +1 -1
  86. package/dist/server/routes/services.d.ts.map +1 -1
  87. package/dist/server/routes/services.js +275 -24
  88. package/dist/server/routes/services.js.map +1 -1
  89. package/dist/server/services/bind-host.d.ts +30 -0
  90. package/dist/server/services/bind-host.d.ts.map +1 -0
  91. package/dist/server/services/bind-host.js +182 -0
  92. package/dist/server/services/bind-host.js.map +1 -0
  93. package/dist/server/services/cache-manager.d.ts +2 -1
  94. package/dist/server/services/cache-manager.d.ts.map +1 -1
  95. package/dist/server/services/cache-manager.js +4 -14
  96. package/dist/server/services/cache-manager.js.map +1 -1
  97. package/dist/server/services/config-manager.d.ts +28 -35
  98. package/dist/server/services/config-manager.d.ts.map +1 -1
  99. package/dist/server/services/config-manager.js +475 -189
  100. package/dist/server/services/config-manager.js.map +1 -1
  101. package/dist/server/services/dynamo-explorer.d.ts +1 -0
  102. package/dist/server/services/dynamo-explorer.d.ts.map +1 -1
  103. package/dist/server/services/dynamo-explorer.js +28 -6
  104. package/dist/server/services/dynamo-explorer.js.map +1 -1
  105. package/dist/server/services/explorer-region.d.ts +2 -0
  106. package/dist/server/services/explorer-region.d.ts.map +1 -0
  107. package/dist/server/services/explorer-region.js +23 -0
  108. package/dist/server/services/explorer-region.js.map +1 -0
  109. package/dist/server/services/gateway-manager.d.ts.map +1 -1
  110. package/dist/server/services/gateway-manager.js +18 -5
  111. package/dist/server/services/gateway-manager.js.map +1 -1
  112. package/dist/server/services/invocation-activity.d.ts +45 -0
  113. package/dist/server/services/invocation-activity.d.ts.map +1 -0
  114. package/dist/server/services/invocation-activity.js +102 -0
  115. package/dist/server/services/invocation-activity.js.map +1 -0
  116. package/dist/server/services/lambda-runtime-manager.d.ts +7 -1
  117. package/dist/server/services/lambda-runtime-manager.d.ts.map +1 -1
  118. package/dist/server/services/lambda-runtime-manager.js +118 -10
  119. package/dist/server/services/lambda-runtime-manager.js.map +1 -1
  120. package/dist/server/services/opensearch-explorer.d.ts.map +1 -1
  121. package/dist/server/services/opensearch-explorer.js +14 -10
  122. package/dist/server/services/opensearch-explorer.js.map +1 -1
  123. package/dist/server/services/project-scope.d.ts +2 -0
  124. package/dist/server/services/project-scope.d.ts.map +1 -0
  125. package/dist/server/services/project-scope.js +23 -0
  126. package/dist/server/services/project-scope.js.map +1 -0
  127. package/dist/server/services/queue-inspector.d.ts.map +1 -1
  128. package/dist/server/services/queue-inspector.js +15 -6
  129. package/dist/server/services/queue-inspector.js.map +1 -1
  130. package/dist/server/services/resource-provisioner.d.ts +0 -2
  131. package/dist/server/services/resource-provisioner.d.ts.map +1 -1
  132. package/dist/server/services/resource-provisioner.js +24 -14
  133. package/dist/server/services/resource-provisioner.js.map +1 -1
  134. package/dist/server/services/s3-explorer.js +3 -3
  135. package/dist/server/services/s3-explorer.js.map +1 -1
  136. package/dist/server/services/secrets-explorer.d.ts.map +1 -1
  137. package/dist/server/services/secrets-explorer.js +10 -3
  138. package/dist/server/services/secrets-explorer.js.map +1 -1
  139. package/dist/server/services/seed-manager.d.ts.map +1 -1
  140. package/dist/server/services/seed-manager.js +17 -8
  141. package/dist/server/services/seed-manager.js.map +1 -1
  142. package/dist/server/services/serverless-state-parser.d.ts +10 -0
  143. package/dist/server/services/serverless-state-parser.d.ts.map +1 -1
  144. package/dist/server/services/serverless-state-parser.js +20 -1
  145. package/dist/server/services/serverless-state-parser.js.map +1 -1
  146. package/dist/server/services/service-registrar.d.ts.map +1 -1
  147. package/dist/server/services/service-registrar.js +19 -5
  148. package/dist/server/services/service-registrar.js.map +1 -1
  149. package/dist/server/services/service-scanner.d.ts +20 -0
  150. package/dist/server/services/service-scanner.d.ts.map +1 -0
  151. package/dist/server/services/service-scanner.js +173 -0
  152. package/dist/server/services/service-scanner.js.map +1 -0
  153. package/dist/server/services/source-watcher.d.ts.map +1 -1
  154. package/dist/server/services/source-watcher.js +31 -6
  155. package/dist/server/services/source-watcher.js.map +1 -1
  156. package/dist/ui/assets/ApisPage-BOuimwWz.js +1 -0
  157. package/dist/ui/assets/BucketDetailPage-vLYwhoQt.js +1 -0
  158. package/dist/ui/assets/BucketsPage-Cu5lU1Hp.js +1 -0
  159. package/dist/ui/assets/DynamoPage-y3TTEPUL.js +1 -0
  160. package/dist/ui/assets/DynamoTablePage-BnmSRZjc.js +3 -0
  161. package/dist/ui/assets/LambdaDetailPage-B5q_d27t.js +4 -0
  162. package/dist/ui/assets/LambdasPage-CJpRoEDx.js +1 -0
  163. package/dist/ui/assets/OnboardingPage-D02dymQi.js +1 -0
  164. package/dist/ui/assets/OpenSearchCollectionPage-B_wWTlfL.js +1 -0
  165. package/dist/ui/assets/OpenSearchPage-BLEgfF_g.js +1 -0
  166. package/dist/ui/assets/{OverviewPage-DS6XJ7kG.css → OverviewPage-C1pBJ8aP.css} +1 -1
  167. package/dist/ui/assets/OverviewPage-EdJWiAt6.js +1 -0
  168. package/dist/ui/assets/QueueDetailPage-ByEYHvHe.js +1 -0
  169. package/dist/ui/assets/QueuesPage-CvwYHSll.js +1 -0
  170. package/dist/ui/assets/SecretsPage-DASw8Ntf.js +1 -0
  171. package/dist/ui/assets/ServiceDetailPage-BMsUDg6L.js +2 -0
  172. package/dist/ui/assets/ServicesPage-2JuiIiRr.js +2 -0
  173. package/dist/ui/assets/SettingsPage-DStO4bgN.js +1 -0
  174. package/dist/ui/assets/engine-D0mFFNbL.js +1 -0
  175. package/dist/ui/assets/index-BIEznsi4.js +37 -0
  176. package/dist/ui/assets/index-BRMvNP3Y.css +1 -0
  177. package/dist/ui/assets/onboarding-DQmlvyWU.js +1 -0
  178. package/dist/ui/assets/resourceIcons-baecADXA.js +1 -0
  179. package/dist/ui/assets/triggerIcons-DIPWQkJ3.js +1 -0
  180. package/dist/ui/favicon.svg +37 -0
  181. package/dist/ui/index.html +5 -3
  182. package/package.json +11 -11
  183. package/dist/server/engine/aoss-sidecar.d.ts +0 -13
  184. package/dist/server/engine/aoss-sidecar.d.ts.map +0 -1
  185. package/dist/server/engine/aoss-sidecar.js +0 -93
  186. package/dist/server/engine/aoss-sidecar.js.map +0 -1
  187. package/dist/server/engine/backends/localstack-backend.d.ts +0 -25
  188. package/dist/server/engine/backends/localstack-backend.d.ts.map +0 -1
  189. package/dist/server/engine/backends/localstack-backend.js +0 -199
  190. package/dist/server/engine/backends/localstack-backend.js.map +0 -1
  191. package/dist/server/engine/engine-backend.d.ts +0 -17
  192. package/dist/server/engine/engine-backend.d.ts.map +0 -1
  193. package/dist/server/engine/engine-backend.js +0 -7
  194. package/dist/server/engine/engine-backend.js.map +0 -1
  195. package/dist/server/services/localstack-manager.d.ts +0 -17
  196. package/dist/server/services/localstack-manager.d.ts.map +0 -1
  197. package/dist/server/services/localstack-manager.js +0 -31
  198. package/dist/server/services/localstack-manager.js.map +0 -1
  199. package/dist/ui/assets/ApisPage-CXYcWDhm.js +0 -1
  200. package/dist/ui/assets/BucketDetailPage-BwS2--Pe.js +0 -1
  201. package/dist/ui/assets/BucketsPage-khuiYc9C.js +0 -1
  202. package/dist/ui/assets/DynamoPage-aYo0QC0N.js +0 -1
  203. package/dist/ui/assets/DynamoTablePage-rFuV2pgd.js +0 -3
  204. package/dist/ui/assets/LambdaDetailPage-CJmu8M_N.js +0 -4
  205. package/dist/ui/assets/LambdasPage-DniNyKMe.js +0 -1
  206. package/dist/ui/assets/OpenSearchCollectionPage-BBwjgbB9.js +0 -1
  207. package/dist/ui/assets/OpenSearchPage-D5RxJGRo.js +0 -1
  208. package/dist/ui/assets/OverviewPage-Cyt1Xdp8.js +0 -1
  209. package/dist/ui/assets/QueueDetailPage-3FPsowIp.js +0 -1
  210. package/dist/ui/assets/QueuesPage-CUoDDgHr.js +0 -1
  211. package/dist/ui/assets/SecretsPage-B_mTnUf3.js +0 -1
  212. package/dist/ui/assets/ServiceDetailPage-BcdSjA6e.js +0 -2
  213. package/dist/ui/assets/ServicesPage-CU6fb3f8.js +0 -2
  214. package/dist/ui/assets/SettingsPage-BG8cYxgC.js +0 -1
  215. package/dist/ui/assets/index-BxwA-UT0.css +0 -1
  216. package/dist/ui/assets/index-DGsXIPJ-.js +0 -37
  217. package/packages/serverless-plugin/README.md +0 -115
  218. package/packages/serverless-plugin/dist/index.d.ts +0 -2
  219. package/packages/serverless-plugin/dist/index.d.ts.map +0 -1
  220. package/packages/serverless-plugin/dist/index.js +0 -107
  221. 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
 
@@ -142,6 +160,10 @@ function getOrchestratorPath() {
142
160
  }
143
161
 
144
162
  function startOrchestrator() {
163
+ // Before anything else — including the already-running short-circuit — so a
164
+ // stale 0.x flag or config value always fails loudly instead of looking like
165
+ // a successful no-op.
166
+
145
167
  const { pidFile, logFile } = runtimePaths();
146
168
 
147
169
  if (fs.existsSync(pidFile)) {
@@ -150,15 +172,11 @@ function startOrchestrator() {
150
172
  process.kill(pid, 0);
151
173
  const config = loadConfig();
152
174
  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
- }
175
+ console.log(t('start.already', { pid }));
176
+ console.log(t('start.server', { url: `http://localhost:${cfg.serverPort}` }));
177
+ console.log(t('start.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
160
178
  if (cfg.enableDynamoProxy) {
161
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
179
+ console.log(t('start.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
162
180
  }
163
181
  return;
164
182
  } catch (e) {
@@ -169,12 +187,12 @@ function startOrchestrator() {
169
187
  const orchestratorPath = getOrchestratorPath();
170
188
 
171
189
  if (!orchestratorPath) {
172
- console.error('❌ Orchestrator not found or not built.');
190
+ console.error(t('start.notBuilt'));
173
191
  console.error('');
174
- console.error('If you are developing LSS, run:');
192
+ console.error(t('start.notBuiltDev'));
175
193
  console.error(' cd /path/to/local-serverless-stack && npm run build');
176
194
  console.error('');
177
- console.error('If you installed via npm, please report this as a bug.');
195
+ console.error(t('start.notBuiltBug'));
178
196
  process.exit(1);
179
197
  }
180
198
 
@@ -184,33 +202,14 @@ function startOrchestrator() {
184
202
  const config = loadConfig();
185
203
  const cfg = getConfig(config);
186
204
 
187
- // Check for flags
188
205
  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
206
 
204
207
  // Build environment variables from config
205
208
  const env = { ...process.env };
206
- /* istanbul ignore else: getConfig() always defaults serverPort to 3100, so the else is unreachable */
209
+ /* istanbul ignore else: getConfig() always defaults serverPort, so the else is unreachable */
207
210
  if (cfg.serverPort) {
208
211
  env.PORT = cfg.serverPort;
209
212
  }
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
213
  if (enableDynamoProxy) {
215
214
  env.LSS_ENABLE_DYNAMO_PROXY = 'true';
216
215
  }
@@ -218,30 +217,9 @@ function startOrchestrator() {
218
217
  if (cfg.dynamoProxyPort) {
219
218
  env.LSS_DYNAMO_PROXY_PORT = cfg.dynamoProxyPort;
220
219
  }
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
220
  // 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.
221
+ // identical serverPort/seedsDir/region (not just the hand-translated subset
222
+ // above). Keeps the two config loaders in agreement.
245
223
  if (EXPLICIT_CONFIG) {
246
224
  env.LSS_CONFIG_PATH = EXPLICIT_CONFIG;
247
225
  }
@@ -257,24 +235,20 @@ function startOrchestrator() {
257
235
  fs.closeSync(logFd);
258
236
  fs.writeFileSync(pidFile, child.pid.toString());
259
237
 
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
- }
238
+ console.log(t('start.started', { pid: child.pid }));
239
+ console.log(t('start.server', { url: `http://localhost:${cfg.serverPort}` }));
240
+ console.log(t('start.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
267
241
  if (enableDynamoProxy) {
268
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
242
+ console.log(t('start.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
269
243
  }
270
- console.log('📝 Logs:', logFile);
244
+ console.log(t('start.logs', { path: logFile }));
271
245
 
272
246
  setTimeout(() => {
273
247
  try {
274
248
  process.kill(child.pid, 0);
275
- console.log('✅ Service is running');
249
+ console.log(t('start.running'));
276
250
  } catch (e) {
277
- console.error('❌ Service failed to start. Check logs:', logFile);
251
+ console.error(t('start.failed', { path: logFile }));
278
252
  if (fs.existsSync(pidFile)) {
279
253
  fs.unlinkSync(pidFile);
280
254
  }
@@ -318,7 +292,7 @@ function waitForExit(pid, timeoutMs = 10000, intervalMs = 200) {
318
292
  async function stopOrchestrator() {
319
293
  const { pidFile } = runtimePaths();
320
294
  if (!fs.existsSync(pidFile)) {
321
- console.log('⚠️ LSS Orchestrator is not running');
295
+ console.log(t('stop.notRunning'));
322
296
  return;
323
297
  }
324
298
 
@@ -327,7 +301,7 @@ async function stopOrchestrator() {
327
301
  try {
328
302
  process.kill(pid, 'SIGTERM');
329
303
  } catch (e) {
330
- console.error('❌ Failed to stop process:', e.message);
304
+ console.error(t('stop.failed', { error: e.message }));
331
305
  if (fs.existsSync(pidFile)) {
332
306
  fs.unlinkSync(pidFile);
333
307
  }
@@ -337,16 +311,16 @@ async function stopOrchestrator() {
337
311
  const exited = await waitForExit(pid);
338
312
  fs.unlinkSync(pidFile);
339
313
  if (exited) {
340
- console.log('🛑 LSS Orchestrator stopped (PID:', pid + ')');
314
+ console.log(t('stop.stopped', { pid }));
341
315
  } 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.`);
316
+ console.warn(t('stop.timeout', { pid }));
343
317
  }
344
318
  }
345
319
 
346
320
  function showStatus() {
347
321
  const { pidFile, logFile } = runtimePaths();
348
322
  if (!fs.existsSync(pidFile)) {
349
- console.log('⚪ LSS Orchestrator: NOT RUNNING');
323
+ console.log(t('status.notRunning'));
350
324
  return;
351
325
  }
352
326
 
@@ -356,15 +330,15 @@ function showStatus() {
356
330
  process.kill(pid, 0);
357
331
  const config = loadConfig();
358
332
  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}`);
333
+ console.log(t('status.running', { pid }));
334
+ console.log(t('status.server', { url: `http://localhost:${cfg.serverPort}` }));
335
+ console.log(t('status.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
362
336
  if (cfg.enableDynamoProxy) {
363
- console.log(`🔄 DynamoDB Proxy: http://localhost:${cfg.dynamoProxyPort} (enabled)`);
337
+ console.log(t('status.dynamoProxy', { url: `http://localhost:${cfg.dynamoProxyPort}` }));
364
338
  }
365
- console.log('📝 Logs:', logFile);
339
+ console.log(t('status.logs', { path: logFile }));
366
340
  } catch (e) {
367
- console.log('⚪ LSS Orchestrator: NOT RUNNING (stale PID file)');
341
+ console.log(t('status.stale'));
368
342
  fs.unlinkSync(pidFile);
369
343
  }
370
344
  }
@@ -379,13 +353,13 @@ function getServerPort() {
379
353
  // orchestrator startup window) — fall back through every signal we have so
380
354
  // `formatError` is guaranteed not to return an empty string.
381
355
  function formatError(e) {
382
- if (!e) return 'erro desconhecido (sem detalhes)';
383
- if (typeof e === 'string') return e.trim() || 'erro desconhecido (sem detalhes)';
356
+ if (!e) return t('error.unknown');
357
+ if (typeof e === 'string') return e.trim() || t('error.unknown');
384
358
  if (e.message && String(e.message).trim()) return String(e.message).trim();
385
- if (e.code) return `erro de I/O (${e.code})`;
359
+ if (e.code) return t('error.io', { code: e.code });
386
360
  if (e.name) return e.name;
387
361
  const s = String(e);
388
- return s && s !== '[object Object]' ? s : 'erro desconhecido (sem detalhes)';
362
+ return s && s !== '[object Object]' ? s : t('error.unknown');
389
363
  }
390
364
 
391
365
  function buildHttpError(res, data) {
@@ -395,7 +369,9 @@ function buildHttpError(res, data) {
395
369
  if (fromBody && String(fromBody).trim()) return new Error(String(fromBody).trim());
396
370
  const snippet = data && data.length < 300 ? data.trim() : '';
397
371
  const statusText = res.statusMessage ? `${res.statusCode} ${res.statusMessage}` : `${res.statusCode}`;
398
- return new Error(snippet ? `HTTP ${statusText}: ${snippet}` : `HTTP ${statusText} (sem corpo de erro)`);
372
+ // `HTTP <status>: <snippet>` is pure wire detail, so only the "no body at
373
+ // all" variant carries translatable prose.
374
+ return new Error(snippet ? `HTTP ${statusText}: ${snippet}` : t('error.httpNoBody', { status: statusText }));
399
375
  }
400
376
 
401
377
  function postJson(path, body) {
@@ -427,7 +403,7 @@ function postJson(path, body) {
427
403
  });
428
404
  },
429
405
  );
430
- req.on('error', err => reject(err && err.message ? err : new Error(`falha na conexão HTTP com o orchestrator: ${formatError(err)}`)));
406
+ req.on('error', err => reject(err && err.message ? err : new Error(t('error.httpConnection', { error: formatError(err) }))));
431
407
  req.write(payload);
432
408
  req.end();
433
409
  });
@@ -436,7 +412,7 @@ function postJson(path, body) {
436
412
  function ensureRunningOrExit() {
437
413
  const { pidFile } = runtimePaths();
438
414
  if (!fs.existsSync(pidFile)) {
439
- console.error('❌ LSS Orchestrator is not running. Start it with: npx lss start');
415
+ console.error(t('error.notRunning'));
440
416
  process.exit(1);
441
417
  }
442
418
  }
@@ -445,19 +421,19 @@ function printSeedRunResults(results) {
445
421
  let missingTables = 0;
446
422
  for (const r of results) {
447
423
  if (r.skipped) {
448
- console.log(` ⚠ ${r.tableName}: skipped (${r.reason})`);
449
- if (r.reason && r.reason.includes('does not exist in LocalStack')) {
424
+ console.log(t('seed.skipped', { table: r.tableName, reason: r.reason }));
425
+ if (r.reason && r.reason.includes('does not exist in the engine')) {
450
426
  missingTables++;
451
427
  }
452
428
  } else {
453
- console.log(` ✓ ${r.tableName}: ${r.inserted} item(s) inserted`);
429
+ console.log(t('seed.inserted', { table: r.tableName, count: r.inserted }));
454
430
  }
455
431
  }
456
432
  return { missingTables };
457
433
  }
458
434
 
459
435
  // 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
436
+ // the DynamoDB tables actually living in the engine. This is the difference
461
437
  // between "I didn't deploy yet" (no live tables at all) and "my seed file
462
438
  // name doesn't match the CFN TableName" (live tables exist, just not those).
463
439
  function printSeedMismatchDiagnostic({ entries, liveTables, focusTable }) {
@@ -466,37 +442,39 @@ function printSeedMismatchDiagnostic({ entries, liveTables, focusTable }) {
466
442
 
467
443
  console.log('');
468
444
  if (focusTable) {
469
- console.log(`📂 Arquivo de seed inspecionado: ${focusTable}.json`);
445
+ console.log(t('seed.diagFile', { table: focusTable }));
470
446
  } else if (focusList.length > 0) {
471
- console.log('📂 Arquivos de seed inspecionados (tabela esperada):');
447
+ console.log(t('seed.diagFiles'));
472
448
  for (const name of focusList) console.log(` - ${name}`);
473
449
  } else {
474
- console.log('📂 Nenhum arquivo *.json encontrado no seedsDir.');
450
+ console.log(t('seed.diagNoFiles'));
475
451
  }
476
452
 
477
453
  if (liveTables && liveTables.length > 0) {
478
454
  console.log('');
479
- console.log(`🗂️ Tabelas vivas no LocalStack (${liveTables.length}):`);
455
+ console.log(t('seed.diagLiveTables', { count: liveTables.length }));
480
456
  for (const name of liveTables) console.log(` - ${name}`);
481
457
  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.');
458
+ console.log(t('seed.diagMatchHint'));
459
+ console.log(t('seed.diagPrefixHint'));
484
460
  } else {
485
461
  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.');
462
+ console.log(t('seed.diagNoLiveTables'));
463
+ console.log(t('seed.diagNotProvisioned'));
464
+ // The commands themselves are never translated — only the comment that
465
+ // explains what each one buys you.
466
+ console.log(` npx lss start # ${t('seed.diagStepStart')}`);
467
+ console.log(` npx serverless deploy # ${t('seed.diagStepDeploy')}`);
468
+ console.log(t('seed.diagRetry'));
491
469
  }
492
470
  }
493
471
 
494
472
  function printSeedClearResults(results) {
495
473
  for (const r of results) {
496
474
  if (r.skipped) {
497
- console.log(` ⚠ ${r.tableName}: skipped (${r.reason})`);
475
+ console.log(t('seed.skipped', { table: r.tableName, reason: r.reason }));
498
476
  } else {
499
- console.log(` ✓ ${r.tableName}: ${r.deleted} item(s) deleted`);
477
+ console.log(t('seed.deleted', { table: r.tableName, count: r.deleted }));
500
478
  }
501
479
  }
502
480
  }
@@ -504,7 +482,7 @@ function printSeedClearResults(results) {
504
482
  async function runSeed(tableName) {
505
483
  ensureRunningOrExit();
506
484
  try {
507
- console.log(tableName ? `🌱 Seeding ${tableName}...` : '🌱 Seeding all tables with seed files...');
485
+ console.log(tableName ? t('seed.runningTable', { table: tableName }) : t('seed.running'));
508
486
  const res = await postJson('/api/seeds/run', tableName ? { tableName } : {});
509
487
  const { missingTables } = printSeedRunResults(res.results || []);
510
488
  if (missingTables > 0) {
@@ -517,11 +495,11 @@ async function runSeed(tableName) {
517
495
  });
518
496
  } catch (diagErr) {
519
497
  // Diagnostic is best-effort; don't fail the seed because the hint failed.
520
- console.log(`(não consegui detalhar tabelas vivas: ${formatError(diagErr)})`);
498
+ console.log(t('seed.diagFailed', { error: formatError(diagErr) }));
521
499
  }
522
500
  }
523
501
  } catch (e) {
524
- console.error('❌ Seed failed:', formatError(e));
502
+ console.error(t('seed.failed'), formatError(e));
525
503
  process.exit(1);
526
504
  }
527
505
  }
@@ -550,7 +528,7 @@ function getJson(path) {
550
528
  });
551
529
  },
552
530
  );
553
- req.on('error', err => reject(err && err.message ? err : new Error(`falha na conexão HTTP com o orchestrator: ${formatError(err)}`)));
531
+ req.on('error', err => reject(err && err.message ? err : new Error(t('error.httpConnection', { error: formatError(err) }))));
554
532
  req.end();
555
533
  });
556
534
  }
@@ -562,7 +540,7 @@ function promptConfirmation(expectedWord) {
562
540
  input: process.stdin,
563
541
  output: process.stdout,
564
542
  });
565
- rl.question(`Digite "${expectedWord}" para prosseguir (ou qualquer outra coisa para cancelar): `, answer => {
543
+ rl.question(t('seed.confirmPrompt', { word: expectedWord }), answer => {
566
544
  rl.close();
567
545
  resolve(answer.trim() === expectedWord);
568
546
  });
@@ -577,8 +555,8 @@ async function clearSeed(tableName) {
577
555
 
578
556
  // Show the user exactly what's about to be wiped before they confirm.
579
557
  // 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.
558
+ // talking to the local engine and not AWS — the orchestrator only ever
559
+ // connects to the configured local engine endpoint.
582
560
  let scopeDescription;
583
561
  try {
584
562
  const list = await getJson('/api/seeds');
@@ -589,138 +567,300 @@ async function clearSeed(tableName) {
589
567
 
590
568
  if (liveTargets.length === 0) {
591
569
  if (tableName) {
592
- console.log(`⚠️ Nenhuma tabela "${tableName}" existente no LocalStack para limpar.`);
570
+ console.log(t('clear.noSuchTable', { table: tableName }));
593
571
  } else if (entries.length === 0) {
594
- console.log('⚠️ Nenhum arquivo de seed (*.json) encontrado no seedsDir — nada para limpar.');
572
+ console.log(t('clear.noSeedFiles'));
595
573
  } else {
596
- console.log(`⚠️ ${entries.length} arquivo(s) de seed encontrados, mas NENHUMA das tabelas correspondentes existe no LocalStack.`);
574
+ console.log(t('clear.noneLive', { count: entries.length }));
597
575
  }
598
576
  printSeedMismatchDiagnostic({ entries, liveTables, focusTable: tableName });
599
577
  return;
600
578
  }
601
579
 
602
580
  scopeDescription = tableName
603
- ? `a tabela "${tableName}"`
604
- : `${liveTargets.length} tabela(s): ${liveTargets.map(e => e.tableName).join(', ')}`;
581
+ ? t('clear.scopeTable', { table: tableName })
582
+ : t('clear.scopeTables', {
583
+ count: liveTargets.length,
584
+ tables: liveTargets.map(e => e.tableName).join(', '),
585
+ });
605
586
 
606
587
  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.');
588
+ console.log(t('clear.warning'));
589
+ console.log(t('clear.target', { scope: scopeDescription }));
590
+ console.log(t('clear.engine', { url: `http://localhost:${cfg.selfEnginePort}` }));
591
+ console.log(t('clear.noAws'));
611
592
  console.log('');
612
593
  } catch (e) {
613
- console.error('❌ Não consegui listar as tabelas antes de limpar:', formatError(e));
594
+ console.error(t('clear.listFailed'), formatError(e));
614
595
  process.exit(1);
615
596
  }
616
597
 
617
598
  if (!skipConfirm) {
599
+ // The word itself is NOT translated: it is a magic token CI scripts and the
600
+ // docs already depend on, so it stays `confirmar` in every locale.
618
601
  const ok = await promptConfirmation('confirmar');
619
602
  if (!ok) {
620
- console.log('🚫 Cancelado — nenhuma alteração feita.');
603
+ console.log(t('seed.aborted'));
621
604
  return;
622
605
  }
623
606
  } else {
624
- console.log('↳ --yes informado, pulando confirmação interativa.');
607
+ console.log(t('clear.skipPrompt'));
625
608
  }
626
609
 
627
610
  try {
628
- console.log(tableName ? `🧹 Clearing ${tableName}...` : '🧹 Clearing all seeded tables...');
611
+ console.log(tableName ? t('seed.clearingTable', { table: tableName }) : t('seed.clearing'));
629
612
  const res = await postJson('/api/seeds/clear', tableName ? { tableName } : {});
630
613
  printSeedClearResults(res.results || []);
631
614
  } catch (e) {
632
- console.error('❌ Clear failed:', formatError(e));
615
+ console.error(t('clear.failed'), formatError(e));
616
+ process.exit(1);
617
+ }
618
+ }
619
+
620
+ /**
621
+ * `lss mcp` — run the Model Context Protocol server on stdio.
622
+ *
623
+ * Started by an MCP client (Claude Code reads .mcp.json), never by a human at a
624
+ * prompt: stdout is the JSON-RPC frame stream, so nothing else may be written
625
+ * there. This process only talks HTTP to an already-running orchestrator — it
626
+ * never boots one — so `lss start` has to have happened first.
627
+ */
628
+ /**
629
+ * `lss register [path...]` — register services with the running orchestrator.
630
+ *
631
+ * This replaces the retired serverless-lss plugin: registration is a plain
632
+ * POST of { servicePath }, and the orchestrator resolves everything else
633
+ * (packaging via autoPackage when needed, then name/region/custom.lss ports
634
+ * from the packaged serverless-state.json).
635
+ */
636
+ async function registerServices(paths) {
637
+ ensureRunningOrExit();
638
+ const targets = paths.length > 0 ? paths : ['.'];
639
+ let failed = 0;
640
+ for (const target of targets) {
641
+ const servicePath = path.resolve(target);
642
+ if (!fs.existsSync(servicePath)) {
643
+ console.error(t('register.notFound', { target }));
644
+ failed++;
645
+ continue;
646
+ }
647
+ try {
648
+ const result = await postJson('/api/services/register', { servicePath });
649
+ console.log(t('register.ok', {
650
+ name: result.serviceName,
651
+ resources: result.resourcesCount,
652
+ functions: result.functionsCount,
653
+ routes: result.routesCount,
654
+ }));
655
+ // Warnings come from the orchestrator already worded; only the bullet is ours.
656
+ for (const warning of result.warnings || []) {
657
+ console.log(` ⚠ ${warning}`);
658
+ }
659
+ } catch (e) {
660
+ console.error(t('register.failed', { target, error: formatError(e) }));
661
+ failed++;
662
+ }
663
+ }
664
+ if (failed > 0) {
633
665
  process.exit(1);
634
666
  }
635
667
  }
636
668
 
669
+ /**
670
+ * `lss scan` — list every Serverless/osls service found under the project
671
+ * root, with the same flags onboarding shows.
672
+ *
673
+ * The three flags mirror the checklist: `registered` (known to this
674
+ * orchestrator), `installed` (node_modules resolvable, so packaging can run
675
+ * without an install first) and `packaged` (a template already exists). The
676
+ * effective package command gets its own line because it is per-service
677
+ * configurable (`servicePackaging`) and is what the install→package→register
678
+ * buttons would run.
679
+ */
680
+ /**
681
+ * Render one scan warning. The orchestrator sends a stable `code` plus the
682
+ * English `message` it produced; the code is what gets localised, and the
683
+ * message is the fallback for a code this CLI has not been taught yet — a
684
+ * newer server's warning still prints instead of disappearing.
685
+ */
686
+ function scanWarningText(warning) {
687
+ if (!warning || typeof warning !== 'object') return String(warning);
688
+ const key = `scan.warning.${warning.code}`;
689
+ const translated = t(key, warning.params);
690
+ return translated === key ? warning.message : translated;
691
+ }
692
+
693
+ async function scanServices() {
694
+ ensureRunningOrExit();
695
+ try {
696
+ const result = await getJson('/api/services/scan');
697
+ const services = result.services || [];
698
+ if (services.length === 0) {
699
+ console.log(t('scan.none', { root: result.projectRoot }));
700
+ return;
701
+ }
702
+ console.log(`${t('scan.header', { count: services.length, root: result.projectRoot })}\n`);
703
+ for (const svc of services) {
704
+ const flags = [
705
+ svc.registered ? t('scan.registered') : t('scan.notRegistered'),
706
+ svc.installed ? t('scan.installed') : t('scan.notInstalled'),
707
+ svc.packaged ? t('scan.packaged') : t('scan.notPackaged'),
708
+ ].join(', ');
709
+ const ports = svc.apiPort ? ` api:${svc.apiPort}${svc.invokePort ? ` invoke:${svc.invokePort}` : ''}` : '';
710
+ console.log(` ${svc.registered ? '✓' : '·'} ${svc.name} (${svc.relPath}) — ${flags}${ports}`);
711
+ if (svc.packageCommand) {
712
+ console.log(` ${t('scan.packageCommand', { command: svc.packageCommand })}`);
713
+ }
714
+ for (const warning of svc.warnings || []) {
715
+ console.log(` ⚠ ${scanWarningText(warning)}`);
716
+ }
717
+ }
718
+ console.log(t('scan.hint'));
719
+ } catch (e) {
720
+ console.error(t('scan.failed'), formatError(e));
721
+ process.exit(1);
722
+ }
723
+ }
724
+
725
+ function getMcpServerPath() {
726
+ const candidates = [
727
+ path.join(__dirname, '../dist/mcp/server.js'),
728
+ path.join(__dirname, '..', 'dist', 'mcp', 'server.js'),
729
+ ];
730
+ return candidates.find(candidate => fs.existsSync(candidate)) || null;
731
+ }
732
+
733
+ function getPackageVersion() {
734
+ try {
735
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8')).version || '0.0.0';
736
+ } catch {
737
+ return '0.0.0';
738
+ }
739
+ }
740
+
741
+ function runMcpServer() {
742
+ const serverPath = getMcpServerPath();
743
+ const version = getPackageVersion();
744
+ /* 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 */
745
+ if (!serverPath) {
746
+ console.error(t('mcp.missing'));
747
+ process.exit(1); // never returns
748
+ }
749
+ // Dynamic import: the MCP build is ESM, and loading it only for this command
750
+ // keeps every other CLI invocation free of it.
751
+ /* istanbul ignore next: process entry point — loads the ESM build and binds this process's stdio */
752
+ import(pathToFileURL(serverPath).href)
753
+ .then(mod => mod.main(version))
754
+ .catch(error => {
755
+ console.error(t('mcp.startFailed'), error && error.message ? error.message : error);
756
+ process.exit(1);
757
+ });
758
+ }
759
+
760
+ // One ` <name> <description>` row of the help screen. The description is
761
+ // wrapped and hanging-indented under itself so a translation can be longer than
762
+ // the English original without shredding the column — the reason the help text
763
+ // is assembled here instead of living in the catalogue as one pre-formatted blob.
764
+ function helpRow(name, description, nameWidth, textWidth) {
765
+ const gutter = ' '.repeat(2 + nameWidth);
766
+ const wrapped = [];
767
+ let current = '';
768
+ for (const word of description.split(' ')) {
769
+ if (current && current.length + 1 + word.length > textWidth) {
770
+ wrapped.push(current);
771
+ current = word;
772
+ } else {
773
+ current = current ? `${current} ${word}` : word;
774
+ }
775
+ }
776
+ wrapped.push(current);
777
+ return [` ${name.padEnd(nameWidth)}${wrapped[0]}`, ...wrapped.slice(1).map(line => gutter + line)]
778
+ .join('\n');
779
+ }
780
+
781
+ // A ` npx lss … # what it does` example line. The command is never translated.
782
+ function helpExample(command, comment) {
783
+ return ` ${command.padEnd(43)}# ${comment}`;
784
+ }
785
+
637
786
  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
- `);
787
+ // Command names, flags, env var names, paths and the JSON template are
788
+ // identifiers — they stay here, untranslated. Only prose goes through t().
789
+ const cmd = (name, key) => helpRow(name, t(key), 19, 56);
790
+ const opt = (name, key) => helpRow(name, t(key), 29, 46);
791
+
792
+ console.log([
793
+ '',
794
+ 'Local Serverless Stack (LSS) CLI',
795
+ '',
796
+ t('help.usage'),
797
+ '',
798
+ t('help.commands'),
799
+ cmd('start', 'help.cmd.start'),
800
+ cmd('stop', 'help.cmd.stop'),
801
+ cmd('status', 'help.cmd.status'),
802
+ cmd('logs', 'help.cmd.logs'),
803
+ cmd('seed [table]', 'help.cmd.seed'),
804
+ cmd('scan', 'help.cmd.scan'),
805
+ cmd('register [path...]', 'help.cmd.register'),
806
+ cmd('seed:clear [table]', 'help.cmd.seedClear'),
807
+ cmd('mcp', 'help.cmd.mcp'),
808
+ cmd('help', 'help.cmd.help'),
809
+ '',
810
+ t('help.options'),
811
+ opt('--config <path>', 'help.opt.config'),
812
+ opt('--enable-dynamo-proxy', 'help.opt.dynamoProxy'),
813
+ opt('--yes, -y', 'help.opt.yes'),
814
+ '',
815
+ t('help.environment'),
816
+ opt('LSS_DASHBOARD_PORT', 'help.env.dashboardPort'),
817
+ opt('LSS_ENGINE_PORT', 'help.env.enginePort'),
818
+ opt('LSS_ENGINE_DATA_DIR', 'help.env.engineDataDir'),
819
+ opt('AWS_REGION', 'help.env.awsRegion'),
820
+ opt('LSS_LANG', 'help.env.lang'),
821
+ '',
822
+ t('help.configuration'),
823
+ ` ${t('help.config.intro')}`,
824
+ '',
825
+ ` ${t('help.config.example')}`,
826
+ ' {',
827
+ ' "serverPort": 3100,',
828
+ ' "selfEngine": { "port": 14566 },',
829
+ ' "enableDynamoProxy": false,',
830
+ ' "dynamoProxyPort": 8000,',
831
+ ' "region": "us-east-1",',
832
+ ' "persistence": true,',
833
+ ' "debug": false,',
834
+ ' "stateDir": ".lss"',
835
+ ' }',
836
+ '',
837
+ helpRow('', t('help.config.stateDir'), 0, 76),
838
+ '',
839
+ t('help.examples'),
840
+ helpExample('npx lss start', t('help.ex.start')),
841
+ helpExample('npx lss start --enable-dynamo-proxy', t('help.ex.startProxy')),
842
+ helpExample('npx lss mcp', t('help.ex.mcp')),
843
+ helpExample('npx lss stop', t('help.ex.stop')),
844
+ helpExample('npx lss status', t('help.ex.status')),
845
+ helpExample('npx lss logs', t('help.ex.logs')),
846
+ helpExample('npx lss scan', t('help.ex.scan')),
847
+ helpExample('npx lss register', t('help.ex.register')),
848
+ helpExample('npx lss seed', t('help.ex.seed')),
849
+ helpExample('npx lss seed users', t('help.ex.seedTable')),
850
+ helpExample('npx lss seed:clear users', t('help.ex.seedClear')),
851
+ helpExample('npx lss seed:clear users --yes', t('help.ex.seedClearYes')),
852
+ '',
853
+ ].join('\n'));
714
854
  }
715
855
 
716
856
  function showLogs() {
717
857
  const { logFile } = runtimePaths();
718
858
  if (!fs.existsSync(logFile)) {
719
- console.log('⚠️ No logs found');
859
+ console.log(t('logs.missing', { path: logFile }));
720
860
  return;
721
861
  }
722
862
 
723
- console.log('📝 Logs from:', logFile);
863
+ console.log(t('logs.from', { path: logFile }));
724
864
  console.log('---');
725
865
  const logs = fs.readFileSync(logFile, 'utf8');
726
866
  const lines = logs.split('\n');
@@ -732,11 +872,23 @@ function showLogs() {
732
872
  // Skip anything that looks like a flag so `seed:clear --yes` doesn't pass
733
873
  // "--yes" as the table name.
734
874
  function firstPositional() {
875
+ return allPositionals()[0];
876
+ }
877
+
878
+ // Every non-flag argument after the command, skipping flag VALUES too
879
+ // (`--config <path>` consumes the next token).
880
+ function allPositionals() {
881
+ const flagsWithValue = new Set(['--config']);
882
+ const out = [];
735
883
  for (let i = 3; i < process.argv.length; i++) {
736
884
  const arg = process.argv[i];
737
- if (!arg.startsWith('-')) return arg;
885
+ if (arg.startsWith('-')) {
886
+ if (flagsWithValue.has(arg)) i++;
887
+ continue;
888
+ }
889
+ out.push(arg);
738
890
  }
739
- return undefined;
891
+ return out;
740
892
  }
741
893
 
742
894
  // Exported so the pure/in-process helpers can be unit-tested by requiring this
@@ -746,6 +898,12 @@ function firstPositional() {
746
898
  module.exports = {
747
899
  loadConfig,
748
900
  getConfig,
901
+ allPositionals,
902
+ registerServices,
903
+ scanServices,
904
+ getMcpServerPath,
905
+ getPackageVersion,
906
+ runMcpServer,
749
907
  getArgValue,
750
908
  runtimePaths,
751
909
  getOrchestratorPath,
@@ -792,13 +950,22 @@ if (require.main === module) {
792
950
  case 'seed:clear':
793
951
  clearSeed(firstPositional());
794
952
  break;
953
+ case 'register':
954
+ registerServices(allPositionals());
955
+ break;
956
+ case 'scan':
957
+ scanServices();
958
+ break;
959
+ case 'mcp':
960
+ runMcpServer();
961
+ break;
795
962
  case 'help':
796
963
  case '--help':
797
964
  case '-h':
798
965
  showHelp();
799
966
  break;
800
967
  default:
801
- console.log('❌ Unknown command:', command);
968
+ console.log(t('help.unknown', { command }));
802
969
  showHelp();
803
970
  process.exit(1);
804
971
  }