@ontologie/cli 0.1.0-preview.1

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 (266) hide show
  1. package/README.md +492 -0
  2. package/dist/cache/keys.d.ts +11 -0
  3. package/dist/cache/keys.d.ts.map +1 -0
  4. package/dist/cache/keys.js +14 -0
  5. package/dist/cache/keys.js.map +1 -0
  6. package/dist/cache/store.d.ts +23 -0
  7. package/dist/cache/store.d.ts.map +1 -0
  8. package/dist/cache/store.js +160 -0
  9. package/dist/cache/store.js.map +1 -0
  10. package/dist/cli-compat.d.ts +6 -0
  11. package/dist/cli-compat.d.ts.map +1 -0
  12. package/dist/cli-compat.js +11 -0
  13. package/dist/cli-compat.js.map +1 -0
  14. package/dist/cli.d.ts +30 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +119 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/client.d.ts +7 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +15 -0
  21. package/dist/client.js.map +1 -0
  22. package/dist/commands/actions.d.ts +22 -0
  23. package/dist/commands/actions.d.ts.map +1 -0
  24. package/dist/commands/actions.js +211 -0
  25. package/dist/commands/actions.js.map +1 -0
  26. package/dist/commands/agent-files.d.ts +27 -0
  27. package/dist/commands/agent-files.d.ts.map +1 -0
  28. package/dist/commands/agent-files.js +167 -0
  29. package/dist/commands/agent-files.js.map +1 -0
  30. package/dist/commands/agent.d.ts +23 -0
  31. package/dist/commands/agent.d.ts.map +1 -0
  32. package/dist/commands/agent.js +314 -0
  33. package/dist/commands/agent.js.map +1 -0
  34. package/dist/commands/audit.d.ts +11 -0
  35. package/dist/commands/audit.d.ts.map +1 -0
  36. package/dist/commands/audit.js +94 -0
  37. package/dist/commands/audit.js.map +1 -0
  38. package/dist/commands/cache.d.ts +8 -0
  39. package/dist/commands/cache.d.ts.map +1 -0
  40. package/dist/commands/cache.js +40 -0
  41. package/dist/commands/cache.js.map +1 -0
  42. package/dist/commands/capabilities.d.ts +56 -0
  43. package/dist/commands/capabilities.d.ts.map +1 -0
  44. package/dist/commands/capabilities.js +304 -0
  45. package/dist/commands/capabilities.js.map +1 -0
  46. package/dist/commands/check.d.ts +7 -0
  47. package/dist/commands/check.d.ts.map +1 -0
  48. package/dist/commands/check.js +16 -0
  49. package/dist/commands/check.js.map +1 -0
  50. package/dist/commands/config.d.ts +6 -0
  51. package/dist/commands/config.d.ts.map +1 -0
  52. package/dist/commands/config.js +133 -0
  53. package/dist/commands/config.js.map +1 -0
  54. package/dist/commands/context.d.ts +6 -0
  55. package/dist/commands/context.d.ts.map +1 -0
  56. package/dist/commands/context.js +226 -0
  57. package/dist/commands/context.js.map +1 -0
  58. package/dist/commands/dev.d.ts +15 -0
  59. package/dist/commands/dev.d.ts.map +1 -0
  60. package/dist/commands/dev.js +62 -0
  61. package/dist/commands/dev.js.map +1 -0
  62. package/dist/commands/docs-alias.d.ts +14 -0
  63. package/dist/commands/docs-alias.d.ts.map +1 -0
  64. package/dist/commands/docs-alias.js +28 -0
  65. package/dist/commands/docs-alias.js.map +1 -0
  66. package/dist/commands/docs.d.ts +6 -0
  67. package/dist/commands/docs.d.ts.map +1 -0
  68. package/dist/commands/docs.js +67 -0
  69. package/dist/commands/docs.js.map +1 -0
  70. package/dist/commands/doctor.d.ts +6 -0
  71. package/dist/commands/doctor.d.ts.map +1 -0
  72. package/dist/commands/doctor.js +161 -0
  73. package/dist/commands/doctor.js.map +1 -0
  74. package/dist/commands/generate.d.ts +7 -0
  75. package/dist/commands/generate.d.ts.map +1 -0
  76. package/dist/commands/generate.js +36 -0
  77. package/dist/commands/generate.js.map +1 -0
  78. package/dist/commands/graph.d.ts +9 -0
  79. package/dist/commands/graph.d.ts.map +1 -0
  80. package/dist/commands/graph.js +149 -0
  81. package/dist/commands/graph.js.map +1 -0
  82. package/dist/commands/import.d.ts +19 -0
  83. package/dist/commands/import.d.ts.map +1 -0
  84. package/dist/commands/import.js +330 -0
  85. package/dist/commands/import.js.map +1 -0
  86. package/dist/commands/index.d.ts +80 -0
  87. package/dist/commands/index.d.ts.map +1 -0
  88. package/dist/commands/index.js +345 -0
  89. package/dist/commands/index.js.map +1 -0
  90. package/dist/commands/init.d.ts +7 -0
  91. package/dist/commands/init.d.ts.map +1 -0
  92. package/dist/commands/init.js +101 -0
  93. package/dist/commands/init.js.map +1 -0
  94. package/dist/commands/instances.d.ts +7 -0
  95. package/dist/commands/instances.d.ts.map +1 -0
  96. package/dist/commands/instances.js +418 -0
  97. package/dist/commands/instances.js.map +1 -0
  98. package/dist/commands/keys.d.ts +6 -0
  99. package/dist/commands/keys.d.ts.map +1 -0
  100. package/dist/commands/keys.js +113 -0
  101. package/dist/commands/keys.js.map +1 -0
  102. package/dist/commands/knowledge.d.ts +6 -0
  103. package/dist/commands/knowledge.d.ts.map +1 -0
  104. package/dist/commands/knowledge.js +76 -0
  105. package/dist/commands/knowledge.js.map +1 -0
  106. package/dist/commands/model.d.ts +3 -0
  107. package/dist/commands/model.d.ts.map +1 -0
  108. package/dist/commands/model.js +40 -0
  109. package/dist/commands/model.js.map +1 -0
  110. package/dist/commands/nodes.d.ts +6 -0
  111. package/dist/commands/nodes.d.ts.map +1 -0
  112. package/dist/commands/nodes.js +111 -0
  113. package/dist/commands/nodes.js.map +1 -0
  114. package/dist/commands/openapi.d.ts +7 -0
  115. package/dist/commands/openapi.d.ts.map +1 -0
  116. package/dist/commands/openapi.js +17 -0
  117. package/dist/commands/openapi.js.map +1 -0
  118. package/dist/commands/plan.d.ts +19 -0
  119. package/dist/commands/plan.d.ts.map +1 -0
  120. package/dist/commands/plan.js +563 -0
  121. package/dist/commands/plan.js.map +1 -0
  122. package/dist/commands/process.d.ts +3 -0
  123. package/dist/commands/process.d.ts.map +1 -0
  124. package/dist/commands/process.js +67 -0
  125. package/dist/commands/process.js.map +1 -0
  126. package/dist/commands/query.d.ts +26 -0
  127. package/dist/commands/query.d.ts.map +1 -0
  128. package/dist/commands/query.js +253 -0
  129. package/dist/commands/query.js.map +1 -0
  130. package/dist/commands/schema.d.ts +24 -0
  131. package/dist/commands/schema.d.ts.map +1 -0
  132. package/dist/commands/schema.js +933 -0
  133. package/dist/commands/schema.js.map +1 -0
  134. package/dist/commands/search.d.ts +10 -0
  135. package/dist/commands/search.d.ts.map +1 -0
  136. package/dist/commands/search.js +74 -0
  137. package/dist/commands/search.js.map +1 -0
  138. package/dist/commands/shared.d.ts +32 -0
  139. package/dist/commands/shared.d.ts.map +1 -0
  140. package/dist/commands/shared.js +63 -0
  141. package/dist/commands/shared.js.map +1 -0
  142. package/dist/commands/usage.d.ts +6 -0
  143. package/dist/commands/usage.d.ts.map +1 -0
  144. package/dist/commands/usage.js +86 -0
  145. package/dist/commands/usage.js.map +1 -0
  146. package/dist/commands/validators.d.ts +30 -0
  147. package/dist/commands/validators.d.ts.map +1 -0
  148. package/dist/commands/validators.js +93 -0
  149. package/dist/commands/validators.js.map +1 -0
  150. package/dist/commands/whoami.d.ts +6 -0
  151. package/dist/commands/whoami.d.ts.map +1 -0
  152. package/dist/commands/whoami.js +48 -0
  153. package/dist/commands/whoami.js.map +1 -0
  154. package/dist/config.d.ts +47 -0
  155. package/dist/config.d.ts.map +1 -0
  156. package/dist/config.js +127 -0
  157. package/dist/config.js.map +1 -0
  158. package/dist/credentials.d.ts +9 -0
  159. package/dist/credentials.d.ts.map +1 -0
  160. package/dist/credentials.js +79 -0
  161. package/dist/credentials.js.map +1 -0
  162. package/dist/keychain.d.ts +9 -0
  163. package/dist/keychain.d.ts.map +1 -0
  164. package/dist/keychain.js +51 -0
  165. package/dist/keychain.js.map +1 -0
  166. package/dist/output/csv.d.ts +5 -0
  167. package/dist/output/csv.d.ts.map +1 -0
  168. package/dist/output/csv.js +22 -0
  169. package/dist/output/csv.js.map +1 -0
  170. package/dist/output/envelope.schema.d.ts +1053 -0
  171. package/dist/output/envelope.schema.d.ts.map +1 -0
  172. package/dist/output/envelope.schema.js +256 -0
  173. package/dist/output/envelope.schema.js.map +1 -0
  174. package/dist/output/errors.d.ts +58 -0
  175. package/dist/output/errors.d.ts.map +1 -0
  176. package/dist/output/errors.js +339 -0
  177. package/dist/output/errors.js.map +1 -0
  178. package/dist/output/formatter.d.ts +27 -0
  179. package/dist/output/formatter.d.ts.map +1 -0
  180. package/dist/output/formatter.js +80 -0
  181. package/dist/output/formatter.js.map +1 -0
  182. package/dist/output/json.d.ts +41 -0
  183. package/dist/output/json.d.ts.map +1 -0
  184. package/dist/output/json.js +215 -0
  185. package/dist/output/json.js.map +1 -0
  186. package/dist/output/markdown.d.ts +6 -0
  187. package/dist/output/markdown.d.ts.map +1 -0
  188. package/dist/output/markdown.js +51 -0
  189. package/dist/output/markdown.js.map +1 -0
  190. package/dist/output/meta.d.ts +49 -0
  191. package/dist/output/meta.d.ts.map +1 -0
  192. package/dist/output/meta.js +96 -0
  193. package/dist/output/meta.js.map +1 -0
  194. package/dist/output/plain.d.ts +6 -0
  195. package/dist/output/plain.d.ts.map +1 -0
  196. package/dist/output/plain.js +51 -0
  197. package/dist/output/plain.js.map +1 -0
  198. package/dist/output/table.d.ts +5 -0
  199. package/dist/output/table.d.ts.map +1 -0
  200. package/dist/output/table.js +54 -0
  201. package/dist/output/table.js.map +1 -0
  202. package/dist/output/types.d.ts +54 -0
  203. package/dist/output/types.d.ts.map +1 -0
  204. package/dist/output/types.js +80 -0
  205. package/dist/output/types.js.map +1 -0
  206. package/dist/output/warnings.d.ts +15 -0
  207. package/dist/output/warnings.d.ts.map +1 -0
  208. package/dist/output/warnings.js +46 -0
  209. package/dist/output/warnings.js.map +1 -0
  210. package/dist/output/yaml.d.ts +6 -0
  211. package/dist/output/yaml.d.ts.map +1 -0
  212. package/dist/output/yaml.js +9 -0
  213. package/dist/output/yaml.js.map +1 -0
  214. package/dist/schema/breaking-changes.d.ts +17 -0
  215. package/dist/schema/breaking-changes.d.ts.map +1 -0
  216. package/dist/schema/breaking-changes.js +108 -0
  217. package/dist/schema/breaking-changes.js.map +1 -0
  218. package/dist/schema/helpers.d.ts +18 -0
  219. package/dist/schema/helpers.d.ts.map +1 -0
  220. package/dist/schema/helpers.js +48 -0
  221. package/dist/schema/helpers.js.map +1 -0
  222. package/dist/schema/load-schema-file.d.ts +13 -0
  223. package/dist/schema/load-schema-file.d.ts.map +1 -0
  224. package/dist/schema/load-schema-file.js +88 -0
  225. package/dist/schema/load-schema-file.js.map +1 -0
  226. package/dist/schema/lockfile.d.ts +6 -0
  227. package/dist/schema/lockfile.d.ts.map +1 -0
  228. package/dist/schema/lockfile.js +34 -0
  229. package/dist/schema/lockfile.js.map +1 -0
  230. package/dist/schema/manifest-client.d.ts +31 -0
  231. package/dist/schema/manifest-client.d.ts.map +1 -0
  232. package/dist/schema/manifest-client.js +282 -0
  233. package/dist/schema/manifest-client.js.map +1 -0
  234. package/dist/schema/output.d.ts +38 -0
  235. package/dist/schema/output.d.ts.map +1 -0
  236. package/dist/schema/output.js +95 -0
  237. package/dist/schema/output.js.map +1 -0
  238. package/dist/stdin.d.ts +8 -0
  239. package/dist/stdin.d.ts.map +1 -0
  240. package/dist/stdin.js +21 -0
  241. package/dist/stdin.js.map +1 -0
  242. package/dist/templates/basic.d.ts +6 -0
  243. package/dist/templates/basic.d.ts.map +1 -0
  244. package/dist/templates/basic.js +90 -0
  245. package/dist/templates/basic.js.map +1 -0
  246. package/dist/templates/contract-review.d.ts +9 -0
  247. package/dist/templates/contract-review.d.ts.map +1 -0
  248. package/dist/templates/contract-review.js +172 -0
  249. package/dist/templates/contract-review.js.map +1 -0
  250. package/dist/templates/customer-onboarding.d.ts +9 -0
  251. package/dist/templates/customer-onboarding.d.ts.map +1 -0
  252. package/dist/templates/customer-onboarding.js +176 -0
  253. package/dist/templates/customer-onboarding.js.map +1 -0
  254. package/dist/templates/index.d.ts +11 -0
  255. package/dist/templates/index.d.ts.map +1 -0
  256. package/dist/templates/index.js +16 -0
  257. package/dist/templates/index.js.map +1 -0
  258. package/dist/templates/react-dashboard.d.ts +6 -0
  259. package/dist/templates/react-dashboard.d.ts.map +1 -0
  260. package/dist/templates/react-dashboard.js +146 -0
  261. package/dist/templates/react-dashboard.js.map +1 -0
  262. package/dist/templates/vendor-risk.d.ts +9 -0
  263. package/dist/templates/vendor-risk.d.ts.map +1 -0
  264. package/dist/templates/vendor-risk.js +186 -0
  265. package/dist/templates/vendor-risk.js.map +1 -0
  266. package/package.json +47 -0
