dsh-d365-kit 0.34.1 → 0.34.3

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 (75) hide show
  1. package/CHANGELOG.md +318 -308
  2. package/CHANGELOG.zh-CN.md +317 -307
  3. package/INSTALL.md +94 -94
  4. package/INSTALL.zh-CN.md +93 -93
  5. package/LICENSE +21 -21
  6. package/README.md +336 -327
  7. package/README.zh-CN.md +336 -327
  8. package/client.js +1818 -1818
  9. package/cordis.patch.yml +44 -44
  10. package/docs/en/architecture.md +172 -172
  11. package/docs/en/checks.md +184 -184
  12. package/docs/en/installation.md +151 -151
  13. package/docs/en/permissions.md +161 -161
  14. package/docs/en/scenarios.md +202 -202
  15. package/docs/en/tools.md +665 -665
  16. package/docs/en/troubleshooting.md +131 -131
  17. package/docs/zh/architecture.md +172 -172
  18. package/docs/zh/checks.md +184 -184
  19. package/docs/zh/installation.md +151 -151
  20. package/docs/zh/permissions.md +161 -161
  21. package/docs/zh/scenarios.md +202 -202
  22. package/docs/zh/tools.md +665 -665
  23. package/docs/zh/troubleshooting.md +131 -131
  24. package/examples/prompts.en.md +206 -206
  25. package/examples/prompts.zh-CN.md +206 -206
  26. package/examples/waivers.example.json +20 -20
  27. package/icon.svg +6 -6
  28. package/index.js +139 -139
  29. package/lib/auth.js +181 -181
  30. package/lib/client.js +496 -496
  31. package/lib/code-check.js +549 -549
  32. package/lib/config.js +323 -323
  33. package/lib/contract-history.js +49 -49
  34. package/lib/contract.js +583 -583
  35. package/lib/csharp-check.js +493 -493
  36. package/lib/data-migrate.js +1318 -1318
  37. package/lib/data-policy.js +78 -78
  38. package/lib/error-codes.js +80 -80
  39. package/lib/gitlab.js +161 -161
  40. package/lib/health.js +205 -205
  41. package/lib/migrate.js +732 -732
  42. package/lib/preflight.js +116 -116
  43. package/lib/refs.js +191 -191
  44. package/lib/resources.js +320 -320
  45. package/lib/retention-defaults.js +17 -17
  46. package/lib/retention.js +384 -384
  47. package/lib/runtime.js +232 -232
  48. package/lib/settings-http.js +512 -512
  49. package/lib/settings-store.js +371 -371
  50. package/lib/snapshot.js +149 -149
  51. package/lib/tool-code-check.js +133 -133
  52. package/lib/tool-configure.js +169 -169
  53. package/lib/tool-contract.js +376 -376
  54. package/lib/tool-data-sync.js +559 -559
  55. package/lib/tool-data-write.js +203 -203
  56. package/lib/tool-data.js +282 -282
  57. package/lib/tool-diff.js +484 -484
  58. package/lib/tool-environments.js +106 -106
  59. package/lib/tool-flows.js +540 -540
  60. package/lib/tool-health.js +127 -127
  61. package/lib/tool-layers.js +497 -497
  62. package/lib/tool-metadata.js +417 -417
  63. package/lib/tool-migrate.js +102 -102
  64. package/lib/tool-refs.js +206 -206
  65. package/lib/tool-reports.js +299 -299
  66. package/lib/tool-sync.js +430 -430
  67. package/lib/tool-write.js +563 -563
  68. package/lib/waivers.js +83 -83
  69. package/lib/write-actions.js +1374 -1374
  70. package/lib/write-policy.js +199 -199
  71. package/lib/write-store.js +167 -167
  72. package/locale/en.json +68 -68
  73. package/locale/zh.json +68 -68
  74. package/package.json +2 -2
  75. package/skills/d365-troubleshooting/SKILL.md +115 -115
