@illumiarq/lumis 1.2.8 → 1.2.10

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 (296) hide show
  1. package/README.md +38 -0
  2. package/dist/adapter-loader.d.ts +67 -0
  3. package/dist/adapter-loader.d.ts.map +1 -0
  4. package/dist/adapter-loader.js +273 -0
  5. package/dist/adapter-loader.js.map +1 -0
  6. package/dist/cli.d.ts +16 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +173 -402
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/commands.command.d.ts +38 -0
  11. package/dist/commands/commands.command.d.ts.map +1 -0
  12. package/dist/commands/commands.command.js +176 -0
  13. package/dist/commands/commands.command.js.map +1 -0
  14. package/dist/commands/doctor.command.d.ts +9 -0
  15. package/dist/commands/doctor.command.d.ts.map +1 -0
  16. package/dist/commands/doctor.command.js +922 -0
  17. package/dist/commands/doctor.command.js.map +1 -0
  18. package/dist/commands/init.command.d.ts +9 -0
  19. package/dist/commands/init.command.d.ts.map +1 -0
  20. package/dist/commands/init.command.js +135 -0
  21. package/dist/commands/init.command.js.map +1 -0
  22. package/dist/commands/intent.command.d.ts +26 -0
  23. package/dist/commands/intent.command.d.ts.map +1 -0
  24. package/dist/commands/intent.command.js +211 -0
  25. package/dist/commands/intent.command.js.map +1 -0
  26. package/dist/commands/ir-rebuild.command.d.ts +9 -0
  27. package/dist/commands/ir-rebuild.command.d.ts.map +1 -0
  28. package/dist/commands/ir-rebuild.command.js +36 -0
  29. package/dist/commands/ir-rebuild.command.js.map +1 -0
  30. package/dist/commands/ir-show.command.d.ts +9 -0
  31. package/dist/commands/ir-show.command.d.ts.map +1 -0
  32. package/dist/commands/ir-show.command.js +32 -0
  33. package/dist/commands/ir-show.command.js.map +1 -0
  34. package/dist/commands/make.command.d.ts +9 -0
  35. package/dist/commands/make.command.d.ts.map +1 -0
  36. package/dist/commands/make.command.js +93 -0
  37. package/dist/commands/make.command.js.map +1 -0
  38. package/dist/commands/pack.command.d.ts +16 -0
  39. package/dist/commands/pack.command.d.ts.map +1 -0
  40. package/dist/commands/pack.command.js +44 -0
  41. package/dist/commands/pack.command.js.map +1 -0
  42. package/dist/commands/runtime.command.d.ts +6 -0
  43. package/dist/commands/runtime.command.d.ts.map +1 -0
  44. package/dist/commands/runtime.command.js +379 -0
  45. package/dist/commands/runtime.command.js.map +1 -0
  46. package/dist/commands/runtime.d.ts +45 -0
  47. package/dist/commands/runtime.d.ts.map +1 -0
  48. package/dist/commands/runtime.js +66 -0
  49. package/dist/commands/runtime.js.map +1 -0
  50. package/dist/commands/tinker-agent-plan.d.ts +26 -0
  51. package/dist/commands/tinker-agent-plan.d.ts.map +1 -0
  52. package/dist/commands/tinker-agent-plan.js +115 -0
  53. package/dist/commands/tinker-agent-plan.js.map +1 -0
  54. package/dist/commands/tinker-chat-commands.d.ts +19 -0
  55. package/dist/commands/tinker-chat-commands.d.ts.map +1 -0
  56. package/dist/commands/tinker-chat-commands.js +263 -0
  57. package/dist/commands/tinker-chat-commands.js.map +1 -0
  58. package/dist/commands/tinker-chat-runtime.d.ts +30 -0
  59. package/dist/commands/tinker-chat-runtime.d.ts.map +1 -0
  60. package/dist/commands/tinker-chat-runtime.js +227 -0
  61. package/dist/commands/tinker-chat-runtime.js.map +1 -0
  62. package/dist/commands/tinker-classic-mode.d.ts +15 -0
  63. package/dist/commands/tinker-classic-mode.d.ts.map +1 -0
  64. package/dist/commands/tinker-classic-mode.js +160 -0
  65. package/dist/commands/tinker-classic-mode.js.map +1 -0
  66. package/dist/commands/tinker-eval.command.d.ts +3 -0
  67. package/dist/commands/tinker-eval.command.d.ts.map +1 -0
  68. package/dist/commands/tinker-eval.command.js +151 -0
  69. package/dist/commands/tinker-eval.command.js.map +1 -0
  70. package/dist/commands/tinker-repl-display.d.ts +15 -0
  71. package/dist/commands/tinker-repl-display.d.ts.map +1 -0
  72. package/dist/commands/tinker-repl-display.js +147 -0
  73. package/dist/commands/tinker-repl-display.js.map +1 -0
  74. package/dist/commands/tinker-session-runtime.d.ts +27 -0
  75. package/dist/commands/tinker-session-runtime.d.ts.map +1 -0
  76. package/dist/commands/tinker-session-runtime.js +196 -0
  77. package/dist/commands/tinker-session-runtime.js.map +1 -0
  78. package/dist/commands/tinker.command.d.ts +9 -0
  79. package/dist/commands/tinker.command.d.ts.map +1 -0
  80. package/dist/commands/tinker.command.js +142 -0
  81. package/dist/commands/tinker.command.js.map +1 -0
  82. package/dist/commands/use.command.d.ts +10 -0
  83. package/dist/commands/use.command.d.ts.map +1 -0
  84. package/dist/commands/use.command.js +69 -0
  85. package/dist/commands/use.command.js.map +1 -0
  86. package/dist/config-loader.d.ts +110 -0
  87. package/dist/config-loader.d.ts.map +1 -0
  88. package/dist/config-loader.js +119 -0
  89. package/dist/config-loader.js.map +1 -0
  90. package/dist/index.d.ts +19 -15
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +12 -12
  93. package/dist/index.js.map +1 -1
  94. package/dist/launcher.d.ts +16 -0
  95. package/dist/launcher.d.ts.map +1 -0
  96. package/dist/launcher.js +301 -0
  97. package/dist/launcher.js.map +1 -0
  98. package/dist/lib/file-scanner.d.ts +8 -0
  99. package/dist/lib/file-scanner.d.ts.map +1 -0
  100. package/dist/lib/file-scanner.js +70 -0
  101. package/dist/lib/file-scanner.js.map +1 -0
  102. package/dist/lib/tinker-memory.d.ts +19 -0
  103. package/dist/lib/tinker-memory.d.ts.map +1 -0
  104. package/dist/lib/tinker-memory.js +107 -0
  105. package/dist/lib/tinker-memory.js.map +1 -0
  106. package/dist/lib/tinker-observer.d.ts +47 -0
  107. package/dist/lib/tinker-observer.d.ts.map +1 -0
  108. package/dist/lib/tinker-observer.js +77 -0
  109. package/dist/lib/tinker-observer.js.map +1 -0
  110. package/dist/lib/tinker-policy.d.ts +21 -0
  111. package/dist/lib/tinker-policy.d.ts.map +1 -0
  112. package/dist/lib/tinker-policy.js +102 -0
  113. package/dist/lib/tinker-policy.js.map +1 -0
  114. package/dist/lib/tinker-replay.d.ts +51 -0
  115. package/dist/lib/tinker-replay.d.ts.map +1 -0
  116. package/dist/lib/tinker-replay.js +123 -0
  117. package/dist/lib/tinker-replay.js.map +1 -0
  118. package/dist/{console.d.ts → output/ansi.d.ts} +4 -8
  119. package/dist/output/ansi.d.ts.map +1 -0
  120. package/dist/{console.js → output/ansi.js} +4 -12
  121. package/dist/output/ansi.js.map +1 -0
  122. package/dist/output/plan-display.d.ts +4 -0
  123. package/dist/output/plan-display.d.ts.map +1 -0
  124. package/dist/output/plan-display.js +49 -0
  125. package/dist/output/plan-display.js.map +1 -0
  126. package/dist/output/result-display.d.ts +5 -0
  127. package/dist/output/result-display.d.ts.map +1 -0
  128. package/dist/output/result-display.js +17 -0
  129. package/dist/output/result-display.js.map +1 -0
  130. package/dist/plugins/tinker-plugin.d.ts +86 -0
  131. package/dist/plugins/tinker-plugin.d.ts.map +1 -0
  132. package/dist/plugins/tinker-plugin.js +42 -0
  133. package/dist/plugins/tinker-plugin.js.map +1 -0
  134. package/dist/router-preferences.d.ts +5 -0
  135. package/dist/router-preferences.d.ts.map +1 -0
  136. package/dist/router-preferences.js +22 -0
  137. package/dist/router-preferences.js.map +1 -0
  138. package/dist/runtime/executor.d.ts +33 -0
  139. package/dist/runtime/executor.d.ts.map +1 -0
  140. package/dist/runtime/executor.js +180 -0
  141. package/dist/runtime/executor.js.map +1 -0
  142. package/dist/runtime/orchestration.d.ts +43 -0
  143. package/dist/runtime/orchestration.d.ts.map +1 -0
  144. package/dist/runtime/orchestration.js +209 -0
  145. package/dist/runtime/orchestration.js.map +1 -0
  146. package/dist/runtime/policy.d.ts +49 -0
  147. package/dist/runtime/policy.d.ts.map +1 -0
  148. package/dist/runtime/policy.js +78 -0
  149. package/dist/runtime/policy.js.map +1 -0
  150. package/dist/runtime/profiles.d.ts +15 -0
  151. package/dist/runtime/profiles.d.ts.map +1 -0
  152. package/dist/runtime/profiles.js +131 -0
  153. package/dist/runtime/profiles.js.map +1 -0
  154. package/dist/runtime/transcripts.d.ts +59 -0
  155. package/dist/runtime/transcripts.d.ts.map +1 -0
  156. package/dist/runtime/transcripts.js +172 -0
  157. package/dist/runtime/transcripts.js.map +1 -0
  158. package/dist/tools/git-diff.d.ts +3 -0
  159. package/dist/tools/git-diff.d.ts.map +1 -0
  160. package/dist/tools/git-diff.js +16 -0
  161. package/dist/tools/git-diff.js.map +1 -0
  162. package/dist/tools/index.d.ts +24 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +88 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/lint.d.ts +3 -0
  167. package/dist/tools/lint.d.ts.map +1 -0
  168. package/dist/tools/lint.js +35 -0
  169. package/dist/tools/lint.js.map +1 -0
  170. package/dist/tools/read-file.d.ts +3 -0
  171. package/dist/tools/read-file.d.ts.map +1 -0
  172. package/dist/tools/read-file.js +47 -0
  173. package/dist/tools/read-file.js.map +1 -0
  174. package/dist/tools/run-command.d.ts +3 -0
  175. package/dist/tools/run-command.d.ts.map +1 -0
  176. package/dist/tools/run-command.js +71 -0
  177. package/dist/tools/run-command.js.map +1 -0
  178. package/dist/tools/run-tests.d.ts +3 -0
  179. package/dist/tools/run-tests.d.ts.map +1 -0
  180. package/dist/tools/run-tests.js +49 -0
  181. package/dist/tools/run-tests.js.map +1 -0
  182. package/dist/tools/tool-dispatch.d.ts +32 -0
  183. package/dist/tools/tool-dispatch.d.ts.map +1 -0
  184. package/dist/tools/tool-dispatch.js +120 -0
  185. package/dist/tools/tool-dispatch.js.map +1 -0
  186. package/dist/tools/typecheck.d.ts +3 -0
  187. package/dist/tools/typecheck.d.ts.map +1 -0
  188. package/dist/tools/typecheck.js +48 -0
  189. package/dist/tools/typecheck.js.map +1 -0
  190. package/dist/tools/types.d.ts +62 -0
  191. package/dist/tools/types.d.ts.map +1 -0
  192. package/dist/tools/types.js +7 -0
  193. package/dist/tools/types.js.map +1 -0
  194. package/dist/tools/write-file.d.ts +15 -0
  195. package/dist/tools/write-file.d.ts.map +1 -0
  196. package/dist/tools/write-file.js +52 -0
  197. package/dist/tools/write-file.js.map +1 -0
  198. package/dist/tracing/types.d.ts +10 -0
  199. package/dist/tracing/types.d.ts.map +1 -0
  200. package/dist/tracing/types.js +2 -0
  201. package/dist/tracing/types.js.map +1 -0
  202. package/package.json +37 -19
  203. package/dist/bridges/project-bridge.d.ts +0 -2
  204. package/dist/bridges/project-bridge.d.ts.map +0 -1
  205. package/dist/bridges/project-bridge.js +0 -171
  206. package/dist/bridges/project-bridge.js.map +0 -1
  207. package/dist/commands/app/cache-workflows.d.ts +0 -7
  208. package/dist/commands/app/cache-workflows.d.ts.map +0 -1
  209. package/dist/commands/app/cache-workflows.js +0 -139
  210. package/dist/commands/app/cache-workflows.js.map +0 -1
  211. package/dist/commands/app/command-runtime.d.ts +0 -11
  212. package/dist/commands/app/command-runtime.d.ts.map +0 -1
  213. package/dist/commands/app/command-runtime.js +0 -41
  214. package/dist/commands/app/command-runtime.js.map +0 -1
  215. package/dist/commands/app/config-cache.d.ts +0 -3
  216. package/dist/commands/app/config-cache.d.ts.map +0 -1
  217. package/dist/commands/app/config-cache.js +0 -75
  218. package/dist/commands/app/config-cache.js.map +0 -1
  219. package/dist/commands/app/config-publish.d.ts +0 -2
  220. package/dist/commands/app/config-publish.d.ts.map +0 -1
  221. package/dist/commands/app/config-publish.js +0 -68
  222. package/dist/commands/app/config-publish.js.map +0 -1
  223. package/dist/commands/app/config-stubs.d.ts +0 -3
  224. package/dist/commands/app/config-stubs.d.ts.map +0 -1
  225. package/dist/commands/app/config-stubs.js +0 -154
  226. package/dist/commands/app/config-stubs.js.map +0 -1
  227. package/dist/commands/app/database.d.ts +0 -3
  228. package/dist/commands/app/database.d.ts.map +0 -1
  229. package/dist/commands/app/database.js +0 -221
  230. package/dist/commands/app/database.js.map +0 -1
  231. package/dist/commands/app/docs.d.ts +0 -7
  232. package/dist/commands/app/docs.d.ts.map +0 -1
  233. package/dist/commands/app/docs.js +0 -61
  234. package/dist/commands/app/docs.js.map +0 -1
  235. package/dist/commands/app/routes.d.ts +0 -5
  236. package/dist/commands/app/routes.d.ts.map +0 -1
  237. package/dist/commands/app/routes.js +0 -95
  238. package/dist/commands/app/routes.js.map +0 -1
  239. package/dist/commands/app/security.d.ts +0 -3
  240. package/dist/commands/app/security.d.ts.map +0 -1
  241. package/dist/commands/app/security.js +0 -15
  242. package/dist/commands/app/security.js.map +0 -1
  243. package/dist/commands/app/worker-schedule.d.ts +0 -5
  244. package/dist/commands/app/worker-schedule.d.ts.map +0 -1
  245. package/dist/commands/app/worker-schedule.js +0 -94
  246. package/dist/commands/app/worker-schedule.js.map +0 -1
  247. package/dist/commands/app-commands.d.ts +0 -19
  248. package/dist/commands/app-commands.d.ts.map +0 -1
  249. package/dist/commands/app-commands.js +0 -77
  250. package/dist/commands/app-commands.js.map +0 -1
  251. package/dist/commands/build-vercel-fn.d.ts +0 -9
  252. package/dist/commands/build-vercel-fn.d.ts.map +0 -1
  253. package/dist/commands/build-vercel-fn.js +0 -32
  254. package/dist/commands/build-vercel-fn.js.map +0 -1
  255. package/dist/commands/build.d.ts +0 -14
  256. package/dist/commands/build.d.ts.map +0 -1
  257. package/dist/commands/build.js +0 -24
  258. package/dist/commands/build.js.map +0 -1
  259. package/dist/commands/info.d.ts +0 -2
  260. package/dist/commands/info.d.ts.map +0 -1
  261. package/dist/commands/info.js +0 -25
  262. package/dist/commands/info.js.map +0 -1
  263. package/dist/commands/keys.d.ts +0 -3
  264. package/dist/commands/keys.d.ts.map +0 -1
  265. package/dist/commands/keys.js +0 -60
  266. package/dist/commands/keys.js.map +0 -1
  267. package/dist/commands/maintenance.d.ts +0 -9
  268. package/dist/commands/maintenance.d.ts.map +0 -1
  269. package/dist/commands/maintenance.js +0 -38
  270. package/dist/commands/maintenance.js.map +0 -1
  271. package/dist/commands/module-list.d.ts +0 -2
  272. package/dist/commands/module-list.d.ts.map +0 -1
  273. package/dist/commands/module-list.js +0 -27
  274. package/dist/commands/module-list.js.map +0 -1
  275. package/dist/commands/preview.d.ts +0 -14
  276. package/dist/commands/preview.d.ts.map +0 -1
  277. package/dist/commands/preview.js +0 -49
  278. package/dist/commands/preview.js.map +0 -1
  279. package/dist/commands/serve.d.ts +0 -16
  280. package/dist/commands/serve.d.ts.map +0 -1
  281. package/dist/commands/serve.js +0 -61
  282. package/dist/commands/serve.js.map +0 -1
  283. package/dist/console.d.ts.map +0 -1
  284. package/dist/console.js.map +0 -1
  285. package/dist/paths.d.ts +0 -21
  286. package/dist/paths.d.ts.map +0 -1
  287. package/dist/paths.js +0 -47
  288. package/dist/paths.js.map +0 -1
  289. package/dist/server-wrapper.d.ts +0 -12
  290. package/dist/server-wrapper.d.ts.map +0 -1
  291. package/dist/server-wrapper.js +0 -63
  292. package/dist/server-wrapper.js.map +0 -1
  293. package/dist/vercel-wrapper.d.ts +0 -6
  294. package/dist/vercel-wrapper.d.ts.map +0 -1
  295. package/dist/vercel-wrapper.js +0 -50
  296. package/dist/vercel-wrapper.js.map +0 -1
