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.
- package/CHANGELOG.md +95 -0
- package/README.md +242 -0
- package/VERSION +1 -0
- package/dist/api/admin-templates.d.ts +15 -0
- package/dist/api/admin-templates.d.ts.map +1 -0
- package/dist/api/admin-templates.js +611 -0
- package/dist/api/admin-templates.js.map +1 -0
- package/dist/api/analysis.d.ts +8 -0
- package/dist/api/analysis.d.ts.map +1 -0
- package/dist/api/analysis.js +106 -0
- package/dist/api/analysis.js.map +1 -0
- package/dist/api/health.d.ts +6 -0
- package/dist/api/health.d.ts.map +1 -0
- package/dist/api/health.js +116 -0
- package/dist/api/health.js.map +1 -0
- package/dist/api/plans.d.ts +3 -0
- package/dist/api/plans.d.ts.map +1 -0
- package/dist/api/plans.js +184 -0
- package/dist/api/plans.js.map +1 -0
- package/dist/api/templates.d.ts +9 -0
- package/dist/api/templates.d.ts.map +1 -0
- package/dist/api/templates.js +211 -0
- package/dist/api/templates.js.map +1 -0
- package/dist/core/graph.d.ts +7 -0
- package/dist/core/graph.d.ts.map +1 -0
- package/dist/core/graph.js +31 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/nodes/analyzing.d.ts +3 -0
- package/dist/core/nodes/analyzing.d.ts.map +1 -0
- package/dist/core/nodes/analyzing.js +38 -0
- package/dist/core/nodes/analyzing.js.map +1 -0
- package/dist/core/nodes/completed.d.ts +3 -0
- package/dist/core/nodes/completed.d.ts.map +1 -0
- package/dist/core/nodes/completed.js +8 -0
- package/dist/core/nodes/completed.js.map +1 -0
- package/dist/core/nodes/interviewing.d.ts +13 -0
- package/dist/core/nodes/interviewing.d.ts.map +1 -0
- package/dist/core/nodes/interviewing.js +130 -0
- package/dist/core/nodes/interviewing.js.map +1 -0
- package/dist/core/nodes/planning.d.ts +15 -0
- package/dist/core/nodes/planning.d.ts.map +1 -0
- package/dist/core/nodes/planning.js +40 -0
- package/dist/core/nodes/planning.js.map +1 -0
- package/dist/core/types/index.d.ts +69 -0
- package/dist/core/types/index.d.ts.map +1 -0
- package/dist/core/types/index.js +49 -0
- package/dist/core/types/index.js.map +1 -0
- package/dist/domains/interview/entities/interview.entity.d.ts +21 -0
- package/dist/domains/interview/entities/interview.entity.d.ts.map +1 -0
- package/dist/domains/interview/entities/interview.entity.js +20 -0
- package/dist/domains/interview/entities/interview.entity.js.map +1 -0
- package/dist/domains/interview/entities/message.entity.d.ts +15 -0
- package/dist/domains/interview/entities/message.entity.d.ts.map +1 -0
- package/dist/domains/interview/entities/message.entity.js +11 -0
- package/dist/domains/interview/entities/message.entity.js.map +1 -0
- package/dist/domains/interview/entities/response.entity.d.ts +17 -0
- package/dist/domains/interview/entities/response.entity.d.ts.map +1 -0
- package/dist/domains/interview/entities/response.entity.js +12 -0
- package/dist/domains/interview/entities/response.entity.js.map +1 -0
- package/dist/domains/interview/services/interview.domain.d.ts +8 -0
- package/dist/domains/interview/services/interview.domain.d.ts.map +1 -0
- package/dist/domains/interview/services/interview.domain.js +30 -0
- package/dist/domains/interview/services/interview.domain.js.map +1 -0
- package/dist/integrations/dingtalk/client.d.ts +32 -0
- package/dist/integrations/dingtalk/client.d.ts.map +1 -0
- package/dist/integrations/dingtalk/client.js +89 -0
- package/dist/integrations/dingtalk/client.js.map +1 -0
- package/dist/integrations/dingtalk/message-sender.d.ts +24 -0
- package/dist/integrations/dingtalk/message-sender.d.ts.map +1 -0
- package/dist/integrations/dingtalk/message-sender.js +81 -0
- package/dist/integrations/dingtalk/message-sender.js.map +1 -0
- package/dist/integrations/dingtalk/middleware.d.ts +4 -0
- package/dist/integrations/dingtalk/middleware.d.ts.map +1 -0
- package/dist/integrations/dingtalk/middleware.js +29 -0
- package/dist/integrations/dingtalk/middleware.js.map +1 -0
- package/dist/integrations/dingtalk/stream-client.d.ts +112 -0
- package/dist/integrations/dingtalk/stream-client.d.ts.map +1 -0
- package/dist/integrations/dingtalk/stream-client.js +305 -0
- package/dist/integrations/dingtalk/stream-client.js.map +1 -0
- package/dist/integrations/dingtalk/token-manager.d.ts +22 -0
- package/dist/integrations/dingtalk/token-manager.d.ts.map +1 -0
- package/dist/integrations/dingtalk/token-manager.js +109 -0
- package/dist/integrations/dingtalk/token-manager.js.map +1 -0
- package/dist/integrations/llm/alibaba.d.ts +10 -0
- package/dist/integrations/llm/alibaba.d.ts.map +1 -0
- package/dist/integrations/llm/alibaba.js +60 -0
- package/dist/integrations/llm/alibaba.js.map +1 -0
- package/dist/integrations/llm/base.d.ts +28 -0
- package/dist/integrations/llm/base.d.ts.map +1 -0
- package/dist/integrations/llm/base.js +2 -0
- package/dist/integrations/llm/base.js.map +1 -0
- package/dist/integrations/llm/volcengine.d.ts +12 -0
- package/dist/integrations/llm/volcengine.d.ts.map +1 -0
- package/dist/integrations/llm/volcengine.js +80 -0
- package/dist/integrations/llm/volcengine.js.map +1 -0
- package/dist/middleware/admin-auth.d.ts +3 -0
- package/dist/middleware/admin-auth.d.ts.map +1 -0
- package/dist/middleware/admin-auth.js +37 -0
- package/dist/middleware/admin-auth.js.map +1 -0
- package/dist/middleware/api-auth.d.ts +10 -0
- package/dist/middleware/api-auth.d.ts.map +1 -0
- package/dist/middleware/api-auth.js +30 -0
- package/dist/middleware/api-auth.js.map +1 -0
- package/dist/repositories/interview-state-mapper.d.ts +28 -0
- package/dist/repositories/interview-state-mapper.d.ts.map +1 -0
- package/dist/repositories/interview-state-mapper.js +32 -0
- package/dist/repositories/interview-state-mapper.js.map +1 -0
- package/dist/repositories/interview-state.repository.d.ts +34 -0
- package/dist/repositories/interview-state.repository.d.ts.map +1 -0
- package/dist/repositories/interview-state.repository.js +220 -0
- package/dist/repositories/interview-state.repository.js.map +1 -0
- package/dist/repositories/interview.repository.d.ts +29 -0
- package/dist/repositories/interview.repository.d.ts.map +1 -0
- package/dist/repositories/interview.repository.js +36 -0
- package/dist/repositories/interview.repository.js.map +1 -0
- package/dist/repositories/message.repository.d.ts +17 -0
- package/dist/repositories/message.repository.d.ts.map +1 -0
- package/dist/repositories/message.repository.js +37 -0
- package/dist/repositories/message.repository.js.map +1 -0
- package/dist/repositories/template.repository.d.ts +117 -0
- package/dist/repositories/template.repository.d.ts.map +1 -0
- package/dist/repositories/template.repository.js +280 -0
- package/dist/repositories/template.repository.js.map +1 -0
- package/dist/schemas/dimensions.d.ts +61 -0
- package/dist/schemas/dimensions.d.ts.map +1 -0
- package/dist/schemas/dimensions.js +44 -0
- package/dist/schemas/dimensions.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +217 -0
- package/dist/server.js.map +1 -0
- package/dist/services/analysis.service.d.ts +81 -0
- package/dist/services/analysis.service.d.ts.map +1 -0
- package/dist/services/analysis.service.js +229 -0
- package/dist/services/analysis.service.js.map +1 -0
- package/dist/services/analytics.service.d.ts +44 -0
- package/dist/services/analytics.service.d.ts.map +1 -0
- package/dist/services/analytics.service.js +222 -0
- package/dist/services/analytics.service.js.map +1 -0
- package/dist/services/asr.service.d.ts +9 -0
- package/dist/services/asr.service.d.ts.map +1 -0
- package/dist/services/asr.service.js +41 -0
- package/dist/services/asr.service.js.map +1 -0
- package/dist/services/audit-cleanup.service.d.ts +9 -0
- package/dist/services/audit-cleanup.service.d.ts.map +1 -0
- package/dist/services/audit-cleanup.service.js +27 -0
- package/dist/services/audit-cleanup.service.js.map +1 -0
- package/dist/services/batch-aggregation.service.d.ts +34 -0
- package/dist/services/batch-aggregation.service.d.ts.map +1 -0
- package/dist/services/batch-aggregation.service.js +91 -0
- package/dist/services/batch-aggregation.service.js.map +1 -0
- package/dist/services/conversation-engine.d.ts +16 -0
- package/dist/services/conversation-engine.d.ts.map +1 -0
- package/dist/services/conversation-engine.js +49 -0
- package/dist/services/conversation-engine.js.map +1 -0
- package/dist/services/dead-letter.service.d.ts +12 -0
- package/dist/services/dead-letter.service.d.ts.map +1 -0
- package/dist/services/dead-letter.service.js +24 -0
- package/dist/services/dead-letter.service.js.map +1 -0
- package/dist/services/followup.service.d.ts +18 -0
- package/dist/services/followup.service.d.ts.map +1 -0
- package/dist/services/followup.service.js +146 -0
- package/dist/services/followup.service.js.map +1 -0
- package/dist/services/interview-plan-base.service.d.ts +226 -0
- package/dist/services/interview-plan-base.service.d.ts.map +1 -0
- package/dist/services/interview-plan-base.service.js +217 -0
- package/dist/services/interview-plan-base.service.js.map +1 -0
- package/dist/services/interview-plan-members.service.d.ts +45 -0
- package/dist/services/interview-plan-members.service.d.ts.map +1 -0
- package/dist/services/interview-plan-members.service.js +237 -0
- package/dist/services/interview-plan-members.service.js.map +1 -0
- package/dist/services/interview-plan-send.service.d.ts +27 -0
- package/dist/services/interview-plan-send.service.d.ts.map +1 -0
- package/dist/services/interview-plan-send.service.js +250 -0
- package/dist/services/interview-plan-send.service.js.map +1 -0
- package/dist/services/interview-plan.service.d.ts +8 -0
- package/dist/services/interview-plan.service.d.ts.map +1 -0
- package/dist/services/interview-plan.service.js +8 -0
- package/dist/services/interview-plan.service.js.map +1 -0
- package/dist/services/llm-clustering.d.ts +16 -0
- package/dist/services/llm-clustering.d.ts.map +1 -0
- package/dist/services/llm-clustering.js +91 -0
- package/dist/services/llm-clustering.js.map +1 -0
- package/dist/services/prompt.service.d.ts +13 -0
- package/dist/services/prompt.service.d.ts.map +1 -0
- package/dist/services/prompt.service.js +151 -0
- package/dist/services/prompt.service.js.map +1 -0
- package/dist/services/report.service.d.ts +28 -0
- package/dist/services/report.service.d.ts.map +1 -0
- package/dist/services/report.service.js +169 -0
- package/dist/services/report.service.js.map +1 -0
- package/dist/services/stream-message-utils.d.ts +31 -0
- package/dist/services/stream-message-utils.d.ts.map +1 -0
- package/dist/services/stream-message-utils.js +80 -0
- package/dist/services/stream-message-utils.js.map +1 -0
- package/dist/services/stream-message.service.d.ts +29 -0
- package/dist/services/stream-message.service.d.ts.map +1 -0
- package/dist/services/stream-message.service.js +184 -0
- package/dist/services/stream-message.service.js.map +1 -0
- package/dist/services/template-dimension.service.d.ts +21 -0
- package/dist/services/template-dimension.service.d.ts.map +1 -0
- package/dist/services/template-dimension.service.js +9 -0
- package/dist/services/template-dimension.service.js.map +1 -0
- package/dist/utils/db.d.ts +10 -0
- package/dist/utils/db.d.ts.map +1 -0
- package/dist/utils/db.js +21 -0
- package/dist/utils/db.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/markdown.d.ts +2 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/utils/markdown.js +39 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/pii-anonymizer.d.ts +2 -0
- package/dist/utils/pii-anonymizer.d.ts.map +1 -0
- package/dist/utils/pii-anonymizer.js +13 -0
- package/dist/utils/pii-anonymizer.js.map +1 -0
- package/dist/utils/retry.d.ts +8 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +28 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/security.d.ts +26 -0
- package/dist/utils/security.d.ts.map +1 -0
- package/dist/utils/security.js +99 -0
- package/dist/utils/security.js.map +1 -0
- package/package.json +80 -0
- package/prisma/schema.prisma +228 -0
- package/public/css/admin.css +73 -0
- package/public/js/alpine.min.js +2670 -0
- package/public/js/htmx.min.js +3422 -0
- package/scripts/cli.mjs +794 -0
package/scripts/cli.mjs
ADDED
|
@@ -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
|
+
}
|