scene-capability-engine 3.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 (336) hide show
  1. package/CHANGELOG.md +2513 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/README.zh.md +630 -0
  5. package/bin/kiro-spec-engine.js +796 -0
  6. package/bin/kse.js +3 -0
  7. package/bin/sce.js +3 -0
  8. package/bin/sco.js +3 -0
  9. package/docs/331-poc-adaptation-roadmap.md +156 -0
  10. package/docs/331-poc-dual-track-integration-guide.md +120 -0
  11. package/docs/331-poc-weekly-delivery-checklist.md +52 -0
  12. package/docs/OFFLINE_INSTALL.md +96 -0
  13. package/docs/README.md +279 -0
  14. package/docs/adopt-migration-guide.md +599 -0
  15. package/docs/adoption-guide.md +616 -0
  16. package/docs/agent-hooks-analysis.md +815 -0
  17. package/docs/architecture.md +733 -0
  18. package/docs/articles/ai-driven-development-philosophy-and-practice-review.md +208 -0
  19. package/docs/articles/ai-driven-development-philosophy-and-practice.en.md +459 -0
  20. package/docs/articles/ai-driven-development-philosophy-and-practice.md +492 -0
  21. package/docs/autonomous-control-guide.md +851 -0
  22. package/docs/command-reference.md +1368 -0
  23. package/docs/community.md +115 -0
  24. package/docs/cross-tool-guide.md +555 -0
  25. package/docs/developer-guide.md +619 -0
  26. package/docs/document-governance.md +865 -0
  27. package/docs/environment-management-guide.md +526 -0
  28. package/docs/examples/add-export-command/design.md +194 -0
  29. package/docs/examples/add-export-command/requirements.md +110 -0
  30. package/docs/examples/add-export-command/tasks.md +88 -0
  31. package/docs/examples/add-rest-api/design.md +855 -0
  32. package/docs/examples/add-rest-api/requirements.md +323 -0
  33. package/docs/examples/add-rest-api/tasks.md +355 -0
  34. package/docs/examples/add-user-dashboard/design.md +192 -0
  35. package/docs/examples/add-user-dashboard/requirements.md +143 -0
  36. package/docs/examples/add-user-dashboard/tasks.md +91 -0
  37. package/docs/faq.md +697 -0
  38. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.json +156 -0
  39. package/docs/handoffs/evidence/ontology/moqui-template-baseline-2026-02-17-232922.md +24 -0
  40. package/docs/images/wechat-qr.png +0 -0
  41. package/docs/integration-modes.md +529 -0
  42. package/docs/integration-philosophy.md +313 -0
  43. package/docs/knowledge-management-guide.md +263 -0
  44. package/docs/manual-workflows-guide.md +418 -0
  45. package/docs/moqui-capability-matrix.md +73 -0
  46. package/docs/moqui-template-core-library-playbook.md +109 -0
  47. package/docs/multi-agent-coordination-guide.md +553 -0
  48. package/docs/multi-repo-management-guide.md +1344 -0
  49. package/docs/quick-start-with-ai-tools.md +375 -0
  50. package/docs/quick-start.md +146 -0
  51. package/docs/release-checklist.md +121 -0
  52. package/docs/releases/README.md +13 -0
  53. package/docs/releases/v1.46.2-validation.md +45 -0
  54. package/docs/releases/v1.46.2.md +50 -0
  55. package/docs/scene-runtime-guide.md +347 -0
  56. package/docs/spec-collaboration-guide.md +369 -0
  57. package/docs/spec-locking-guide.md +225 -0
  58. package/docs/spec-numbering-guide.md +348 -0
  59. package/docs/spec-workflow.md +519 -0
  60. package/docs/steering-strategy-guide.md +196 -0
  61. package/docs/team-collaboration-guide.md +465 -0
  62. package/docs/testing-strategy.md +272 -0
  63. package/docs/tools/claude-guide.md +654 -0
  64. package/docs/tools/cursor-guide.md +706 -0
  65. package/docs/tools/generic-guide.md +446 -0
  66. package/docs/tools/kiro-guide.md +308 -0
  67. package/docs/tools/vscode-guide.md +445 -0
  68. package/docs/tools/windsurf-guide.md +391 -0
  69. package/docs/troubleshooting.md +1135 -0
  70. package/docs/upgrade-guide.md +639 -0
  71. package/docs/value-observability-guide.md +127 -0
  72. package/docs/zh/README.md +341 -0
  73. package/docs/zh/quick-start.md +764 -0
  74. package/docs/zh/release-checklist.md +121 -0
  75. package/docs/zh/releases/README.md +13 -0
  76. package/docs/zh/releases/v1.46.2-validation.md +45 -0
  77. package/docs/zh/releases/v1.46.2.md +50 -0
  78. package/docs/zh/spec-numbering-guide.md +348 -0
  79. package/docs/zh/tools/claude-guide.md +349 -0
  80. package/docs/zh/tools/cursor-guide.md +281 -0
  81. package/docs/zh/tools/generic-guide.md +499 -0
  82. package/docs/zh/tools/kiro-guide.md +342 -0
  83. package/docs/zh/tools/vscode-guide.md +449 -0
  84. package/docs/zh/tools/windsurf-guide.md +378 -0
  85. package/docs/zh/value-observability-guide.md +127 -0
  86. package/docs//344/272/244/344/273/230/346/270/205/345/215/225.md +75 -0
  87. package/lib/adoption/adoption-logger.js +487 -0
  88. package/lib/adoption/adoption-strategy.js +538 -0
  89. package/lib/adoption/backup-manager.js +420 -0
  90. package/lib/adoption/conflict-resolver.js +410 -0
  91. package/lib/adoption/detection-engine.js +275 -0
  92. package/lib/adoption/diff-viewer.js +226 -0
  93. package/lib/adoption/error-formatter.js +509 -0
  94. package/lib/adoption/file-classifier.js +385 -0
  95. package/lib/adoption/progress-reporter.js +534 -0
  96. package/lib/adoption/smart-orchestrator.js +470 -0
  97. package/lib/adoption/strategy-selector.js +218 -0
  98. package/lib/adoption/summary-generator.js +493 -0
  99. package/lib/adoption/template-sync.js +605 -0
  100. package/lib/auto/autonomous-engine.js +485 -0
  101. package/lib/auto/checkpoint-manager.js +300 -0
  102. package/lib/auto/close-loop-runner.js +2476 -0
  103. package/lib/auto/config-schema.js +176 -0
  104. package/lib/auto/decision-engine.js +344 -0
  105. package/lib/auto/error-recovery-manager.js +580 -0
  106. package/lib/auto/goal-decomposer.js +278 -0
  107. package/lib/auto/progress-tracker.js +502 -0
  108. package/lib/auto/safety-manager.js +186 -0
  109. package/lib/auto/semantic-decomposer.js +137 -0
  110. package/lib/auto/state-manager.js +126 -0
  111. package/lib/auto/task-queue-manager.js +340 -0
  112. package/lib/backup/backup-system.js +372 -0
  113. package/lib/backup/selective-backup.js +207 -0
  114. package/lib/collab/agent-registry.js +240 -0
  115. package/lib/collab/collab-manager.js +285 -0
  116. package/lib/collab/contract-manager.js +320 -0
  117. package/lib/collab/coordinator.js +370 -0
  118. package/lib/collab/dependency-manager.js +280 -0
  119. package/lib/collab/index.js +20 -0
  120. package/lib/collab/integration-manager.js +202 -0
  121. package/lib/collab/merge-coordinator.js +252 -0
  122. package/lib/collab/metadata-manager.js +233 -0
  123. package/lib/collab/multi-agent-config.js +120 -0
  124. package/lib/collab/spec-lifecycle-manager.js +304 -0
  125. package/lib/collab/sync-barrier.js +88 -0
  126. package/lib/collab/visualizer.js +208 -0
  127. package/lib/commands/adopt.js +749 -0
  128. package/lib/commands/auto.js +19559 -0
  129. package/lib/commands/collab.js +275 -0
  130. package/lib/commands/context.js +99 -0
  131. package/lib/commands/docs.js +808 -0
  132. package/lib/commands/doctor.js +273 -0
  133. package/lib/commands/env.js +420 -0
  134. package/lib/commands/knowledge.js +309 -0
  135. package/lib/commands/lock.js +235 -0
  136. package/lib/commands/ops.js +409 -0
  137. package/lib/commands/orchestrate.js +446 -0
  138. package/lib/commands/prompt.js +105 -0
  139. package/lib/commands/repo.js +118 -0
  140. package/lib/commands/rollback.js +219 -0
  141. package/lib/commands/scene.js +15549 -0
  142. package/lib/commands/spec-bootstrap.js +147 -0
  143. package/lib/commands/spec-gate.js +157 -0
  144. package/lib/commands/spec-pipeline.js +205 -0
  145. package/lib/commands/status.js +321 -0
  146. package/lib/commands/task.js +199 -0
  147. package/lib/commands/templates.js +654 -0
  148. package/lib/commands/upgrade.js +231 -0
  149. package/lib/commands/value.js +569 -0
  150. package/lib/commands/watch.js +684 -0
  151. package/lib/commands/workflows.js +240 -0
  152. package/lib/commands/workspace-multi.js +325 -0
  153. package/lib/commands/workspace.js +189 -0
  154. package/lib/context/context-exporter.js +378 -0
  155. package/lib/context/prompt-generator.js +482 -0
  156. package/lib/data/moqui-capability-lexicon.json +45 -0
  157. package/lib/environment/backup-system.js +189 -0
  158. package/lib/environment/environment-manager.js +379 -0
  159. package/lib/environment/environment-registry.js +168 -0
  160. package/lib/gitignore/gitignore-backup.js +229 -0
  161. package/lib/gitignore/gitignore-detector.js +239 -0
  162. package/lib/gitignore/gitignore-integration.js +267 -0
  163. package/lib/gitignore/gitignore-transformer.js +193 -0
  164. package/lib/gitignore/layered-rules-template.js +42 -0
  165. package/lib/governance/archive-tool.js +284 -0
  166. package/lib/governance/cleanup-tool.js +237 -0
  167. package/lib/governance/config-manager.js +186 -0
  168. package/lib/governance/diagnostic-engine.js +271 -0
  169. package/lib/governance/doc-reference-checker.js +200 -0
  170. package/lib/governance/execution-logger.js +243 -0
  171. package/lib/governance/file-scanner.js +285 -0
  172. package/lib/governance/hooks-manager.js +333 -0
  173. package/lib/governance/reporter.js +337 -0
  174. package/lib/governance/validation-engine.js +181 -0
  175. package/lib/i18n.js +79 -0
  176. package/lib/knowledge/entry-manager.js +208 -0
  177. package/lib/knowledge/index-manager.js +261 -0
  178. package/lib/knowledge/knowledge-manager.js +273 -0
  179. package/lib/knowledge/template-manager.js +191 -0
  180. package/lib/lock/index.js +21 -0
  181. package/lib/lock/lock-file.js +192 -0
  182. package/lib/lock/lock-manager.js +321 -0
  183. package/lib/lock/machine-identifier.js +135 -0
  184. package/lib/lock/steering-file-lock.js +207 -0
  185. package/lib/lock/task-lock-manager.js +345 -0
  186. package/lib/operations/audit-logger.js +293 -0
  187. package/lib/operations/feedback-manager.js +1147 -0
  188. package/lib/operations/index.js +23 -0
  189. package/lib/operations/models/index.js +170 -0
  190. package/lib/operations/operations-manager.js +151 -0
  191. package/lib/operations/operations-validator.js +280 -0
  192. package/lib/operations/permission-manager.js +354 -0
  193. package/lib/operations/template-loader.js +143 -0
  194. package/lib/orchestrator/agent-spawner.js +629 -0
  195. package/lib/orchestrator/bootstrap-prompt-builder.js +236 -0
  196. package/lib/orchestrator/index.js +19 -0
  197. package/lib/orchestrator/orchestration-engine.js +1270 -0
  198. package/lib/orchestrator/orchestrator-config.js +173 -0
  199. package/lib/orchestrator/status-monitor.js +591 -0
  200. package/lib/python-checker.js +209 -0
  201. package/lib/repo/config-manager.js +580 -0
  202. package/lib/repo/errors/config-error.js +13 -0
  203. package/lib/repo/errors/git-error.js +15 -0
  204. package/lib/repo/errors/repo-error.js +14 -0
  205. package/lib/repo/git-operations.js +181 -0
  206. package/lib/repo/handlers/.gitkeep +1 -0
  207. package/lib/repo/handlers/exec-handler.js +155 -0
  208. package/lib/repo/handlers/health-handler.js +169 -0
  209. package/lib/repo/handlers/init-handler.js +197 -0
  210. package/lib/repo/handlers/status-handler.js +176 -0
  211. package/lib/repo/output-formatter.js +184 -0
  212. package/lib/repo/path-resolver.js +178 -0
  213. package/lib/repo/repo-manager.js +514 -0
  214. package/lib/scene-runtime/audit-emitter.js +59 -0
  215. package/lib/scene-runtime/binding-plugin-loader.js +351 -0
  216. package/lib/scene-runtime/binding-registry.js +349 -0
  217. package/lib/scene-runtime/eval-bridge.js +44 -0
  218. package/lib/scene-runtime/index.js +19 -0
  219. package/lib/scene-runtime/moqui-adapter.js +620 -0
  220. package/lib/scene-runtime/moqui-client.js +606 -0
  221. package/lib/scene-runtime/moqui-extractor.js +2029 -0
  222. package/lib/scene-runtime/plan-compiler.js +208 -0
  223. package/lib/scene-runtime/policy-gate.js +58 -0
  224. package/lib/scene-runtime/runtime-executor.js +358 -0
  225. package/lib/scene-runtime/scene-loader.js +96 -0
  226. package/lib/scene-runtime/scene-ontology.js +959 -0
  227. package/lib/scene-runtime/scene-template-linter.js +852 -0
  228. package/lib/scene-runtime/templates/scene-template-erp-query-v0.1.yaml +28 -0
  229. package/lib/scene-runtime/templates/scene-template-hybrid-shadow-v0.1.yaml +34 -0
  230. package/lib/spec/bootstrap/context-collector.js +48 -0
  231. package/lib/spec/bootstrap/draft-generator.js +158 -0
  232. package/lib/spec/bootstrap/questionnaire-engine.js +70 -0
  233. package/lib/spec/bootstrap/trace-emitter.js +59 -0
  234. package/lib/spec/multi-spec-orchestrate.js +93 -0
  235. package/lib/spec/pipeline/constants.js +6 -0
  236. package/lib/spec/pipeline/stage-adapters.js +118 -0
  237. package/lib/spec/pipeline/stage-runner.js +146 -0
  238. package/lib/spec/pipeline/state-store.js +119 -0
  239. package/lib/spec-gate/engine/gate-engine.js +165 -0
  240. package/lib/spec-gate/policy/default-policy.js +22 -0
  241. package/lib/spec-gate/policy/policy-loader.js +103 -0
  242. package/lib/spec-gate/result-emitter.js +81 -0
  243. package/lib/spec-gate/rules/default-rules.js +156 -0
  244. package/lib/spec-gate/rules/rule-registry.js +51 -0
  245. package/lib/steering/adoption-config.js +164 -0
  246. package/lib/steering/compliance-auto-fixer.js +204 -0
  247. package/lib/steering/compliance-cache.js +99 -0
  248. package/lib/steering/compliance-error-reporter.js +70 -0
  249. package/lib/steering/context-sync-manager.js +273 -0
  250. package/lib/steering/index.js +92 -0
  251. package/lib/steering/spec-steering.js +230 -0
  252. package/lib/steering/steering-compliance-checker.js +73 -0
  253. package/lib/steering/steering-loader.js +144 -0
  254. package/lib/steering/steering-manager.js +289 -0
  255. package/lib/task/index.js +12 -0
  256. package/lib/task/task-claimer.js +489 -0
  257. package/lib/task/task-status-store.js +418 -0
  258. package/lib/templates/cache-manager.js +440 -0
  259. package/lib/templates/content-generalizer.js +247 -0
  260. package/lib/templates/frontmatter-generator.js +128 -0
  261. package/lib/templates/git-handler.js +471 -0
  262. package/lib/templates/metadata-collector.js +328 -0
  263. package/lib/templates/path-utils.js +144 -0
  264. package/lib/templates/registry-parser.js +505 -0
  265. package/lib/templates/spec-reader.js +216 -0
  266. package/lib/templates/template-applicator.js +249 -0
  267. package/lib/templates/template-creator.js +256 -0
  268. package/lib/templates/template-error.js +143 -0
  269. package/lib/templates/template-exporter.js +502 -0
  270. package/lib/templates/template-manager.js +782 -0
  271. package/lib/templates/template-validator.js +361 -0
  272. package/lib/upgrade/migration-engine.js +382 -0
  273. package/lib/upgrade/migrations/.gitkeep +52 -0
  274. package/lib/upgrade/migrations/1.0.0-to-1.1.0.js +78 -0
  275. package/lib/utils/file-diff.js +177 -0
  276. package/lib/utils/fs-utils.js +274 -0
  277. package/lib/utils/tool-detector.js +383 -0
  278. package/lib/utils/validation.js +324 -0
  279. package/lib/value/gate-summary-emitter.js +99 -0
  280. package/lib/value/metric-contract-loader.js +210 -0
  281. package/lib/value/risk-evaluator.js +117 -0
  282. package/lib/value/weekly-snapshot-builder.js +61 -0
  283. package/lib/version/version-checker.js +156 -0
  284. package/lib/version/version-manager.js +327 -0
  285. package/lib/watch/action-executor.js +458 -0
  286. package/lib/watch/event-debouncer.js +323 -0
  287. package/lib/watch/execution-logger.js +550 -0
  288. package/lib/watch/file-watcher.js +499 -0
  289. package/lib/watch/presets.js +266 -0
  290. package/lib/watch/watch-manager.js +533 -0
  291. package/lib/workspace/multi/global-config.js +150 -0
  292. package/lib/workspace/multi/index.js +22 -0
  293. package/lib/workspace/multi/path-utils.js +173 -0
  294. package/lib/workspace/multi/workspace-context-resolver.js +244 -0
  295. package/lib/workspace/multi/workspace-registry.js +196 -0
  296. package/lib/workspace/multi/workspace-state-manager.js +537 -0
  297. package/lib/workspace/multi/workspace.js +90 -0
  298. package/lib/workspace/workspace-manager.js +370 -0
  299. package/lib/workspace/workspace-sync.js +356 -0
  300. package/locales/en.json +114 -0
  301. package/locales/zh.json +114 -0
  302. package/package.json +102 -0
  303. package/template/.kiro/README.md +247 -0
  304. package/template/.kiro/hooks/check-spec-on-create.kiro.hook +17 -0
  305. package/template/.kiro/hooks/run-tests-on-save.kiro.hook +13 -0
  306. package/template/.kiro/hooks/sync-tasks-on-edit.kiro.hook +16 -0
  307. package/template/.kiro/specs/SPEC_WORKFLOW_GUIDE.md +134 -0
  308. package/template/.kiro/steering/CORE_PRINCIPLES.md +133 -0
  309. package/template/.kiro/steering/CURRENT_CONTEXT.md +30 -0
  310. package/template/.kiro/steering/ENVIRONMENT.md +35 -0
  311. package/template/.kiro/steering/RULES_GUIDE.md +46 -0
  312. package/template/.kiro/templates/operations/default/change-impact.md +112 -0
  313. package/template/.kiro/templates/operations/default/deployment.md +91 -0
  314. package/template/.kiro/templates/operations/default/feedback-response.md +269 -0
  315. package/template/.kiro/templates/operations/default/migration-plan.md +172 -0
  316. package/template/.kiro/templates/operations/default/monitoring.md +135 -0
  317. package/template/.kiro/templates/operations/default/operations.md +135 -0
  318. package/template/.kiro/templates/operations/default/rollback.md +143 -0
  319. package/template/.kiro/templates/operations/default/tools.yaml +364 -0
  320. package/template/.kiro/templates/operations/default/troubleshooting.md +123 -0
  321. package/template/.kiro/tools/backup_manager.py +295 -0
  322. package/template/.kiro/tools/configuration_manager.py +218 -0
  323. package/template/.kiro/tools/document_evaluator.py +550 -0
  324. package/template/.kiro/tools/enhancement_logger.py +168 -0
  325. package/template/.kiro/tools/error_handler.py +335 -0
  326. package/template/.kiro/tools/improvement_identifier.py +444 -0
  327. package/template/.kiro/tools/modification_applicator.py +737 -0
  328. package/template/.kiro/tools/quality_gate_enforcer.py +207 -0
  329. package/template/.kiro/tools/quality_scorer.py +305 -0
  330. package/template/.kiro/tools/report_generator.py +154 -0
  331. package/template/.kiro/tools/ultrawork_enhancer.py +676 -0
  332. package/template/.kiro/tools/ultrawork_enhancer_refactored.py +0 -0
  333. package/template/.kiro/tools/ultrawork_enhancer_v2.py +463 -0
  334. package/template/.kiro/tools/ultrawork_enhancer_v3.py +606 -0
  335. package/template/.kiro/tools/workflow_quality_gate.py +100 -0
  336. package/template/README.md +111 -0