@@ -0,0 +1,922 @@
1
+ /**
2
+ * Project diagnostics command.
3
+ *
4
+ * This command verifies core runtime readiness for Lumis by checking config,
5
+ * pack/adapter detection, command inventory availability, and persisted IR state.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { diffIR, loadIR } from '@lumiarq/context';
10
+ import { createPlanWithAdapter } from '@lumiarq/planner';
11
+ import { collectNormalizedCommands } from './commands.command.js';
12
+ import { parseRawIntent } from './intent.command.js';
13
+ import { resolveRuntimePolicy } from '../runtime/policy.js';
14
+ import { validateRuntimeProfileStore } from '../runtime/profiles.js';
15
+ import { loadRuntimeTranscriptIndex, summarizeRuntimeTranscripts } from '../runtime/transcripts.js';
16
+ /**
17
+ * Parse doctor command options.
18
+ *
19
+ * Supported flags:
20
+ * - --intent <raw intent text>
21
+ * - --intent-file <path to file with one intent per line>
22
+ * - --strict-intent-conflicts (fail command when conflicts are found)
23
+ *
24
+ * @param commandArgs - Raw doctor command arguments.
25
+ * @param projectRoot - Project root used to resolve relative intent-file paths.
26
+ * @returns Parsed doctor options.
27
+ */
28
+ function parseDoctorOptions(commandArgs, projectRoot) {
29
+ const intentTexts = [];
30
+ let strictIntentConflicts = false;
31
+ let index = 0;
32
+ while (index < commandArgs.length) {
33
+ const token = commandArgs[index];
34
+ if (token === '--strict-intent-conflicts') {
35
+ strictIntentConflicts = true;
36
+ index += 1;
37
+ continue;
38
+ }
39
+ if (token === '--intent') {
40
+ index += 1;
41
+ const parts = [];
42
+ while (index < commandArgs.length && !commandArgs[index]?.startsWith('--')) {
43
+ const part = commandArgs[index];
44
+ if (part) {
45
+ parts.push(part);
46
+ }
47
+ index += 1;
48
+ }
49
+ const intent = parts.join(' ').trim();
50
+ if (intent) {
51
+ intentTexts.push(intent);
52
+ }
53
+ continue;
54
+ }
55
+ if (token === '--intent-file') {
56
+ const rawPath = commandArgs[index + 1];
57
+ if (!rawPath) {
58
+ index += 1;
59
+ continue;
60
+ }
61
+ const resolvedPath = path.isAbsolute(rawPath) ? rawPath : path.join(projectRoot, rawPath);
62
+ if (fs.existsSync(resolvedPath)) {
63
+ const fileIntents = fs
64
+ .readFileSync(resolvedPath, 'utf8')
65
+ .split(/\r?\n/g)
66
+ .map((line) => line.trim())
67
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
68
+ intentTexts.push(...fileIntents);
69
+ }
70
+ index += 2;
71
+ continue;
72
+ }
73
+ index += 1;
74
+ }
75
+ return { intentTexts, strictIntentConflicts };
76
+ }
77
+ function normalizeTargetFile(targetFile) {
78
+ return path.resolve(targetFile).replaceAll('\\', '/');
79
+ }
80
+ function hasSensitiveInlineEnv(env) {
81
+ if (!env || typeof env !== 'object') {
82
+ return false;
83
+ }
84
+ for (const [key, value] of Object.entries(env)) {
85
+ if (!/token|secret|password|key/i.test(key)) {
86
+ continue;
87
+ }
88
+ if (typeof value === 'string' && value.trim().length > 0) {
89
+ return true;
90
+ }
91
+ }
92
+ return false;
93
+ }
94
+ /**
95
+ * Identify deterministic conflicts across multiple planned intents.
96
+ *
97
+ * @param plannedIntents - Planned intent summaries including write targets.
98
+ * @returns List of target-level conflicts.
99
+ */
100
+ function detectIntentConflicts(plannedIntents) {
101
+ const byTarget = new Map();
102
+ for (const planned of plannedIntents) {
103
+ for (const step of planned.stepSummaries) {
104
+ const normalizedTarget = normalizeTargetFile(step.targetFile);
105
+ const bucket = byTarget.get(normalizedTarget) ?? [];
106
+ bucket.push({ operation: step.operation, raw: planned.raw });
107
+ byTarget.set(normalizedTarget, bucket);
108
+ }
109
+ }
110
+ const conflicts = [];
111
+ for (const [targetFile, writes] of byTarget.entries()) {
112
+ if (writes.length <= 1) {
113
+ continue;
114
+ }
115
+ const operations = Array.from(new Set(writes.map((write) => write.operation))).sort();
116
+ const intents = Array.from(new Set(writes.map((write) => write.raw)));
117
+ const hasDelete = operations.includes('delete');
118
+ const hasCreate = operations.includes('create');
119
+ const mixedOperations = operations.length > 1;
120
+ if (hasDelete && operations.some((operation) => operation !== 'delete')) {
121
+ conflicts.push({
122
+ targetFile,
123
+ operations,
124
+ intents,
125
+ reason: 'delete overlaps with non-delete operations on the same target',
126
+ });
127
+ continue;
128
+ }
129
+ if (hasCreate && writes.length > 1) {
130
+ conflicts.push({
131
+ targetFile,
132
+ operations,
133
+ intents,
134
+ reason: 'multiple steps attempt to create the same target',
135
+ });
136
+ continue;
137
+ }
138
+ if (mixedOperations) {
139
+ conflicts.push({
140
+ targetFile,
141
+ operations,
142
+ intents,
143
+ reason: 'mixed operations target the same file',
144
+ });
145
+ continue;
146
+ }
147
+ if (operations[0] === 'append' || operations[0] === 'update') {
148
+ conflicts.push({
149
+ targetFile,
150
+ operations,
151
+ intents,
152
+ reason: 'multiple write operations target the same file',
153
+ });
154
+ }
155
+ }
156
+ return conflicts;
157
+ }
158
+ /**
159
+ * Render one diagnostic line in a stable format.
160
+ *
161
+ * @param result - Single diagnostic result row.
162
+ * @returns Formatted diagnostic line.
163
+ */
164
+ function formatCheck(result) {
165
+ return `[${result.status.toUpperCase()}][${result.severity.toUpperCase()}] ${result.name}: ${result.detail}`;
166
+ }
167
+ /**
168
+ * Security baseline checks: secret hygiene, env documentation, command boundary.
169
+ *
170
+ * All checks are non-blocking (warn/pass) unless a plaintext secret pattern is
171
+ * found directly in a committed env file, which is a critical failure.
172
+ */
173
+ function appendSecurityBaselineChecks(projectRoot, config, checks) {
174
+ // --- .env.example presence ---
175
+ const envExamplePath = path.join(projectRoot, '.env.example');
176
+ checks.push(fs.existsSync(envExamplePath)
177
+ ? {
178
+ name: 'security/env-example',
179
+ status: 'pass',
180
+ severity: 'info',
181
+ detail: '.env.example present — secret surface is documented',
182
+ }
183
+ : {
184
+ name: 'security/env-example',
185
+ status: 'warn',
186
+ severity: 'warning',
187
+ detail: '.env.example not found — secret surface is undocumented',
188
+ suggestion: 'Create .env.example with all required env var names (values redacted) so collaborators know what secrets to provision.',
189
+ });
190
+ // --- .env plaintext secret scan ---
191
+ const envPath = path.join(projectRoot, '.env');
192
+ if (fs.existsSync(envPath)) {
193
+ const envContent = fs.readFileSync(envPath, 'utf8');
194
+ // Heuristic: lines where the value looks like a real secret (long, non-placeholder, non-empty).
195
+ const PLACEHOLDER_RE = /^(your[-_]|<|YOUR_|REPLACE|todo|placeholder|xxx|changeme)/i;
196
+ const SECRET_KEY_RE = /(?:secret|token|password|key|private|api[-_]?key|auth)/i;
197
+ const suspiciousLines = [];
198
+ envContent.split(/\r?\n/).forEach((line, idx) => {
199
+ const match = /^([A-Z_][A-Z0-9_]*)=(.+)$/i.exec(line.trim());
200
+ if (!match)
201
+ return;
202
+ const keyName = match[1];
203
+ const value = match[2];
204
+ if (keyName && value && SECRET_KEY_RE.test(keyName) && value.length >= 16 && !PLACEHOLDER_RE.test(value)) {
205
+ suspiciousLines.push(idx + 1);
206
+ }
207
+ });
208
+ checks.push(suspiciousLines.length === 0
209
+ ? {
210
+ name: 'security/env-secret-scan',
211
+ status: 'pass',
212
+ severity: 'info',
213
+ detail: '.env present — no obvious plaintext secrets detected',
214
+ }
215
+ : {
216
+ name: 'security/env-secret-scan',
217
+ status: 'fail',
218
+ severity: 'critical',
219
+ detail: `.env may contain plaintext secrets on line(s): ${suspiciousLines.join(', ')}. Ensure .env is gitignored.`,
220
+ suggestion: 'Add .env to .gitignore immediately. Rotate any exposed credentials. Use .env.example for documentation.',
221
+ });
222
+ }
223
+ else {
224
+ checks.push({
225
+ name: 'security/env-secret-scan',
226
+ status: 'pass',
227
+ severity: 'info',
228
+ detail: 'no .env file at project root — nothing to scan',
229
+ });
230
+ }
231
+ // --- .env in .gitignore ---
232
+ const gitignorePath = path.join(projectRoot, '.gitignore');
233
+ if (fs.existsSync(gitignorePath)) {
234
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
235
+ const envIgnored = gitignoreContent.split(/\r?\n/).some((line) => {
236
+ const trimmed = line.trim();
237
+ return trimmed === '.env' || trimmed === '*.env' || trimmed === '.env*';
238
+ });
239
+ checks.push(envIgnored
240
+ ? {
241
+ name: 'security/env-gitignored',
242
+ status: 'pass',
243
+ severity: 'info',
244
+ detail: '.env is gitignored',
245
+ }
246
+ : {
247
+ name: 'security/env-gitignored',
248
+ status: 'warn',
249
+ severity: 'critical',
250
+ detail: '.env is NOT listed in .gitignore — secrets may be committed',
251
+ suggestion: 'Add ".env" to .gitignore immediately.',
252
+ });
253
+ }
254
+ // --- commandAllowlist configured when runtime is enabled ---
255
+ const runtimeEnabled = config.runtime?.enabled !== false;
256
+ const hasAllowlist = (config.runtime?.policy?.commandAllowlist ?? []).length > 0;
257
+ if (runtimeEnabled && !hasAllowlist) {
258
+ checks.push({
259
+ name: 'security/command-allowlist',
260
+ status: 'warn',
261
+ severity: 'warning',
262
+ detail: 'No commandAllowlist configured — all commands are permitted in runtime profiles',
263
+ suggestion: 'Set runtime.policy.commandAllowlist in pkg/lumis.config.ts to restrict which commands the lumis runtime may execute. ' +
264
+ 'Example: ["pnpm *", "node *"]. Use mode: "strict" to block unlisted commands.',
265
+ });
266
+ }
267
+ else if (runtimeEnabled) {
268
+ checks.push({
269
+ name: 'security/command-allowlist',
270
+ status: 'pass',
271
+ severity: 'info',
272
+ detail: `commandAllowlist configured with ${config.runtime?.policy?.commandAllowlist?.length} entry(s)`,
273
+ });
274
+ }
275
+ }
276
+ /**
277
+ * Supply-chain policy checks: license allowlist compliance and cached audit result.
278
+ *
279
+ * License check: scans direct dependencies from package.json against
280
+ * node_modules/<pkg>/package.json license fields and the configured allowedLicenses list.
281
+ *
282
+ * Audit check: reads a cached .lumis/audit.json produced by `pnpm audit --json`
283
+ * or `npm audit --json`. When absent, emits an advisory suggesting the file be generated.
284
+ */
285
+ function appendSupplyChainChecks(projectRoot, config, checks) {
286
+ const pkgPath = path.join(projectRoot, 'package.json');
287
+ if (!fs.existsSync(pkgPath)) {
288
+ return;
289
+ }
290
+ let pkg;
291
+ try {
292
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
293
+ }
294
+ catch {
295
+ checks.push({
296
+ name: 'supply-chain/package-json',
297
+ status: 'warn',
298
+ severity: 'warning',
299
+ detail: 'package.json could not be parsed — supply-chain checks skipped',
300
+ });
301
+ return;
302
+ }
303
+ // --- License allowlist check ---
304
+ const allowedLicenses = config.supply?.allowedLicenses;
305
+ if (allowedLicenses && allowedLicenses.length > 0) {
306
+ const directDeps = Object.keys({
307
+ ...pkg.dependencies,
308
+ ...pkg.devDependencies,
309
+ });
310
+ const violations = [];
311
+ for (const depName of directDeps) {
312
+ const depPkgPath = path.join(projectRoot, 'node_modules', depName, 'package.json');
313
+ if (!fs.existsSync(depPkgPath))
314
+ continue;
315
+ try {
316
+ const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf8'));
317
+ const license = typeof depPkg.license === 'string' ? depPkg.license : undefined;
318
+ if (license && !allowedLicenses.includes(license)) {
319
+ violations.push(`${depName} (${license})`);
320
+ }
321
+ }
322
+ catch {
323
+ // skip unreadable dep packages
324
+ }
325
+ }
326
+ checks.push(violations.length === 0
327
+ ? {
328
+ name: 'supply-chain/license-allowlist',
329
+ status: 'pass',
330
+ severity: 'info',
331
+ detail: `all direct dependencies use permitted licenses (${allowedLicenses.join(', ')})`,
332
+ }
333
+ : {
334
+ name: 'supply-chain/license-allowlist',
335
+ status: 'fail',
336
+ severity: 'critical',
337
+ detail: `${violations.length} dependency(s) use disallowed licenses: ${violations.slice(0, 5).join('; ')}${violations.length > 5 ? ` …and ${violations.length - 5} more` : ''}`,
338
+ suggestion: `Add missing licenses to supply.allowedLicenses in pkg/lumis.config.ts, ` +
339
+ `or replace the offending dependencies with licensed-compatible alternatives.`,
340
+ });
341
+ }
342
+ else {
343
+ checks.push({
344
+ name: 'supply-chain/license-allowlist',
345
+ status: 'warn',
346
+ severity: 'warning',
347
+ detail: 'No supply.allowedLicenses configured — license compliance is unchecked',
348
+ suggestion: 'Set supply.allowedLicenses in pkg/lumis.config.ts (e.g. ["MIT","ISC","Apache-2.0"]) to enforce license policy for direct dependencies.',
349
+ });
350
+ }
351
+ // --- Cached audit result check ---
352
+ const auditCachePath = path.join(projectRoot, '.lumis', 'audit.json');
353
+ if (!fs.existsSync(auditCachePath)) {
354
+ checks.push({
355
+ name: 'supply-chain/audit-cache',
356
+ status: 'warn',
357
+ severity: 'warning',
358
+ detail: 'No cached audit result found at .lumis/audit.json',
359
+ suggestion: 'Run `pnpm audit --json > .lumis/audit.json` (or `npm audit --json > .lumis/audit.json`) ' +
360
+ 'and commit .lumis/audit.json to surface vulnerability counts in `lumis doctor`.',
361
+ });
362
+ return;
363
+ }
364
+ let auditResult;
365
+ try {
366
+ auditResult = JSON.parse(fs.readFileSync(auditCachePath, 'utf8'));
367
+ }
368
+ catch {
369
+ checks.push({
370
+ name: 'supply-chain/audit-cache',
371
+ status: 'warn',
372
+ severity: 'warning',
373
+ detail: '.lumis/audit.json could not be parsed — regenerate with `pnpm audit --json`',
374
+ });
375
+ return;
376
+ }
377
+ const SEVERITY_ORDER = ['info', 'low', 'moderate', 'high', 'critical'];
378
+ const failOnSeverity = config.supply?.failOnSeverity ?? 'critical';
379
+ const maxVulnerabilities = config.supply?.maxVulnerabilities ?? 0;
380
+ const failSeverityIndex = SEVERITY_ORDER.indexOf(failOnSeverity);
381
+ const vulnMap = auditResult.metadata?.vulnerabilities ??
382
+ auditResult.vulnerabilities ??
383
+ {};
384
+ const totalAtOrAbove = SEVERITY_ORDER.slice(failSeverityIndex).reduce((sum, sev) => sum + (vulnMap[sev] ?? 0), 0);
385
+ checks.push(totalAtOrAbove <= maxVulnerabilities
386
+ ? {
387
+ name: 'supply-chain/audit-cache',
388
+ status: 'pass',
389
+ severity: 'info',
390
+ detail: `audit cache: ${totalAtOrAbove} vulnerability(s) at or above "${failOnSeverity}" (threshold: ${maxVulnerabilities})`,
391
+ }
392
+ : {
393
+ name: 'supply-chain/audit-cache',
394
+ status: 'fail',
395
+ severity: 'critical',
396
+ detail: `audit cache: ${totalAtOrAbove} vulnerability(s) at or above "${failOnSeverity}" ` +
397
+ `exceed threshold of ${maxVulnerabilities}`,
398
+ suggestion: 'Run `pnpm audit --fix` or update offending packages. ' +
399
+ 'Adjust supply.maxVulnerabilities or supply.failOnSeverity in pkg/lumis.config.ts if needed.',
400
+ });
401
+ }
402
+ const ROOT_TOOL_CONFIG_PATTERNS = [
403
+ { pattern: /^drizzle\.config\.(ts|js|mjs|cjs)$/, canonical: 'pkg/drizzle.config.ts', checkName: 'drizzle-config-root' },
404
+ { pattern: /^vitest\.config\.(ts|js|mjs|cjs)$/, canonical: 'pkg/vitest.config.ts', checkName: 'vitest-config-root' },
405
+ { pattern: /^prettier\.config\.(ts|js|cjs|mjs)$/, canonical: 'pkg/prettier.config.ts', checkName: 'prettier-config-root' },
406
+ { pattern: /^eslint\.config\.(ts|js|mjs|cjs)$/, canonical: 'pkg/eslint.config.ts', checkName: 'eslint-config-root' },
407
+ { pattern: /^\.eslintrc(\.(json|js|cjs|yaml|yml))?$/, canonical: 'pkg/eslint.config.ts', checkName: 'eslintrc-root' },
408
+ { pattern: /^tailwind\.config\.(ts|js|mjs|cjs)$/, canonical: 'pkg/tailwind.config.ts', checkName: 'tailwind-config-root' },
409
+ { pattern: /^lumis\.config\.(ts|json)$/, canonical: 'pkg/lumis.config.ts', checkName: 'lumis-config-root' },
410
+ ];
411
+ /**
412
+ * Warn when legacy root-level tool configs exist without a pkg/ canonical counterpart.
413
+ */
414
+ function appendConfigHygieneChecks(projectRoot, checks) {
415
+ let entries = [];
416
+ try {
417
+ entries = fs.readdirSync(projectRoot);
418
+ }
419
+ catch {
420
+ return;
421
+ }
422
+ for (const { pattern, canonical, checkName } of ROOT_TOOL_CONFIG_PATTERNS) {
423
+ const matches = entries.filter((name) => pattern.test(name));
424
+ if (matches.length === 0)
425
+ continue;
426
+ const canonicalPath = path.join(projectRoot, canonical);
427
+ if (fs.existsSync(canonicalPath)) {
428
+ checks.push({
429
+ name: checkName,
430
+ status: 'warn',
431
+ severity: 'warning',
432
+ detail: `legacy root config ${matches.join(', ')} — canonical ${canonical} also exists`,
433
+ suggestion: `Remove root ${matches[0]} once you confirm ${canonical} is wired via lumis.`,
434
+ });
435
+ continue;
436
+ }
437
+ checks.push({
438
+ name: checkName,
439
+ status: 'warn',
440
+ severity: 'warning',
441
+ detail: `root-level ${matches.join(', ')} detected`,
442
+ suggestion: `Move tool config to ${canonical} (flat pkg/ layout). Lumis proxies tools from pkg/.`,
443
+ });
444
+ }
445
+ }
446
+ /**
447
+ * Non-blocking Vercel deploy hygiene checks when vercel.json is present at project root.
448
+ */
449
+ function appendVercelDeploymentChecks(projectRoot, checks) {
450
+ const vercelPath = path.join(projectRoot, 'vercel.json');
451
+ if (!fs.existsSync(vercelPath)) {
452
+ checks.push({
453
+ name: 'vercel-config',
454
+ status: 'pass',
455
+ severity: 'info',
456
+ detail: 'no vercel.json in project root',
457
+ });
458
+ return;
459
+ }
460
+ let parsed;
461
+ try {
462
+ parsed = JSON.parse(fs.readFileSync(vercelPath, 'utf8'));
463
+ }
464
+ catch {
465
+ checks.push({
466
+ name: 'vercel-config',
467
+ status: 'warn',
468
+ severity: 'warning',
469
+ detail: 'vercel.json exists but is not valid JSON',
470
+ suggestion: 'Fix JSON syntax in vercel.json.',
471
+ });
472
+ return;
473
+ }
474
+ if (!parsed || typeof parsed !== 'object') {
475
+ checks.push({
476
+ name: 'vercel-config',
477
+ status: 'warn',
478
+ severity: 'warning',
479
+ detail: 'vercel.json is not a JSON object',
480
+ });
481
+ return;
482
+ }
483
+ const cfg = parsed;
484
+ const routes = cfg.routes;
485
+ const usesFilesystemHandle = Array.isArray(routes) &&
486
+ routes.some((entry) => {
487
+ if (!entry || typeof entry !== 'object') {
488
+ return false;
489
+ }
490
+ return entry.handle === 'filesystem';
491
+ });
492
+ const rewrites = cfg.rewrites;
493
+ const hasRewrites = Array.isArray(rewrites) && rewrites.length > 0;
494
+ if (usesFilesystemHandle) {
495
+ checks.push({
496
+ name: 'vercel-routing',
497
+ status: 'warn',
498
+ severity: 'warning',
499
+ detail: 'vercel.json uses deprecated routes with handle: filesystem; prefer rewrites for current Vercel routing',
500
+ suggestion: 'Replace routes with rewrites so static files resolve first and dynamic traffic hits api/index.',
501
+ });
502
+ }
503
+ else if (hasRewrites) {
504
+ checks.push({
505
+ name: 'vercel-routing',
506
+ status: 'pass',
507
+ severity: 'info',
508
+ detail: 'vercel.json uses rewrites for catch-all routing',
509
+ });
510
+ }
511
+ else {
512
+ checks.push({
513
+ name: 'vercel-routing',
514
+ status: 'pass',
515
+ severity: 'info',
516
+ detail: 'vercel.json has no rewrites array (fine when not using a serverless catch-all)',
517
+ });
518
+ }
519
+ const installCommand = typeof cfg.installCommand === 'string' && cfg.installCommand.trim().length > 0
520
+ ? cfg.installCommand.trim()
521
+ : '';
522
+ checks.push(installCommand
523
+ ? {
524
+ name: 'vercel-install-command',
525
+ status: 'pass',
526
+ severity: 'info',
527
+ detail: `installCommand is set (${installCommand})`,
528
+ }
529
+ : {
530
+ name: 'vercel-install-command',
531
+ status: 'warn',
532
+ severity: 'warning',
533
+ detail: 'vercel.json has no installCommand; Vercel may default to npm install',
534
+ suggestion: 'Set installCommand to pnpm install --frozen-lockfile when using pnpm.',
535
+ });
536
+ const pkgPath = path.join(projectRoot, 'package.json');
537
+ let packageManager = '';
538
+ if (fs.existsSync(pkgPath)) {
539
+ try {
540
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
541
+ packageManager = typeof pkg.packageManager === 'string' ? pkg.packageManager : '';
542
+ }
543
+ catch {
544
+ // ignore malformed package.json here; other tooling will surface it
545
+ }
546
+ }
547
+ checks.push(packageManager
548
+ ? {
549
+ name: 'package-manager-field',
550
+ status: 'pass',
551
+ severity: 'info',
552
+ detail: `package.json declares packageManager=${packageManager}`,
553
+ }
554
+ : {
555
+ name: 'package-manager-field',
556
+ status: 'warn',
557
+ severity: 'warning',
558
+ detail: 'package.json has no packageManager field; Vercel may pick a different pnpm than local',
559
+ suggestion: 'Add "packageManager": "pnpm@x.y.z" to package.json to pin pnpm via Corepack.',
560
+ });
561
+ const apiBundle = path.join(projectRoot, 'api', 'index.js');
562
+ checks.push(fs.existsSync(apiBundle)
563
+ ? {
564
+ name: 'vercel-api-bundle',
565
+ status: 'pass',
566
+ severity: 'info',
567
+ detail: 'api/index.js exists (Vercel serverless entry present)',
568
+ }
569
+ : {
570
+ name: 'vercel-api-bundle',
571
+ status: 'pass',
572
+ severity: 'info',
573
+ detail: 'api/index.js not found yet; run pnpm run build:vercel before deploy to confirm output',
574
+ });
575
+ }
576
+ /**
577
+ * Run diagnostic checks for host readiness and print a summary.
578
+ *
579
+ * @param context - Shared command runtime context.
580
+ * @returns Exit code 0 when no failing checks exist, otherwise 1.
581
+ */
582
+ export async function executeDoctorCommand(context) {
583
+ const options = parseDoctorOptions(context.commandArgs, context.projectRoot);
584
+ const isCI = process.env.CI === 'true';
585
+ const checks = [];
586
+ checks.push({
587
+ name: 'config',
588
+ status: 'pass',
589
+ severity: 'info',
590
+ detail: context.config.adapter
591
+ ? `configured adapter preference = ${context.config.adapter}`
592
+ : 'no configured adapter preference',
593
+ });
594
+ const pack = await context.resolvePack();
595
+ const packFramework = pack?.manifest.frameworks?.[0] ?? 'unknown';
596
+ checks.push(pack
597
+ ? {
598
+ name: 'pack-detection',
599
+ status: 'pass',
600
+ severity: 'info',
601
+ detail: `${pack.manifest.name} (${packFramework})`,
602
+ }
603
+ : {
604
+ name: 'pack-detection',
605
+ status: 'warn',
606
+ severity: 'critical',
607
+ detail: 'no compatible pack detected for this project root (critical in CI)',
608
+ suggestion: 'Install a supported pack dependency (for example @lumiarq/framework or @lumiarq/pack-nextjs), then run "lumis init" to refresh local config.',
609
+ });
610
+ const adapter = await context.resolveAdapter();
611
+ checks.push(adapter
612
+ ? {
613
+ name: 'adapter-resolution',
614
+ status: 'pass',
615
+ severity: 'info',
616
+ detail: adapter.name,
617
+ }
618
+ : {
619
+ name: 'adapter-resolution',
620
+ status: 'warn',
621
+ severity: 'critical',
622
+ detail: 'no adapter resolved (critical in CI)',
623
+ suggestion: 'Set "adapter" in .lumis/config.json or add a pack fingerprint dependency, then rerun "lumis doctor".',
624
+ });
625
+ const commands = await collectNormalizedCommands(context);
626
+ checks.push({
627
+ name: 'command-inventory',
628
+ status: commands.length > 0 ? 'pass' : 'warn',
629
+ severity: commands.length > 0 ? 'info' : 'warning',
630
+ detail: `${commands.length} command(s) available from pack/project sources`,
631
+ ...(commands.length === 0 && {
632
+ suggestion: 'Define commands in your pack manifest or project command map so Lumis can route intents.',
633
+ }),
634
+ });
635
+ appendConfigHygieneChecks(context.projectRoot, checks);
636
+ appendVercelDeploymentChecks(context.projectRoot, checks);
637
+ const ir = loadIR(context.projectRoot);
638
+ checks.push(ir
639
+ ? {
640
+ name: 'ir-state',
641
+ status: 'pass',
642
+ severity: 'info',
643
+ detail: `framework=${ir.framework}, modules=${ir.modules.length}`,
644
+ }
645
+ : {
646
+ name: 'ir-state',
647
+ status: 'warn',
648
+ severity: 'warning',
649
+ detail: 'no persisted IR found (.lumis/ir.json)',
650
+ suggestion: 'Run "lumis ir rebuild" to create a baseline IR snapshot before generation.',
651
+ });
652
+ const runtimeProfilesPath = path.join(context.projectRoot, '.lumis', 'runtime-profiles.json');
653
+ const runtimePolicy = resolveRuntimePolicy(context.config);
654
+ checks.push({
655
+ name: 'runtime-policy',
656
+ status: 'pass',
657
+ severity: 'info',
658
+ detail: `mode=${runtimePolicy.mode}, requireApprovalForRemote=${runtimePolicy.requireApprovalForRemote}, ` +
659
+ `approvalEnvVar=${runtimePolicy.approvalEnvVar}`,
660
+ });
661
+ if (!fs.existsSync(runtimeProfilesPath)) {
662
+ checks.push({
663
+ name: 'runtime-profiles',
664
+ status: 'pass',
665
+ severity: 'info',
666
+ detail: 'runtime profile store not found; default local profile will be used',
667
+ });
668
+ }
669
+ else {
670
+ try {
671
+ const rawProfiles = fs.readFileSync(runtimeProfilesPath, 'utf8');
672
+ const parsedProfiles = JSON.parse(rawProfiles);
673
+ const validation = validateRuntimeProfileStore(parsedProfiles);
674
+ if (!validation.ok) {
675
+ checks.push({
676
+ name: 'runtime-profiles',
677
+ status: 'fail',
678
+ severity: 'critical',
679
+ detail: `invalid runtime profile store: ${validation.errors.join('; ')}`,
680
+ suggestion: 'Fix .lumis/runtime-profiles.json to match schema version 1.0 before running runtime orchestration.',
681
+ });
682
+ }
683
+ else {
684
+ checks.push({
685
+ name: 'runtime-profiles',
686
+ status: 'pass',
687
+ severity: 'info',
688
+ detail: `${validation.value.profiles.length} profile(s) configured`,
689
+ });
690
+ const missingDefault = validation.value.defaultProfileId &&
691
+ !validation.value.profiles.some((profile) => profile.id === validation.value.defaultProfileId);
692
+ if (missingDefault) {
693
+ checks.push({
694
+ name: 'runtime-default-profile',
695
+ status: 'fail',
696
+ severity: 'critical',
697
+ detail: `defaultProfileId=${validation.value.defaultProfileId} does not reference an existing profile`,
698
+ suggestion: 'Set defaultProfileId to an existing profile id or remove it to use explicit selection.',
699
+ });
700
+ }
701
+ else {
702
+ checks.push({
703
+ name: 'runtime-default-profile',
704
+ status: 'pass',
705
+ severity: 'info',
706
+ detail: validation.value.defaultProfileId
707
+ ? `default profile = ${validation.value.defaultProfileId}`
708
+ : 'no default profile set',
709
+ });
710
+ }
711
+ const hasInlineSecrets = validation.value.profiles.some((profile) => hasSensitiveInlineEnv(profile.env));
712
+ const hasRemoteProfiles = validation.value.profiles.some((profile) => profile.kind !== 'local');
713
+ if (hasRemoteProfiles && !runtimePolicy.requireApprovalForRemote) {
714
+ checks.push({
715
+ name: 'runtime-policy-approval',
716
+ status: 'warn',
717
+ severity: 'critical',
718
+ detail: 'remote runtime profiles exist but approval gate is disabled',
719
+ suggestion: 'Enable runtime.policy.requireApprovalForRemote or restrict execution to local profile only.',
720
+ });
721
+ }
722
+ else {
723
+ checks.push({
724
+ name: 'runtime-policy-approval',
725
+ status: 'pass',
726
+ severity: 'info',
727
+ detail: hasRemoteProfiles
728
+ ? 'remote profile approval gate is enabled'
729
+ : 'no remote profiles configured',
730
+ });
731
+ }
732
+ checks.push(hasInlineSecrets
733
+ ? {
734
+ name: 'runtime-secret-safety',
735
+ status: 'warn',
736
+ severity: 'critical',
737
+ detail: 'runtime profiles contain inline sensitive env values',
738
+ suggestion: 'Move sensitive values to environment variables and reference those variables from secure runtime context only.',
739
+ }
740
+ : {
741
+ name: 'runtime-secret-safety',
742
+ status: 'pass',
743
+ severity: 'info',
744
+ detail: 'no inline sensitive env values detected in runtime profiles',
745
+ });
746
+ }
747
+ }
748
+ catch (error) {
749
+ checks.push({
750
+ name: 'runtime-profiles',
751
+ status: 'fail',
752
+ severity: 'critical',
753
+ detail: `unable to parse runtime profile store: ${error instanceof Error ? error.message : 'unknown error'}`,
754
+ suggestion: 'Ensure .lumis/runtime-profiles.json is valid JSON.',
755
+ });
756
+ }
757
+ }
758
+ const runtimeTranscriptsDir = path.join(context.projectRoot, '.lumis', 'transcripts');
759
+ if (!fs.existsSync(runtimeTranscriptsDir)) {
760
+ checks.push({
761
+ name: 'runtime-transcripts-index',
762
+ status: 'pass',
763
+ severity: 'info',
764
+ detail: 'no runtime transcripts recorded yet',
765
+ });
766
+ checks.push({
767
+ name: 'runtime-transcripts-summary',
768
+ status: 'pass',
769
+ severity: 'info',
770
+ detail: 'latest=none, success=0, failed=0, total=0',
771
+ });
772
+ }
773
+ else {
774
+ const index = loadRuntimeTranscriptIndex(context.projectRoot);
775
+ const summary = summarizeRuntimeTranscripts(index);
776
+ const missingFiles = index.entries.filter((entry) => !fs.existsSync(path.join(context.projectRoot, entry.transcriptFile)));
777
+ checks.push({
778
+ name: 'runtime-transcripts-index',
779
+ status: 'pass',
780
+ severity: 'info',
781
+ detail: `${index.entries.length} runtime transcript entr${index.entries.length === 1 ? 'y' : 'ies'} indexed`,
782
+ });
783
+ checks.push({
784
+ name: 'runtime-transcripts-summary',
785
+ status: 'pass',
786
+ severity: 'info',
787
+ detail: `latest=${summary.latestEntry ? `${summary.latestEntry.label}:${summary.latestEntry.success ? 'ok' : 'fail'}` : 'none'}, ` +
788
+ `success=${summary.successful}, failed=${summary.failed}, total=${summary.total}`,
789
+ });
790
+ if (missingFiles.length > 0) {
791
+ checks.push({
792
+ name: 'runtime-transcripts-integrity',
793
+ status: 'warn',
794
+ severity: 'warning',
795
+ detail: `${missingFiles.length} transcript file(s) referenced by index are missing`,
796
+ suggestion: 'Prune stale transcript index entries or regenerate index by rerunning runtime tasks.',
797
+ });
798
+ }
799
+ else {
800
+ checks.push({
801
+ name: 'runtime-transcripts-integrity',
802
+ status: 'pass',
803
+ severity: 'info',
804
+ detail: 'runtime transcript index matches on-disk transcript files',
805
+ });
806
+ }
807
+ }
808
+ if (ir && adapter) {
809
+ try {
810
+ const rebuilt = await adapter.buildIR(context.projectRoot);
811
+ const moduleDiff = diffIR(ir, rebuilt);
812
+ const frameworkChanged = ir.framework !== rebuilt.framework;
813
+ const versionChanged = ir.irVersion !== rebuilt.irVersion;
814
+ const hasDrift = frameworkChanged ||
815
+ versionChanged ||
816
+ moduleDiff.addedModules.length > 0 ||
817
+ moduleDiff.removedModules.length > 0 ||
818
+ moduleDiff.changedModules.length > 0;
819
+ checks.push(hasDrift
820
+ ? {
821
+ name: 'ir-drift',
822
+ status: 'warn',
823
+ severity: 'warning',
824
+ detail: `persisted IR differs from current project ` +
825
+ `(added=${moduleDiff.addedModules.length}, removed=${moduleDiff.removedModules.length}, ` +
826
+ `changed=${moduleDiff.changedModules.length}, frameworkChanged=${frameworkChanged}, ` +
827
+ `versionChanged=${versionChanged}). Run "lumis ir rebuild".`,
828
+ suggestion: 'Review the drift summary, then run "lumis ir rebuild" and commit the updated .lumis/ir.json file.',
829
+ }
830
+ : {
831
+ name: 'ir-drift',
832
+ status: 'pass',
833
+ severity: 'info',
834
+ detail: 'persisted IR is in sync with current project state',
835
+ });
836
+ }
837
+ catch (error) {
838
+ checks.push({
839
+ name: 'ir-drift',
840
+ status: 'warn',
841
+ severity: 'warning',
842
+ detail: `unable to compare persisted IR with project state: ` +
843
+ (error instanceof Error ? error.message : 'unknown error'),
844
+ suggestion: 'Run "lumis ir rebuild" to refresh IR manually, then rerun "lumis doctor".',
845
+ });
846
+ }
847
+ }
848
+ if (adapter && options.intentTexts.length > 0) {
849
+ const parsedIntents = options.intentTexts
850
+ .map((raw) => ({ raw, parsed: parseRawIntent(raw) }));
851
+ const invalidIntents = parsedIntents.filter((entry) => !entry.parsed).map((entry) => entry.raw);
852
+ if (invalidIntents.length > 0) {
853
+ checks.push({
854
+ name: 'intent-conflicts',
855
+ status: 'fail',
856
+ severity: 'critical',
857
+ detail: `invalid intent input(s): ${invalidIntents.join(' | ')}`,
858
+ suggestion: 'Use fully formed intents (for example "create action LoginAction in Auth") or move batch intents to --intent-file.',
859
+ });
860
+ }
861
+ else {
862
+ const baseIR = ir ?? await adapter.buildIR(context.projectRoot);
863
+ const plannedIntents = [];
864
+ for (const parsed of parsedIntents) {
865
+ if (!parsed.parsed) {
866
+ continue;
867
+ }
868
+ const plan = await createPlanWithAdapter(parsed.parsed, baseIR, adapter);
869
+ plannedIntents.push({
870
+ raw: parsed.raw,
871
+ stepSummaries: plan.steps.map((step) => ({ targetFile: step.targetFile, operation: step.operation })),
872
+ });
873
+ }
874
+ const conflicts = detectIntentConflicts(plannedIntents);
875
+ if (conflicts.length === 0) {
876
+ checks.push({
877
+ name: 'intent-conflicts',
878
+ status: 'pass',
879
+ severity: 'info',
880
+ detail: `no intent conflicts detected across ${plannedIntents.length} intent(s)`,
881
+ });
882
+ }
883
+ else {
884
+ const strictMode = options.strictIntentConflicts || isCI;
885
+ const severity = strictMode ? 'fail' : 'warn';
886
+ const preview = conflicts
887
+ .slice(0, 3)
888
+ .map((conflict) => `${conflict.targetFile} [${conflict.operations.join(',')}]`)
889
+ .join('; ');
890
+ checks.push({
891
+ name: 'intent-conflicts',
892
+ status: severity,
893
+ severity: strictMode ? 'critical' : 'warning',
894
+ detail: `${conflicts.length} conflict(s) detected. ` +
895
+ `${strictMode ? `failing (${isCI ? 'CI mode' : 'strict mode'}).` : 'run with --strict-intent-conflicts to fail locally; CI fails critical conflicts by default.'} ` +
896
+ `Examples: ${preview}`,
897
+ suggestion: 'Split or reorder overlapping intents so each target file has one deterministic write path. ' +
898
+ 'Run "lumis make --dry-run <intent>" on each intent individually to preview its plan, then rerun "lumis doctor --intent <intent1> --intent <intent2>" to confirm conflicts are resolved.',
899
+ });
900
+ }
901
+ }
902
+ }
903
+ appendSecurityBaselineChecks(context.projectRoot, context.config, checks);
904
+ appendSupplyChainChecks(context.projectRoot, context.config, checks);
905
+ context.write('Lumis doctor report:\n');
906
+ for (const check of checks) {
907
+ context.write(`${formatCheck(check)}\n`);
908
+ if (check.status !== 'pass' && check.suggestion) {
909
+ context.write(` -> Suggestion: ${check.suggestion}\n`);
910
+ }
911
+ }
912
+ const failedChecks = checks.filter((check) => check.status === 'fail').length;
913
+ const issueChecks = checks.filter((check) => check.status !== 'pass');
914
+ const criticalChecks = issueChecks.filter((check) => check.severity === 'critical').length;
915
+ const warningChecks = issueChecks.filter((check) => check.severity === 'warning').length;
916
+ const infoChecks = issueChecks.filter((check) => check.severity === 'info').length;
917
+ const ciFailures = isCI ? criticalChecks : 0;
918
+ context.write(`Summary: ${checks.length} checks, ${criticalChecks} critical, ${warningChecks} warning, ` +
919
+ `${infoChecks} info issue(s), ${failedChecks} hard failure(s)\n`);
920
+ return failedChecks > 0 || ciFailures > 0 ? 1 : 0;
921
+ }
922
+ //# sourceMappingURL=doctor.command.js.map