package/lib/runtime.js CHANGED
@@ -1,232 +1,232 @@
1
- /**
2
- * Shared plumbing for the metadata tools: environment selection, workspace
3
- * artifact writing, and the plain-text formatting used in tool results.
4
- *
5
- * Every tool result is "summary + path": the model gets a short, readable
6
- * digest, and the complete data lands in a file it can read in full when the
7
- * question needs it. That keeps results small without losing evidence.
8
- */
9
- import { mkdir, writeFile } from 'node:fs/promises'
10
- import { join, resolve } from 'node:path'
11
- import { DataverseClient } from './client.js'
12
- import { D365Error } from './errors.js'
13
- import { credentialStatus, dataDirectory } from './config.js'
14
- import { KEY_FIELDS, COMPARE_FIELDS, rowKey } from './resources.js'
15
-
16
- /** Rows kept in the inline digest before the rest is left to the artifact. */
17
- export const INLINE_ROWS = 12
18
-
19
- /**
20
- * Pick and validate the environments a call targets.
21
- * @param config - normalized config.
22
- * @param args - tool arguments (`environment`, `environments`, `allEnvironments`).
23
- * @returns the ordered list of environments to query.
24
- */
25
- export function pickEnvironments(config, args = {}) {
26
- const available = config.environments
27
- const keys = Object.keys(available)
28
- if (!keys.length) {
29
- throw new D365Error('还没有配置任何 D365 环境', {
30
- code: 'no-environments',
31
- hint: '在插件配置的 environments 下加入环境(url + 凭据环境变量名),见 cordis.patch.yml 的注释示例。',
32
- })
33
- }
34
-
35
- if (args.allEnvironments) return keys.map((key) => available[key])
36
-
37
- const requested = []
38
- if (Array.isArray(args.environments)) requested.push(...args.environments)
39
- if (args.environment) requested.push(args.environment)
40
-
41
- const unique = [...new Set(requested.filter((key) => typeof key === 'string' && key.trim()))]
42
- if (!unique.length) {
43
- const fallback = config.defaultEnvironment
44
- if (!fallback) throw new D365Error('未指定环境,且没有默认环境', { code: 'no-default' })
45
- return [available[fallback]]
46
- }
47
-
48
- const missing = unique.filter((key) => !available[key])
49
- if (missing.length) {
50
- throw new D365Error(`未知环境:${missing.join(', ')}`, {
51
- code: 'unknown-environment',
52
- hint: `可用环境:${keys.join(', ')}`,
53
- })
54
- }
55
- return unique.map((key) => available[key])
56
- }
57
-
58
- /**
59
- * Create a client for one environment.
60
- * @param env - normalized environment.
61
- * @param config - normalized config (for timeout).
62
- * @returns a read-only client.
63
- */
64
- export function makeClient(env, config) {
65
- return new DataverseClient(env, { timeoutMs: config.timeoutMs })
66
- }
67
-
68
- /**
69
- * Read the organization record plus the platform version.
70
- *
71
- * No `$select`: real deployments reject columns this plugin assumed were
72
- * universal (`version`, then `organizationstructure` — both 0x80060888 "Could
73
- * not find a property named ..."), and an unfiltered row is the only shape that
74
- * works everywhere. The record is optional — the version endpoint alone proves
75
- * the connection — so a rejected organization read is not fatal on its own.
76
- * @returns `{ ...organizationProperties, version? }`
77
- */
78
- export async function readOrganization(client, signal) {
79
- let record = {}
80
- let failure
81
- try {
82
- const page = await client.getPaged('organizations', {}, 1, signal)
83
- record = page.records[0] ?? {}
84
- } catch (error) {
85
- failure = error
86
- }
87
-
88
- const version = await client.readVersion(signal)
89
- if (!Object.keys(record).length && !version && failure) throw failure
90
- return { ...record, version }
91
- }
92
-
93
- /**
94
- * Report environments and whether they can authenticate, without calling the
95
- * network unless `check` is set.
96
- * @returns a printable status report.
97
- */
98
- export async function environmentStatus(config, options = {}) {
99
- const report = []
100
- for (const env of Object.values(config.environments)) {
101
- const status = credentialStatus(env)
102
- const row = {
103
- env,
104
- ...status,
105
- reachable: undefined,
106
- error: undefined,
107
- }
108
- if (options.check) {
109
- try {
110
- const client = makeClient(env, config)
111
- row.organization = await readOrganization(client, options.signal)
112
- row.reachable = true
113
- } catch (error) {
114
- row.reachable = false
115
- row.error = error.message
116
- }
117
- }
118
- report.push(row)
119
- }
120
- return report
121
- }
122
-
123
- /** Resolve the directory artifacts are written to. */
124
- export function artifactRoot(config) {
125
- return join(dataDirectory(config), '.d365', 'query')
126
- }
127
-
128
- /**
129
- * Write one artifact and return its absolute path.
130
- *
131
- * Absolute matters: the artifact lives under the config directory (usually
132
- * `~/Documents/deepseek-harness/...`), while the plugin's cwd is the profile
133
- * directory. A path relative to that cwd (`../../../Documents/...`) cannot be
134
- * opened from the session workspace the model actually reads files in.
135
- * @param name - filename without extension.
136
- * @param payload - JSON-serialisable data.
137
- */
138
- export async function writeArtifact(config, name, payload) {
139
- const root = artifactRoot(config)
140
- await mkdir(root, { recursive: true })
141
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
142
- const file = join(root, `${stamp}-${name.replace(/[^a-zA-Z0-9._-]+/g, '-')}.json`)
143
- await writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
144
- return resolve(file).replace(/\\/g, '/')
145
- }
146
-
147
- /** Truncate a cell so one wide field cannot blow up the digest. */
148
- function cell(value, width = 40) {
149
- if (value === undefined || value === null) return ''
150
- const text = typeof value === 'object' ? JSON.stringify(value) : String(value)
151
- return text.length > width ? `${text.slice(0, width - 1)}…` : text
152
- }
153
-
154
- /**
155
- * Render rows as a fixed-width markdown table; omitted columns are counted.
156
- * @param rows - records already flattened.
157
- * @param columns - column names in display order.
158
- */
159
- export function formatTable(rows, columns, options = {}) {
160
- const width = options.width ?? 40
161
- const limit = options.limit ?? INLINE_ROWS
162
- const shown = rows.slice(0, limit)
163
- const header = `| ${columns.map((c) => cell(c, 24)).join(' | ')} |`
164
- const divider = `| ${columns.map(() => '---').join(' | ')} |`
165
- const body = shown.map((row) => `| ${columns.map((c) => cell(row?.[c], width)).join(' | ')} |`)
166
- const lines = [header, divider, ...body]
167
- if (rows.length > shown.length) lines.push(`| … 其余 ${rows.length - shown.length} 行见文件 |`)
168
- return lines.join('\n')
169
- }
170
-
171
- /**
172
- * Compare rows from several environments and describe what differs.
173
- * Presence is always compared; a few resources also compare meaningful fields.
174
- * @param resource - resource key, for key/compare field lookup.
175
- * @param perEnv - array of `{ env, rows }`.
176
- * @returns `{ lines, missing, extra, changed }` where `lines` is printable text.
177
- */
178
- export function compareEnvironments(resource, perEnv) {
179
- if (perEnv.length < 2) return null
180
-
181
- const keysByEnv = new Map()
182
- const rowsByEnv = new Map()
183
- for (const { env, rows } of perEnv) {
184
- const map = new Map()
185
- for (const row of rows) map.set(rowKey(resource, row), row)
186
- keysByEnv.set(env.key, map)
187
- rowsByEnv.set(env.key, rows)
188
- }
189
-
190
- const envKeys = perEnv.map((entry) => entry.env.key)
191
- const allKeys = [...new Set(perEnv.flatMap((entry) => [...keysByEnv.get(entry.env.key).keys()]))]
192
-
193
- const onlyIn = new Map()
194
- const changed = []
195
- const compareFields = COMPARE_FIELDS[resource] ?? []
196
-
197
- for (const key of allKeys) {
198
- const present = envKeys.filter((envKey) => keysByEnv.get(envKey).has(key))
199
- if (present.length !== envKeys.length) {
200
- const missingIn = envKeys.filter((envKey) => !present.includes(envKey))
201
- const label = `${key} 仅存在于 ${present.join('/')},缺失于 ${missingIn.join('/')}`
202
- onlyIn.set(key, { present, missingIn, label })
203
- }
204
- if (compareFields.length && present.length > 1) {
205
- const diffs = []
206
- for (const field of compareFields) {
207
- const values = present.map((envKey) => String(keysByEnv.get(envKey).get(key)?.[field] ?? ''))
208
- if (new Set(values).size > 1) {
209
- diffs.push(`${field}: ${present.map((envKey, i) => `${envKey}=${values[i] || '∅'}`).join(' / ')}`)
210
- }
211
- }
212
- if (diffs.length) changed.push(`${key} — ${diffs.join(';')}`)
213
- }
214
- }
215
-
216
- const lines = []
217
- if (!onlyIn.size && !changed.length) {
218
- lines.push(`跨环境对比(${envKeys.join(' / ')}):按 ${KEY_FIELDS[resource] ?? '名称'} 对齐后无差异。`)
219
- } else {
220
- if (onlyIn.size) {
221
- lines.push(`跨环境差异:${onlyIn.size} 个对象的环境分布不一致`)
222
- for (const entry of [...onlyIn.values()].slice(0, 15)) lines.push(`- ${entry.label}`)
223
- if (onlyIn.size > 15) lines.push(`- … 其余 ${onlyIn.size - 15} 条见文件`)
224
- }
225
- if (changed.length) {
226
- lines.push(`字段值差异:${changed.length} 个对象`)
227
- for (const line of changed.slice(0, 15)) lines.push(`- ${line}`)
228
- if (changed.length > 15) lines.push(`- … 其余 ${changed.length - 15} 条见文件`)
229
- }
230
- }
231
- return { lines, onlyIn, changed, allKeys }
232
- }
1
+ /**
2
+ * Shared plumbing for the metadata tools: environment selection, workspace
3
+ * artifact writing, and the plain-text formatting used in tool results.
4
+ *
5
+ * Every tool result is "summary + path": the model gets a short, readable
6
+ * digest, and the complete data lands in a file it can read in full when the
7
+ * question needs it. That keeps results small without losing evidence.
8
+ */
9
+ import { mkdir, writeFile } from 'node:fs/promises'
10
+ import { join, resolve } from 'node:path'
11
+ import { DataverseClient } from './client.js'
12
+ import { D365Error } from './errors.js'
13
+ import { credentialStatus, dataDirectory } from './config.js'
14
+ import { KEY_FIELDS, COMPARE_FIELDS, rowKey } from './resources.js'
15
+
16
+ /** Rows kept in the inline digest before the rest is left to the artifact. */
17
+ export const INLINE_ROWS = 12
18
+
19
+ /**
20
+ * Pick and validate the environments a call targets.
21
+ * @param config - normalized config.
22
+ * @param args - tool arguments (`environment`, `environments`, `allEnvironments`).
23
+ * @returns the ordered list of environments to query.
24
+ */
25
+ export function pickEnvironments(config, args = {}) {
26
+ const available = config.environments
27
+ const keys = Object.keys(available)
28
+ if (!keys.length) {
29
+ throw new D365Error('还没有配置任何 D365 环境', {
30
+ code: 'no-environments',
31
+ hint: '在插件配置的 environments 下加入环境(url + 凭据环境变量名),见 cordis.patch.yml 的注释示例。',
32
+ })
33
+ }
34
+
35
+ if (args.allEnvironments) return keys.map((key) => available[key])
36
+
37
+ const requested = []
38
+ if (Array.isArray(args.environments)) requested.push(...args.environments)
39
+ if (args.environment) requested.push(args.environment)
40
+
41
+ const unique = [...new Set(requested.filter((key) => typeof key === 'string' && key.trim()))]
42
+ if (!unique.length) {
43
+ const fallback = config.defaultEnvironment
44
+ if (!fallback) throw new D365Error('未指定环境,且没有默认环境', { code: 'no-default' })
45
+ return [available[fallback]]
46
+ }
47
+
48
+ const missing = unique.filter((key) => !available[key])
49
+ if (missing.length) {
50
+ throw new D365Error(`未知环境:${missing.join(', ')}`, {
51
+ code: 'unknown-environment',
52
+ hint: `可用环境:${keys.join(', ')}`,
53
+ })
54
+ }
55
+ return unique.map((key) => available[key])
56
+ }
57
+
58
+ /**
59
+ * Create a client for one environment.
60
+ * @param env - normalized environment.
61
+ * @param config - normalized config (for timeout).
62
+ * @returns a read-only client.
63
+ */
64
+ export function makeClient(env, config) {
65
+ return new DataverseClient(env, { timeoutMs: config.timeoutMs })
66
+ }
67
+
68
+ /**
69
+ * Read the organization record plus the platform version.
70
+ *
71
+ * No `$select`: real deployments reject columns this plugin assumed were
72
+ * universal (`version`, then `organizationstructure` — both 0x80060888 "Could
73
+ * not find a property named ..."), and an unfiltered row is the only shape that
74
+ * works everywhere. The record is optional — the version endpoint alone proves
75
+ * the connection — so a rejected organization read is not fatal on its own.
76
+ * @returns `{ ...organizationProperties, version? }`
77
+ */
78
+ export async function readOrganization(client, signal) {
79
+ let record = {}
80
+ let failure
81
+ try {
82
+ const page = await client.getPaged('organizations', {}, 1, signal)
83
+ record = page.records[0] ?? {}
84
+ } catch (error) {
85
+ failure = error
86
+ }
87
+
88
+ const version = await client.readVersion(signal)
89
+ if (!Object.keys(record).length && !version && failure) throw failure
90
+ return { ...record, version }
91
+ }
92
+
93
+ /**
94
+ * Report environments and whether they can authenticate, without calling the
95
+ * network unless `check` is set.
96
+ * @returns a printable status report.
97
+ */
98
+ export async function environmentStatus(config, options = {}) {
99
+ const report = []
100
+ for (const env of Object.values(config.environments)) {
101
+ const status = credentialStatus(env)
102
+ const row = {
103
+ env,
104
+ ...status,
105
+ reachable: undefined,
106
+ error: undefined,
107
+ }
108
+ if (options.check) {
109
+ try {
110
+ const client = makeClient(env, config)
111
+ row.organization = await readOrganization(client, options.signal)
112
+ row.reachable = true
113
+ } catch (error) {
114
+ row.reachable = false
115
+ row.error = error.message
116
+ }
117
+ }
118
+ report.push(row)
119
+ }
120
+ return report
121
+ }
122
+
123
+ /** Resolve the directory artifacts are written to. */
124
+ export function artifactRoot(config) {
125
+ return join(dataDirectory(config), '.d365', 'query')
126
+ }
127
+
128
+ /**
129
+ * Write one artifact and return its absolute path.
130
+ *
131
+ * Absolute matters: the artifact lives under the config directory (usually
132
+ * `~/Documents/deepseek-harness/...`), while the plugin's cwd is the profile
133
+ * directory. A path relative to that cwd (`../../../Documents/...`) cannot be
134
+ * opened from the session workspace the model actually reads files in.
135
+ * @param name - filename without extension.
136
+ * @param payload - JSON-serialisable data.
137
+ */
138
+ export async function writeArtifact(config, name, payload) {
139
+ const root = artifactRoot(config)
140
+ await mkdir(root, { recursive: true })
141
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
142
+ const file = join(root, `${stamp}-${name.replace(/[^a-zA-Z0-9._-]+/g, '-')}.json`)
143
+ await writeFile(file, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
144
+ return resolve(file).replace(/\\/g, '/')
145
+ }
146
+
147
+ /** Truncate a cell so one wide field cannot blow up the digest. */
148
+ function cell(value, width = 40) {
149
+ if (value === undefined || value === null) return ''
150
+ const text = typeof value === 'object' ? JSON.stringify(value) : String(value)
151
+ return text.length > width ? `${text.slice(0, width - 1)}…` : text
152
+ }
153
+
154
+ /**
155
+ * Render rows as a fixed-width markdown table; omitted columns are counted.
156
+ * @param rows - records already flattened.
157
+ * @param columns - column names in display order.
158
+ */
159
+ export function formatTable(rows, columns, options = {}) {
160
+ const width = options.width ?? 40
161
+ const limit = options.limit ?? INLINE_ROWS
162
+ const shown = rows.slice(0, limit)
163
+ const header = `| ${columns.map((c) => cell(c, 24)).join(' | ')} |`
164
+ const divider = `| ${columns.map(() => '---').join(' | ')} |`
165
+ const body = shown.map((row) => `| ${columns.map((c) => cell(row?.[c], width)).join(' | ')} |`)
166
+ const lines = [header, divider, ...body]
167
+ if (rows.length > shown.length) lines.push(`| … 其余 ${rows.length - shown.length} 行见文件 |`)
168
+ return lines.join('\n')
169
+ }
170
+
171
+ /**
172
+ * Compare rows from several environments and describe what differs.
173
+ * Presence is always compared; a few resources also compare meaningful fields.
174
+ * @param resource - resource key, for key/compare field lookup.
175
+ * @param perEnv - array of `{ env, rows }`.
176
+ * @returns `{ lines, missing, extra, changed }` where `lines` is printable text.
177
+ */
178
+ export function compareEnvironments(resource, perEnv) {
179
+ if (perEnv.length < 2) return null
180
+
181
+ const keysByEnv = new Map()
182
+ const rowsByEnv = new Map()
183
+ for (const { env, rows } of perEnv) {
184
+ const map = new Map()
185
+ for (const row of rows) map.set(rowKey(resource, row), row)
186
+ keysByEnv.set(env.key, map)
187
+ rowsByEnv.set(env.key, rows)
188
+ }
189
+
190
+ const envKeys = perEnv.map((entry) => entry.env.key)
191
+ const allKeys = [...new Set(perEnv.flatMap((entry) => [...keysByEnv.get(entry.env.key).keys()]))]
192
+
193
+ const onlyIn = new Map()
194
+ const changed = []
195
+ const compareFields = COMPARE_FIELDS[resource] ?? []
196
+
197
+ for (const key of allKeys) {
198
+ const present = envKeys.filter((envKey) => keysByEnv.get(envKey).has(key))
199
+ if (present.length !== envKeys.length) {
200
+ const missingIn = envKeys.filter((envKey) => !present.includes(envKey))
201
+ const label = `${key} 仅存在于 ${present.join('/')},缺失于 ${missingIn.join('/')}`
202
+ onlyIn.set(key, { present, missingIn, label })
203
+ }
204
+ if (compareFields.length && present.length > 1) {
205
+ const diffs = []
206
+ for (const field of compareFields) {
207
+ const values = present.map((envKey) => String(keysByEnv.get(envKey).get(key)?.[field] ?? ''))
208
+ if (new Set(values).size > 1) {
209
+ diffs.push(`${field}: ${present.map((envKey, i) => `${envKey}=${values[i] || '∅'}`).join(' / ')}`)
210
+ }
211
+ }
212
+ if (diffs.length) changed.push(`${key} — ${diffs.join(';')}`)
213
+ }
214
+ }
215
+
216
+ const lines = []
217
+ if (!onlyIn.size && !changed.length) {
218
+ lines.push(`跨环境对比(${envKeys.join(' / ')}):按 ${KEY_FIELDS[resource] ?? '名称'} 对齐后无差异。`)
219
+ } else {
220
+ if (onlyIn.size) {
221
+ lines.push(`跨环境差异:${onlyIn.size} 个对象的环境分布不一致`)
222
+ for (const entry of [...onlyIn.values()].slice(0, 15)) lines.push(`- ${entry.label}`)
223
+ if (onlyIn.size > 15) lines.push(`- … 其余 ${onlyIn.size - 15} 条见文件`)
224
+ }
225
+ if (changed.length) {
226
+ lines.push(`字段值差异:${changed.length} 个对象`)
227
+ for (const line of changed.slice(0, 15)) lines.push(`- ${line}`)
228
+ if (changed.length > 15) lines.push(`- … 其余 ${changed.length - 15} 条见文件`)
229
+ }
230
+ }
231
+ return { lines, onlyIn, changed, allKeys }
232
+ }