@aopslab/domain-cli-docman 0.1.4

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/src/main.ts ADDED
@@ -0,0 +1,1747 @@
1
+ #!/usr/bin/env node
2
+ import './env-bootstrap.js'
3
+ import { DEFAULT_TENANT_AS_UUID_STRING, sanitizeUrl } from '@aopslab/xf-core'
4
+ import type {
5
+ DocmanCliCommandDescriptor,
6
+ DocmanCliManifestArtifact,
7
+ DocmanCliProjection,
8
+ } from '@aopslab/domain-tooling-docman'
9
+ import type { DomainRequest, DomainRouteManifestEntry } from '@aopslab/domain-host-plugin-docman'
10
+ import { createDocmanCliCommandSurface } from './command-surface.js'
11
+ import {
12
+ DEFAULT_DOCMAN_SCOPE_ID,
13
+ applyDocmanRuntimeEnv,
14
+ getDefaultDocmanSqlitePath,
15
+ getDefaultDocmanSqliteRepoUrl,
16
+ inferDocmanRepoDialect,
17
+ readDocmanCliStoredConfig,
18
+ resolveDocmanRuntimeConfig,
19
+ writeDocmanCliStoredConfig,
20
+ } from './config-host.js'
21
+ import { applyDocmanPgSchema } from '@aopslab/domain-pg-bootstrap-docman'
22
+ import type {
23
+ DocmanCliStoredConfig,
24
+ DocmanRuntimeMode,
25
+ LoadedDocmanCliStoredConfig,
26
+ } from './config-host.js'
27
+ import type { DocmanExecutionMode, DocmanOperationSpec, OptionValue, ParsedArgv, RuntimeContext } from './cli-types.js'
28
+ import { buildDocmanHostRegistrationManifest } from './host-registration.js'
29
+ import { ensureDocmanSqliteSchemaReady } from './sqlite-bootstrap.js'
30
+ import { existsSync, readFileSync } from 'node:fs'
31
+ import os from 'node:os'
32
+ import { resolve } from 'node:path'
33
+ type DocmanHostRuntime = {
34
+ plugin: {
35
+ manifest: {
36
+ routes: DomainRouteManifestEntry[]
37
+ }
38
+ execute: (input: {
39
+ request: DomainRequest
40
+ match: { route: DomainRouteManifestEntry; params: Record<string, string> }
41
+ }) => Promise<unknown>
42
+ }
43
+ routesByOperationId: ReadonlyMap<string, DomainRouteManifestEntry>
44
+ }
45
+
46
+ type DocmanCliRuntimeModules = {
47
+ tooling: typeof import('@aopslab/domain-tooling-docman')
48
+ hostPlugin: typeof import('@aopslab/domain-host-plugin-docman')
49
+ }
50
+
51
+ const AOPS_CONFIG_FILENAME = 'aops.config.json'
52
+ const HOST_CONFIG_FILENAME = 'host.config.json'
53
+ const DIRECT_OPERATION_COMMANDS = new Set(['op', 'operation', 'run'])
54
+ const RESERVED_NON_OPERATION_COMMANDS = new Set(['help', 'tools', 'ops', 'manifest', 'tool', 'invoke', 'config', 'init', 'setup'])
55
+ const GENERATED_OPERATION_JSON_ARGS = new Set(['input', 'data', 'patch', 'filter', 'options', 'config'])
56
+ const HOST_CONFIG_ENV_KEYS = ['DOCMAN_CLI_HOST_CONFIG_PATH', 'HOST_NODE_CONFIG'] as const
57
+ const DOCMAN_ENV_SANITIZE_KEYS = [
58
+ 'TENANT_ID',
59
+ 'DOCMAN_SCOPE_ID',
60
+ 'AOPS_PG_URL',
61
+ 'DOCMAN_REPO_URL',
62
+ 'DOCMAN_PG_URL',
63
+ 'DOCMAN_SQLITE_URL',
64
+ 'DOCUMENT_REPO_URL',
65
+ 'DOCUMENT_GROUP_REPO_URL',
66
+ 'DOCUMENT_VERSION_REPO_URL',
67
+ 'SECTION_REPO_URL',
68
+ 'PAGE_REPO_URL',
69
+ 'PAGE_VERSION_REPO_URL',
70
+ 'DOCUMENT_SECTION_LINK_REPO_URL',
71
+ 'SECTION_PAGE_LINK_REPO_URL',
72
+ 'SNIPPET_REPO_URL',
73
+ 'PAGE_SNIPPET_LINK_REPO_URL',
74
+ 'EMBED_REPO_URL',
75
+ 'PAGE_EMBED_LINK_REPO_URL',
76
+ ] as const
77
+
78
+ let docmanCliRuntimeModulesPromise: Promise<DocmanCliRuntimeModules> | null = null
79
+ let docmanHostRuntimePromise: Promise<DocmanHostRuntime> | null = null
80
+ let docmanCliPackageVersionCache: string | null = null
81
+
82
+ async function getDocmanCliRuntimeModules(): Promise<DocmanCliRuntimeModules> {
83
+ if (docmanCliRuntimeModulesPromise) return docmanCliRuntimeModulesPromise
84
+
85
+ docmanCliRuntimeModulesPromise = (async () => {
86
+ const [tooling, hostPlugin] = await Promise.all([
87
+ import('@aopslab/domain-tooling-docman'),
88
+ import('@aopslab/domain-host-plugin-docman'),
89
+ ])
90
+ return { tooling, hostPlugin }
91
+ })()
92
+
93
+ return docmanCliRuntimeModulesPromise
94
+ }
95
+
96
+ function normalizeOptionKey(raw: string): string {
97
+ return raw.trim().replace(/^--+/, '').toLowerCase()
98
+ }
99
+
100
+ function pushOptionValue(target: Record<string, OptionValue>, key: string, value: OptionValue): void {
101
+ const existing = target[key]
102
+ if (existing === undefined) {
103
+ target[key] = value
104
+ return
105
+ }
106
+ if (Array.isArray(existing)) {
107
+ existing.push(String(value))
108
+ target[key] = existing
109
+ return
110
+ }
111
+ target[key] = [String(existing), String(value)]
112
+ }
113
+
114
+ function parseArgv(argv: string[]): ParsedArgv {
115
+ const positionals: string[] = []
116
+ const options: Record<string, OptionValue> = {}
117
+
118
+ for (let index = 0; index < argv.length; index += 1) {
119
+ const token = argv[index]
120
+ if (token === '--') continue
121
+
122
+ if (!token.startsWith('--')) {
123
+ positionals.push(token)
124
+ continue
125
+ }
126
+
127
+ const eqAt = token.indexOf('=')
128
+ if (eqAt > -1) {
129
+ const key = normalizeOptionKey(token.slice(0, eqAt))
130
+ const value = token.slice(eqAt + 1)
131
+ pushOptionValue(options, key, value)
132
+ continue
133
+ }
134
+
135
+ const key = normalizeOptionKey(token)
136
+ const next = argv[index + 1]
137
+ if (!next || next.startsWith('--')) {
138
+ pushOptionValue(options, key, true)
139
+ continue
140
+ }
141
+ pushOptionValue(options, key, next)
142
+ index += 1
143
+ }
144
+
145
+ return { positionals, options }
146
+ }
147
+
148
+ function getOptionValue(parsed: ParsedArgv, key: string): OptionValue | undefined {
149
+ return parsed.options[key]
150
+ }
151
+
152
+ function getStringOption(parsed: ParsedArgv, key: string): string | undefined {
153
+ const value = getOptionValue(parsed, key)
154
+ if (Array.isArray(value)) {
155
+ const last = value[value.length - 1]
156
+ return String(last).trim() || undefined
157
+ }
158
+ if (value === undefined || value === null || value === true || value === false) return undefined
159
+ const normalized = String(value).trim()
160
+ return normalized.length > 0 ? normalized : undefined
161
+ }
162
+
163
+ function getBooleanFromString(value: string, fallback: boolean): boolean {
164
+ const normalized = String(value).trim().toLowerCase()
165
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
166
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
167
+ return fallback
168
+ }
169
+
170
+ function getBooleanOption(parsed: ParsedArgv, key: string, fallback = false): boolean {
171
+ const value = getOptionValue(parsed, key)
172
+ if (value === undefined) return fallback
173
+ if (Array.isArray(value)) return getBooleanFromString(value[value.length - 1], fallback)
174
+ if (typeof value === 'boolean') return value
175
+ return getBooleanFromString(value, fallback)
176
+ }
177
+
178
+ function getBooleanFromEnv(keys: readonly string[], fallback = false): boolean {
179
+ for (const key of keys) {
180
+ const value = normalizeNonEmptyString(process.env[key])
181
+ if (!value) continue
182
+ return getBooleanFromString(value, fallback)
183
+ }
184
+ return fallback
185
+ }
186
+
187
+ function getScopeIdOption(parsed: ParsedArgv): string | undefined {
188
+ return getStringOption(parsed, 'scope-id') ?? getStringOption(parsed, 'workspace-id')
189
+ }
190
+
191
+ function normalizeDocmanScopeId(value: unknown): string | undefined {
192
+ const normalized = normalizeNonEmptyString(value)
193
+ if (!normalized) return undefined
194
+ return normalized.toLowerCase() === 'default' ? DEFAULT_DOCMAN_SCOPE_ID : normalized
195
+ }
196
+
197
+ function areHooksDisabled(parsed: ParsedArgv): boolean {
198
+ if (getOptionValue(parsed, 'no-hook') !== undefined) {
199
+ return getBooleanOption(parsed, 'no-hook', false)
200
+ }
201
+ return getBooleanFromEnv(['DOCMAN_CLI_NO_HOOK', 'DOCMAN_CLI_DISABLE_HOOKS'], false)
202
+ }
203
+
204
+ function requireStringOption(parsed: ParsedArgv, key: string, label: string): string {
205
+ const value = getStringOption(parsed, key)
206
+ if (!value) {
207
+ throw new Error(`missing_required_option:${label}`)
208
+ }
209
+ return value
210
+ }
211
+
212
+ function parseJsonInput(raw: unknown, label: string): unknown {
213
+ if (raw === undefined || raw === null) return undefined
214
+ if (typeof raw !== 'string') return raw
215
+ const trimmed = raw.trim()
216
+ if (!trimmed) return undefined
217
+ if (trimmed.startsWith('@')) {
218
+ const filePath = trimmed.slice(1).trim()
219
+ if (!filePath) throw new Error(`invalid_json_${label}`)
220
+ const content = readFileSync(filePath, 'utf8')
221
+ return JSON.parse(content)
222
+ }
223
+ return JSON.parse(trimmed)
224
+ }
225
+
226
+ function normalizeIdentifier(value: string): string {
227
+ return String(value ?? '').trim().toLowerCase().replace(/^\/+/, '')
228
+ }
229
+
230
+ function normalizeOperationIdentifier(raw: string): string {
231
+ const normalized = raw
232
+ .trim()
233
+ .toLowerCase()
234
+ .replace(/^\/+/, '')
235
+ .replace(/^operations\//, '')
236
+ .replace(/^api\/docman\/operations\//, '')
237
+ .replace(/\//g, '.')
238
+ .replace(/\.+/g, '.')
239
+ if (!normalized) return normalized
240
+ return normalized.startsWith('docman.') ? normalized : `docman.${normalized}`
241
+ }
242
+
243
+ function toKebabCase(value: string): string {
244
+ return value
245
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
246
+ .replace(/[_\s.]+/g, '-')
247
+ .toLowerCase()
248
+ }
249
+
250
+ function toOptionKeyCandidatesFromArgName(argName: string): string[] {
251
+ const kebab = toKebabCase(argName)
252
+ const compact = kebab.replace(/-/g, '')
253
+ const lowered = argName.toLowerCase()
254
+ return [...new Set([kebab, compact, lowered])]
255
+ }
256
+
257
+ function getOptionValueByAnyKey(parsed: ParsedArgv, keys: string[]): OptionValue | undefined {
258
+ for (const key of keys) {
259
+ const value = getOptionValue(parsed, key)
260
+ if (value !== undefined) return value
261
+ }
262
+ return undefined
263
+ }
264
+
265
+ function splitCsvStrings(values: string[]): string[] {
266
+ return values
267
+ .flatMap((value) => value.split(','))
268
+ .map((value) => value.trim())
269
+ .filter(Boolean)
270
+ }
271
+
272
+ function tryParseBooleanToken(value: string): boolean | undefined {
273
+ const normalized = value.trim().toLowerCase()
274
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
275
+ if (['0', 'false', 'no', 'off'].includes(normalized)) return false
276
+ return undefined
277
+ }
278
+
279
+ function shouldTreatAsArrayArg(argName: string): boolean {
280
+ return argName.endsWith('Ids') || argName.endsWith('Paths')
281
+ }
282
+
283
+ function coerceOperationArgValue(raw: OptionValue, argName: string): unknown {
284
+ if (typeof raw === 'boolean') return raw
285
+
286
+ const argKey = argName.trim().toLowerCase()
287
+ const asArray = Array.isArray(raw) ? raw.map((item) => String(item)) : null
288
+ if (asArray) {
289
+ if (shouldTreatAsArrayArg(argName)) return splitCsvStrings(asArray)
290
+ if (GENERATED_OPERATION_JSON_ARGS.has(argKey) && asArray.length > 0) {
291
+ return parseJsonInput(asArray[asArray.length - 1], argName)
292
+ }
293
+ return asArray.map((item) => {
294
+ const boolValue = tryParseBooleanToken(item)
295
+ return boolValue ?? item
296
+ })
297
+ }
298
+
299
+ const value = String(raw).trim()
300
+ if (!value) return value
301
+ if (GENERATED_OPERATION_JSON_ARGS.has(argKey)) {
302
+ return parseJsonInput(value, argName)
303
+ }
304
+ if (shouldTreatAsArrayArg(argName)) {
305
+ return splitCsvStrings([value])
306
+ }
307
+ const boolValue = tryParseBooleanToken(value)
308
+ if (boolValue !== undefined) return boolValue
309
+ return value
310
+ }
311
+
312
+ async function resolveOperationIdentifier(
313
+ rawIdentifier: string,
314
+ operationMap: ReadonlyMap<string, DocmanOperationSpec>,
315
+ ): Promise<string | null> {
316
+ const { tooling } = await getDocmanCliRuntimeModules()
317
+
318
+ const resolvedByToolId = tooling.resolveDocmanOperationIdByToolId(rawIdentifier, { refresh: true })
319
+ if (resolvedByToolId) {
320
+ const normalizedByToolId = normalizeOperationIdentifier(resolvedByToolId)
321
+ if (operationMap.has(normalizedByToolId)) return normalizedByToolId
322
+ }
323
+
324
+ const normalized = normalizeOperationIdentifier(rawIdentifier)
325
+ if (normalized && operationMap.has(normalized)) return normalized
326
+
327
+ const spec = tooling.getDocmanOperationSpecById(rawIdentifier, { refresh: true })
328
+ if (!spec) return null
329
+ const normalizedFromSpec = normalizeOperationIdentifier(spec.operationId)
330
+ return operationMap.has(normalizedFromSpec) ? normalizedFromSpec : null
331
+ }
332
+
333
+ async function resolveGeneratedOperationId(
334
+ parsed: ParsedArgv,
335
+ operationMap: ReadonlyMap<string, DocmanOperationSpec>,
336
+ ): Promise<string | null> {
337
+ const command = parsed.positionals[0]?.toLowerCase()
338
+ const subcommand = parsed.positionals[1]?.toLowerCase()
339
+ if (!command) return null
340
+
341
+ if (DIRECT_OPERATION_COMMANDS.has(command)) {
342
+ const identifier = parsed.positionals[1] ?? getStringOption(parsed, 'id')
343
+ if (!identifier) throw new Error('missing_required_option:<operation-id-or-tool-id>')
344
+ const resolved = await resolveOperationIdentifier(identifier, operationMap)
345
+ if (!resolved) throw new Error(`unknown_docman_operation_identifier:${identifier}`)
346
+ return resolved
347
+ }
348
+
349
+ if (RESERVED_NON_OPERATION_COMMANDS.has(command)) return null
350
+
351
+ if (command.includes('.')) {
352
+ const direct = await resolveOperationIdentifier(command, operationMap)
353
+ if (direct) return direct
354
+ }
355
+
356
+ if (subcommand) {
357
+ const nested = normalizeOperationIdentifier(`${command}.${subcommand}`)
358
+ if (operationMap.has(nested)) return nested
359
+ }
360
+
361
+ const topLevel = normalizeOperationIdentifier(command)
362
+ if (operationMap.has(topLevel)) return topLevel
363
+
364
+ return null
365
+ }
366
+
367
+ function buildGeneratedOperationInput(
368
+ operation: DocmanOperationSpec,
369
+ parsed: ParsedArgv,
370
+ runtime: RuntimeContext,
371
+ ): Record<string, unknown> {
372
+ const explicitInput = parseJsonInput(getStringOption(parsed, 'input'), 'input')
373
+ if (explicitInput !== undefined) {
374
+ if (!explicitInput || typeof explicitInput !== 'object' || Array.isArray(explicitInput)) {
375
+ throw new Error('invalid_json_input:expected_object')
376
+ }
377
+ return explicitInput as Record<string, unknown>
378
+ }
379
+
380
+ const input: Record<string, unknown> = {}
381
+ for (const arg of operation.args) {
382
+ const rawValue = getOptionValueByAnyKey(parsed, toOptionKeyCandidatesFromArgName(arg.name))
383
+ if (rawValue === undefined) {
384
+ if (arg.name === 'scopeId') {
385
+ input.scopeId = runtime.scopeId
386
+ continue
387
+ }
388
+ if (arg.name === 'workspaceId') {
389
+ input.workspaceId = runtime.workspaceId
390
+ continue
391
+ }
392
+ if (!arg.optional) {
393
+ throw new Error(`missing_required_option:--${toKebabCase(arg.name)}`)
394
+ }
395
+ continue
396
+ }
397
+ input[arg.name] = coerceOperationArgValue(rawValue, arg.name)
398
+ }
399
+ return input
400
+ }
401
+
402
+ function printJson(value: unknown): void {
403
+ process.stdout.write(`${JSON.stringify(value === undefined ? null : value, null, 2)}\n`)
404
+ }
405
+
406
+ function printVersion(): void {
407
+ process.stdout.write(`${getDocmanCliPackageVersion()}\n`)
408
+ }
409
+
410
+ function printHelpLines(lines: string[]): void {
411
+ process.stdout.write(`${lines.join('\n')}\n`)
412
+ }
413
+
414
+ function appendTextSection(target: string[], title: string, lines: string[]): void {
415
+ const normalized = lines.map((line) => String(line ?? '').trimEnd()).filter(Boolean)
416
+ if (normalized.length === 0) return
417
+ if (target.length > 0 && target[target.length - 1] !== '') target.push('')
418
+ target.push(`${title}:`)
419
+ for (const line of normalized) {
420
+ target.push(` ${line}`)
421
+ }
422
+ }
423
+
424
+ function toStringList(value: unknown): string[] {
425
+ if (!Array.isArray(value)) return []
426
+ return value
427
+ .map((entry) => String(entry ?? '').trim())
428
+ .filter(Boolean)
429
+ }
430
+
431
+ function renderCommandDescriptor(descriptor: DocmanCliCommandDescriptor): string[] {
432
+ const lines = [descriptor.title]
433
+ for (const section of descriptor.sections) {
434
+ appendTextSection(lines, section.title, section.lines)
435
+ }
436
+ return lines
437
+ }
438
+
439
+ function buildStandaloneInitHelpLines(): string[] {
440
+ return [
441
+ 'docman init',
442
+ '',
443
+ 'Purpose:',
444
+ ' Initialize standalone Docman runtime config for the local operator flow.',
445
+ '',
446
+ 'Usage:',
447
+ ' docman init [--runtime-mode single-user] [--repo-url <url>] [--scope-id <id>] [--json]',
448
+ '',
449
+ 'Notes:',
450
+ ' - Defaults to single-user mode.',
451
+ ' - Defaults repo URL to ~/.aops/docman.aops.sqlite.',
452
+ ` - Defaults scope id to global default scope ${DEFAULT_DOCMAN_SCOPE_ID}.`,
453
+ ' - Ensures SQLite schema readiness when the resolved repo dialect is SQLite.',
454
+ ]
455
+ }
456
+
457
+ function buildStandaloneSetupHelpLines(): string[] {
458
+ return [
459
+ 'docman setup runtime',
460
+ '',
461
+ 'Purpose:',
462
+ ' Update standalone Docman runtime storage and bootstrap state.',
463
+ '',
464
+ 'Usage:',
465
+ ' docman setup runtime [--runtime-mode single-user|multi-user] [--repo-url <url>] [--scope-id <id>] [--json]',
466
+ '',
467
+ 'Notes:',
468
+ ' - This command is rerunnable.',
469
+ ' - SQLite repos are bootstrapped automatically.',
470
+ ' - Desktop host reads the same persisted config owner as the CLI.',
471
+ ]
472
+ }
473
+
474
+ function findCommandDescriptorById(
475
+ projection: DocmanCliProjection,
476
+ id: string,
477
+ ): DocmanCliCommandDescriptor | null {
478
+ return projection.commandsById[id] ?? null
479
+ }
480
+
481
+ async function buildCliProjection(): Promise<DocmanCliProjection> {
482
+ const { tooling } = await getDocmanCliRuntimeModules()
483
+ return tooling.buildDocmanCliProjection({ refresh: true })
484
+ }
485
+
486
+ function toProjectionOperationId(operationId: string): string {
487
+ return normalizeOperationIdentifier(operationId).replace(/^docman\./, '')
488
+ }
489
+
490
+ function normalizeManifestHelpSubcommand(subcommand: string | undefined): string | undefined {
491
+ if (!subcommand) return undefined
492
+ if (subcommand === 'hrm') return 'host-registration'
493
+ if (subcommand === 'operations') return 'ops'
494
+ return subcommand
495
+ }
496
+
497
+ async function printHelp(): Promise<void> {
498
+ const projection = await buildCliProjection()
499
+ const descriptor = findCommandDescriptorById(projection, 'docman')
500
+ if (!descriptor) {
501
+ throw new Error('missing_cli_help_root')
502
+ }
503
+ const lines = renderCommandDescriptor(descriptor)
504
+ lines.push(
505
+ '',
506
+ 'Standalone setup commands:',
507
+ ' docman init',
508
+ ' docman setup runtime',
509
+ )
510
+ printHelpLines(lines)
511
+ }
512
+
513
+ async function tryPrintCommandHelp(parsed: ParsedArgv): Promise<boolean> {
514
+ const command = parsed.positionals[0]?.toLowerCase()
515
+ const subcommand = normalizeManifestHelpSubcommand(parsed.positionals[1]?.toLowerCase())
516
+
517
+ if (!command) return false
518
+
519
+ const projection = await buildCliProjection()
520
+ const renderDescriptor = (id: string, trailingMessage?: string): boolean => {
521
+ const descriptor = findCommandDescriptorById(projection, id)
522
+ if (!descriptor) return false
523
+ const lines = renderCommandDescriptor(descriptor)
524
+ if (trailingMessage) {
525
+ lines.push('', trailingMessage)
526
+ }
527
+ printHelpLines(lines)
528
+ return true
529
+ }
530
+
531
+ if (command === 'version') return renderDescriptor('version')
532
+ if (command === 'init') {
533
+ printHelpLines(buildStandaloneInitHelpLines())
534
+ return true
535
+ }
536
+ if (command === 'setup') {
537
+ printHelpLines(buildStandaloneSetupHelpLines())
538
+ return true
539
+ }
540
+ if (command === 'config') {
541
+ if (subcommand === 'show') return renderDescriptor('config.show')
542
+ if (subcommand === 'set') return renderDescriptor('config.set')
543
+ return renderDescriptor('config')
544
+ }
545
+ if (command === 'manifest') {
546
+ if (!subcommand) return renderDescriptor('manifest')
547
+ if (subcommand === 'get') return renderDescriptor('manifest.get')
548
+ if (subcommand === 'show') return renderDescriptor('manifest.show')
549
+ if (subcommand === 'dcm') return renderDescriptor('manifest.dcm')
550
+ if (subcommand === 'routes') return renderDescriptor('manifest.routes')
551
+ if (subcommand === 'agent') return renderDescriptor('manifest.agent')
552
+ if (subcommand === 'cli') return renderDescriptor('manifest.cli')
553
+ if (subcommand === 'host-registration') return renderDescriptor('manifest.host-registration')
554
+ if (subcommand === 'ops') return renderDescriptor('manifest.ops')
555
+ return renderDescriptor('manifest')
556
+ }
557
+ if (command === 'tools') return renderDescriptor('tools')
558
+ if (command === 'ops') return renderDescriptor('ops')
559
+ if (command === 'tool' || command === 'invoke') {
560
+ const { tooling } = await getDocmanCliRuntimeModules()
561
+ const identifier = getStringOption(parsed, 'id')
562
+ if (!identifier) return renderDescriptor('tool')
563
+ const spec = tooling.getDocmanOperationSpecById(identifier, { refresh: true })
564
+ if (!spec) {
565
+ return renderDescriptor('tool', `Unknown tool or operation id: ${identifier}`)
566
+ }
567
+ return renderDescriptor(toProjectionOperationId(spec.operationId))
568
+ }
569
+ if (DIRECT_OPERATION_COMMANDS.has(command)) {
570
+ const { tooling } = await getDocmanCliRuntimeModules()
571
+ const identifier = parsed.positionals[1] ?? getStringOption(parsed, 'id')
572
+ if (!identifier) return renderDescriptor('op')
573
+ const spec = tooling.getDocmanOperationSpecById(identifier, { refresh: true })
574
+ if (!spec) {
575
+ return renderDescriptor('op', `Unknown operation or tool id: ${identifier}`)
576
+ }
577
+ return renderDescriptor(toProjectionOperationId(spec.operationId))
578
+ }
579
+
580
+ const { tooling } = await getDocmanCliRuntimeModules()
581
+ const operationSpecs = tooling.listDocmanToolingOperations({ refresh: true })
582
+ const operationMap = new Map(
583
+ operationSpecs.map((operation) => [normalizeOperationIdentifier(operation.operationId), operation]),
584
+ )
585
+ const resolvedOperationId = await resolveGeneratedOperationId(parsed, operationMap)
586
+ if (!resolvedOperationId) return false
587
+ return renderDescriptor(toProjectionOperationId(resolvedOperationId))
588
+ }
589
+
590
+ function normalizeNonEmptyString(value: unknown): string | undefined {
591
+ if (typeof value !== 'string') return undefined
592
+ const normalized = value.trim()
593
+ return normalized.length > 0 ? normalized : undefined
594
+ }
595
+
596
+ function normalizeRepoUrlValue(value: unknown): string | undefined {
597
+ const normalized = normalizeNonEmptyString(value)
598
+ if (!normalized) return undefined
599
+ return normalized.replace(/(?:\\r|\r)+$/g, '')
600
+ }
601
+
602
+ function normalizeRuntimeMode(value: unknown): DocmanRuntimeMode | undefined {
603
+ const normalized = normalizeNonEmptyString(value)?.toLowerCase()
604
+ if (!normalized) return undefined
605
+ if (normalized === 'single-user' || normalized === 'single_user' || normalized === 'single') {
606
+ return 'single-user'
607
+ }
608
+ if (normalized === 'multi-user' || normalized === 'multi_user' || normalized === 'multi') {
609
+ return 'multi-user'
610
+ }
611
+ return undefined
612
+ }
613
+
614
+ function getDocmanCliPackageVersion(): string {
615
+ if (docmanCliPackageVersionCache) return docmanCliPackageVersionCache
616
+
617
+ try {
618
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as {
619
+ version?: unknown
620
+ }
621
+ docmanCliPackageVersionCache = normalizeNonEmptyString(packageJson.version) ?? '0.0.0'
622
+ } catch {
623
+ docmanCliPackageVersionCache = '0.0.0'
624
+ }
625
+
626
+ return docmanCliPackageVersionCache
627
+ }
628
+
629
+ function toRecord(input: unknown): Record<string, unknown> {
630
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
631
+ return input as Record<string, unknown>
632
+ }
633
+
634
+ function resolveHomeDirectory(processEnv: NodeJS.ProcessEnv): string {
635
+ const envHome =
636
+ normalizeNonEmptyString(processEnv.HOME)
637
+ ?? normalizeNonEmptyString(processEnv.USERPROFILE)
638
+ return resolve(envHome ?? os.homedir())
639
+ }
640
+
641
+ function resolveExecutionMode(): DocmanExecutionMode {
642
+ const configured = normalizeNonEmptyString(process.env.DOCMAN_CLI_EXECUTION_MODE)?.toLowerCase()
643
+ if (configured === 'tooling') return 'tooling'
644
+ return 'host'
645
+ }
646
+
647
+ function sanitizeBlankEnvValues(processEnv: NodeJS.ProcessEnv): void {
648
+ for (const key of DOCMAN_ENV_SANITIZE_KEYS) {
649
+ if (typeof processEnv[key] === 'string' && processEnv[key]?.trim() === '') {
650
+ delete processEnv[key]
651
+ }
652
+ }
653
+ }
654
+
655
+ function buildHostRequestContext(): DomainRequest['context'] {
656
+ const scopeId =
657
+ normalizeDocmanScopeId(process.env.DOCMAN_SCOPE_ID)
658
+ ?? normalizeDocmanScopeId(process.env.DOCMAN_WORKSPACE_ID)
659
+ ?? DEFAULT_DOCMAN_SCOPE_ID
660
+ return {
661
+ tenantId: normalizeNonEmptyString(process.env.TENANT_ID) ?? DEFAULT_TENANT_AS_UUID_STRING,
662
+ scopeId,
663
+ workspaceId: normalizeDocmanScopeId(process.env.DOCMAN_WORKSPACE_ID) ?? scopeId,
664
+ locale: normalizeNonEmptyString(process.env.DOCMAN_LOCALE) ?? 'tr',
665
+ fallbackLocale: normalizeNonEmptyString(process.env.DOCMAN_FALLBACK_LOCALE) ?? 'en',
666
+ principal: null,
667
+ }
668
+ }
669
+
670
+ function patternToPathSegments(pattern: string, params: Record<string, string>): string[] {
671
+ const raw = pattern.trim().replace(/^\/+|\/+$/g, '')
672
+ if (!raw) return []
673
+ const output: string[] = []
674
+ const segments = raw.split('/').map((segment) => segment.trim()).filter(Boolean)
675
+ for (const patternSegment of segments) {
676
+ if (patternSegment === '*') {
677
+ const splat = normalizeNonEmptyString(params.splat)
678
+ if (!splat) continue
679
+ output.push(...splat.split('/').map((part) => encodeURIComponent(part)))
680
+ continue
681
+ }
682
+ if (patternSegment.startsWith(':')) {
683
+ const key = patternSegment.slice(1).trim()
684
+ const value = normalizeNonEmptyString(params[key])
685
+ if (!key || !value) {
686
+ throw new Error(`missing_required_arg:${key || 'path'}`)
687
+ }
688
+ output.push(encodeURIComponent(value))
689
+ continue
690
+ }
691
+ output.push(patternSegment)
692
+ }
693
+ return output
694
+ }
695
+
696
+ function resolveRouteParams(route: DomainRouteManifestEntry, input: unknown): Record<string, string> {
697
+ const inputRecord = toRecord(input)
698
+ const pathParamCandidates = toRecord(inputRecord.pathParams)
699
+ const params: Record<string, string> = {}
700
+ const segments = route.pattern
701
+ .trim()
702
+ .replace(/^\/+|\/+$/g, '')
703
+ .split('/')
704
+ .map((segment: string) => segment.trim())
705
+ .filter(Boolean)
706
+
707
+ for (const segment of segments) {
708
+ if (segment === '*') {
709
+ const splatValue = normalizeNonEmptyString(pathParamCandidates.splat) ?? normalizeNonEmptyString(inputRecord.splat)
710
+ if (splatValue) params.splat = splatValue
711
+ continue
712
+ }
713
+ if (!segment.startsWith(':')) continue
714
+ const key = segment.slice(1).trim()
715
+ if (!key) continue
716
+ const value = normalizeNonEmptyString(pathParamCandidates[key]) ?? normalizeNonEmptyString(inputRecord[key])
717
+ if (!value) {
718
+ throw new Error(`missing_required_arg:${key}`)
719
+ }
720
+ params[key] = value
721
+ }
722
+
723
+ return params
724
+ }
725
+
726
+ function buildHostDomainRequest(
727
+ route: DomainRouteManifestEntry,
728
+ pathSegments: string[],
729
+ input: unknown,
730
+ ): DomainRequest {
731
+ const baseUrl = `https://docman-cli.local/api/docman${pathSegments.length > 0 ? `/${pathSegments.join('/')}` : ''}`
732
+ return {
733
+ method: route.method,
734
+ domain: 'docman',
735
+ path: pathSegments,
736
+ query: new URLSearchParams(),
737
+ body: input,
738
+ headers: new Headers(),
739
+ url: new URL(baseUrl),
740
+ context: buildHostRequestContext(),
741
+ }
742
+ }
743
+
744
+ async function getDocmanHostRuntime(): Promise<DocmanHostRuntime> {
745
+ if (docmanHostRuntimePromise) return docmanHostRuntimePromise
746
+
747
+ docmanHostRuntimePromise = (async () => {
748
+ const { hostPlugin } = await getDocmanCliRuntimeModules()
749
+ const plugin = hostPlugin.createDocmanPlugin({
750
+ defaultScopeId: normalizeNonEmptyString(process.env.DOCMAN_SCOPE_ID),
751
+ })
752
+ const routesByOperationId = new Map<string, DomainRouteManifestEntry>()
753
+ for (const route of plugin.manifest.routes) {
754
+ const normalizedOperationId = normalizeOperationIdentifier(route.operation)
755
+ if (!normalizedOperationId) continue
756
+ routesByOperationId.set(normalizedOperationId, route)
757
+ }
758
+ return { plugin, routesByOperationId }
759
+ })()
760
+
761
+ return docmanHostRuntimePromise
762
+ }
763
+
764
+ async function resolveOperationIdFromIdentifier(identifier: string): Promise<string | null> {
765
+ const { tooling } = await getDocmanCliRuntimeModules()
766
+ const normalizedDirect = normalizeOperationIdentifier(identifier)
767
+ const directSpec = tooling.getDocmanOperationSpecById(normalizedDirect || identifier, { refresh: true })
768
+ if (directSpec) return normalizeOperationIdentifier(directSpec.operationId)
769
+
770
+ const resolvedByAlias = tooling.resolveDocmanOperationIdByToolId(identifier, { refresh: true })
771
+ if (resolvedByAlias) return normalizeOperationIdentifier(resolvedByAlias)
772
+
773
+ return null
774
+ }
775
+
776
+ async function runDocmanOperationById(operationId: string, input: unknown = {}): Promise<unknown> {
777
+ const { tooling } = await getDocmanCliRuntimeModules()
778
+ if (resolveExecutionMode() === 'tooling') {
779
+ return tooling.runDocmanOperationById(operationId, input, { refresh: true })
780
+ }
781
+
782
+ const runtime = await getDocmanHostRuntime()
783
+ const normalizedOperationId = normalizeOperationIdentifier(operationId)
784
+ const route = runtime.routesByOperationId.get(normalizedOperationId)
785
+ if (!route) {
786
+ return tooling.runDocmanOperationById(operationId, input, { refresh: true })
787
+ }
788
+
789
+ const params = resolveRouteParams(route, input)
790
+ const path = patternToPathSegments(route.pattern, params)
791
+ const request = buildHostDomainRequest(route, path, input)
792
+ return runtime.plugin.execute({
793
+ request,
794
+ match: { route, params },
795
+ })
796
+ }
797
+
798
+ async function runDocmanToolById(identifier: string, input: unknown = {}): Promise<unknown> {
799
+ const { tooling } = await getDocmanCliRuntimeModules()
800
+ if (resolveExecutionMode() === 'tooling') {
801
+ return tooling.runDocmanToolById(identifier, input, { refresh: true })
802
+ }
803
+ const operationId = await resolveOperationIdFromIdentifier(identifier)
804
+ if (!operationId) {
805
+ return tooling.runDocmanToolById(identifier, input, { refresh: true })
806
+ }
807
+ return runDocmanOperationById(operationId, input)
808
+ }
809
+
810
+ function resolveOptionalHostConfigPath(
811
+ parsed: ParsedArgv,
812
+ processEnv: NodeJS.ProcessEnv,
813
+ storedConfig: DocmanCliStoredConfig,
814
+ ): string | null {
815
+ const optionPathRaw = normalizeNonEmptyString(getStringOption(parsed, 'host-config'))
816
+ if (optionPathRaw) {
817
+ const resolvedOptionPath = resolve(optionPathRaw)
818
+ if (existsSync(resolvedOptionPath)) return resolvedOptionPath
819
+ const candidateFile = resolvedOptionPath.toLowerCase().endsWith('.json')
820
+ ? resolvedOptionPath
821
+ : resolve(resolvedOptionPath, HOST_CONFIG_FILENAME)
822
+ if (existsSync(candidateFile)) return candidateFile
823
+ throw new Error(`host_config_not_found:${optionPathRaw}`)
824
+ }
825
+
826
+ for (const envKey of HOST_CONFIG_ENV_KEYS) {
827
+ const envValue = normalizeNonEmptyString(processEnv[envKey])
828
+ if (!envValue) continue
829
+ const resolvedEnvPath = resolve(envValue)
830
+ if (existsSync(resolvedEnvPath)) return resolvedEnvPath
831
+ const candidateFile = resolvedEnvPath.toLowerCase().endsWith('.json')
832
+ ? resolvedEnvPath
833
+ : resolve(resolvedEnvPath, HOST_CONFIG_FILENAME)
834
+ if (existsSync(candidateFile)) return candidateFile
835
+ }
836
+
837
+ const storedPathRaw = normalizeNonEmptyString(storedConfig.hostConfigPath)
838
+ if (storedPathRaw) {
839
+ const resolvedStoredPath = resolve(storedPathRaw)
840
+ if (existsSync(resolvedStoredPath)) return resolvedStoredPath
841
+ const candidateFile = resolvedStoredPath.toLowerCase().endsWith('.json')
842
+ ? resolvedStoredPath
843
+ : resolve(resolvedStoredPath, HOST_CONFIG_FILENAME)
844
+ if (existsSync(candidateFile)) return candidateFile
845
+ }
846
+
847
+ const localCandidate = resolve(process.cwd(), HOST_CONFIG_FILENAME)
848
+ if (existsSync(localCandidate)) return localCandidate
849
+ return null
850
+ }
851
+
852
+ function resolveConfigBindingValue(binding: unknown, processEnv: NodeJS.ProcessEnv): string | undefined {
853
+ const direct = normalizeNonEmptyString(binding)
854
+ if (direct) return direct
855
+
856
+ const bindingRecord = toRecord(binding)
857
+ const fromEnv = normalizeNonEmptyString(bindingRecord.fromEnv)
858
+ if (fromEnv) {
859
+ const envValue = normalizeNonEmptyString(processEnv[fromEnv])
860
+ if (envValue) return envValue
861
+ }
862
+ return normalizeNonEmptyString(bindingRecord.value) ?? normalizeNonEmptyString(bindingRecord.default)
863
+ }
864
+
865
+ function applyOptionalHostRuntimeEnv(
866
+ parsed: ParsedArgv,
867
+ processEnv: NodeJS.ProcessEnv,
868
+ storedConfig: DocmanCliStoredConfig,
869
+ ): void {
870
+ const hostConfigPath = resolveOptionalHostConfigPath(parsed, processEnv, storedConfig)
871
+ if (!hostConfigPath) return
872
+
873
+ let config: Record<string, unknown> = {}
874
+ try {
875
+ config = toRecord(JSON.parse(readFileSync(hostConfigPath, 'utf8')))
876
+ } catch (error) {
877
+ throw new Error(`host_config_parse_failed:${hostConfigPath}:${error instanceof Error ? error.message : String(error)}`)
878
+ }
879
+
880
+ const runtimeEnv = toRecord(toRecord(config.runtime).env)
881
+ for (const [targetKeyRaw, binding] of Object.entries(runtimeEnv)) {
882
+ const targetKey = targetKeyRaw.trim()
883
+ if (!targetKey) continue
884
+ if (targetKey.toUpperCase() === 'DOCMAN_REPO_URL') {
885
+ continue
886
+ }
887
+ const resolved = resolveConfigBindingValue(binding, processEnv)
888
+ if (resolved !== undefined) {
889
+ process.env[targetKey] = resolved
890
+ }
891
+ }
892
+ }
893
+
894
+ function resolveAopsConfigFilePath(processEnv: NodeJS.ProcessEnv): string {
895
+ const configPathRaw = processEnv.AOPS_CLI_CONFIG_PATH ?? processEnv.AGENT_OPS_CONFIG_PATH
896
+ const configPath = typeof configPathRaw === 'string' ? configPathRaw.trim() : ''
897
+
898
+ if (configPath.length > 0) {
899
+ const resolvedPath = resolve(configPath)
900
+ if (resolvedPath.toLowerCase().endsWith('.json')) {
901
+ return resolvedPath
902
+ }
903
+ return resolve(resolvedPath, AOPS_CONFIG_FILENAME)
904
+ }
905
+
906
+ return resolve(resolveHomeDirectory(processEnv), '.aops', AOPS_CONFIG_FILENAME)
907
+ }
908
+
909
+ function readAopsConfig(processEnv: NodeJS.ProcessEnv): Record<string, unknown> {
910
+ const configPath = resolveAopsConfigFilePath(processEnv)
911
+ if (!existsSync(configPath)) return {}
912
+ try {
913
+ const content = readFileSync(configPath, 'utf8')
914
+ return toRecord(JSON.parse(content))
915
+ } catch {
916
+ return {}
917
+ }
918
+ }
919
+
920
+ function resolveConfiguredDocmanRepoUrl(processEnv: NodeJS.ProcessEnv): string | undefined {
921
+ const config = readAopsConfig(processEnv)
922
+
923
+ const topLevel =
924
+ normalizeRepoUrlValue(config.docmanPgUrl) ??
925
+ normalizeRepoUrlValue(config.docmanSqliteUrl) ??
926
+ normalizeRepoUrlValue(config.docmanRepoUrl) ??
927
+ normalizeRepoUrlValue(config.DOCMAN_PG_URL) ??
928
+ normalizeRepoUrlValue(config.DOCMAN_SQLITE_URL) ??
929
+ normalizeRepoUrlValue(config.DOCMAN_REPO_URL)
930
+ if (topLevel) return topLevel
931
+
932
+ const docmanConfig = toRecord(config.docman)
933
+ const docmanLevel =
934
+ normalizeRepoUrlValue(docmanConfig.pgUrl) ??
935
+ normalizeRepoUrlValue(docmanConfig.sqliteUrl) ??
936
+ normalizeRepoUrlValue(docmanConfig.repoUrl) ??
937
+ normalizeRepoUrlValue(docmanConfig.repositoryUrl) ??
938
+ normalizeRepoUrlValue(docmanConfig.dbUrl)
939
+ if (docmanLevel) return docmanLevel
940
+
941
+ const runtimeEnv = toRecord(toRecord(config.runtime).env)
942
+ const runtimeValue =
943
+ resolveConfigBindingValue(runtimeEnv.DOCMAN_PG_URL, processEnv) ??
944
+ resolveConfigBindingValue(runtimeEnv.DOCMAN_SQLITE_URL, processEnv) ??
945
+ resolveConfigBindingValue(runtimeEnv.DOCMAN_REPO_URL, processEnv)
946
+ if (runtimeValue) return runtimeValue
947
+
948
+ const envSection = toRecord(config.env)
949
+ return normalizeRepoUrlValue(resolveConfigBindingValue(envSection.DOCMAN_PG_URL, processEnv))
950
+ ?? normalizeRepoUrlValue(resolveConfigBindingValue(envSection.DOCMAN_SQLITE_URL, processEnv))
951
+ ?? normalizeRepoUrlValue(resolveConfigBindingValue(envSection.DOCMAN_REPO_URL, processEnv))
952
+ }
953
+
954
+ function inferDefaultRepoUrl(processEnv: NodeJS.ProcessEnv): string {
955
+ const configuredRepoUrl = resolveConfiguredDocmanRepoUrl(processEnv)
956
+ if (configuredRepoUrl) return configuredRepoUrl
957
+ return getDefaultDocmanSqliteRepoUrl(processEnv)
958
+ }
959
+
960
+ function maskRepoUrlForDisplay(repoUrl: unknown): string | undefined {
961
+ const normalized = normalizeRepoUrlValue(repoUrl)
962
+ if (!normalized) return undefined
963
+ if (inferDocmanRepoDialect(normalized) === 'sqlite') return normalized
964
+ return sanitizeUrl(normalized)
965
+ }
966
+
967
+ function maskStoredConfigForDisplay(stored: DocmanCliStoredConfig): Record<string, unknown> {
968
+ const output: Record<string, unknown> = {
969
+ ...stored,
970
+ ...(stored.repoUrl ? { repoUrl: maskRepoUrlForDisplay(stored.repoUrl) } : {}),
971
+ }
972
+ return output
973
+ }
974
+
975
+ function buildConfigSetOutput(stored: LoadedDocmanCliStoredConfig): Record<string, unknown> {
976
+ return {
977
+ configPath: stored.path,
978
+ exists: stored.exists,
979
+ stored: maskStoredConfigForDisplay(stored.config),
980
+ }
981
+ }
982
+
983
+ function buildConfigShowOutput(processEnv: NodeJS.ProcessEnv): Record<string, unknown> {
984
+ const loaded = readDocmanCliStoredConfig(processEnv)
985
+ const stored = loaded.config
986
+ const defaultSqlitePath = getDefaultDocmanSqlitePath(processEnv)
987
+ const defaultSqliteRepoUrl = getDefaultDocmanSqliteRepoUrl(processEnv)
988
+
989
+ const envRepoUrl =
990
+ normalizeRepoUrlValue(processEnv.DOCMAN_REPO_URL)
991
+ ?? normalizeRepoUrlValue(processEnv.DOCMAN_SQLITE_URL)
992
+ ?? normalizeRepoUrlValue(processEnv.DOCMAN_PG_URL)
993
+ const envRepoUrlBootstrapSource = normalizeNonEmptyString(processEnv.DOCMAN_CLI_BOOTSTRAPPED_REPO_URL_SOURCE)
994
+ const envTenantId = normalizeNonEmptyString(processEnv.TENANT_ID)
995
+ const envScopeId =
996
+ normalizeDocmanScopeId(processEnv.DOCMAN_SCOPE_ID)
997
+ ?? normalizeDocmanScopeId(processEnv.DOCMAN_WORKSPACE_ID)
998
+ const envRuntimeMode = normalizeRuntimeMode(processEnv.DOCMAN_RUNTIME_MODE)
999
+ const envLogLevel = normalizeNonEmptyString(processEnv.LOG_LEVEL)
1000
+ const envExecutionModeRaw = normalizeNonEmptyString(processEnv.DOCMAN_CLI_EXECUTION_MODE)?.toLowerCase()
1001
+ const envExecutionMode =
1002
+ envExecutionModeRaw === 'host' || envExecutionModeRaw === 'tooling'
1003
+ ? envExecutionModeRaw
1004
+ : undefined
1005
+ const envHostConfigPath =
1006
+ normalizeNonEmptyString(processEnv.DOCMAN_CLI_HOST_CONFIG_PATH)
1007
+ ?? normalizeNonEmptyString(processEnv.HOST_NODE_CONFIG)
1008
+
1009
+ const aopsConfigRepoUrl = resolveConfiguredDocmanRepoUrl(processEnv)
1010
+ const effectiveRepoUrl = envRepoUrl ?? stored.repoUrl ?? aopsConfigRepoUrl ?? defaultSqliteRepoUrl
1011
+
1012
+ return {
1013
+ configPath: loaded.path,
1014
+ exists: loaded.exists,
1015
+ stored: maskStoredConfigForDisplay(stored),
1016
+ defaults: {
1017
+ aopsConfigRepoUrl: maskRepoUrlForDisplay(aopsConfigRepoUrl) ?? null,
1018
+ defaultSqlitePath,
1019
+ defaultSqliteRepoUrl,
1020
+ },
1021
+ effective: {
1022
+ repoUrl: maskRepoUrlForDisplay(effectiveRepoUrl) ?? null,
1023
+ repoUrlSource:
1024
+ envRepoUrl
1025
+ ? envRepoUrlBootstrapSource === 'stored-config'
1026
+ ? 'stored-config'
1027
+ : 'env'
1028
+ : stored.repoUrl
1029
+ ? 'stored-config'
1030
+ : aopsConfigRepoUrl
1031
+ ? 'aops-config'
1032
+ : 'default-sqlite',
1033
+ runtimeMode: envRuntimeMode ?? stored.runtimeMode ?? 'single-user',
1034
+ runtimeModeSource: envRuntimeMode ? 'env' : stored.runtimeMode ? 'stored-config' : 'default',
1035
+ executionMode: envExecutionMode ?? stored.executionMode ?? 'host',
1036
+ executionModeSource: envExecutionMode ? 'env' : stored.executionMode ? 'stored-config' : 'default',
1037
+ scopeId: envScopeId ?? stored.scopeId ?? DEFAULT_DOCMAN_SCOPE_ID,
1038
+ scopeIdSource: envScopeId ? 'env' : stored.scopeId ? 'stored-config' : 'default',
1039
+ tenantId: envTenantId ?? stored.tenantId ?? DEFAULT_TENANT_AS_UUID_STRING,
1040
+ tenantIdSource: envTenantId ? 'env' : stored.tenantId ? 'stored-config' : 'default',
1041
+ logLevel: envLogLevel ?? stored.logLevel ?? null,
1042
+ logLevelSource: envLogLevel ? 'env' : stored.logLevel ? 'stored-config' : 'unset',
1043
+ hostConfigPath: envHostConfigPath ?? stored.hostConfigPath ?? null,
1044
+ hostConfigPathSource: envHostConfigPath ? 'env' : stored.hostConfigPath ? 'stored-config' : 'unset',
1045
+ },
1046
+ }
1047
+ }
1048
+
1049
+ async function buildRuntimeContext(parsed: ParsedArgv): Promise<RuntimeContext> {
1050
+ sanitizeBlankEnvValues(process.env)
1051
+ const storedConfig = readDocmanCliStoredConfig(process.env).config
1052
+
1053
+ const executionMode = getStringOption(parsed, 'execution-mode')
1054
+ if (executionMode) {
1055
+ const normalizedMode = executionMode.trim().toLowerCase()
1056
+ if (normalizedMode === 'host' || normalizedMode === 'tooling') {
1057
+ process.env.DOCMAN_CLI_EXECUTION_MODE = normalizedMode
1058
+ } else {
1059
+ throw new Error(`invalid_execution_mode:${executionMode}`)
1060
+ }
1061
+ } else if (!normalizeNonEmptyString(process.env.DOCMAN_CLI_EXECUTION_MODE) && storedConfig.executionMode) {
1062
+ process.env.DOCMAN_CLI_EXECUTION_MODE = storedConfig.executionMode
1063
+ }
1064
+
1065
+ applyOptionalHostRuntimeEnv(parsed, process.env, storedConfig)
1066
+
1067
+ const runtimeConfig = resolveDocmanRuntimeConfig(
1068
+ {
1069
+ runtimeMode: getStringOption(parsed, 'runtime-mode'),
1070
+ repoUrl: normalizeRepoUrlValue(getStringOption(parsed, 'repo-url')),
1071
+ scopeId: getScopeIdOption(parsed),
1072
+ },
1073
+ process.env,
1074
+ )
1075
+
1076
+ const resolvedRepoUrl =
1077
+ runtimeConfig.repoUrlSource === 'default-sqlite'
1078
+ ? resolveConfiguredDocmanRepoUrl(process.env) ?? runtimeConfig.repoUrl
1079
+ : runtimeConfig.repoUrl
1080
+
1081
+ if (!normalizeNonEmptyString(resolvedRepoUrl)) {
1082
+ throw new Error('missing_repo_url:empty')
1083
+ }
1084
+
1085
+ const repoDialect = inferDocmanRepoDialect(resolvedRepoUrl)
1086
+ applyDocmanRuntimeEnv(
1087
+ {
1088
+ runtimeMode: runtimeConfig.runtimeMode,
1089
+ repoUrl: resolvedRepoUrl,
1090
+ repoDialect,
1091
+ scopeId: runtimeConfig.scopeId,
1092
+ },
1093
+ process.env,
1094
+ )
1095
+ if (repoDialect === 'sqlite') {
1096
+ ensureDocmanSqliteSchemaReady(resolvedRepoUrl)
1097
+ }
1098
+
1099
+ const tenantId =
1100
+ getStringOption(parsed, 'tenant-id')
1101
+ ?? normalizeNonEmptyString(process.env.TENANT_ID)
1102
+ ?? storedConfig.tenantId
1103
+ ?? DEFAULT_TENANT_AS_UUID_STRING
1104
+ const scopeId =
1105
+ normalizeDocmanScopeId(process.env.DOCMAN_SCOPE_ID)
1106
+ ?? normalizeDocmanScopeId(process.env.DOCMAN_WORKSPACE_ID)
1107
+ ?? storedConfig.scopeId
1108
+ ?? DEFAULT_DOCMAN_SCOPE_ID
1109
+ const logLevel =
1110
+ getStringOption(parsed, 'log-level')
1111
+ ?? normalizeNonEmptyString(process.env.LOG_LEVEL)
1112
+ ?? storedConfig.logLevel
1113
+
1114
+ process.env.TENANT_ID = tenantId
1115
+ process.env.DOCMAN_SCOPE_ID = scopeId
1116
+ process.env.DOCMAN_WORKSPACE_ID = scopeId
1117
+ if (logLevel) process.env.LOG_LEVEL = logLevel
1118
+ sanitizeBlankEnvValues(process.env)
1119
+
1120
+ const { tooling } = await getDocmanCliRuntimeModules()
1121
+ tooling.clearDocmanToolingRuntimeCaches()
1122
+
1123
+ return { scopeId, workspaceId: scopeId, tenantId }
1124
+ }
1125
+
1126
+ const commandSurface = createDocmanCliCommandSurface({
1127
+ buildConfigSetOutput,
1128
+ buildConfigShowOutput,
1129
+ getStringOption,
1130
+ parseJsonInput,
1131
+ printJson,
1132
+ runOperationById: runDocmanOperationById,
1133
+ writeStoredConfig: writeDocmanCliStoredConfig,
1134
+ })
1135
+
1136
+ function buildStandaloneRuntimeOutput(params: {
1137
+ action: string
1138
+ written: LoadedDocmanCliStoredConfig
1139
+ runtime: ReturnType<typeof resolveDocmanRuntimeConfig>
1140
+ }): Record<string, unknown> {
1141
+ return {
1142
+ ok: true,
1143
+ action: params.action,
1144
+ configPath: params.written.path,
1145
+ configExists: params.written.exists,
1146
+ stored: maskStoredConfigForDisplay(params.written.config),
1147
+ runtime: {
1148
+ runtimeMode: params.runtime.runtimeMode,
1149
+ runtimeModeSource: params.runtime.runtimeModeSource,
1150
+ repoUrl: maskRepoUrlForDisplay(params.runtime.repoUrl) ?? params.runtime.repoUrl,
1151
+ repoUrlSource: params.runtime.repoUrlSource,
1152
+ repoDialect: params.runtime.repoDialect,
1153
+ scopeId: params.runtime.scopeId,
1154
+ scopeIdSource: params.runtime.scopeIdSource,
1155
+ },
1156
+ }
1157
+ }
1158
+
1159
+ async function ensureDocmanRuntimeStorageReady(repoUrl: string): Promise<void> {
1160
+ if (inferDocmanRepoDialect(repoUrl) === 'sqlite') {
1161
+ ensureDocmanSqliteSchemaReady(repoUrl)
1162
+ return
1163
+ }
1164
+
1165
+ await applyDocmanPgSchema({ repoUrl })
1166
+ }
1167
+
1168
+ async function handleInit(parsed: ParsedArgv): Promise<void> {
1169
+ const runtimeMode = normalizeRuntimeMode(getStringOption(parsed, 'runtime-mode')) ?? 'single-user'
1170
+ const repoUrl = normalizeRepoUrlValue(getStringOption(parsed, 'repo-url')) ?? getDefaultDocmanSqliteRepoUrl(process.env)
1171
+ const scopeId = normalizeDocmanScopeId(getScopeIdOption(parsed)) ?? DEFAULT_DOCMAN_SCOPE_ID
1172
+
1173
+ const written = writeDocmanCliStoredConfig(
1174
+ {
1175
+ runtimeMode,
1176
+ repoUrl,
1177
+ scopeId,
1178
+ },
1179
+ process.env,
1180
+ )
1181
+
1182
+ const runtime = resolveDocmanRuntimeConfig({}, process.env)
1183
+ await ensureDocmanRuntimeStorageReady(runtime.repoUrl)
1184
+ applyDocmanRuntimeEnv(
1185
+ {
1186
+ runtimeMode: runtime.runtimeMode,
1187
+ repoUrl: runtime.repoUrl,
1188
+ repoDialect: runtime.repoDialect,
1189
+ scopeId: runtime.scopeId,
1190
+ },
1191
+ process.env,
1192
+ )
1193
+
1194
+ printJson(buildStandaloneRuntimeOutput({ action: 'init', written, runtime }))
1195
+ }
1196
+
1197
+ async function handleSetup(subcommand: string | undefined, parsed: ParsedArgv): Promise<void> {
1198
+ const resolvedSubcommand = subcommand ?? 'runtime'
1199
+ if (resolvedSubcommand !== 'runtime') {
1200
+ throw new Error(`unknown_setup_subcommand:${resolvedSubcommand}`)
1201
+ }
1202
+
1203
+ const loaded = readDocmanCliStoredConfig(process.env)
1204
+ const nextRepoUrl =
1205
+ normalizeRepoUrlValue(getStringOption(parsed, 'repo-url'))
1206
+ ?? loaded.config.repoUrl
1207
+ ?? inferDefaultRepoUrl(process.env)
1208
+ const nextRuntimeMode = normalizeRuntimeMode(getStringOption(parsed, 'runtime-mode')) ?? loaded.config.runtimeMode ?? 'single-user'
1209
+ const nextScopeId =
1210
+ normalizeDocmanScopeId(getScopeIdOption(parsed))
1211
+ ?? loaded.config.scopeId
1212
+ ?? DEFAULT_DOCMAN_SCOPE_ID
1213
+
1214
+ const written = writeDocmanCliStoredConfig(
1215
+ {
1216
+ repoUrl: nextRepoUrl,
1217
+ runtimeMode: nextRuntimeMode,
1218
+ scopeId: nextScopeId,
1219
+ },
1220
+ process.env,
1221
+ )
1222
+
1223
+ const runtime = resolveDocmanRuntimeConfig({}, process.env)
1224
+ await ensureDocmanRuntimeStorageReady(runtime.repoUrl)
1225
+ applyDocmanRuntimeEnv(
1226
+ {
1227
+ runtimeMode: runtime.runtimeMode,
1228
+ repoUrl: runtime.repoUrl,
1229
+ repoDialect: runtime.repoDialect,
1230
+ scopeId: runtime.scopeId,
1231
+ },
1232
+ process.env,
1233
+ )
1234
+
1235
+ printJson(buildStandaloneRuntimeOutput({ action: 'setup-runtime', written, runtime }))
1236
+ }
1237
+
1238
+ type DocmanManifestPayloads = {
1239
+ dcm: unknown
1240
+ routes: unknown
1241
+ agent: unknown
1242
+ cli: DocmanCliProjection
1243
+ hostRegistration: ReturnType<typeof buildDocmanHostRegistrationManifest>
1244
+ operations: unknown
1245
+ }
1246
+
1247
+ async function buildManifestPayloads(): Promise<DocmanManifestPayloads> {
1248
+ const { tooling } = await getDocmanCliRuntimeModules()
1249
+ return {
1250
+ dcm: tooling.buildDocmanDomainCapabilityManifest({ includeDocs: true, refresh: true }),
1251
+ routes: tooling.buildDocmanHostRouteProjection({ refresh: true }),
1252
+ agent: tooling.buildDocmanAgentManifest({ refresh: true }),
1253
+ cli: tooling.buildDocmanCliProjection({ refresh: true }),
1254
+ hostRegistration: buildDocmanHostRegistrationManifest({ mode: 'installed' }),
1255
+ operations: tooling.listDocmanToolingOperations({ refresh: true }),
1256
+ }
1257
+ }
1258
+
1259
+ function resolveManifestArtifactId(
1260
+ identifier: string,
1261
+ projection: DocmanCliProjection,
1262
+ ): DocmanCliManifestArtifact['id'] | null {
1263
+ const normalized = normalizeIdentifier(identifier)
1264
+ for (const artifact of projection.artifacts) {
1265
+ if (normalizeIdentifier(artifact.id) === normalized) return artifact.id
1266
+ for (const alias of artifact.aliases ?? []) {
1267
+ if (normalizeIdentifier(alias) === normalized) return artifact.id
1268
+ }
1269
+ }
1270
+ return null
1271
+ }
1272
+
1273
+ function getManifestArtifactValue(
1274
+ payloads: DocmanManifestPayloads,
1275
+ artifactId: DocmanCliManifestArtifact['id'],
1276
+ ): unknown {
1277
+ if (artifactId === 'dcm') return payloads.dcm
1278
+ if (artifactId === 'routes') return payloads.routes
1279
+ if (artifactId === 'agent') return payloads.agent
1280
+ if (artifactId === 'cli') return payloads.cli
1281
+ if (artifactId === 'host-registration') return payloads.hostRegistration
1282
+ return payloads.operations
1283
+ }
1284
+
1285
+ function tokenizeManifestPath(rawPath: string): string[] {
1286
+ return rawPath
1287
+ .replace(/\[(\d+)\]/g, '.$1')
1288
+ .split('.')
1289
+ .map((segment) => segment.trim())
1290
+ .filter(Boolean)
1291
+ }
1292
+
1293
+ function resolveManifestPathValue(
1294
+ root: unknown,
1295
+ rawPath: string | undefined,
1296
+ ): { found: boolean; value: unknown } {
1297
+ if (!rawPath) return { found: true, value: root }
1298
+
1299
+ const segments = tokenizeManifestPath(rawPath)
1300
+ let current: unknown = root
1301
+ let index = 0
1302
+
1303
+ while (index < segments.length) {
1304
+ const segment = segments[index]
1305
+
1306
+ if (Array.isArray(current)) {
1307
+ const numericIndex = Number(segment)
1308
+ if (!Number.isInteger(numericIndex) || numericIndex < 0 || numericIndex >= current.length) {
1309
+ return { found: false, value: null }
1310
+ }
1311
+ current = current[numericIndex]
1312
+ index += 1
1313
+ continue
1314
+ }
1315
+
1316
+ if (!current || typeof current !== 'object') {
1317
+ return { found: false, value: null }
1318
+ }
1319
+
1320
+ const record = current as Record<string, unknown>
1321
+ if (Object.prototype.hasOwnProperty.call(record, segment)) {
1322
+ current = record[segment]
1323
+ index += 1
1324
+ continue
1325
+ }
1326
+
1327
+ let matched = false
1328
+ for (let end = segments.length; end > index; end -= 1) {
1329
+ const composite = segments.slice(index, end).join('.')
1330
+ if (!Object.prototype.hasOwnProperty.call(record, composite)) continue
1331
+ current = record[composite]
1332
+ index = end
1333
+ matched = true
1334
+ break
1335
+ }
1336
+
1337
+ if (!matched) {
1338
+ return { found: false, value: null }
1339
+ }
1340
+ }
1341
+
1342
+ return { found: true, value: current }
1343
+ }
1344
+
1345
+ function renderManifestScalar(value: unknown): string {
1346
+ if (value === null) return 'null'
1347
+ if (value === undefined) return 'undefined'
1348
+ if (typeof value === 'string') return value
1349
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
1350
+ return JSON.stringify(value)
1351
+ }
1352
+
1353
+ function renderManifestValueLines(value: unknown, indent = ''): string[] {
1354
+ if (
1355
+ value === null ||
1356
+ value === undefined ||
1357
+ typeof value === 'string' ||
1358
+ typeof value === 'number' ||
1359
+ typeof value === 'boolean'
1360
+ ) {
1361
+ return [`${indent}${renderManifestScalar(value)}`]
1362
+ }
1363
+
1364
+ if (Array.isArray(value)) {
1365
+ if (value.length === 0) return [`${indent}[]`]
1366
+ const lines: string[] = []
1367
+ for (const item of value) {
1368
+ if (
1369
+ item === null ||
1370
+ item === undefined ||
1371
+ typeof item === 'string' ||
1372
+ typeof item === 'number' ||
1373
+ typeof item === 'boolean'
1374
+ ) {
1375
+ lines.push(`${indent}- ${renderManifestScalar(item)}`)
1376
+ continue
1377
+ }
1378
+ lines.push(`${indent}-`)
1379
+ lines.push(...renderManifestValueLines(item, `${indent} `))
1380
+ }
1381
+ return lines
1382
+ }
1383
+
1384
+ const record = value as Record<string, unknown>
1385
+ const keys = Object.keys(record).sort()
1386
+ if (keys.length === 0) return [`${indent}{}`]
1387
+
1388
+ const lines: string[] = []
1389
+ for (const key of keys) {
1390
+ const entry = record[key]
1391
+ if (
1392
+ entry === null ||
1393
+ entry === undefined ||
1394
+ typeof entry === 'string' ||
1395
+ typeof entry === 'number' ||
1396
+ typeof entry === 'boolean'
1397
+ ) {
1398
+ lines.push(`${indent}${key}: ${renderManifestScalar(entry)}`)
1399
+ continue
1400
+ }
1401
+ lines.push(`${indent}${key}:`)
1402
+ lines.push(...renderManifestValueLines(entry, `${indent} `))
1403
+ }
1404
+ return lines
1405
+ }
1406
+
1407
+ function buildManifestShowLines(
1408
+ artifactId: DocmanCliManifestArtifact['id'],
1409
+ value: unknown,
1410
+ rawPath?: string,
1411
+ ): string[] {
1412
+ const title = `docman manifest show ${artifactId}${rawPath ? ` --path ${rawPath}` : ''}`
1413
+ if (rawPath) {
1414
+ const lines = [title]
1415
+ appendTextSection(lines, 'Value', renderManifestValueLines(value))
1416
+ return lines
1417
+ }
1418
+
1419
+ if (artifactId === 'dcm') {
1420
+ const manifest = toRecord(value)
1421
+ const domain = toRecord(manifest.domain)
1422
+ const capabilities = toRecord(manifest.capabilities)
1423
+ const docs = toRecord(manifest.docs)
1424
+ const domainDocs = toRecord(docs.domain)
1425
+ const operations = Array.isArray(capabilities.operations)
1426
+ ? capabilities.operations as Array<Record<string, unknown>>
1427
+ : []
1428
+ const resources = Array.isArray(capabilities.resources)
1429
+ ? capabilities.resources as Array<Record<string, unknown>>
1430
+ : []
1431
+ const policyOps = toRecord(toRecord(manifest.policies).operations)
1432
+ const schemaRefs = toRecord(toRecord(manifest.contracts).schemas)
1433
+ const lines = [title]
1434
+ appendTextSection(lines, 'Identity', [
1435
+ `manifest version: ${renderManifestScalar(manifest.manifestVersion)}`,
1436
+ `domain: ${renderManifestScalar(domain.id)} v${renderManifestScalar(domain.version)}`,
1437
+ `display name: ${renderManifestScalar(domain.displayName)}`,
1438
+ ])
1439
+ appendTextSection(lines, 'Summary', [
1440
+ normalizeNonEmptyString(domain.description) ?? '',
1441
+ normalizeNonEmptyString(domainDocs.summary) ?? '',
1442
+ ])
1443
+ appendTextSection(lines, 'Counts', [
1444
+ `operations: ${operations.length}`,
1445
+ `resources: ${resources.length}`,
1446
+ `policies: ${Object.keys(policyOps).length}`,
1447
+ `schemas: ${Object.keys(schemaRefs).length}`,
1448
+ ])
1449
+ appendTextSection(lines, 'Resources', resources.map((resource) => {
1450
+ const resourceId = renderManifestScalar(resource.resourceId)
1451
+ const resourceTitle = normalizeNonEmptyString(resource.title)
1452
+ return resourceTitle ? `${resourceId} ${resourceTitle}` : resourceId
1453
+ }))
1454
+ appendTextSection(lines, 'Operations', operations.map((operation) => {
1455
+ const operationId = renderManifestScalar(operation.operationId)
1456
+ const operationTitle = normalizeNonEmptyString(operation.title)
1457
+ return operationTitle ? `${operationId} ${operationTitle}` : operationId
1458
+ }))
1459
+ appendTextSection(lines, 'Browse', [
1460
+ 'docman manifest get dcm --path docs.operations.document.list',
1461
+ 'docman manifest show dcm --path docs.operations.document.list',
1462
+ 'docman manifest get dcm --path contracts.schemas',
1463
+ ])
1464
+ return lines
1465
+ }
1466
+
1467
+ if (artifactId === 'routes') {
1468
+ const routes = Array.isArray(value) ? value as Array<Record<string, unknown>> : []
1469
+ const lines = [title]
1470
+ appendTextSection(lines, 'Counts', [`routes: ${routes.length}`])
1471
+ appendTextSection(lines, 'Routes', routes.map((route) => (
1472
+ `${renderManifestScalar(route.method)} ${renderManifestScalar(route.pattern)} -> ${renderManifestScalar(route.operation)}`
1473
+ )))
1474
+ return lines
1475
+ }
1476
+
1477
+ if (artifactId === 'agent') {
1478
+ const manifest = toRecord(value)
1479
+ const tools = Array.isArray(manifest.tools) ? manifest.tools as Array<Record<string, unknown>> : []
1480
+ const lines = [title]
1481
+ appendTextSection(lines, 'Identity', [
1482
+ `kind: ${renderManifestScalar(manifest.kind)}`,
1483
+ `version: ${renderManifestScalar(manifest.version)}`,
1484
+ `domain: ${renderManifestScalar(manifest.domain)}`,
1485
+ ])
1486
+ appendTextSection(lines, 'Counts', [`tools: ${tools.length}`])
1487
+ appendTextSection(lines, 'Tools', tools.map((tool) => {
1488
+ const toolId = renderManifestScalar(tool.toolId)
1489
+ const summary = normalizeNonEmptyString(tool.summary)
1490
+ return summary ? `${toolId} ${summary}` : toolId
1491
+ }))
1492
+ return lines
1493
+ }
1494
+
1495
+ if (artifactId === 'cli') {
1496
+ const projection = value as DocmanCliProjection
1497
+ const commands = Array.isArray(projection.commands) ? projection.commands : []
1498
+ const staticCount = commands.filter((command: DocmanCliCommandDescriptor) => command.kind !== 'operation').length
1499
+ const operationCount = commands.filter((command: DocmanCliCommandDescriptor) => command.kind === 'operation').length
1500
+ const lines = [title]
1501
+ appendTextSection(lines, 'Identity', [
1502
+ `kind: ${projection.kind}`,
1503
+ `version: ${projection.version}`,
1504
+ `domain: ${projection.domain}`,
1505
+ ])
1506
+ appendTextSection(lines, 'Counts', [
1507
+ `commands: ${commands.length}`,
1508
+ `static/root commands: ${staticCount}`,
1509
+ `operation commands: ${operationCount}`,
1510
+ `artifacts: ${projection.artifacts.length}`,
1511
+ ])
1512
+ appendTextSection(lines, 'Source of Truth', projection.sourceOfTruth.notes)
1513
+ appendTextSection(lines, 'Browse', [
1514
+ 'docman manifest get cli --path commandsById.config.set',
1515
+ 'docman manifest get cli --path commandsById.document.list',
1516
+ 'docman manifest show cli --path commandsById.document.compose.fetch',
1517
+ ])
1518
+ appendTextSection(lines, 'Commands', commands.map((command: DocmanCliCommandDescriptor) => {
1519
+ const summary = normalizeNonEmptyString(command.summary)
1520
+ return summary ? `${command.id} ${summary}` : command.id
1521
+ }))
1522
+ return lines
1523
+ }
1524
+
1525
+ if (artifactId === 'host-registration') {
1526
+ const manifest = toRecord(value)
1527
+ const providers = Array.isArray(manifest.manifestProviders)
1528
+ ? manifest.manifestProviders as Array<Record<string, unknown>>
1529
+ : []
1530
+ const plugins = Array.isArray(manifest.plugins)
1531
+ ? manifest.plugins as Array<Record<string, unknown>>
1532
+ : []
1533
+ const lines = [title]
1534
+ appendTextSection(lines, 'Identity', [
1535
+ `kind: ${renderManifestScalar(manifest.kind)}`,
1536
+ `domain: ${renderManifestScalar(manifest.domain)}`,
1537
+ `packageName: ${renderManifestScalar(manifest.packageName)}`,
1538
+ `baseDir: ${renderManifestScalar(manifest.baseDir)}`,
1539
+ ])
1540
+ appendTextSection(lines, 'Description', [normalizeNonEmptyString(manifest.description) ?? ''])
1541
+ appendTextSection(lines, 'Manifest Providers', providers.map((provider) => (
1542
+ `${renderManifestScalar(provider.id)} -> ${renderManifestScalar(provider.module)}#${renderManifestScalar(provider.exportName)}`
1543
+ )))
1544
+ appendTextSection(lines, 'Plugins', plugins.map((plugin) => (
1545
+ `${renderManifestScalar(plugin.domain)} -> ${renderManifestScalar(plugin.module)}#${renderManifestScalar(plugin.factory)}`
1546
+ )))
1547
+ appendTextSection(lines, 'Notes', toStringList(manifest.notes))
1548
+ return lines
1549
+ }
1550
+
1551
+ const operations = Array.isArray(value) ? value as Array<Record<string, unknown>> : []
1552
+ const lines = [title]
1553
+ appendTextSection(lines, 'Counts', [`operations: ${operations.length}`])
1554
+ appendTextSection(lines, 'Operations', operations.map((operation) => {
1555
+ const operationId = renderManifestScalar(operation.operationId)
1556
+ const summary = normalizeNonEmptyString(operation.summary)
1557
+ return summary ? `${operationId} ${summary}` : operationId
1558
+ }))
1559
+ return lines
1560
+ }
1561
+
1562
+ async function handleManifest(parsed: ParsedArgv): Promise<void> {
1563
+ const subcommand = normalizeManifestHelpSubcommand(parsed.positionals[1]?.toLowerCase())
1564
+ const payloads = await buildManifestPayloads()
1565
+ const projection = payloads.cli
1566
+
1567
+ if (!subcommand || subcommand === 'all') {
1568
+ printJson({
1569
+ dcm: payloads.dcm,
1570
+ routes: payloads.routes,
1571
+ agent: payloads.agent,
1572
+ cli: payloads.cli,
1573
+ hostRegistration: payloads.hostRegistration,
1574
+ operations: payloads.operations,
1575
+ })
1576
+ return
1577
+ }
1578
+
1579
+ if (subcommand === 'get' || subcommand === 'show') {
1580
+ const artifactInput = parsed.positionals[2]
1581
+ if (!artifactInput) throw new Error('missing_required_option:<artifact>')
1582
+ const artifactId = resolveManifestArtifactId(artifactInput, projection)
1583
+ if (!artifactId) throw new Error(`unknown_manifest_artifact:${artifactInput}`)
1584
+ const rawPath = getStringOption(parsed, 'path')
1585
+ const target = getManifestArtifactValue(payloads, artifactId)
1586
+ const resolved = resolveManifestPathValue(target, rawPath)
1587
+ if (!resolved.found) {
1588
+ throw new Error(`manifest_path_not_found:${artifactId}:${rawPath ?? '<root>'}`)
1589
+ }
1590
+ if (subcommand === 'get') {
1591
+ printJson(resolved.value)
1592
+ return
1593
+ }
1594
+ printHelpLines(buildManifestShowLines(artifactId, resolved.value, rawPath))
1595
+ return
1596
+ }
1597
+
1598
+ const artifactId = resolveManifestArtifactId(subcommand, projection)
1599
+ if (!artifactId) {
1600
+ throw new Error(`unknown_manifest_subcommand:${subcommand}`)
1601
+ }
1602
+ printJson(getManifestArtifactValue(payloads, artifactId))
1603
+ }
1604
+
1605
+ async function executeResolvedOperation(
1606
+ operationId: string,
1607
+ parsed: ParsedArgv,
1608
+ runtime: RuntimeContext,
1609
+ operationMap: ReadonlyMap<string, DocmanOperationSpec>,
1610
+ options?: { allowHooks?: boolean },
1611
+ ): Promise<void> {
1612
+ const allowHooks = options?.allowHooks !== false
1613
+ if (allowHooks) {
1614
+ const handledByHook = await commandSurface.tryExecuteOperationViaHook(operationId, parsed)
1615
+ if (handledByHook) return
1616
+ }
1617
+
1618
+ const { tooling } = await getDocmanCliRuntimeModules()
1619
+ const operation = operationMap.get(operationId) ?? tooling.getDocmanOperationSpecById(operationId, { refresh: true })
1620
+ if (!operation) {
1621
+ throw new Error(`unknown_docman_operation:${operationId}`)
1622
+ }
1623
+
1624
+ const input = buildGeneratedOperationInput(operation, parsed, runtime)
1625
+ const output = await runDocmanOperationById(operation.operationId, input)
1626
+ printJson(output)
1627
+ }
1628
+
1629
+ async function main(): Promise<void> {
1630
+ const parsed = parseArgv(process.argv.slice(2))
1631
+ const helpMode = parsed.positionals[0]?.toLowerCase() === 'help'
1632
+ const effectiveParsed: ParsedArgv = helpMode
1633
+ ? {
1634
+ positionals: parsed.positionals.slice(1),
1635
+ options: {
1636
+ ...parsed.options,
1637
+ help: true,
1638
+ },
1639
+ }
1640
+ : parsed
1641
+ const command = effectiveParsed.positionals[0]?.toLowerCase()
1642
+ const subcommand = effectiveParsed.positionals[1]?.toLowerCase()
1643
+ const helpRequested = getBooleanOption(effectiveParsed, 'help', false)
1644
+
1645
+ if (getBooleanOption(parsed, 'version', false) || (command === 'version' && !helpRequested)) {
1646
+ printVersion()
1647
+ return
1648
+ }
1649
+
1650
+ if (!command || helpRequested) {
1651
+ if (command && await tryPrintCommandHelp(effectiveParsed)) return
1652
+ await printHelp()
1653
+ return
1654
+ }
1655
+
1656
+ if (command === 'tools') {
1657
+ const { tooling } = await getDocmanCliRuntimeModules()
1658
+ printJson(tooling.listDocmanToolingTools({ refresh: true }))
1659
+ return
1660
+ }
1661
+
1662
+ if (command === 'init') {
1663
+ await handleInit(effectiveParsed)
1664
+ return
1665
+ }
1666
+
1667
+ if (command === 'setup') {
1668
+ await handleSetup(subcommand, effectiveParsed)
1669
+ return
1670
+ }
1671
+
1672
+ if (command === 'ops') {
1673
+ const { tooling } = await getDocmanCliRuntimeModules()
1674
+ printJson(tooling.listDocmanToolingOperations({ refresh: true }))
1675
+ return
1676
+ }
1677
+
1678
+ if (command === 'manifest') {
1679
+ await handleManifest(effectiveParsed)
1680
+ return
1681
+ }
1682
+
1683
+ if (command === 'config') {
1684
+ await commandSurface.handleConfig(subcommand, effectiveParsed)
1685
+ return
1686
+ }
1687
+
1688
+ const { tooling } = await getDocmanCliRuntimeModules()
1689
+ const operationSpecs = tooling.listDocmanToolingOperations({ refresh: true })
1690
+ const operationMap = new Map(
1691
+ operationSpecs.map((operation) => [normalizeOperationIdentifier(operation.operationId), operation]),
1692
+ )
1693
+ const resolvedOperationId = await resolveGeneratedOperationId(effectiveParsed, operationMap)
1694
+ const rawOperationMode = command ? DIRECT_OPERATION_COMMANDS.has(command) : false
1695
+ const hooksDisabled = areHooksDisabled(effectiveParsed)
1696
+
1697
+ if (!resolvedOperationId && DIRECT_OPERATION_COMMANDS.has(command)) {
1698
+ throw new Error(`unknown_operation_command:${command}`)
1699
+ }
1700
+
1701
+ if (command === 'tool' || command === 'invoke') {
1702
+ await buildRuntimeContext(effectiveParsed)
1703
+ const identifier = requireStringOption(effectiveParsed, 'id', '--id')
1704
+ const input = parseJsonInput(getStringOption(effectiveParsed, 'input'), 'input') ?? {}
1705
+ const output = await runDocmanToolById(identifier, input)
1706
+ printJson(output)
1707
+ return
1708
+ }
1709
+
1710
+ const runtime = await buildRuntimeContext(effectiveParsed)
1711
+
1712
+ if (resolvedOperationId) {
1713
+ const allowHooks = !rawOperationMode && !hooksDisabled
1714
+ await executeResolvedOperation(resolvedOperationId, effectiveParsed, runtime, operationMap, { allowHooks })
1715
+ return
1716
+ }
1717
+
1718
+ throw new Error(`unknown_command:${command}`)
1719
+ }
1720
+
1721
+ async function disconnectRepoHandles(): Promise<void> {
1722
+ try {
1723
+ const drizzleModule = await import('@aopslab/xf-db-drizzle')
1724
+ const tasks: Promise<unknown>[] = []
1725
+ if (typeof drizzleModule.drizzleDisconnect === 'function') {
1726
+ tasks.push(drizzleModule.drizzleDisconnect())
1727
+ }
1728
+ if (typeof drizzleModule.drizzleSqliteDisconnect === 'function') {
1729
+ tasks.push(drizzleModule.drizzleSqliteDisconnect())
1730
+ }
1731
+ if (tasks.length > 0) {
1732
+ await Promise.allSettled(tasks)
1733
+ }
1734
+ } catch {}
1735
+ }
1736
+
1737
+ void (async () => {
1738
+ try {
1739
+ await main()
1740
+ } catch (error) {
1741
+ const message = error instanceof Error ? error.message : String(error)
1742
+ process.stderr.write(`${message}\n`)
1743
+ process.exitCode = 1
1744
+ } finally {
1745
+ await disconnectRepoHandles()
1746
+ }
1747
+ })()