@@ -0,0 +1,606 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const { URL } = require('url');
4
+
5
+ const DEFAULT_TIMEOUT = 30000;
6
+ const DEFAULT_RETRY_COUNT = 2;
7
+ const DEFAULT_RETRY_DELAY = 1000;
8
+ const DEFAULT_MAX_RETRY_DELAY = 30000;
9
+
10
+ const RETRYABLE_NETWORK_ERRORS = [
11
+ 'ECONNREFUSED',
12
+ 'ENOTFOUND',
13
+ 'ECONNRESET',
14
+ 'ETIMEDOUT',
15
+ 'EPIPE',
16
+ 'EAI_AGAIN',
17
+ 'EHOSTUNREACH',
18
+ 'ENETUNREACH'
19
+ ];
20
+
21
+ function isRetryableNetworkError(error) {
22
+ if (!error || !error.code) {
23
+ return false;
24
+ }
25
+
26
+ return RETRYABLE_NETWORK_ERRORS.includes(error.code);
27
+ }
28
+
29
+ function isRetryableStatusCode(statusCode) {
30
+ return statusCode === 429 || (statusCode >= 500 && statusCode <= 599);
31
+ }
32
+
33
+ function parseRetryAfterMs(headers = {}) {
34
+ if (!headers || typeof headers !== 'object') {
35
+ return null;
36
+ }
37
+
38
+ const retryAfterValue = headers['retry-after'] || headers['Retry-After'];
39
+
40
+ if (retryAfterValue === undefined || retryAfterValue === null) {
41
+ return null;
42
+ }
43
+
44
+ const asNumber = Number(retryAfterValue);
45
+ if (Number.isFinite(asNumber) && asNumber >= 0) {
46
+ return Math.floor(asNumber * 1000);
47
+ }
48
+
49
+ const asDateMs = Date.parse(String(retryAfterValue));
50
+ if (!Number.isFinite(asDateMs)) {
51
+ return null;
52
+ }
53
+
54
+ return Math.max(0, asDateMs - Date.now());
55
+ }
56
+
57
+ function clampRetryDelay(ms, maxRetryDelay) {
58
+ const parsed = Number(ms);
59
+
60
+ if (!Number.isFinite(parsed)) {
61
+ return 0;
62
+ }
63
+
64
+ const bounded = Math.max(0, parsed);
65
+ if (!Number.isFinite(maxRetryDelay) || maxRetryDelay <= 0) {
66
+ return Math.floor(bounded);
67
+ }
68
+
69
+ return Math.floor(Math.min(maxRetryDelay, bounded));
70
+ }
71
+
72
+ function computeRetryDelayMs(attempt, baseRetryDelay, retryAfterMs, maxRetryDelay) {
73
+ if (Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
74
+ return clampRetryDelay(retryAfterMs, maxRetryDelay);
75
+ }
76
+
77
+ const exponent = Math.max(0, attempt);
78
+ const backoff = Number(baseRetryDelay) * Math.pow(2, exponent);
79
+
80
+ return clampRetryDelay(backoff, maxRetryDelay);
81
+ }
82
+
83
+ function delay(ms) {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+
87
+ function buildQueryString(query) {
88
+ if (!query || typeof query !== 'object') {
89
+ return '';
90
+ }
91
+
92
+ const parts = [];
93
+
94
+ for (const [key, value] of Object.entries(query)) {
95
+ if (value === undefined || value === null) {
96
+ continue;
97
+ }
98
+
99
+ parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
100
+ }
101
+
102
+ return parts.length > 0 ? `?${parts.join('&')}` : '';
103
+ }
104
+
105
+ class MoquiClient {
106
+ /**
107
+ * @param {Object} config - Adapter_Config object
108
+ * @param {string} config.baseUrl - Moqui instance root URL
109
+ * @param {Object} config.credentials - { username, password }
110
+ * @param {number} [config.timeout=30000] - Request timeout in ms
111
+ * @param {number} [config.retryCount=2] - Max retry attempts for retryable errors
112
+ * @param {number} [config.retryDelay=1000] - Delay between retries in ms
113
+ */
114
+ constructor(config = {}) {
115
+ this.config = {
116
+ baseUrl: String(config.baseUrl || '').replace(/\/+$/, ''),
117
+ credentials: config.credentials || {},
118
+ timeout: typeof config.timeout === 'number' ? config.timeout : DEFAULT_TIMEOUT,
119
+ retryCount: typeof config.retryCount === 'number' ? config.retryCount : DEFAULT_RETRY_COUNT,
120
+ retryDelay: typeof config.retryDelay === 'number' ? config.retryDelay : DEFAULT_RETRY_DELAY,
121
+ maxRetryDelay: typeof config.maxRetryDelay === 'number' ? config.maxRetryDelay : DEFAULT_MAX_RETRY_DELAY
122
+ };
123
+
124
+ this.accessToken = null;
125
+ this.refreshTokenValue = null;
126
+ this.authenticated = false;
127
+ }
128
+
129
+ /**
130
+ * Low-level HTTP request using Node.js built-in http/https.
131
+ * @param {string} method - HTTP method
132
+ * @param {string} fullUrl - Complete URL to request
133
+ * @param {Object} [options] - { body, headers, timeout }
134
+ * @returns {Promise<{ statusCode, headers, body }>}
135
+ */
136
+ async _httpRequest(method, fullUrl, options = {}) {
137
+ return new Promise((resolve, reject) => {
138
+ let parsedUrl;
139
+
140
+ try {
141
+ parsedUrl = new URL(fullUrl);
142
+ } catch (error) {
143
+ reject(new Error(`Invalid URL: ${fullUrl}`));
144
+ return;
145
+ }
146
+
147
+ const isHttps = parsedUrl.protocol === 'https:';
148
+ const transport = isHttps ? https : http;
149
+ const timeout = typeof options.timeout === 'number' ? options.timeout : this.config.timeout;
150
+
151
+ const requestHeaders = {
152
+ 'Content-Type': 'application/json',
153
+ 'Accept': 'application/json',
154
+ ...(options.headers || {})
155
+ };
156
+
157
+ let bodyData = null;
158
+
159
+ if (options.body !== undefined && options.body !== null) {
160
+ bodyData = typeof options.body === 'string'
161
+ ? options.body
162
+ : JSON.stringify(options.body);
163
+ requestHeaders['Content-Length'] = Buffer.byteLength(bodyData);
164
+ }
165
+
166
+ const requestOptions = {
167
+ method: method.toUpperCase(),
168
+ hostname: parsedUrl.hostname,
169
+ port: parsedUrl.port || (isHttps ? 443 : 80),
170
+ path: parsedUrl.pathname + parsedUrl.search,
171
+ headers: requestHeaders
172
+ };
173
+
174
+ let timedOut = false;
175
+ const req = transport.request(requestOptions, (res) => {
176
+ const chunks = [];
177
+
178
+ res.on('data', (chunk) => {
179
+ chunks.push(chunk);
180
+ });
181
+
182
+ res.on('end', () => {
183
+ if (timedOut) {
184
+ return;
185
+ }
186
+
187
+ const rawBody = Buffer.concat(chunks).toString('utf8');
188
+ let parsedBody;
189
+
190
+ try {
191
+ parsedBody = rawBody ? JSON.parse(rawBody) : null;
192
+ } catch (error) {
193
+ parsedBody = rawBody;
194
+ }
195
+
196
+ resolve({
197
+ statusCode: res.statusCode,
198
+ headers: res.headers,
199
+ body: parsedBody
200
+ });
201
+ });
202
+ });
203
+
204
+ if (timeout > 0) {
205
+ req.setTimeout(timeout, () => {
206
+ timedOut = true;
207
+ req.destroy();
208
+ reject(Object.assign(new Error(`Request timed out after ${timeout}ms`), { code: 'TIMEOUT' }));
209
+ });
210
+ }
211
+
212
+ req.on('error', (error) => {
213
+ if (timedOut) {
214
+ return;
215
+ }
216
+
217
+ reject(error);
218
+ });
219
+
220
+ if (bodyData) {
221
+ req.write(bodyData);
222
+ }
223
+
224
+ req.end();
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Authenticate with Moqui and store JWT token pair.
230
+ * POST /api/v1/auth/login { username, password }
231
+ * @returns {Promise<{ success: boolean, error?: string }>}
232
+ */
233
+ async login() {
234
+ const url = `${this.config.baseUrl}/api/v1/auth/login`;
235
+ const { username, password } = this.config.credentials;
236
+
237
+ try {
238
+ const response = await this._httpRequest('POST', url, {
239
+ body: { username, password }
240
+ });
241
+
242
+ if (response.statusCode === 200 && response.body) {
243
+ const body = response.body;
244
+
245
+ if (body.accessToken && body.refreshToken) {
246
+ this.accessToken = body.accessToken;
247
+ this.refreshTokenValue = body.refreshToken;
248
+ this.authenticated = true;
249
+ return { success: true };
250
+ }
251
+
252
+ if (body.data && body.data.accessToken && body.data.refreshToken) {
253
+ this.accessToken = body.data.accessToken;
254
+ this.refreshTokenValue = body.data.refreshToken;
255
+ this.authenticated = true;
256
+ return { success: true };
257
+ }
258
+ }
259
+
260
+ const errorMessage = (response.body && response.body.error && response.body.error.message)
261
+ || (response.body && response.body.message)
262
+ || `Login failed with status ${response.statusCode}`;
263
+
264
+ this.accessToken = null;
265
+ this.refreshTokenValue = null;
266
+ this.authenticated = false;
267
+
268
+ return { success: false, error: errorMessage };
269
+ } catch (error) {
270
+ this.accessToken = null;
271
+ this.refreshTokenValue = null;
272
+ this.authenticated = false;
273
+
274
+ if (error.code === 'TIMEOUT') {
275
+ return { success: false, error: `Login timed out after ${this.config.timeout}ms` };
276
+ }
277
+
278
+ if (isRetryableNetworkError(error)) {
279
+ return { success: false, error: `Network error: ${error.message} (${url})` };
280
+ }
281
+
282
+ return { success: false, error: error.message };
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Refresh access token using stored refresh token.
288
+ * POST /api/v1/auth/refresh { refreshToken }
289
+ * @returns {Promise<boolean>} true if refresh succeeded
290
+ */
291
+ async refreshToken() {
292
+ if (!this.refreshTokenValue) {
293
+ return false;
294
+ }
295
+
296
+ const url = `${this.config.baseUrl}/api/v1/auth/refresh`;
297
+
298
+ try {
299
+ const response = await this._httpRequest('POST', url, {
300
+ body: { refreshToken: this.refreshTokenValue }
301
+ });
302
+
303
+ if (response.statusCode === 200 && response.body) {
304
+ const body = response.body;
305
+
306
+ if (body.accessToken) {
307
+ this.accessToken = body.accessToken;
308
+
309
+ if (body.refreshToken) {
310
+ this.refreshTokenValue = body.refreshToken;
311
+ }
312
+
313
+ this.authenticated = true;
314
+ return true;
315
+ }
316
+
317
+ if (body.data && body.data.accessToken) {
318
+ this.accessToken = body.data.accessToken;
319
+
320
+ if (body.data.refreshToken) {
321
+ this.refreshTokenValue = body.data.refreshToken;
322
+ }
323
+
324
+ this.authenticated = true;
325
+ return true;
326
+ }
327
+ }
328
+
329
+ return false;
330
+ } catch (error) {
331
+ return false;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Invalidate current token pair.
337
+ * POST /api/v1/auth/logout
338
+ * @returns {Promise<void>}
339
+ */
340
+ async logout() {
341
+ if (!this.accessToken) {
342
+ this.authenticated = false;
343
+ return;
344
+ }
345
+
346
+ const url = `${this.config.baseUrl}/api/v1/auth/logout`;
347
+
348
+ try {
349
+ await this._httpRequest('POST', url, {
350
+ headers: {
351
+ 'Authorization': `Bearer ${this.accessToken}`
352
+ }
353
+ });
354
+ } catch (error) {
355
+ // Ignore logout errors silently
356
+ }
357
+
358
+ this.accessToken = null;
359
+ this.refreshTokenValue = null;
360
+ this.authenticated = false;
361
+ }
362
+
363
+ /**
364
+ * Send authenticated HTTP request with retry logic.
365
+ * Automatically handles 401 → refresh → retry flow.
366
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
367
+ * @param {string} path - API path (e.g., '/api/v1/entities/OrderHeader')
368
+ * @param {Object} [options] - { body, query, headers }
369
+ * @returns {Promise<Object>} Result object with success/error info
370
+ */
371
+ async request(method, path, options = {}) {
372
+ const queryString = buildQueryString(options.query);
373
+ const fullUrl = `${this.config.baseUrl}${path}${queryString}`;
374
+
375
+ const requestHeaders = {
376
+ ...(options.headers || {})
377
+ };
378
+
379
+ if (this.accessToken) {
380
+ requestHeaders['Authorization'] = `Bearer ${this.accessToken}`;
381
+ }
382
+
383
+ const requestOptions = {
384
+ body: options.body,
385
+ headers: requestHeaders,
386
+ timeout: this.config.timeout
387
+ };
388
+
389
+ let lastError = null;
390
+ const maxAttempts = this.config.retryCount + 1;
391
+
392
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
393
+ try {
394
+ // Update auth header in case token was refreshed
395
+ if (this.accessToken) {
396
+ requestOptions.headers['Authorization'] = `Bearer ${this.accessToken}`;
397
+ }
398
+
399
+ const response = await this._httpRequest(method, fullUrl, requestOptions);
400
+
401
+ // Handle 401 — try refresh then re-login
402
+ if (response.statusCode === 401) {
403
+ const handled = await this._handle401();
404
+
405
+ if (handled) {
406
+ // Update auth header and retry the request once
407
+ requestOptions.headers['Authorization'] = `Bearer ${this.accessToken}`;
408
+ const retryResponse = await this._httpRequest(method, fullUrl, requestOptions);
409
+
410
+ if (retryResponse.statusCode === 401) {
411
+ return {
412
+ success: false,
413
+ error: {
414
+ code: 'AUTH_FAILED',
415
+ message: 'Authentication failed after token refresh and re-login'
416
+ }
417
+ };
418
+ }
419
+
420
+ return this._normalizeResponse(retryResponse);
421
+ }
422
+
423
+ return {
424
+ success: false,
425
+ error: {
426
+ code: 'AUTH_FAILED',
427
+ message: 'Authentication failed — token refresh and re-login both failed'
428
+ }
429
+ };
430
+ }
431
+
432
+ // Retry on 429 and 5xx
433
+ if (isRetryableStatusCode(response.statusCode)) {
434
+ const isRateLimited = response.statusCode === 429;
435
+
436
+ lastError = {
437
+ success: false,
438
+ error: {
439
+ code: isRateLimited ? 'RATE_LIMITED' : 'MOQUI_ERROR',
440
+ message: isRateLimited
441
+ ? `Rate limited: ${response.statusCode}`
442
+ : `Server error: ${response.statusCode}`,
443
+ details: response.body
444
+ }
445
+ };
446
+
447
+ if (attempt >= maxAttempts - 1) {
448
+ break;
449
+ }
450
+
451
+ const retryDelayMs = computeRetryDelayMs(
452
+ attempt,
453
+ this.config.retryDelay,
454
+ parseRetryAfterMs(response.headers),
455
+ this.config.maxRetryDelay
456
+ );
457
+
458
+ if (retryDelayMs > 0) {
459
+ await delay(retryDelayMs);
460
+ }
461
+
462
+ continue;
463
+ }
464
+
465
+ // Success or non-retryable error
466
+ return this._normalizeResponse(response);
467
+ } catch (error) {
468
+ if (error.code === 'TIMEOUT') {
469
+ return {
470
+ success: false,
471
+ error: {
472
+ code: 'TIMEOUT',
473
+ message: `Request timed out after ${this.config.timeout}ms`
474
+ }
475
+ };
476
+ }
477
+
478
+ if (isRetryableNetworkError(error)) {
479
+ lastError = {
480
+ success: false,
481
+ error: {
482
+ code: 'NETWORK_ERROR',
483
+ message: `Network error: ${error.message} (${fullUrl})`
484
+ }
485
+ };
486
+
487
+ if (attempt >= maxAttempts - 1) {
488
+ break;
489
+ }
490
+
491
+ const retryDelayMs = computeRetryDelayMs(
492
+ attempt,
493
+ this.config.retryDelay,
494
+ null,
495
+ this.config.maxRetryDelay
496
+ );
497
+
498
+ if (retryDelayMs > 0) {
499
+ await delay(retryDelayMs);
500
+ }
501
+
502
+ continue;
503
+ }
504
+
505
+ // Non-retryable error
506
+ return {
507
+ success: false,
508
+ error: {
509
+ code: 'NETWORK_ERROR',
510
+ message: `${error.message} (${fullUrl})`
511
+ }
512
+ };
513
+ }
514
+ }
515
+
516
+ // All retries exhausted — return last error
517
+ return lastError || {
518
+ success: false,
519
+ error: {
520
+ code: 'NETWORK_ERROR',
521
+ message: `Request failed after ${maxAttempts} attempts`
522
+ }
523
+ };
524
+ }
525
+
526
+ /**
527
+ * Handle 401 response: try refresh → if fails try re-login.
528
+ * @returns {Promise<boolean>} true if authentication was restored
529
+ * @private
530
+ */
531
+ async _handle401() {
532
+ // Step 1: Try token refresh
533
+ const refreshed = await this.refreshToken();
534
+
535
+ if (refreshed) {
536
+ return true;
537
+ }
538
+
539
+ // Step 2: Try full re-login
540
+ const loginResult = await this.login();
541
+
542
+ return loginResult.success;
543
+ }
544
+
545
+ /**
546
+ * Normalize an HTTP response into a standard result object.
547
+ * @param {Object} response - { statusCode, headers, body }
548
+ * @returns {Object} Normalized result
549
+ * @private
550
+ */
551
+ _normalizeResponse(response) {
552
+ const body = response.body;
553
+
554
+ // If the body is already in Moqui response format
555
+ if (body && typeof body === 'object' && typeof body.success === 'boolean') {
556
+ return body;
557
+ }
558
+
559
+ // Successful HTTP status
560
+ if (response.statusCode >= 200 && response.statusCode < 300) {
561
+ return {
562
+ success: true,
563
+ data: body,
564
+ meta: {}
565
+ };
566
+ }
567
+
568
+ // Error HTTP status
569
+ const errorInfo = (body && typeof body === 'object' && body.error) || {};
570
+
571
+ return {
572
+ success: false,
573
+ error: {
574
+ code: errorInfo.code || `HTTP_${response.statusCode}`,
575
+ message: errorInfo.message || `Request failed with status ${response.statusCode}`,
576
+ details: errorInfo.details || body
577
+ }
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Check if client is authenticated (has valid token pair).
583
+ * @returns {boolean}
584
+ */
585
+ isAuthenticated() {
586
+ return this.authenticated && this.accessToken !== null;
587
+ }
588
+
589
+ /**
590
+ * Dispose client resources and logout.
591
+ * @returns {Promise<void>}
592
+ */
593
+ async dispose() {
594
+ try {
595
+ await this.logout();
596
+ } catch (error) {
597
+ // Ignore errors during dispose — silently clean up
598
+ }
599
+
600
+ this.accessToken = null;
601
+ this.refreshTokenValue = null;
602
+ this.authenticated = false;
603
+ }
604
+ }
605
+
606
+ module.exports = MoquiClient;