dialog-survey 1.13.3 → 1.13.5

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/VERSION CHANGED
@@ -1 +1 @@
1
- 1.13.3
1
+ 1.13.5
@@ -10,6 +10,9 @@ export interface StartupConfigProblem {
10
10
  * Kept pure (env in, findings out) so the checks are unit-testable without
11
11
  * booting a server, and so `/health/ready` and the startup log cannot drift
12
12
  * apart — they answer the same question.
13
+ *
14
+ * The function itself is only the fold: per-area logic lives in the `CHECKS`
15
+ * above so that no single function carries all of the branching.
13
16
  */
14
17
  export declare function collectStartupConfigProblems(env?: NodeJS.ProcessEnv): StartupConfigProblem[];
15
18
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"startup-config-check.d.ts","sourceRoot":"","sources":["../../../src/utils/startup-config-check.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,oBAAoB,EAAE,CA4CxB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CAiBlF"}
1
+ {"version":3,"file":"startup-config-check.d.ts","sourceRoot":"","sources":["../../../src/utils/startup-config-check.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,oBAAoB;IACnC,yCAAyC;IACzC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB;AA4GD;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,oBAAoB,EAAE,CAExB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,IAAI,CAiBlF"}
@@ -22,57 +22,115 @@ function looksLikePlaceholder(value) {
22
22
  const normalized = value.trim().toLowerCase();
23
23
  return PLACEHOLDER_MARKERS.some((marker) => normalized.includes(marker));
24
24
  }
25
+ /** First non-empty value among `names`, or `''` when none is set. */
26
+ function firstSet(env, names) {
27
+ for (const name of names) {
28
+ const value = env[name];
29
+ if (value)
30
+ return value;
31
+ }
32
+ return '';
33
+ }
25
34
  /**
26
- * Everything that would make the running process unable to do its job.
35
+ * The LLM key, under any of the names this project has shipped with.
27
36
  *
28
- * Kept pure (env in, findings out) so the checks are unit-testable without
29
- * booting a server, and so `/health/ready` and the startup log cannot drift
30
- * apart — they answer the same question.
37
+ * Checked as three outcomes rather than two so the message can say what is
38
+ * wrong: a key that is present but still the installer's placeholder is a
39
+ * different operator mistake from a key that was never filled in.
31
40
  */
32
- export function collectStartupConfigProblems(env = process.env) {
33
- const problems = [];
34
- const llmKey = env['LLM_API_KEY'] || env['VOLCENGINE_API_KEY'] || env['ANTHROPIC_AUTH_TOKEN'] || '';
35
- if (!llmKey) {
36
- problems.push({
37
- variables: ['LLM_API_KEY'],
38
- message: 'no LLM API key configured — every interview turn will fail',
39
- });
41
+ function checkLlmKey(env) {
42
+ const key = firstSet(env, ['LLM_API_KEY', 'VOLCENGINE_API_KEY', 'ANTHROPIC_AUTH_TOKEN']);
43
+ if (!key) {
44
+ return [
45
+ {
46
+ variables: ['LLM_API_KEY'],
47
+ message: 'no LLM API key configured — every interview turn will fail',
48
+ },
49
+ ];
40
50
  }
41
- else if (looksLikePlaceholder(llmKey)) {
42
- problems.push({
43
- variables: ['LLM_API_KEY'],
44
- message: 'LLM API key is still a placeholder value — every interview turn will fail',
45
- });
51
+ if (looksLikePlaceholder(key)) {
52
+ return [
53
+ {
54
+ variables: ['LLM_API_KEY'],
55
+ message: 'LLM API key is still a placeholder value — every interview turn will fail',
56
+ },
57
+ ];
46
58
  }
47
- const dingTalk = dingTalkCredentialsFromEnv(env);
48
- const agentId = env['DINGTALK_AGENT_ID'] || '';
49
- const missingDingTalk = [];
50
- if (!dingTalk.appKey)
51
- missingDingTalk.push('DINGTALK_CLIENT_ID');
52
- if (!dingTalk.appSecret)
53
- missingDingTalk.push('DINGTALK_CLIENT_SECRET');
54
- if (!agentId)
55
- missingDingTalk.push('DINGTALK_AGENT_ID');
56
- if (missingDingTalk.length > 0) {
57
- problems.push({
58
- variables: missingDingTalk,
59
- message: 'DingTalk Stream credentials incomplete — no messages will be received',
60
- });
59
+ return [];
60
+ }
61
+ /** Which DingTalk Stream variables are required, paired with how to read them. */
62
+ const DINGTALK_REQUIRED = [
63
+ {
64
+ variable: 'DINGTALK_CLIENT_ID',
65
+ read: (env) => dingTalkCredentialsFromEnv(env).appKey,
66
+ },
67
+ {
68
+ variable: 'DINGTALK_CLIENT_SECRET',
69
+ read: (env) => dingTalkCredentialsFromEnv(env).appSecret,
70
+ },
71
+ { variable: 'DINGTALK_AGENT_ID', read: (env) => env['DINGTALK_AGENT_ID'] ?? '' },
72
+ ];
73
+ /**
74
+ * DingTalk Stream credentials, in one of two states: incomplete, or complete but
75
+ * still carrying placeholders.
76
+ *
77
+ * Built from `DINGTALK_REQUIRED` rather than three hand-written `if`s, so adding
78
+ * a required variable is a table row instead of another branch. That is what
79
+ * brought this module's highest cyclomatic complexity under the threshold.
80
+ */
81
+ function checkDingTalk(env) {
82
+ const missing = DINGTALK_REQUIRED.filter(({ read }) => !read(env)).map(({ variable }) => variable);
83
+ if (missing.length > 0) {
84
+ return [
85
+ {
86
+ variables: missing,
87
+ message: 'DingTalk Stream credentials incomplete — no messages will be received',
88
+ },
89
+ ];
61
90
  }
62
- else if (looksLikePlaceholder(dingTalk.appKey) || looksLikePlaceholder(dingTalk.appSecret)) {
63
- problems.push({
91
+ const { appKey, appSecret } = dingTalkCredentialsFromEnv(env);
92
+ const hasPlaceholder = looksLikePlaceholder(appKey) || looksLikePlaceholder(appSecret);
93
+ if (!hasPlaceholder)
94
+ return [];
95
+ return [
96
+ {
64
97
  variables: ['DINGTALK_CLIENT_ID', 'DINGTALK_CLIENT_SECRET'],
65
98
  message: 'DingTalk credentials are still placeholder values — no messages will be received',
66
- });
67
- }
68
- const adminHash = env['ADMIN_PASSWORD_HASH'] || '';
69
- if (!adminHash) {
70
- problems.push({
99
+ },
100
+ ];
101
+ }
102
+ /** The admin login. A missing hash makes the whole admin UI unusable. */
103
+ function checkAdminLogin(env) {
104
+ if (env['ADMIN_PASSWORD_HASH'])
105
+ return [];
106
+ return [
107
+ {
71
108
  variables: ['ADMIN_USERNAME', 'ADMIN_PASSWORD_HASH'],
72
109
  message: 'admin login is not configured — the admin UI cannot be used',
73
- });
74
- }
75
- return problems;
110
+ },
111
+ ];
112
+ }
113
+ /**
114
+ * Every independent area of configuration, in the order an operator should read
115
+ * them. Each entry reports zero or more problems; the aggregator only flattens.
116
+ */
117
+ const CHECKS = [
118
+ checkLlmKey,
119
+ checkDingTalk,
120
+ checkAdminLogin,
121
+ ];
122
+ /**
123
+ * Everything that would make the running process unable to do its job.
124
+ *
125
+ * Kept pure (env in, findings out) so the checks are unit-testable without
126
+ * booting a server, and so `/health/ready` and the startup log cannot drift
127
+ * apart — they answer the same question.
128
+ *
129
+ * The function itself is only the fold: per-area logic lives in the `CHECKS`
130
+ * above so that no single function carries all of the branching.
131
+ */
132
+ export function collectStartupConfigProblems(env = process.env) {
133
+ return CHECKS.flatMap((check) => check(env));
76
134
  }
77
135
  /**
78
136
  * Log the summary. One aggregated `error` line naming every missing piece, plus
@@ -1 +1 @@
1
- {"version":3,"file":"startup-config-check.js","sourceRoot":"","sources":["../../../src/utils/startup-config-check.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"startup-config-check.js","sourceRoot":"","sources":["../../../src/utils/startup-config-check.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dialog-survey",
3
- "version": "1.13.3",
3
+ "version": "1.13.5",
4
4
  "type": "module",
5
5
  "description": "AI-powered survey dialog bot that conducts async multi-turn conversations via DingTalk",
6
6
  "repository": {
@@ -20,7 +20,9 @@ set -euo pipefail
20
20
  # The exit code of release-it is passed through unchanged, so CI or a caller can
21
21
  # tell "nothing to release" from "the release failed".
22
22
 
23
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ # No SCRIPT_DIR here on purpose: releasing must operate on the repository containing the
24
+ # CURRENT directory (see below), so the script's own location is never consulted. The
25
+ # variable was previously assigned and never read, which shellcheck reports as SC2034.
24
26
 
25
27
  # Release the repository containing the CURRENT directory rather than the one
26
28
  # this script lives in. Releasing is irreversible (annotated tag + GitHub
@@ -106,12 +108,44 @@ run_post_release_check() {
106
108
 
107
109
  # A silently-dead hook yields a commit without VERSION or AGENTS.md. The
108
110
  # changelog plugin's omission is equally invisible, so CHANGELOG.md is included.
111
+ #
112
+ # BUT presence-in-commit is only a PROXY for correctness, and it over-reports in
113
+ # one real case: the fan-out hooks are idempotent. `sync-version.cjs` writes
114
+ # AGENTS.md only when the header version actually differs, so if the previous
115
+ # commit already carried `(vX.Y.Z)` for the version being released — which happens
116
+ # whenever the version line is updated by hand, as a doc-maintenance commit does —
117
+ # the hook finds nothing to change, AGENTS.md legitimately does not appear in the
118
+ # release commit, and the proxy fires on a tree that is already correct.
119
+ # Measured on v1.13.3: VERSION, package.json and the AGENTS.md header all read
120
+ # 1.13.3, the tree was clean and the tag was pushed, yet the check reported
121
+ # "release commit is missing AGENTS.md" and told the operator to roll back a
122
+ # perfectly good release.
123
+ # So the proxy is applied only where it adds information: a file that IS in the
124
+ # commit is fine; a file that is NOT must still hold the right version on disk,
125
+ # which step 1 above already asserts for VERSION/package.json/AGENTS.md. What the
126
+ # proxy still catches — and step 1 cannot — is CHANGELOG.md, which has no version
127
+ # token to compare and is written only by the changelog plugin.
109
128
  RELEASE_FILES="$(git show --name-only --format= "$HEAD_AFTER" | LC_ALL=C sort | tr '\n' ' ')"
129
+ # NOTE the shape of this loop. A `case` cannot express "this pattern first, then
130
+ # the catch-all", because its arms are tried in order and `*` always matches — a
131
+ # `CHANGELOG.md)` arm placed after `*)` is unreachable, and one placed before it is
132
+ # still skipped whenever the file is ABSENT, which is the only case that arm exists
133
+ # to catch. Measured: written as a `case` this reported "unchanged … verified on
134
+ # disk" for a release commit with no CHANGELOG.md at all. The presence test is
135
+ # therefore explicit, with the per-file policy stated alongside it.
110
136
  for expected in VERSION AGENTS.md package.json CHANGELOG.md; do
111
137
  case " $RELEASE_FILES " in
112
- *" $expected "*) ;;
113
- *) fail_post "release commit is missing $expected (found: ${RELEASE_FILES:-none})" ;;
138
+ *" $expected "*) continue ;;
114
139
  esac
140
+ if [ "$expected" = "CHANGELOG.md" ]; then
141
+ # CHANGELOG.md has no on-disk version to compare, so a dead changelog plugin is
142
+ # invisible to the other three assertions: presence is the only evidence it ran.
143
+ fail_post "release commit is missing CHANGELOG.md (found: ${RELEASE_FILES:-none})"
144
+ fi
145
+ # Absent from the commit is acceptable only because step 1 above verified the
146
+ # on-disk content. Record that reasoning so a genuinely dead hook is still visible
147
+ # as an anomaly rather than silently accepted.
148
+ echo "release: $expected unchanged by the release hooks (already at v$POST_VERSION) — verified on disk" >&2
115
149
  done
116
150
 
117
151
  # 3. The working tree must be clean, so the tag matches what is on disk.
@@ -156,7 +190,13 @@ ROLLBACK
156
190
  # argument, when present, overrides TAGS_BEFORE so the tag-set logic can be driven.
157
191
  if [ "${RELEASE_VERIFY_ONLY:-0}" = "1" ]; then
158
192
  TAGS_BEFORE="${1:-$(git tag -l | LC_ALL=C sort)}"
159
- HEAD_BEFORE="$(git rev-parse HEAD)"
193
+ # RELEASE_VERIFY_HEAD_BEFORE (optional) lets a test drive the "did a release commit
194
+ # get created?" branch. Without it HEAD_BEFORE is read from the current HEAD, so
195
+ # HEAD_AFTER can never differ and every invocation reports "no release commit was
196
+ # created" — which is correct for the no-release case but leaves the release-commit
197
+ # assertions unexercisable, including the ones that guard a committed-versus-absent
198
+ # file. Must be a resolvable commit in the fixture repository.
199
+ HEAD_BEFORE="${RELEASE_VERIFY_HEAD_BEFORE:-$(git rev-parse HEAD)}"
160
200
  run_post_release_check
161
201
  exit $?
162
202
  fi
@@ -260,7 +300,10 @@ EFFECTIVE_HOOKS_PATH="$(git config --get core.hooksPath 2>/dev/null || true)"
260
300
  # run - measured, and it is why the first version of this guard silently did
261
301
  # nothing. The default hooks directory is derived from the git dir instead, with
262
302
  # core.hooksPath suppressed for that one query.
263
- DEFAULT_HOOKS_PATH="$(GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0= \
303
+ # SC1007: `GIT_CONFIG_VALUE_0=` with a trailing space reads as an empty value but is
304
+ # easy to mistake for a continuation artefact. The quotes state the intent explicitly:
305
+ # the value IS the empty string, which is what suppresses core.hooksPath for this query.
306
+ DEFAULT_HOOKS_PATH="$(GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0="" \
264
307
  git rev-parse --git-path hooks 2>/dev/null || true)"
265
308
 
266
309
  if [ -n "$EFFECTIVE_HOOKS_PATH" ] && [ "$EFFECTIVE_HOOKS_PATH" != "$DEFAULT_HOOKS_PATH" ]; then