@@ -0,0 +1,933 @@
1
+ /**
2
+ * ontologie schema -- export, diff, push, pull, check, describe, search
3
+ *
4
+ * Rule: stdout = command result, stderr = diagnostics/progress.
5
+ * Machine-readable formats never mix human diagnostics into stdout.
6
+ *
7
+ * Fixes over V2:
8
+ * - schema describe resolves interfaces, linkTypes, and actions (not just objectTypes)
9
+ * - schema check outputs structured JSON on stdout (not just stderr)
10
+ * - schema search validates --types against allowed values
11
+ * - schema pull creates parent directories before writing
12
+ * - schema push refuses incomplete remote manifest (no silent action duplication)
13
+ */
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { resolveConfig } from '../config.js';
17
+ import { createDfClient } from '../client.js';
18
+ import { buildCliMeta, extractMetaFromHeaders } from '../output/meta.js';
19
+ import { output, resolveFormat } from '../output/formatter.js';
20
+ import { withErrorHandler } from './shared.js';
21
+ import { compile, diff, formatDiff, planPush, executePush, emitSchema, generateLockfile, verifyLockfile, parseLockfile, serializeLockfile, } from '@dataforge/schema';
22
+ // Schema modules
23
+ import { loadSchemaFile } from '../schema/load-schema-file.js';
24
+ import { fetchFullManifest, fetchManifestVersion, isEndpointUnavailable } from '../schema/manifest-client.js';
25
+ import { resolveLockfilePath, writeTextFileAtomic } from '../schema/lockfile.js';
26
+ import { isStructuredFormat, readCommandName, readCommandLabel, failCli } from '../schema/output.js';
27
+ import { getBreakingChanges } from '../schema/breaking-changes.js';
28
+ import { isRecord, readOptionalTextFile } from '../schema/helpers.js';
29
+ import { getExitCode, CliUsageError, redactSecretsDeep } from '../output/errors.js';
30
+ const DEFAULT_SCHEMA_FILE = 'dataforge.schema.ts';
31
+ // ---------------------------------------------------------------------------
32
+ // Deterministic diff sorting
33
+ // ---------------------------------------------------------------------------
34
+ function compareStable(a, b) {
35
+ if (a < b)
36
+ return -1;
37
+ if (a > b)
38
+ return 1;
39
+ return 0;
40
+ }
41
+ export function sortDiffChanges(changes) {
42
+ return [...changes].sort((a, b) => {
43
+ const e = compareStable(a.entity ?? '', b.entity ?? '');
44
+ if (e !== 0)
45
+ return e;
46
+ const n = compareStable(a.name ?? '', b.name ?? '');
47
+ if (n !== 0)
48
+ return n;
49
+ return compareStable(a.kind ?? '', b.kind ?? '');
50
+ });
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Search type validation
54
+ // ---------------------------------------------------------------------------
55
+ export const ALLOWED_SEARCH_TYPES = new Set(['ObjectType', 'LinkType', 'Interface', 'Action']);
56
+ export function parseSearchTypes(raw) {
57
+ const types = [...new Set(raw
58
+ .split(',')
59
+ .map((type) => type.trim())
60
+ .filter(Boolean))];
61
+ if (types.length === 0) {
62
+ throw new CliUsageError('At least one schema search type must be provided.', {
63
+ allowed: [...ALLOWED_SEARCH_TYPES],
64
+ });
65
+ }
66
+ const invalid = types.filter((type) => !ALLOWED_SEARCH_TYPES.has(type));
67
+ if (invalid.length > 0) {
68
+ throw new CliUsageError('Invalid schema search type(s).', {
69
+ invalid,
70
+ allowed: [...ALLOWED_SEARCH_TYPES],
71
+ });
72
+ }
73
+ return types.join(',');
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Limit validation
77
+ // ---------------------------------------------------------------------------
78
+ export function parseLimit(raw, defaultValue = 20, maxValue = 100) {
79
+ if (raw === undefined || raw === null || raw === '')
80
+ return defaultValue;
81
+ const limit = Number(raw);
82
+ if (!Number.isInteger(limit) || limit < 1 || limit > maxValue) {
83
+ throw new CliUsageError(`Invalid --limit value: ${String(raw)}`, {
84
+ min: 1,
85
+ max: maxValue,
86
+ default: defaultValue,
87
+ });
88
+ }
89
+ return limit;
90
+ }
91
+ function findDescribeMatches(manifest, query) {
92
+ return [
93
+ ...manifest.objectTypes
94
+ .filter((t) => t.apiName === query || t.displayName === query)
95
+ .map((value) => ({ entity: 'objectType', value })),
96
+ ...manifest.interfaces
97
+ .filter((i) => i.apiName === query || i.displayName === query)
98
+ .map((value) => ({ entity: 'interface', value })),
99
+ ...manifest.linkTypes
100
+ .filter((l) => l.apiName === query || l.relationshipType === query || l.label === query)
101
+ .map((value) => ({ entity: 'linkType', value })),
102
+ ...manifest.actions
103
+ .filter((a) => a.apiName === query ||
104
+ a.displayName === query ||
105
+ (a.objectTypeApiName ? `${a.objectTypeApiName}.${a.apiName}` === query : false))
106
+ .map((value) => ({ entity: 'action', value })),
107
+ ];
108
+ }
109
+ // ---------------------------------------------------------------------------
110
+ // Shared local schema compilation
111
+ // ---------------------------------------------------------------------------
112
+ async function compileLocalSchema(schemaPath, config) {
113
+ const loaded = await loadSchemaFile(schemaPath);
114
+ const objectTypes = loaded.objectTypes ?? [];
115
+ const actions = loaded.actions ?? [];
116
+ const manifest = compile(objectTypes, {
117
+ workspaceId: config.workspaceId,
118
+ ...(config.spaceId ? { espaceId: config.spaceId } : {}),
119
+ actions: actions.length > 0 ? actions : undefined,
120
+ });
121
+ return { schemaPath, objectTypes, actions, manifest };
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Command registration
125
+ // ---------------------------------------------------------------------------
126
+ export function registerSchema(program) {
127
+ const schema = program
128
+ .command('schema')
129
+ .description('Ontology schema operations');
130
+ const getFormat = () => {
131
+ const f = program.opts();
132
+ return f.format === 'json' ? 'json' : f.format === 'jsonl' ? 'jsonl' : 'text';
133
+ };
134
+ schema
135
+ .command('export')
136
+ .description('Export full ontology manifest')
137
+ .action((opts) => withErrorHandler(() => runSchemaExport(program, opts), getFormat)());
138
+ schema
139
+ .command('describe [typeName]')
140
+ .description('Describe ontology schema — all types or a specific type (objectType, linkType, interface, or action)')
141
+ .action((typeName, opts) => withErrorHandler(() => runSchemaDescribe(program, typeName, opts), getFormat)());
142
+ schema
143
+ .command('search <query>')
144
+ .description('Search ontology types and properties by keyword')
145
+ .option('--types <types>', 'Filter by node type (ObjectType, LinkType, Interface, Action)', 'ObjectType,LinkType')
146
+ .option('--limit <n>', 'Maximum number of results (1-100)', '20')
147
+ .option('--include-properties', 'Include property details in results')
148
+ .action((query, opts) => withErrorHandler(() => runSchemaSearch(program, query, opts), getFormat)());
149
+ schema
150
+ .command('diff')
151
+ .description('Compare local dataforge.schema.ts against remote ontology')
152
+ .option('--schema <path>', 'Path to schema file', DEFAULT_SCHEMA_FILE)
153
+ .option('--exit-code', 'Exit with SCHEMA_DRIFT_ERROR when differences are found', false)
154
+ .action((opts) => withErrorHandler(() => runSchemaDiff(program, opts), getFormat)());
155
+ schema
156
+ .command('push')
157
+ .description('Push local schema changes to remote ontology')
158
+ .option('--schema <path>', 'Path to schema file', DEFAULT_SCHEMA_FILE)
159
+ .option('--yes', 'Skip confirmation prompt', false)
160
+ .option('--dry-run', 'Show plan without executing', false)
161
+ .option('--allow-breaking', 'Allow destructive schema changes', false)
162
+ .option('--include-payload', 'Include full command payload in dry-run output', false)
163
+ .action((opts) => withErrorHandler(() => runSchemaPush(program, opts), getFormat)());
164
+ schema
165
+ .command('pull')
166
+ .description('Generate dataforge.schema.ts from remote ontology')
167
+ .option('--output <path>', 'Output file path', DEFAULT_SCHEMA_FILE)
168
+ .option('--force', 'Overwrite output file if it already exists', false)
169
+ .action((opts) => withErrorHandler(() => runSchemaPull(program, opts), getFormat)());
170
+ schema
171
+ .command('check')
172
+ .description('Compile schema and verify lockfile integrity')
173
+ .option('--schema <path>', 'Path to schema file', DEFAULT_SCHEMA_FILE)
174
+ .option('--fail-on-drift', 'Compare lockfile manifestVersion vs remote and exit 1 on mismatch', false)
175
+ .action((opts) => withErrorHandler(() => runSchemaCheck(program, opts), getFormat)());
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // schema export — full manifest via fetchFullManifest
179
+ // ---------------------------------------------------------------------------
180
+ async function runSchemaExport(program, _opts) {
181
+ const flags = program.opts();
182
+ const effectiveFlags = {
183
+ ...flags,
184
+ format: flags.format ?? (process.stdout.isTTY ? 'yaml' : flags.format),
185
+ };
186
+ const config = resolveConfig(flags);
187
+ const client = createDfClient(config);
188
+ const manifest = await fetchFullManifest(client, config);
189
+ let stats;
190
+ try {
191
+ stats = await client.transport.request({
192
+ method: 'GET',
193
+ path: '/api/v1/ontology/stats',
194
+ query: config.spaceId ? { espaceId: config.spaceId } : undefined,
195
+ });
196
+ }
197
+ catch (error) {
198
+ if (!isEndpointUnavailable(error)) {
199
+ process.stderr.write(`Warning: could not fetch ontology stats: ${String(error)}\n`);
200
+ }
201
+ }
202
+ const schemaExport = {
203
+ ...manifest,
204
+ statistics: {
205
+ objectTypes: manifest.objectTypes.length,
206
+ linkTypes: manifest.linkTypes.length,
207
+ interfaces: manifest.interfaces?.length ?? 0,
208
+ actions: manifest.actions?.length ?? 0,
209
+ ...(stats ? { server: stats } : {}),
210
+ },
211
+ };
212
+ const meta = buildCliMeta({
213
+ command: 'schema.export',
214
+ workspaceId: config.workspaceId,
215
+ apiUrl: config.apiUrl,
216
+ manifestVersion: manifest.version ?? null,
217
+ });
218
+ output(schemaExport, meta, effectiveFlags);
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // schema describe — multi-entity resolution (objectType, linkType, interface, action)
222
+ // ---------------------------------------------------------------------------
223
+ async function runSchemaDescribe(program, typeName, _opts) {
224
+ const flags = program.opts();
225
+ const config = resolveConfig(flags);
226
+ const client = createDfClient(config);
227
+ const manifest = await fetchFullManifest(client, config);
228
+ const objectTypes = manifest.objectTypes ?? [];
229
+ const linkTypes = manifest.linkTypes ?? [];
230
+ const manifestActions = manifest.actions ?? [];
231
+ const manifestInterfaces = manifest.interfaces ?? [];
232
+ const meta = buildCliMeta({
233
+ command: 'schema.describe',
234
+ workspaceId: config.workspaceId,
235
+ apiUrl: config.apiUrl,
236
+ manifestVersion: manifest.version ?? null,
237
+ });
238
+ if (typeName) {
239
+ const matches = findDescribeMatches({ objectTypes, interfaces: manifestInterfaces, linkTypes, actions: manifestActions }, typeName);
240
+ // Ambiguity detection
241
+ if (matches.length > 1) {
242
+ failCli({
243
+ code: 'INVALID_USAGE',
244
+ message: `Schema entity name "${typeName}" is ambiguous.`,
245
+ details: {
246
+ query: typeName,
247
+ matches: matches.map((m) => ({
248
+ entity: m.entity,
249
+ apiName: 'apiName' in m.value ? m.value.apiName : null,
250
+ displayName: 'displayName' in m.value ? m.value.displayName : null,
251
+ })),
252
+ hint: 'Use a fully-qualified action name (e.g. ObjectType.actionName) to disambiguate.',
253
+ },
254
+ retryable: false,
255
+ correlationId: meta.requestId,
256
+ }, meta, flags);
257
+ return;
258
+ }
259
+ if (matches.length === 1) {
260
+ const match = matches[0];
261
+ switch (match.entity) {
262
+ case 'objectType': {
263
+ const ot = match.value;
264
+ const properties = ot.properties ?? [];
265
+ const outgoing = linkTypes.filter((l) => l.sourceTypeApiName === ot.apiName);
266
+ const incoming = linkTypes.filter((l) => l.targetTypeApiName === ot.apiName);
267
+ const actions = manifestActions.filter((a) => a.objectTypeApiName === ot.apiName);
268
+ output({
269
+ entity: 'objectType',
270
+ apiName: ot.apiName,
271
+ displayName: ot.displayName,
272
+ description: ot.description,
273
+ primaryKey: ot.primaryKey,
274
+ properties: properties.map((p) => ({
275
+ name: p.apiName,
276
+ dataType: p.dataType,
277
+ required: p.required,
278
+ indexed: p.indexed,
279
+ unique: p.unique,
280
+ semanticType: p.semanticType,
281
+ description: p.description,
282
+ })),
283
+ links: {
284
+ outgoing: outgoing.map((l) => ({
285
+ name: l.apiName.split('__').pop() ?? l.apiName,
286
+ target: l.targetTypeApiName,
287
+ cardinality: l.cardinality,
288
+ label: l.label,
289
+ })),
290
+ incoming: incoming.map((l) => ({
291
+ name: l.inverseName ?? l.apiName,
292
+ source: l.sourceTypeApiName,
293
+ cardinality: l.cardinality,
294
+ })),
295
+ },
296
+ actions: actions.map((a) => ({
297
+ apiName: a.apiName,
298
+ displayName: a.displayName,
299
+ actionType: a.actionType,
300
+ trigger: a.trigger,
301
+ parameters: a.parameters?.length ?? 0,
302
+ description: a.description,
303
+ })),
304
+ statistics: {
305
+ propertyCount: properties.length,
306
+ outgoingLinks: outgoing.length,
307
+ incomingLinks: incoming.length,
308
+ actionCount: actions.length,
309
+ },
310
+ }, meta, flags);
311
+ return;
312
+ }
313
+ case 'interface': {
314
+ const iface = match.value;
315
+ output({
316
+ entity: 'interface',
317
+ apiName: iface.apiName,
318
+ displayName: iface.displayName,
319
+ description: iface.description,
320
+ sharedProperties: (iface.sharedProperties ?? []).map((p) => ({
321
+ name: p.apiName,
322
+ dataType: p.dataType,
323
+ required: p.required,
324
+ indexed: p.indexed,
325
+ unique: p.unique,
326
+ semanticType: p.semanticType,
327
+ description: p.description,
328
+ })),
329
+ }, meta, flags);
330
+ return;
331
+ }
332
+ case 'linkType': {
333
+ const link = match.value;
334
+ output({
335
+ entity: 'linkType',
336
+ apiName: link.apiName,
337
+ source: link.sourceTypeApiName,
338
+ target: link.targetTypeApiName,
339
+ cardinality: link.cardinality,
340
+ relationshipType: link.relationshipType,
341
+ label: link.label,
342
+ inverseName: link.inverseName,
343
+ properties: link.properties ?? [],
344
+ }, meta, flags);
345
+ return;
346
+ }
347
+ case 'action': {
348
+ const action = match.value;
349
+ output({
350
+ entity: 'action',
351
+ apiName: action.apiName,
352
+ displayName: action.displayName,
353
+ objectTypeApiName: action.objectTypeApiName,
354
+ actionType: action.actionType,
355
+ trigger: action.trigger,
356
+ parameters: action.parameters ?? [],
357
+ conditions: action.conditions ?? [],
358
+ effects: action.effects ?? [],
359
+ requiredScopes: action.requiredScopes ?? [],
360
+ }, meta, flags);
361
+ return;
362
+ }
363
+ }
364
+ }
365
+ // Not found
366
+ failCli({
367
+ code: 'NOT_FOUND',
368
+ message: `Schema entity "${typeName}" not found.`,
369
+ details: {
370
+ query: typeName,
371
+ availableObjectTypes: objectTypes.map((t) => t.apiName),
372
+ availableInterfaces: manifestInterfaces.map((i) => i.apiName),
373
+ availableLinkTypes: linkTypes.map((l) => l.apiName),
374
+ availableActions: manifestActions.map((a) => a.objectTypeApiName ? `${a.objectTypeApiName}.${a.apiName}` : a.apiName),
375
+ },
376
+ retryable: false,
377
+ correlationId: meta.requestId,
378
+ }, meta, flags);
379
+ }
380
+ else {
381
+ // No typeName — describe all entity types
382
+ const overview = {
383
+ objectTypes: objectTypes.map((ot) => ({
384
+ entity: 'objectType',
385
+ apiName: ot.apiName,
386
+ displayName: ot.displayName,
387
+ properties: (ot.properties ?? []).length,
388
+ outgoingLinks: linkTypes.filter((l) => l.sourceTypeApiName === ot.apiName).length,
389
+ incomingLinks: linkTypes.filter((l) => l.targetTypeApiName === ot.apiName).length,
390
+ actions: manifestActions.filter((a) => a.objectTypeApiName === ot.apiName).length,
391
+ description: ot.description,
392
+ })),
393
+ interfaces: manifestInterfaces.map((iface) => ({
394
+ entity: 'interface',
395
+ apiName: iface.apiName,
396
+ displayName: iface.displayName,
397
+ sharedProperties: (iface.sharedProperties ?? []).length,
398
+ description: iface.description,
399
+ })),
400
+ linkTypes: linkTypes.map((link) => ({
401
+ entity: 'linkType',
402
+ apiName: link.apiName,
403
+ source: link.sourceTypeApiName,
404
+ target: link.targetTypeApiName,
405
+ cardinality: link.cardinality,
406
+ label: link.label,
407
+ })),
408
+ actions: manifestActions.map((action) => ({
409
+ entity: 'action',
410
+ apiName: action.apiName,
411
+ displayName: action.displayName,
412
+ objectTypeApiName: action.objectTypeApiName,
413
+ actionType: action.actionType,
414
+ })),
415
+ };
416
+ if (isStructuredFormat(flags)) {
417
+ output(overview, meta, flags);
418
+ }
419
+ else {
420
+ output(overview.objectTypes, meta, flags, [
421
+ 'apiName',
422
+ 'displayName',
423
+ 'properties',
424
+ 'outgoingLinks',
425
+ 'incomingLinks',
426
+ 'actions',
427
+ ]);
428
+ }
429
+ }
430
+ }
431
+ // ---------------------------------------------------------------------------
432
+ // schema search — validated types
433
+ // ---------------------------------------------------------------------------
434
+ async function runSchemaSearch(program, query, opts) {
435
+ const flags = program.opts();
436
+ const config = resolveConfig(flags);
437
+ const client = createDfClient(config);
438
+ const validatedTypes = parseSearchTypes(opts.types);
439
+ const limit = parseLimit(opts.limit);
440
+ const searchQuery = {
441
+ q: query,
442
+ types: validatedTypes,
443
+ limit,
444
+ };
445
+ if (opts.includeProperties) {
446
+ searchQuery.includeProperties = 'true';
447
+ }
448
+ const searchResponse = await client.transport.requestWithHeaders({
449
+ method: 'GET',
450
+ path: '/api/v1/ontology/search',
451
+ query: searchQuery,
452
+ });
453
+ const results = searchResponse.data?.results ?? [];
454
+ const items = results.map((r) => ({
455
+ apiName: r.api_name || r.name,
456
+ displayName: r.displayName || r.name,
457
+ type: r.type,
458
+ score: r.score,
459
+ description: r.description,
460
+ highlights: r.highlights,
461
+ ...(opts.includeProperties && r.properties ? { properties: r.properties } : {}),
462
+ }));
463
+ const searchHeaderMeta = extractMetaFromHeaders(searchResponse.headers);
464
+ const meta = buildCliMeta({
465
+ command: 'schema.search',
466
+ workspaceId: config.workspaceId,
467
+ apiUrl: config.apiUrl,
468
+ ...searchHeaderMeta,
469
+ });
470
+ const data = {
471
+ query: searchResponse.data?.query ?? query,
472
+ total: searchResponse.data?.total ?? items.length,
473
+ limit,
474
+ timingMs: searchResponse.data?.timing_ms ?? null,
475
+ items,
476
+ };
477
+ if (isStructuredFormat(flags)) {
478
+ output(data, meta, flags);
479
+ }
480
+ else {
481
+ output(items, meta, flags, ['apiName', 'type', 'score', 'description']);
482
+ }
483
+ }
484
+ // ---------------------------------------------------------------------------
485
+ // schema diff — CSV-aware structured output
486
+ // ---------------------------------------------------------------------------
487
+ async function runSchemaDiff(program, opts) {
488
+ const flags = program.opts();
489
+ const config = resolveConfig(flags);
490
+ const client = createDfClient(config);
491
+ process.stderr.write('Loading local schema...\n');
492
+ const local = await compileLocalSchema(opts.schema, config);
493
+ process.stderr.write('Fetching remote ontology...\n');
494
+ const remoteManifest = await fetchFullManifest(client, config);
495
+ const diffResult = diff(local.manifest, remoteManifest);
496
+ const formatted = formatDiff(diffResult);
497
+ const meta = buildCliMeta({
498
+ command: 'schema.diff',
499
+ workspaceId: config.workspaceId,
500
+ apiUrl: config.apiUrl,
501
+ manifestVersion: remoteManifest.version ?? null,
502
+ });
503
+ const format = resolveFormat(flags);
504
+ if (format === 'csv') {
505
+ output(sortDiffChanges(diffResult.changes ?? []).map((change) => ({
506
+ kind: change.kind,
507
+ entity: change.entity,
508
+ name: change.name,
509
+ parent: change.parent,
510
+ })), meta, flags, ['kind', 'entity', 'name', 'parent']);
511
+ }
512
+ else if (isStructuredFormat(flags)) {
513
+ output({
514
+ hasChanges: diffResult.hasChanges,
515
+ changes: sortDiffChanges(diffResult.changes ?? []),
516
+ summary: formatted,
517
+ }, meta, flags);
518
+ }
519
+ else {
520
+ process.stdout.write(`${formatted}\n`);
521
+ }
522
+ if (!diffResult.hasChanges) {
523
+ process.stderr.write('Local schema is in sync with remote.\n');
524
+ }
525
+ if (opts.exitCode && diffResult.hasChanges) {
526
+ process.exitCode = getExitCode("SCHEMA_DRIFT_ERROR");
527
+ }
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // schema push — convergence-verified, breaking-change-aware, incomplete-guard
531
+ //
532
+ // Confirmation flow: --dry-run shows plan, --yes confirms execution.
533
+ // This is NOT the plan artifact flow (--apply-plan + --idempotency-key).
534
+ // ---------------------------------------------------------------------------
535
+ async function runSchemaPush(program, opts) {
536
+ const flags = program.opts();
537
+ const config = resolveConfig(flags);
538
+ const client = createDfClient(config);
539
+ const meta = buildCliMeta({
540
+ command: 'schema.push',
541
+ workspaceId: config.workspaceId,
542
+ apiUrl: config.apiUrl,
543
+ });
544
+ process.stderr.write('Loading local schema...\n');
545
+ const local = await compileLocalSchema(opts.schema, config);
546
+ process.stderr.write('Fetching remote ontology...\n');
547
+ const remoteManifest = await fetchFullManifest(client, config);
548
+ // Guard: refuse push when remote manifest was obtained via fallback
549
+ // (actions endpoint unavailable). We check via the MANIFEST_FALLBACK warning
550
+ // injected by fetchFullManifest, which indicates the /manifest endpoint
551
+ // returned 404/5xx and actions are not available in fallback mode.
552
+ // Only block when local schema actually defines actions.
553
+ const manifestFallbackUsed = !remoteManifest.actions || ((local.manifest.actions?.length ?? 0) > 0 &&
554
+ (remoteManifest.actions?.length ?? 0) === 0 &&
555
+ remoteManifest.version === '1.0' // fallback always returns version '1.0'
556
+ );
557
+ if (manifestFallbackUsed &&
558
+ (local.manifest.actions?.length ?? 0) > 0) {
559
+ failCli({
560
+ code: 'PRECONDITION_FAILED',
561
+ message: 'Remote manifest is incomplete: actions endpoint unavailable.',
562
+ details: {
563
+ hint: 'Retry when the ontology actions endpoint is available, or verify the remote has no actions before pushing.',
564
+ },
565
+ retryable: true,
566
+ correlationId: meta.requestId,
567
+ }, meta, flags);
568
+ return;
569
+ }
570
+ const diffResult = diff(local.manifest, remoteManifest);
571
+ if (!diffResult.hasChanges) {
572
+ output({
573
+ changed: false,
574
+ executed: false,
575
+ message: 'No changes to push. Schema is in sync.',
576
+ }, meta, flags);
577
+ return;
578
+ }
579
+ const commands = planPush(diffResult, remoteManifest, {
580
+ workspaceId: config.workspaceId,
581
+ espaceId: config.spaceId,
582
+ });
583
+ // Guard: diff says changes, but planner produced no commands
584
+ if (commands.length === 0) {
585
+ failCli({
586
+ code: 'INTERNAL_ERROR',
587
+ message: 'Diff contains changes, but planner produced no commands.',
588
+ details: {
589
+ hint: 'This usually means planPush does not support one or more diff change kinds.',
590
+ changes: diffResult.changes ?? [],
591
+ summary: formatDiff(diffResult),
592
+ },
593
+ retryable: false,
594
+ correlationId: meta.requestId,
595
+ }, meta, flags);
596
+ return;
597
+ }
598
+ // Breaking changes check — consistent for both dry-run and real push
599
+ const breakingChanges = getBreakingChanges(diffResult);
600
+ const canRun = breakingChanges.length === 0 || opts.allowBreaking;
601
+ const dryRunData = {
602
+ canRun,
603
+ changed: true,
604
+ executed: false,
605
+ commandCount: commands.length,
606
+ plannedChanges: commands.map((cmd) => ({
607
+ action: cmd.type,
608
+ name: readCommandName(cmd.payload),
609
+ ...(opts.includePayload ? { payload: redactSecretsDeep(cmd.payload) } : {}),
610
+ })),
611
+ breakingChanges,
612
+ requiredScopes: ['schema.write'],
613
+ estimatedCostUnits: commands.length,
614
+ summary: formatDiff(diffResult),
615
+ };
616
+ if (opts.dryRun) {
617
+ output(dryRunData, meta, flags);
618
+ if (!canRun) {
619
+ process.exitCode = getExitCode("PRECONDITION_FAILED");
620
+ }
621
+ return;
622
+ }
623
+ if (!canRun) {
624
+ failCli({
625
+ code: 'PRECONDITION_FAILED',
626
+ message: 'Breaking changes detected.',
627
+ details: {
628
+ breakingChanges,
629
+ hint: 'Pass --allow-breaking to execute destructive schema changes.',
630
+ },
631
+ retryable: false,
632
+ correlationId: meta.requestId,
633
+ }, meta, flags);
634
+ return;
635
+ }
636
+ if (!opts.yes) {
637
+ failCli({
638
+ code: 'CONFIRMATION_REQUIRED',
639
+ message: `This operation will execute ${commands.length} command(s). Pass --yes to confirm.`,
640
+ details: {
641
+ prompt: `Execute ${commands.length} schema change(s)?`,
642
+ affectedCount: commands.length,
643
+ affectedEntities: commands.map(readCommandLabel),
644
+ },
645
+ retryable: false,
646
+ correlationId: meta.requestId,
647
+ }, meta, flags);
648
+ return;
649
+ }
650
+ // Concurrency check: verify remote hasn't changed since we fetched it
651
+ try {
652
+ const latestVersion = await fetchManifestVersion(client, config);
653
+ if (remoteManifest.version && latestVersion !== remoteManifest.version) {
654
+ failCli({
655
+ code: 'CONFLICT_ERROR',
656
+ message: 'Remote manifest changed while preparing schema push.',
657
+ details: {
658
+ expectedManifestVersion: remoteManifest.version,
659
+ actualManifestVersion: latestVersion,
660
+ hint: 'Re-run schema diff and schema push.',
661
+ },
662
+ retryable: true,
663
+ correlationId: meta.requestId,
664
+ }, meta, flags);
665
+ return;
666
+ }
667
+ }
668
+ catch {
669
+ // If version endpoint is unavailable, proceed without concurrency check
670
+ }
671
+ // Show human diff only for non-structured formats
672
+ if (!isStructuredFormat(flags)) {
673
+ process.stdout.write(`${formatDiff(diffResult)}\n\n`);
674
+ }
675
+ process.stderr.write(`Executing ${commands.length} schema command(s)...\n`);
676
+ const pushResult = await executePush({
677
+ transport: client.transport,
678
+ commands,
679
+ onProgress: (progress) => {
680
+ process.stderr.write(` [${progress.completed}/${progress.total}] ${progress.current}\n`);
681
+ },
682
+ });
683
+ if (pushResult.failed > 0) {
684
+ failCli({
685
+ code: 'INTERNAL_ERROR',
686
+ message: `Schema push partially failed: ${pushResult.failed} command(s) failed.`,
687
+ details: {
688
+ succeeded: pushResult.succeeded,
689
+ failed: pushResult.failed,
690
+ errors: pushResult.errors.map((error) => ({
691
+ command: error.command.type,
692
+ message: String(error.error),
693
+ })),
694
+ },
695
+ retryable: false,
696
+ correlationId: meta.requestId,
697
+ }, meta, flags);
698
+ return;
699
+ }
700
+ // Post-push convergence check
701
+ process.stderr.write('Refreshing remote manifest...\n');
702
+ const refreshedManifest = await fetchFullManifest(client, config);
703
+ const postPushDiff = diff(local.manifest, refreshedManifest);
704
+ if (postPushDiff.hasChanges) {
705
+ failCli({
706
+ code: 'INTERNAL_ERROR',
707
+ message: 'Remote manifest still differs from local schema after push.',
708
+ details: {
709
+ succeeded: pushResult.succeeded,
710
+ failed: pushResult.failed,
711
+ converged: false,
712
+ remainingChanges: postPushDiff.changes ?? [],
713
+ summary: formatDiff(postPushDiff),
714
+ },
715
+ retryable: true,
716
+ correlationId: meta.requestId,
717
+ }, meta, flags);
718
+ return;
719
+ }
720
+ // Lockfile from verified server manifest
721
+ const lockContent = generateLockfile(refreshedManifest);
722
+ const lockPath = resolveLockfilePath(opts.schema);
723
+ await fs.promises.mkdir(path.dirname(lockPath), { recursive: true });
724
+ await writeTextFileAtomic(lockPath, serializeLockfile(lockContent));
725
+ output({
726
+ changed: true,
727
+ executed: true,
728
+ converged: true,
729
+ succeeded: pushResult.succeeded,
730
+ failed: pushResult.failed,
731
+ commandCount: commands.length,
732
+ manifestVersion: refreshedManifest.version,
733
+ lockfile: lockPath,
734
+ }, meta, flags);
735
+ }
736
+ // ---------------------------------------------------------------------------
737
+ // schema pull — --force, directory check, mkdir parent, process.exitCode
738
+ // ---------------------------------------------------------------------------
739
+ async function runSchemaPull(program, opts) {
740
+ const flags = program.opts();
741
+ const config = resolveConfig(flags);
742
+ const client = createDfClient(config);
743
+ const meta = buildCliMeta({
744
+ command: 'schema.pull',
745
+ workspaceId: config.workspaceId,
746
+ apiUrl: config.apiUrl,
747
+ });
748
+ // Check output path BEFORE making network calls
749
+ const outputPath = path.resolve(opts.output);
750
+ try {
751
+ const stat = await fs.promises.stat(outputPath);
752
+ if (stat.isDirectory()) {
753
+ failCli({
754
+ code: 'PRECONDITION_FAILED',
755
+ message: `Output path is a directory: ${outputPath}`,
756
+ details: { outputPath },
757
+ retryable: false,
758
+ correlationId: meta.requestId,
759
+ }, meta, flags);
760
+ return;
761
+ }
762
+ if (!opts.force) {
763
+ failCli({
764
+ code: 'PRECONDITION_FAILED',
765
+ message: `File already exists: ${outputPath}`,
766
+ details: {
767
+ outputPath,
768
+ hint: 'Pass --force to overwrite it.',
769
+ },
770
+ retryable: false,
771
+ correlationId: meta.requestId,
772
+ }, meta, flags);
773
+ return;
774
+ }
775
+ }
776
+ catch (error) {
777
+ const code = isRecord(error) ? error.code : undefined;
778
+ if (code !== 'ENOENT') {
779
+ throw error;
780
+ }
781
+ // File doesn't exist — proceed
782
+ }
783
+ process.stderr.write('Fetching remote ontology...\n');
784
+ const remoteManifest = await fetchFullManifest(client, config);
785
+ const source = emitSchema(remoteManifest);
786
+ // Ensure parent directory exists
787
+ await fs.promises.mkdir(path.dirname(outputPath), { recursive: true });
788
+ await writeTextFileAtomic(outputPath, source);
789
+ const lockPath = resolveLockfilePath(outputPath);
790
+ await fs.promises.mkdir(path.dirname(lockPath), { recursive: true });
791
+ const lockContent = generateLockfile(remoteManifest);
792
+ await writeTextFileAtomic(lockPath, serializeLockfile(lockContent));
793
+ output({
794
+ written: true,
795
+ schemaFile: outputPath,
796
+ lockfile: lockPath,
797
+ version: remoteManifest.version,
798
+ objectTypes: remoteManifest.objectTypes.length,
799
+ linkTypes: remoteManifest.linkTypes.length,
800
+ actions: remoteManifest.actions?.length ?? 0,
801
+ }, meta, flags);
802
+ }
803
+ // ---------------------------------------------------------------------------
804
+ // schema check — structured output on stdout, --fail-on-drift
805
+ // ---------------------------------------------------------------------------
806
+ async function runSchemaCheck(program, opts) {
807
+ const flags = program.opts();
808
+ const config = resolveConfig(flags);
809
+ const meta = buildCliMeta({
810
+ command: 'schema.check',
811
+ workspaceId: config.workspaceId,
812
+ apiUrl: config.apiUrl,
813
+ });
814
+ const result = {
815
+ valid: false,
816
+ schemaFile: opts.schema,
817
+ compiled: null,
818
+ lockfile: {
819
+ present: false,
820
+ drifted: null,
821
+ },
822
+ remote: {
823
+ checked: false,
824
+ drifted: null,
825
+ },
826
+ };
827
+ process.stderr.write('Loading local schema...\n');
828
+ const local = await compileLocalSchema(opts.schema, config);
829
+ const manifest = local.manifest;
830
+ result.compiled = {
831
+ objectTypes: manifest.objectTypes.length,
832
+ linkTypes: manifest.linkTypes.length,
833
+ actions: manifest.actions.length,
834
+ };
835
+ process.stderr.write(`Compiled: ${manifest.objectTypes.length} type(s), ${manifest.linkTypes.length} link(s), ${manifest.actions.length} action(s)\n`);
836
+ // Verify lockfile if it exists (async FS)
837
+ const lockPath = resolveLockfilePath(opts.schema);
838
+ const lockRaw = await readOptionalTextFile(lockPath);
839
+ if (lockRaw !== null) {
840
+ let lockContent;
841
+ try {
842
+ lockContent = parseLockfile(lockRaw);
843
+ }
844
+ catch {
845
+ result.lockfile = {
846
+ present: true,
847
+ path: lockPath,
848
+ drifted: true,
849
+ reason: 'invalid_lockfile',
850
+ };
851
+ failCli({
852
+ code: 'SCHEMA_DRIFT_ERROR',
853
+ message: 'Schema lockfile is invalid or unreadable.',
854
+ details: {
855
+ path: lockPath,
856
+ hint: 'Run `dataforge schema pull --force` or regenerate the lockfile.',
857
+ },
858
+ retryable: false,
859
+ correlationId: meta.requestId,
860
+ }, meta, flags);
861
+ return;
862
+ }
863
+ const { drifted } = verifyLockfile(lockContent, manifest);
864
+ if (drifted) {
865
+ result.lockfile = {
866
+ present: true,
867
+ path: lockPath,
868
+ drifted: true,
869
+ };
870
+ process.stderr.write('Lockfile DRIFT detected — schema has changed since last push/pull.\n');
871
+ process.stderr.write('Run `dataforge schema push` to sync, or `dataforge schema pull` to reset.\n');
872
+ output(result, meta, flags);
873
+ process.exitCode = getExitCode("SCHEMA_DRIFT_ERROR");
874
+ return;
875
+ }
876
+ result.lockfile = {
877
+ present: true,
878
+ path: lockPath,
879
+ drifted: false,
880
+ };
881
+ process.stderr.write('Lockfile OK — no drift detected.\n');
882
+ // --fail-on-drift: strict remote check
883
+ if (opts.failOnDrift) {
884
+ if (!lockContent.manifestVersion) {
885
+ result.remote = {
886
+ checked: true,
887
+ drifted: true,
888
+ reason: 'lockfile has no manifestVersion',
889
+ };
890
+ process.stderr.write('DRIFT: lockfile has no manifestVersion; cannot compare with remote.\n');
891
+ output(result, meta, flags);
892
+ process.exitCode = getExitCode("SCHEMA_DRIFT_ERROR");
893
+ return;
894
+ }
895
+ const client = createDfClient(config);
896
+ const remoteVersion = await fetchManifestVersion(client, config);
897
+ if (lockContent.manifestVersion !== remoteVersion) {
898
+ result.remote = {
899
+ checked: true,
900
+ drifted: true,
901
+ localManifestVersion: lockContent.manifestVersion,
902
+ remoteManifestVersion: remoteVersion,
903
+ };
904
+ process.stderr.write(`DRIFT: local lockfile version ${lockContent.manifestVersion} differs from remote manifest ${remoteVersion}\n`);
905
+ output(result, meta, flags);
906
+ process.exitCode = getExitCode("SCHEMA_DRIFT_ERROR");
907
+ return;
908
+ }
909
+ result.remote = {
910
+ checked: true,
911
+ drifted: false,
912
+ manifestVersion: remoteVersion,
913
+ };
914
+ process.stderr.write(`OK: local and remote versions match (${remoteVersion})\n`);
915
+ }
916
+ }
917
+ else {
918
+ result.lockfile = {
919
+ present: false,
920
+ drifted: null,
921
+ };
922
+ process.stderr.write('No lockfile found. Run `dataforge schema push` or `dataforge schema pull` to generate one.\n');
923
+ if (opts.failOnDrift) {
924
+ output(result, meta, flags);
925
+ process.exitCode = getExitCode("SCHEMA_DRIFT_ERROR");
926
+ return;
927
+ }
928
+ }
929
+ result.valid = true;
930
+ process.stderr.write('Schema check passed.\n');
931
+ output(result, meta, flags);
932
+ }
933
+ //# sourceMappingURL=schema.js.map