dialog-survey 1.1.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 (233) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +242 -0
  3. package/VERSION +1 -0
  4. package/dist/api/admin-templates.d.ts +15 -0
  5. package/dist/api/admin-templates.d.ts.map +1 -0
  6. package/dist/api/admin-templates.js +611 -0
  7. package/dist/api/admin-templates.js.map +1 -0
  8. package/dist/api/analysis.d.ts +8 -0
  9. package/dist/api/analysis.d.ts.map +1 -0
  10. package/dist/api/analysis.js +106 -0
  11. package/dist/api/analysis.js.map +1 -0
  12. package/dist/api/health.d.ts +6 -0
  13. package/dist/api/health.d.ts.map +1 -0
  14. package/dist/api/health.js +116 -0
  15. package/dist/api/health.js.map +1 -0
  16. package/dist/api/plans.d.ts +3 -0
  17. package/dist/api/plans.d.ts.map +1 -0
  18. package/dist/api/plans.js +184 -0
  19. package/dist/api/plans.js.map +1 -0
  20. package/dist/api/templates.d.ts +9 -0
  21. package/dist/api/templates.d.ts.map +1 -0
  22. package/dist/api/templates.js +211 -0
  23. package/dist/api/templates.js.map +1 -0
  24. package/dist/core/graph.d.ts +7 -0
  25. package/dist/core/graph.d.ts.map +1 -0
  26. package/dist/core/graph.js +31 -0
  27. package/dist/core/graph.js.map +1 -0
  28. package/dist/core/nodes/analyzing.d.ts +3 -0
  29. package/dist/core/nodes/analyzing.d.ts.map +1 -0
  30. package/dist/core/nodes/analyzing.js +38 -0
  31. package/dist/core/nodes/analyzing.js.map +1 -0
  32. package/dist/core/nodes/completed.d.ts +3 -0
  33. package/dist/core/nodes/completed.d.ts.map +1 -0
  34. package/dist/core/nodes/completed.js +8 -0
  35. package/dist/core/nodes/completed.js.map +1 -0
  36. package/dist/core/nodes/interviewing.d.ts +13 -0
  37. package/dist/core/nodes/interviewing.d.ts.map +1 -0
  38. package/dist/core/nodes/interviewing.js +130 -0
  39. package/dist/core/nodes/interviewing.js.map +1 -0
  40. package/dist/core/nodes/planning.d.ts +15 -0
  41. package/dist/core/nodes/planning.d.ts.map +1 -0
  42. package/dist/core/nodes/planning.js +40 -0
  43. package/dist/core/nodes/planning.js.map +1 -0
  44. package/dist/core/types/index.d.ts +69 -0
  45. package/dist/core/types/index.d.ts.map +1 -0
  46. package/dist/core/types/index.js +49 -0
  47. package/dist/core/types/index.js.map +1 -0
  48. package/dist/domains/interview/entities/interview.entity.d.ts +21 -0
  49. package/dist/domains/interview/entities/interview.entity.d.ts.map +1 -0
  50. package/dist/domains/interview/entities/interview.entity.js +20 -0
  51. package/dist/domains/interview/entities/interview.entity.js.map +1 -0
  52. package/dist/domains/interview/entities/message.entity.d.ts +15 -0
  53. package/dist/domains/interview/entities/message.entity.d.ts.map +1 -0
  54. package/dist/domains/interview/entities/message.entity.js +11 -0
  55. package/dist/domains/interview/entities/message.entity.js.map +1 -0
  56. package/dist/domains/interview/entities/response.entity.d.ts +17 -0
  57. package/dist/domains/interview/entities/response.entity.d.ts.map +1 -0
  58. package/dist/domains/interview/entities/response.entity.js +12 -0
  59. package/dist/domains/interview/entities/response.entity.js.map +1 -0
  60. package/dist/domains/interview/services/interview.domain.d.ts +8 -0
  61. package/dist/domains/interview/services/interview.domain.d.ts.map +1 -0
  62. package/dist/domains/interview/services/interview.domain.js +30 -0
  63. package/dist/domains/interview/services/interview.domain.js.map +1 -0
  64. package/dist/integrations/dingtalk/client.d.ts +32 -0
  65. package/dist/integrations/dingtalk/client.d.ts.map +1 -0
  66. package/dist/integrations/dingtalk/client.js +89 -0
  67. package/dist/integrations/dingtalk/client.js.map +1 -0
  68. package/dist/integrations/dingtalk/message-sender.d.ts +24 -0
  69. package/dist/integrations/dingtalk/message-sender.d.ts.map +1 -0
  70. package/dist/integrations/dingtalk/message-sender.js +81 -0
  71. package/dist/integrations/dingtalk/message-sender.js.map +1 -0
  72. package/dist/integrations/dingtalk/middleware.d.ts +4 -0
  73. package/dist/integrations/dingtalk/middleware.d.ts.map +1 -0
  74. package/dist/integrations/dingtalk/middleware.js +29 -0
  75. package/dist/integrations/dingtalk/middleware.js.map +1 -0
  76. package/dist/integrations/dingtalk/stream-client.d.ts +112 -0
  77. package/dist/integrations/dingtalk/stream-client.d.ts.map +1 -0
  78. package/dist/integrations/dingtalk/stream-client.js +305 -0
  79. package/dist/integrations/dingtalk/stream-client.js.map +1 -0
  80. package/dist/integrations/dingtalk/token-manager.d.ts +22 -0
  81. package/dist/integrations/dingtalk/token-manager.d.ts.map +1 -0
  82. package/dist/integrations/dingtalk/token-manager.js +109 -0
  83. package/dist/integrations/dingtalk/token-manager.js.map +1 -0
  84. package/dist/integrations/llm/alibaba.d.ts +10 -0
  85. package/dist/integrations/llm/alibaba.d.ts.map +1 -0
  86. package/dist/integrations/llm/alibaba.js +60 -0
  87. package/dist/integrations/llm/alibaba.js.map +1 -0
  88. package/dist/integrations/llm/base.d.ts +28 -0
  89. package/dist/integrations/llm/base.d.ts.map +1 -0
  90. package/dist/integrations/llm/base.js +2 -0
  91. package/dist/integrations/llm/base.js.map +1 -0
  92. package/dist/integrations/llm/volcengine.d.ts +12 -0
  93. package/dist/integrations/llm/volcengine.d.ts.map +1 -0
  94. package/dist/integrations/llm/volcengine.js +80 -0
  95. package/dist/integrations/llm/volcengine.js.map +1 -0
  96. package/dist/middleware/admin-auth.d.ts +3 -0
  97. package/dist/middleware/admin-auth.d.ts.map +1 -0
  98. package/dist/middleware/admin-auth.js +37 -0
  99. package/dist/middleware/admin-auth.js.map +1 -0
  100. package/dist/middleware/api-auth.d.ts +10 -0
  101. package/dist/middleware/api-auth.d.ts.map +1 -0
  102. package/dist/middleware/api-auth.js +30 -0
  103. package/dist/middleware/api-auth.js.map +1 -0
  104. package/dist/repositories/interview-state-mapper.d.ts +28 -0
  105. package/dist/repositories/interview-state-mapper.d.ts.map +1 -0
  106. package/dist/repositories/interview-state-mapper.js +32 -0
  107. package/dist/repositories/interview-state-mapper.js.map +1 -0
  108. package/dist/repositories/interview-state.repository.d.ts +34 -0
  109. package/dist/repositories/interview-state.repository.d.ts.map +1 -0
  110. package/dist/repositories/interview-state.repository.js +220 -0
  111. package/dist/repositories/interview-state.repository.js.map +1 -0
  112. package/dist/repositories/interview.repository.d.ts +29 -0
  113. package/dist/repositories/interview.repository.d.ts.map +1 -0
  114. package/dist/repositories/interview.repository.js +36 -0
  115. package/dist/repositories/interview.repository.js.map +1 -0
  116. package/dist/repositories/message.repository.d.ts +17 -0
  117. package/dist/repositories/message.repository.d.ts.map +1 -0
  118. package/dist/repositories/message.repository.js +37 -0
  119. package/dist/repositories/message.repository.js.map +1 -0
  120. package/dist/repositories/template.repository.d.ts +117 -0
  121. package/dist/repositories/template.repository.d.ts.map +1 -0
  122. package/dist/repositories/template.repository.js +280 -0
  123. package/dist/repositories/template.repository.js.map +1 -0
  124. package/dist/schemas/dimensions.d.ts +61 -0
  125. package/dist/schemas/dimensions.d.ts.map +1 -0
  126. package/dist/schemas/dimensions.js +44 -0
  127. package/dist/schemas/dimensions.js.map +1 -0
  128. package/dist/server.d.ts +10 -0
  129. package/dist/server.d.ts.map +1 -0
  130. package/dist/server.js +217 -0
  131. package/dist/server.js.map +1 -0
  132. package/dist/services/analysis.service.d.ts +81 -0
  133. package/dist/services/analysis.service.d.ts.map +1 -0
  134. package/dist/services/analysis.service.js +229 -0
  135. package/dist/services/analysis.service.js.map +1 -0
  136. package/dist/services/analytics.service.d.ts +44 -0
  137. package/dist/services/analytics.service.d.ts.map +1 -0
  138. package/dist/services/analytics.service.js +222 -0
  139. package/dist/services/analytics.service.js.map +1 -0
  140. package/dist/services/asr.service.d.ts +9 -0
  141. package/dist/services/asr.service.d.ts.map +1 -0
  142. package/dist/services/asr.service.js +41 -0
  143. package/dist/services/asr.service.js.map +1 -0
  144. package/dist/services/audit-cleanup.service.d.ts +9 -0
  145. package/dist/services/audit-cleanup.service.d.ts.map +1 -0
  146. package/dist/services/audit-cleanup.service.js +27 -0
  147. package/dist/services/audit-cleanup.service.js.map +1 -0
  148. package/dist/services/batch-aggregation.service.d.ts +34 -0
  149. package/dist/services/batch-aggregation.service.d.ts.map +1 -0
  150. package/dist/services/batch-aggregation.service.js +91 -0
  151. package/dist/services/batch-aggregation.service.js.map +1 -0
  152. package/dist/services/conversation-engine.d.ts +16 -0
  153. package/dist/services/conversation-engine.d.ts.map +1 -0
  154. package/dist/services/conversation-engine.js +49 -0
  155. package/dist/services/conversation-engine.js.map +1 -0
  156. package/dist/services/dead-letter.service.d.ts +12 -0
  157. package/dist/services/dead-letter.service.d.ts.map +1 -0
  158. package/dist/services/dead-letter.service.js +24 -0
  159. package/dist/services/dead-letter.service.js.map +1 -0
  160. package/dist/services/followup.service.d.ts +18 -0
  161. package/dist/services/followup.service.d.ts.map +1 -0
  162. package/dist/services/followup.service.js +146 -0
  163. package/dist/services/followup.service.js.map +1 -0
  164. package/dist/services/interview-plan-base.service.d.ts +226 -0
  165. package/dist/services/interview-plan-base.service.d.ts.map +1 -0
  166. package/dist/services/interview-plan-base.service.js +217 -0
  167. package/dist/services/interview-plan-base.service.js.map +1 -0
  168. package/dist/services/interview-plan-members.service.d.ts +45 -0
  169. package/dist/services/interview-plan-members.service.d.ts.map +1 -0
  170. package/dist/services/interview-plan-members.service.js +237 -0
  171. package/dist/services/interview-plan-members.service.js.map +1 -0
  172. package/dist/services/interview-plan-send.service.d.ts +27 -0
  173. package/dist/services/interview-plan-send.service.d.ts.map +1 -0
  174. package/dist/services/interview-plan-send.service.js +250 -0
  175. package/dist/services/interview-plan-send.service.js.map +1 -0
  176. package/dist/services/interview-plan.service.d.ts +8 -0
  177. package/dist/services/interview-plan.service.d.ts.map +1 -0
  178. package/dist/services/interview-plan.service.js +8 -0
  179. package/dist/services/interview-plan.service.js.map +1 -0
  180. package/dist/services/llm-clustering.d.ts +16 -0
  181. package/dist/services/llm-clustering.d.ts.map +1 -0
  182. package/dist/services/llm-clustering.js +91 -0
  183. package/dist/services/llm-clustering.js.map +1 -0
  184. package/dist/services/prompt.service.d.ts +13 -0
  185. package/dist/services/prompt.service.d.ts.map +1 -0
  186. package/dist/services/prompt.service.js +151 -0
  187. package/dist/services/prompt.service.js.map +1 -0
  188. package/dist/services/report.service.d.ts +28 -0
  189. package/dist/services/report.service.d.ts.map +1 -0
  190. package/dist/services/report.service.js +169 -0
  191. package/dist/services/report.service.js.map +1 -0
  192. package/dist/services/stream-message-utils.d.ts +31 -0
  193. package/dist/services/stream-message-utils.d.ts.map +1 -0
  194. package/dist/services/stream-message-utils.js +80 -0
  195. package/dist/services/stream-message-utils.js.map +1 -0
  196. package/dist/services/stream-message.service.d.ts +29 -0
  197. package/dist/services/stream-message.service.d.ts.map +1 -0
  198. package/dist/services/stream-message.service.js +184 -0
  199. package/dist/services/stream-message.service.js.map +1 -0
  200. package/dist/services/template-dimension.service.d.ts +21 -0
  201. package/dist/services/template-dimension.service.d.ts.map +1 -0
  202. package/dist/services/template-dimension.service.js +9 -0
  203. package/dist/services/template-dimension.service.js.map +1 -0
  204. package/dist/utils/db.d.ts +10 -0
  205. package/dist/utils/db.d.ts.map +1 -0
  206. package/dist/utils/db.js +21 -0
  207. package/dist/utils/db.js.map +1 -0
  208. package/dist/utils/logger.d.ts +8 -0
  209. package/dist/utils/logger.d.ts.map +1 -0
  210. package/dist/utils/logger.js +21 -0
  211. package/dist/utils/logger.js.map +1 -0
  212. package/dist/utils/markdown.d.ts +2 -0
  213. package/dist/utils/markdown.d.ts.map +1 -0
  214. package/dist/utils/markdown.js +39 -0
  215. package/dist/utils/markdown.js.map +1 -0
  216. package/dist/utils/pii-anonymizer.d.ts +2 -0
  217. package/dist/utils/pii-anonymizer.d.ts.map +1 -0
  218. package/dist/utils/pii-anonymizer.js +13 -0
  219. package/dist/utils/pii-anonymizer.js.map +1 -0
  220. package/dist/utils/retry.d.ts +8 -0
  221. package/dist/utils/retry.d.ts.map +1 -0
  222. package/dist/utils/retry.js +28 -0
  223. package/dist/utils/retry.js.map +1 -0
  224. package/dist/utils/security.d.ts +26 -0
  225. package/dist/utils/security.d.ts.map +1 -0
  226. package/dist/utils/security.js +99 -0
  227. package/dist/utils/security.js.map +1 -0
  228. package/package.json +80 -0
  229. package/prisma/schema.prisma +228 -0
  230. package/public/css/admin.css +73 -0
  231. package/public/js/alpine.min.js +2670 -0
  232. package/public/js/htmx.min.js +3422 -0
  233. package/scripts/cli.mjs +794 -0
@@ -0,0 +1,794 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * dialog-survey CLI — npx-based install and lifecycle management
5
+ *
6
+ * Commands: install, uninstall, start, stop, status, help
7
+ * Zero external dependencies — pure Node.js ESM
8
+ */
9
+
10
+ import { execSync } from "node:child_process";
11
+ import { createInterface } from "node:readline";
12
+ import { existsSync } from "node:fs";
13
+ import { mkdir, cp, rm, writeFile } from "node:fs/promises";
14
+ import { homedir, tmpdir } from "node:os";
15
+ import { join, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import http from "node:http";
18
+
19
+ // ─── Constants ───────────────────────────────────────────────────────────────
20
+
21
+ const INSTALL_DIR = join(homedir(), ".dialog-survey");
22
+ const PM2_APP_NAME = "dialog-survey";
23
+ const HEALTH_URL = "http://localhost:3001/health";
24
+ const HEALTH_TIMEOUT_MS = 30_000;
25
+ const HEALTH_POLL_INTERVAL_MS = 1_000;
26
+ const MIN_NODE_MAJOR = 20;
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = dirname(__filename);
30
+
31
+ // ─── Utilities ───────────────────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Execute a shell command synchronously, returning stdout.
35
+ * Throws with stderr on failure.
36
+ */
37
+ export function exec(cmd, options = {}) {
38
+ return execSync(cmd, {
39
+ encoding: "utf-8",
40
+ stdio: ["pipe", "pipe", "pipe"],
41
+ ...options,
42
+ }).trim();
43
+ }
44
+
45
+ /**
46
+ * Print a message to stdout.
47
+ */
48
+ function log(msg) {
49
+ process.stdout.write(`${msg}\n`);
50
+ }
51
+
52
+ /**
53
+ * Print an error message to stderr.
54
+ */
55
+ function logError(msg) {
56
+ process.stderr.write(`Error: ${msg}\n`);
57
+ }
58
+
59
+ // ─── Argument Parsing ────────────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Parse CLI arguments into a command name and a flags object.
63
+ *
64
+ * @param {string[]} argv - process.argv.slice(2)
65
+ * @returns {{ command: string, flags: Record<string, string>, positional: string[] }}
66
+ */
67
+ export function parseArgs(argv) {
68
+ const command = argv[0] || "help";
69
+ const flags = {};
70
+ const positional = [];
71
+
72
+ for (let i = 1; i < argv.length; i++) {
73
+ const arg = argv[i];
74
+ if (arg.startsWith("--")) {
75
+ const key = arg.slice(2);
76
+ const next = argv[i + 1];
77
+ if (next && !next.startsWith("--")) {
78
+ flags[key] = next;
79
+ i++;
80
+ } else {
81
+ flags[key] = "true";
82
+ }
83
+ } else {
84
+ positional.push(arg);
85
+ }
86
+ }
87
+
88
+ return { command, flags, positional };
89
+ }
90
+
91
+ // ─── Config Generation ───────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Generate config object from CLI flags.
95
+ *
96
+ * @param {Record<string, string>} flags
97
+ * @returns {Record<string, string>}
98
+ */
99
+ export function generateConfigFromFlags(flags) {
100
+ return {
101
+ DATABASE_URL: flags["db-url"] || "",
102
+ DASHSCOPE_API_KEY: flags["dashscope-api-key"] || "",
103
+ DINGTALK_CLIENT_ID: flags["dingtalk-client-id"] || "",
104
+ DINGTALK_CLIENT_SECRET: flags["dingtalk-client-secret"] || "",
105
+ DINGTALK_AGENT_ID: flags["dingtalk-agent-id"] || "",
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Check if all required config values are present.
111
+ *
112
+ * @param {Record<string, string>} config
113
+ * @returns {{ valid: boolean, missing: string[] }}
114
+ */
115
+ export function validateConfig(config) {
116
+ const required = [
117
+ "DATABASE_URL",
118
+ "DASHSCOPE_API_KEY",
119
+ "DINGTALK_CLIENT_ID",
120
+ "DINGTALK_CLIENT_SECRET",
121
+ "DINGTALK_AGENT_ID",
122
+ ];
123
+ const missing = required.filter((key) => !config[key]);
124
+ return { valid: missing.length === 0, missing };
125
+ }
126
+
127
+ // ─── Interactive Prompts ─────────────────────────────────────────────────────
128
+
129
+ /**
130
+ * Prompt user for a single value via readline.
131
+ *
132
+ * @param {string} question
133
+ * @param {import("node:readline").Interface} rl
134
+ * @returns {Promise<string>}
135
+ */
136
+ function prompt(question, rl) {
137
+ return new Promise((resolve) => {
138
+ rl.question(question, (answer) => {
139
+ resolve(answer.trim());
140
+ });
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Collect config interactively via readline prompts.
146
+ *
147
+ * @returns {Promise<Record<string, string>>}
148
+ */
149
+ export async function collectConfigInteractive() {
150
+ const rl = createInterface({
151
+ input: process.stdin,
152
+ output: process.stdout,
153
+ });
154
+
155
+ try {
156
+ log("\n📋 dialog-survey configuration\n");
157
+
158
+ const config = {
159
+ DATABASE_URL: await prompt(
160
+ "Database URL (PostgreSQL connection string): ",
161
+ rl,
162
+ ),
163
+ DASHSCOPE_API_KEY: await prompt("DashScope API Key (LLM): ", rl),
164
+ DINGTALK_CLIENT_ID: await prompt("DingTalk Client ID: ", rl),
165
+ DINGTALK_CLIENT_SECRET: await prompt("DingTalk Client Secret: ", rl),
166
+ DINGTALK_AGENT_ID: await prompt("DingTalk Agent ID: ", rl),
167
+ };
168
+
169
+ return config;
170
+ } finally {
171
+ rl.close();
172
+ }
173
+ }
174
+
175
+ // ─── Prerequisites ───────────────────────────────────────────────────────────
176
+
177
+ /**
178
+ * Check Node.js version >= 20.
179
+ * @returns {{ ok: boolean, message: string }}
180
+ */
181
+ export function checkNodeVersion() {
182
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
183
+ if (major < MIN_NODE_MAJOR) {
184
+ return {
185
+ ok: false,
186
+ message: `Node.js >= ${MIN_NODE_MAJOR} required (current: ${process.versions.node})`,
187
+ };
188
+ }
189
+ return { ok: true, message: `Node.js ${process.versions.node} ✓` };
190
+ }
191
+
192
+ /**
193
+ * Check if PostgreSQL is reachable using the given DATABASE_URL.
194
+ * @param {string} databaseUrl
195
+ * @returns {{ ok: boolean, message: string }}
196
+ */
197
+ export function checkPostgres(databaseUrl) {
198
+ if (!databaseUrl) {
199
+ return { ok: false, message: "DATABASE_URL not provided" };
200
+ }
201
+ try {
202
+ // Extract host and port from postgresql:// URL
203
+ const url = new URL(databaseUrl);
204
+ const host = url.hostname;
205
+ const port = url.port || "5432";
206
+ exec(`pg_isready -h ${host} -p ${port} -q`);
207
+ return { ok: true, message: `PostgreSQL ${host}:${port} ✓` };
208
+ } catch {
209
+ return {
210
+ ok: false,
211
+ message:
212
+ "PostgreSQL not reachable. Ensure PostgreSQL is running and DATABASE_URL is correct.",
213
+ };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Check if port 3001 is available.
219
+ * @returns {{ ok: boolean, message: string }}
220
+ */
221
+ export function checkPort() {
222
+ return new Promise((resolve) => {
223
+ const server = http.createServer();
224
+ server.once("error", (err) => {
225
+ if (err.code === "EADDRINUSE") {
226
+ resolve({
227
+ ok: false,
228
+ message: "Port 3001 is already in use. Stop the existing service first.",
229
+ });
230
+ } else {
231
+ resolve({ ok: false, message: `Port check failed: ${err.message}` });
232
+ }
233
+ });
234
+ server.once("listening", () => {
235
+ server.close(() => {
236
+ resolve({ ok: true, message: "Port 3001 available ✓" });
237
+ });
238
+ });
239
+ server.listen(3001, "0.0.0.0");
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Run all prerequisite checks.
245
+ * @param {string} databaseUrl
246
+ * @returns {Promise<{ ok: boolean, messages: string[] }>}
247
+ */
248
+ export async function checkPrerequisites(databaseUrl) {
249
+ const messages = [];
250
+ let allOk = true;
251
+
252
+ const nodeCheck = checkNodeVersion();
253
+ messages.push(nodeCheck.message);
254
+ if (!nodeCheck.ok) allOk = false;
255
+
256
+ const pgCheck = checkPostgres(databaseUrl);
257
+ messages.push(pgCheck.message);
258
+ if (!pgCheck.ok) allOk = false;
259
+
260
+ const portCheck = await checkPort();
261
+ messages.push(portCheck.message);
262
+ if (!portCheck.ok) allOk = false;
263
+
264
+ return { ok: allOk, messages };
265
+ }
266
+
267
+ // ─── PM2 Helpers ─────────────────────────────────────────────────────────────
268
+
269
+ /**
270
+ * Check if PM2 is installed and accessible.
271
+ * @returns {{ ok: boolean, message: string }}
272
+ */
273
+ export function checkPm2() {
274
+ try {
275
+ exec("pm2 --version");
276
+ return { ok: true, message: "PM2 found ✓" };
277
+ } catch {
278
+ return {
279
+ ok: false,
280
+ message:
281
+ "PM2 is not installed. Install it with: npm install -g pm2",
282
+ };
283
+ }
284
+ }
285
+
286
+ // ─── Health Check ────────────────────────────────────────────────────────────
287
+
288
+ /**
289
+ * Poll the health endpoint until it responds or timeout.
290
+ * @param {string} url
291
+ * @param {number} timeoutMs
292
+ * @param {number} intervalMs
293
+ * @returns {Promise<boolean>}
294
+ */
295
+ export function waitForHealth(
296
+ url = HEALTH_URL,
297
+ timeoutMs = HEALTH_TIMEOUT_MS,
298
+ intervalMs = HEALTH_POLL_INTERVAL_MS,
299
+ ) {
300
+ return new Promise((resolve) => {
301
+ const start = Date.now();
302
+
303
+ const poll = () => {
304
+ if (Date.now() - start > timeoutMs) {
305
+ resolve(false);
306
+ return;
307
+ }
308
+
309
+ const req = http.get(url, (res) => {
310
+ if (res.statusCode === 200) {
311
+ res.resume();
312
+ resolve(true);
313
+ } else {
314
+ res.resume();
315
+ setTimeout(poll, intervalMs);
316
+ }
317
+ });
318
+
319
+ req.on("error", () => {
320
+ setTimeout(poll, intervalMs);
321
+ });
322
+
323
+ req.setTimeout(2000, () => {
324
+ req.destroy();
325
+ setTimeout(poll, intervalMs);
326
+ });
327
+ };
328
+
329
+ poll();
330
+ });
331
+ }
332
+
333
+ // ─── .env Generation ─────────────────────────────────────────────────────────
334
+
335
+ /**
336
+ * Generate .env file content from config.
337
+ * @param {Record<string, string>} config
338
+ * @returns {string}
339
+ */
340
+ export function generateEnvContent(config) {
341
+ const lines = [
342
+ "# Generated by dialog-survey CLI",
343
+ "NODE_ENV=production",
344
+ "PORT=3001",
345
+ "HOST=0.0.0.0",
346
+ "",
347
+ `DATABASE_URL="${config.DATABASE_URL}"`,
348
+ `DASHSCOPE_API_KEY=${config.DASHSCOPE_API_KEY}`,
349
+ "",
350
+ `DINGTALK_CLIENT_ID=${config.DINGTALK_CLIENT_ID}`,
351
+ `DINGTALK_CLIENT_SECRET=${config.DINGTALK_CLIENT_SECRET}`,
352
+ `DINGTALK_AGENT_ID=${config.DINGTALK_AGENT_ID}`,
353
+ "",
354
+ "# Security",
355
+ `ENCRYPTION_KEY=${exec("node -e \"console.log(require('crypto').randomBytes(16).toString('hex'))\"")}`,
356
+ `ADMIN_API_KEY=${exec("node -e \"console.log(require('crypto').randomBytes(16).toString('hex'))\"")}`,
357
+ "",
358
+ "# Logging",
359
+ "LOG_LEVEL=info",
360
+ "",
361
+ ];
362
+ return lines.join("\n");
363
+ }
364
+
365
+ // ─── Commands ────────────────────────────────────────────────────────────────
366
+
367
+ /**
368
+ * Install command — interactive or non-interactive installation.
369
+ * @param {Record<string, string>} flags
370
+ */
371
+ export async function installCommand(flags) {
372
+ log("🚀 dialog-survey installer\n");
373
+
374
+ // Step 1: Collect config
375
+ let config;
376
+ const fromFlags = generateConfigFromFlags(flags);
377
+ const flagValidation = validateConfig(fromFlags);
378
+
379
+ if (flagValidation.valid) {
380
+ config = fromFlags;
381
+ log("Using configuration from CLI flags.\n");
382
+ } else if (flags.help) {
383
+ printInstallHelp();
384
+ return;
385
+ } else {
386
+ log(
387
+ `Non-interactive flags detected but missing: ${flagValidation.missing.join(", ")}`,
388
+ );
389
+ log("Falling back to interactive mode...\n");
390
+ config = await collectConfigInteractive();
391
+ const interactiveValidation = validateConfig(config);
392
+ if (!interactiveValidation.valid) {
393
+ logError(
394
+ `Missing required config: ${interactiveValidation.missing.join(", ")}`,
395
+ );
396
+ process.exitCode = 1;
397
+ return;
398
+ }
399
+ }
400
+
401
+ // Step 2: Check prerequisites
402
+ log("Checking prerequisites...");
403
+ const prereqs = await checkPrerequisites(config.DATABASE_URL);
404
+ for (const msg of prereqs.messages) {
405
+ log(` ${msg}`);
406
+ }
407
+ if (!prereqs.ok) {
408
+ logError("Prerequisites check failed. Fix the issues above and retry.");
409
+ process.exitCode = 1;
410
+ return;
411
+ }
412
+
413
+ // Step 3: Check PM2
414
+ const pm2Check = checkPm2();
415
+ log(` ${pm2Check.message}`);
416
+ if (!pm2Check.ok) {
417
+ logError(pm2Check.message);
418
+ process.exitCode = 1;
419
+ return;
420
+ }
421
+
422
+ // Step 4: Create install directory
423
+ log("\nSetting up installation directory...");
424
+ const sourceDir = join(__dirname, "..");
425
+ await mkdir(INSTALL_DIR, { recursive: true });
426
+ log(` Created ${INSTALL_DIR}`);
427
+
428
+ // Step 5: Copy package files
429
+ log("Copying package files...");
430
+ const filesToCopy = [
431
+ "package.json",
432
+ "package-lock.json",
433
+ "dist",
434
+ "prisma",
435
+ "ecosystem.config.cjs",
436
+ ];
437
+ for (const file of filesToCopy) {
438
+ const src = join(sourceDir, file);
439
+ const dest = join(INSTALL_DIR, file);
440
+ if (existsSync(src)) {
441
+ await cp(src, dest, { recursive: true });
442
+ log(` Copied ${file}`);
443
+ }
444
+ }
445
+
446
+ // Step 6: Generate .env
447
+ log("Generating .env file...");
448
+ const envContent = generateEnvContent(config);
449
+ await writeFile(join(INSTALL_DIR, ".env"), envContent, "utf-8");
450
+ log(` Created ${INSTALL_DIR}/.env`);
451
+
452
+ // Step 7: npm ci
453
+ log("Installing dependencies (npm ci --omit=dev)...");
454
+ try {
455
+ exec("npm ci --omit=dev --ignore-scripts", { cwd: INSTALL_DIR });
456
+ log(" Dependencies installed ✓");
457
+ } catch (err) {
458
+ logError(`npm ci failed: ${err.message}`);
459
+ process.exitCode = 1;
460
+ return;
461
+ }
462
+
463
+ // Step 8: prisma generate
464
+ log("Generating Prisma client...");
465
+ try {
466
+ exec("npx prisma generate", { cwd: INSTALL_DIR });
467
+ log(" Prisma client generated ✓");
468
+ } catch (err) {
469
+ logError(`prisma generate failed: ${err.message}`);
470
+ process.exitCode = 1;
471
+ return;
472
+ }
473
+
474
+ // Step 9: prisma db push
475
+ log("Pushing schema to database...");
476
+ try {
477
+ exec("npx prisma db push", { cwd: INSTALL_DIR });
478
+ log(" Schema pushed ✓");
479
+ } catch (err) {
480
+ logError(`prisma db push failed: ${err.message}`);
481
+ process.exitCode = 1;
482
+ return;
483
+ }
484
+
485
+ // Step 10: build
486
+ log("Building project...");
487
+ try {
488
+ exec("npm run build", { cwd: INSTALL_DIR });
489
+ log(" Build complete ✓");
490
+ } catch (err) {
491
+ logError(`Build failed: ${err.message}`);
492
+ process.exitCode = 1;
493
+ return;
494
+ }
495
+
496
+ // Step 11: PM2 start
497
+ log("Starting service via PM2...");
498
+ try {
499
+ exec(
500
+ `pm2 start ecosystem.config.cjs --name ${PM2_APP_NAME} --env production`,
501
+ { cwd: INSTALL_DIR },
502
+ );
503
+ log(" PM2 process started ✓");
504
+ } catch (err) {
505
+ logError(`PM2 start failed: ${err.message}`);
506
+ process.exitCode = 1;
507
+ return;
508
+ }
509
+
510
+ // Step 12: Wait for health
511
+ log("Waiting for health check...");
512
+ const healthy = await waitForHealth();
513
+ if (healthy) {
514
+ log(" Health check passed ✓");
515
+ log("\n✅ dialog-survey installed and running successfully!");
516
+ log(` Install dir: ${INSTALL_DIR}`);
517
+ log(` Health: ${HEALTH_URL}`);
518
+ log(` Logs: pm2 logs ${PM2_APP_NAME}`);
519
+ log(" Stop: npx dialog-survey stop");
520
+ log(" Status: npx dialog-survey status");
521
+ } else {
522
+ logError("Health check timed out after 30s. Service may still be starting.");
523
+ log(` Check logs: pm2 logs ${PM2_APP_NAME}`);
524
+ process.exitCode = 1;
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Uninstall command — remove installation.
530
+ * @param {Record<string, string>} flags
531
+ */
532
+ export async function uninstallCommand(flags) {
533
+ log("🗑️ dialog-survey uninstaller\n");
534
+
535
+ // Step 1: Stop PM2 process
536
+ const pm2Check = checkPm2();
537
+ if (pm2Check.ok) {
538
+ log("Stopping PM2 process...");
539
+ try {
540
+ exec(`pm2 delete ${PM2_APP_NAME}`);
541
+ log(" PM2 process removed ✓");
542
+ } catch {
543
+ log(" No PM2 process found (may already be removed)");
544
+ }
545
+ }
546
+
547
+ // Step 2: Optional DB removal
548
+ if (flags["remove-db"] === "true") {
549
+ log("\n⚠️ Database removal requested.");
550
+ log("To drop the database, run:");
551
+ log(' psql -c "DROP DATABASE IF EXISTS dialog_survey;"');
552
+ log(" (adjust connection parameters as needed)\n");
553
+ }
554
+
555
+ // Step 3: Remove install directory
556
+ if (existsSync(INSTALL_DIR)) {
557
+ log(`Removing ${INSTALL_DIR}...`);
558
+ await rm(INSTALL_DIR, { recursive: true, force: true });
559
+ log(" Installation directory removed ✓");
560
+ } else {
561
+ log(" No installation directory found.");
562
+ }
563
+
564
+ log("\n✅ dialog-survey uninstalled successfully.");
565
+ }
566
+
567
+ /**
568
+ * Start command — start service via PM2.
569
+ */
570
+ export async function startCommand() {
571
+ log("▶️ Starting dialog-survey...\n");
572
+
573
+ const pm2Check = checkPm2();
574
+ if (!pm2Check.ok) {
575
+ logError(pm2Check.message);
576
+ process.exitCode = 1;
577
+ return;
578
+ }
579
+
580
+ if (!existsSync(join(INSTALL_DIR, "ecosystem.config.cjs"))) {
581
+ logError(
582
+ `No installation found at ${INSTALL_DIR}. Run 'npx dialog-survey install' first.`,
583
+ );
584
+ process.exitCode = 1;
585
+ return;
586
+ }
587
+
588
+ try {
589
+ exec(
590
+ `pm2 start ecosystem.config.cjs --name ${PM2_APP_NAME} --env production`,
591
+ { cwd: INSTALL_DIR },
592
+ );
593
+ log("PM2 process started ✓");
594
+ } catch (err) {
595
+ logError(`PM2 start failed: ${err.message}`);
596
+ process.exitCode = 1;
597
+ return;
598
+ }
599
+
600
+ log("Waiting for health check...");
601
+ const healthy = await waitForHealth();
602
+ if (healthy) {
603
+ log("✅ dialog-survey is running.");
604
+ log(` Health: ${HEALTH_URL}`);
605
+ } else {
606
+ logError("Health check timed out. Check logs: pm2 logs dialog-survey");
607
+ process.exitCode = 1;
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Stop command — stop service via PM2.
613
+ */
614
+ export async function stopCommand() {
615
+ log("⏹ Stopping dialog-survey...\n");
616
+
617
+ const pm2Check = checkPm2();
618
+ if (!pm2Check.ok) {
619
+ logError(pm2Check.message);
620
+ process.exitCode = 1;
621
+ return;
622
+ }
623
+
624
+ try {
625
+ exec(`pm2 stop ${PM2_APP_NAME}`);
626
+ log("✅ dialog-survey stopped.");
627
+ } catch (err) {
628
+ logError(`PM2 stop failed: ${err.message}`);
629
+ process.exitCode = 1;
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Status command — show service status.
635
+ */
636
+ export async function statusCommand() {
637
+ log("📊 dialog-survey status\n");
638
+
639
+ // Check PM2 process
640
+ const pm2Check = checkPm2();
641
+ if (!pm2Check.ok) {
642
+ log(` PM2: ${pm2Check.message}`);
643
+ log(" Status: unknown\n");
644
+ return;
645
+ }
646
+
647
+ try {
648
+ const pm2Status = exec("pm2 jlist");
649
+ const processes = JSON.parse(pm2Status);
650
+ const app = processes.find((p) => p.name === PM2_APP_NAME);
651
+
652
+ if (!app) {
653
+ log(" PM2 process: not found");
654
+ log(" Status: stopped\n");
655
+ log(" Run 'npx dialog-survey start' to start the service.");
656
+ return;
657
+ }
658
+
659
+ const pm2StatusText = app.pm2_env?.status || "unknown";
660
+ log(` PM2 process: ${pm2StatusText}`);
661
+ log(` PID: ${app.pid || "N/A"}`);
662
+ log(` Uptime: ${app.pm2_env?.pm_uptime ? new Date(app.pm2_env.pm_uptime).toISOString() : "N/A"}`);
663
+ log(` Restarts: ${app.pm2_env?.restart_time ?? "N/A"}`);
664
+ log(` Memory: ${app.monit?.memory ? `${Math.round(app.monit.memory / 1024 / 1024)}MB` : "N/A"}`);
665
+
666
+ // Check health endpoint
667
+ const healthy = await waitForHealth(HEALTH_URL, 5000, 500);
668
+ if (healthy) {
669
+ log(` Health: ${HEALTH_URL} ✓`);
670
+ log("\n Status: running ✅");
671
+ } else {
672
+ log(` Health: ${HEALTH_URL} ✗`);
673
+ log("\n Status: degraded ⚠️");
674
+ }
675
+ } catch (err) {
676
+ logError(`Status check failed: ${err.message}`);
677
+ log(" Status: unknown\n");
678
+ }
679
+ }
680
+
681
+ // ─── Help ────────────────────────────────────────────────────────────────────
682
+
683
+ function printHelp() {
684
+ log(`
685
+ dialog-survey CLI — npx-based install and lifecycle management
686
+
687
+ Usage:
688
+ npx dialog-survey <command> [options]
689
+
690
+ Commands:
691
+ install Install dialog-survey (interactive or non-interactive)
692
+ uninstall Remove dialog-survey installation
693
+ start Start the dialog-survey service
694
+ stop Stop the dialog-survey service
695
+ status Show service status
696
+ help Show this help message
697
+
698
+ Install Options:
699
+ --db-url <url> PostgreSQL connection string
700
+ --dashscope-api-key <key> DashScope API key for LLM
701
+ --dingtalk-client-id <id> DingTalk client ID
702
+ --dingtalk-client-secret <sec> DingTalk client secret
703
+ --dingtalk-agent-id <id> DingTalk agent ID
704
+
705
+ Uninstall Options:
706
+ --remove-db Also print instructions to drop the database
707
+
708
+ Examples:
709
+ # Interactive install
710
+ npx dialog-survey install
711
+
712
+ # Non-interactive install
713
+ npx dialog-survey install \\
714
+ --db-url "postgresql://user:pass@localhost:5432/db" \\
715
+ --dashscope-api-key "sk-xxx" \\
716
+ --dingtalk-client-id "xxx" \\
717
+ --dingtalk-client-secret "xxx" \\
718
+ --dingtalk-agent-id "xxx"
719
+
720
+ # Lifecycle management
721
+ npx dialog-survey start
722
+ npx dialog-survey stop
723
+ npx dialog-survey status
724
+ npx dialog-survey uninstall
725
+ `);
726
+ }
727
+
728
+ function printInstallHelp() {
729
+ log(`
730
+ dialog-survey install — Install dialog-survey
731
+
732
+ Usage:
733
+ npx dialog-survey install [options]
734
+
735
+ Options:
736
+ --db-url <url> PostgreSQL connection string (required)
737
+ --dashscope-api-key <key> DashScope API key for LLM (required)
738
+ --dingtalk-client-id <id> DingTalk client ID (required)
739
+ --dingtalk-client-secret <sec> DingTalk client secret (required)
740
+ --dingtalk-agent-id <id> DingTalk agent ID (required)
741
+
742
+ If any required option is missing, the installer falls back to interactive mode.
743
+
744
+ Examples:
745
+ # Interactive — prompts for each value
746
+ npx dialog-survey install
747
+
748
+ # Non-interactive — all values via flags
749
+ npx dialog-survey install \\
750
+ --db-url "postgresql://user:pass@localhost:5432/db" \\
751
+ --dashscope-api-key "sk-xxx" \\
752
+ --dingtalk-client-id "xxx" \\
753
+ --dingtalk-client-secret "xxx" \\
754
+ --dingtalk-agent-id "xxx"
755
+ `);
756
+ }
757
+
758
+ // ─── Main ────────────────────────────────────────────────────────────────────
759
+
760
+ export async function main(argv) {
761
+ const { command, flags } = parseArgs(argv);
762
+
763
+ switch (command) {
764
+ case "install":
765
+ await installCommand(flags);
766
+ break;
767
+ case "uninstall":
768
+ await uninstallCommand(flags);
769
+ break;
770
+ case "start":
771
+ await startCommand();
772
+ break;
773
+ case "stop":
774
+ await stopCommand();
775
+ break;
776
+ case "status":
777
+ await statusCommand();
778
+ break;
779
+ default:
780
+ printHelp();
781
+ break;
782
+ }
783
+ }
784
+
785
+ // Run if invoked directly
786
+ const isMain =
787
+ process.argv[1] &&
788
+ (fileURLToPath(import.meta.resolve(__filename)) ===
789
+ fileURLToPath(import.meta.resolve(process.argv[1])) ||
790
+ process.argv[1].endsWith("cli.mjs"));
791
+
792
+ if (isMain) {
793
+ main(process.argv.slice(2));
794
+ }