@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.
- package/README.md +492 -0
- package/dist/cache/keys.d.ts +11 -0
- package/dist/cache/keys.d.ts.map +1 -0
- package/dist/cache/keys.js +14 -0
- package/dist/cache/keys.js.map +1 -0
- package/dist/cache/store.d.ts +23 -0
- package/dist/cache/store.d.ts.map +1 -0
- package/dist/cache/store.js +160 -0
- package/dist/cache/store.js.map +1 -0
- package/dist/cli-compat.d.ts +6 -0
- package/dist/cli-compat.d.ts.map +1 -0
- package/dist/cli-compat.js +11 -0
- package/dist/cli-compat.js.map +1 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +119 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/actions.d.ts +22 -0
- package/dist/commands/actions.d.ts.map +1 -0
- package/dist/commands/actions.js +211 -0
- package/dist/commands/actions.js.map +1 -0
- package/dist/commands/agent-files.d.ts +27 -0
- package/dist/commands/agent-files.d.ts.map +1 -0
- package/dist/commands/agent-files.js +167 -0
- package/dist/commands/agent-files.js.map +1 -0
- package/dist/commands/agent.d.ts +23 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +314 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/audit.d.ts +11 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +94 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/cache.d.ts +8 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +40 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/capabilities.d.ts +56 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +304 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/check.d.ts +7 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +16 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/config.d.ts +6 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/context.d.ts +6 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +226 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +62 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/docs-alias.d.ts +14 -0
- package/dist/commands/docs-alias.d.ts.map +1 -0
- package/dist/commands/docs-alias.js +28 -0
- package/dist/commands/docs-alias.js.map +1 -0
- package/dist/commands/docs.d.ts +6 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +67 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +161 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +36 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/graph.d.ts +9 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +149 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/import.d.ts +19 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +330 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/index.d.ts +80 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +345 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +101 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/instances.d.ts +7 -0
- package/dist/commands/instances.d.ts.map +1 -0
- package/dist/commands/instances.js +418 -0
- package/dist/commands/instances.js.map +1 -0
- package/dist/commands/keys.d.ts +6 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +113 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/knowledge.d.ts +6 -0
- package/dist/commands/knowledge.d.ts.map +1 -0
- package/dist/commands/knowledge.js +76 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/model.d.ts +3 -0
- package/dist/commands/model.d.ts.map +1 -0
- package/dist/commands/model.js +40 -0
- package/dist/commands/model.js.map +1 -0
- package/dist/commands/nodes.d.ts +6 -0
- package/dist/commands/nodes.d.ts.map +1 -0
- package/dist/commands/nodes.js +111 -0
- package/dist/commands/nodes.js.map +1 -0
- package/dist/commands/openapi.d.ts +7 -0
- package/dist/commands/openapi.d.ts.map +1 -0
- package/dist/commands/openapi.js +17 -0
- package/dist/commands/openapi.js.map +1 -0
- package/dist/commands/plan.d.ts +19 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +563 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/process.d.ts +3 -0
- package/dist/commands/process.d.ts.map +1 -0
- package/dist/commands/process.js +67 -0
- package/dist/commands/process.js.map +1 -0
- package/dist/commands/query.d.ts +26 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +253 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/schema.d.ts +24 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +933 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/search.d.ts +10 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +74 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/shared.d.ts +32 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +63 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/usage.d.ts +6 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +86 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/validators.d.ts +30 -0
- package/dist/commands/validators.d.ts.map +1 -0
- package/dist/commands/validators.js +93 -0
- package/dist/commands/validators.js.map +1 -0
- package/dist/commands/whoami.d.ts +6 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +48 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +9 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +79 -0
- package/dist/credentials.js.map +1 -0
- package/dist/keychain.d.ts +9 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +51 -0
- package/dist/keychain.js.map +1 -0
- package/dist/output/csv.d.ts +5 -0
- package/dist/output/csv.d.ts.map +1 -0
- package/dist/output/csv.js +22 -0
- package/dist/output/csv.js.map +1 -0
- package/dist/output/envelope.schema.d.ts +1053 -0
- package/dist/output/envelope.schema.d.ts.map +1 -0
- package/dist/output/envelope.schema.js +256 -0
- package/dist/output/envelope.schema.js.map +1 -0
- package/dist/output/errors.d.ts +58 -0
- package/dist/output/errors.d.ts.map +1 -0
- package/dist/output/errors.js +339 -0
- package/dist/output/errors.js.map +1 -0
- package/dist/output/formatter.d.ts +27 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +80 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/json.d.ts +41 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/json.js +215 -0
- package/dist/output/json.js.map +1 -0
- package/dist/output/markdown.d.ts +6 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/markdown.js +51 -0
- package/dist/output/markdown.js.map +1 -0
- package/dist/output/meta.d.ts +49 -0
- package/dist/output/meta.d.ts.map +1 -0
- package/dist/output/meta.js +96 -0
- package/dist/output/meta.js.map +1 -0
- package/dist/output/plain.d.ts +6 -0
- package/dist/output/plain.d.ts.map +1 -0
- package/dist/output/plain.js +51 -0
- package/dist/output/plain.js.map +1 -0
- package/dist/output/table.d.ts +5 -0
- package/dist/output/table.d.ts.map +1 -0
- package/dist/output/table.js +54 -0
- package/dist/output/table.js.map +1 -0
- package/dist/output/types.d.ts +54 -0
- package/dist/output/types.d.ts.map +1 -0
- package/dist/output/types.js +80 -0
- package/dist/output/types.js.map +1 -0
- package/dist/output/warnings.d.ts +15 -0
- package/dist/output/warnings.d.ts.map +1 -0
- package/dist/output/warnings.js +46 -0
- package/dist/output/warnings.js.map +1 -0
- package/dist/output/yaml.d.ts +6 -0
- package/dist/output/yaml.d.ts.map +1 -0
- package/dist/output/yaml.js +9 -0
- package/dist/output/yaml.js.map +1 -0
- package/dist/schema/breaking-changes.d.ts +17 -0
- package/dist/schema/breaking-changes.d.ts.map +1 -0
- package/dist/schema/breaking-changes.js +108 -0
- package/dist/schema/breaking-changes.js.map +1 -0
- package/dist/schema/helpers.d.ts +18 -0
- package/dist/schema/helpers.d.ts.map +1 -0
- package/dist/schema/helpers.js +48 -0
- package/dist/schema/helpers.js.map +1 -0
- package/dist/schema/load-schema-file.d.ts +13 -0
- package/dist/schema/load-schema-file.d.ts.map +1 -0
- package/dist/schema/load-schema-file.js +88 -0
- package/dist/schema/load-schema-file.js.map +1 -0
- package/dist/schema/lockfile.d.ts +6 -0
- package/dist/schema/lockfile.d.ts.map +1 -0
- package/dist/schema/lockfile.js +34 -0
- package/dist/schema/lockfile.js.map +1 -0
- package/dist/schema/manifest-client.d.ts +31 -0
- package/dist/schema/manifest-client.d.ts.map +1 -0
- package/dist/schema/manifest-client.js +282 -0
- package/dist/schema/manifest-client.js.map +1 -0
- package/dist/schema/output.d.ts +38 -0
- package/dist/schema/output.d.ts.map +1 -0
- package/dist/schema/output.js +95 -0
- package/dist/schema/output.js.map +1 -0
- package/dist/stdin.d.ts +8 -0
- package/dist/stdin.d.ts.map +1 -0
- package/dist/stdin.js +21 -0
- package/dist/stdin.js.map +1 -0
- package/dist/templates/basic.d.ts +6 -0
- package/dist/templates/basic.d.ts.map +1 -0
- package/dist/templates/basic.js +90 -0
- package/dist/templates/basic.js.map +1 -0
- package/dist/templates/contract-review.d.ts +9 -0
- package/dist/templates/contract-review.d.ts.map +1 -0
- package/dist/templates/contract-review.js +172 -0
- package/dist/templates/contract-review.js.map +1 -0
- package/dist/templates/customer-onboarding.d.ts +9 -0
- package/dist/templates/customer-onboarding.d.ts.map +1 -0
- package/dist/templates/customer-onboarding.js +176 -0
- package/dist/templates/customer-onboarding.js.map +1 -0
- package/dist/templates/index.d.ts +11 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +16 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/react-dashboard.d.ts +6 -0
- package/dist/templates/react-dashboard.d.ts.map +1 -0
- package/dist/templates/react-dashboard.js +146 -0
- package/dist/templates/react-dashboard.js.map +1 -0
- package/dist/templates/vendor-risk.d.ts +9 -0
- package/dist/templates/vendor-risk.d.ts.map +1 -0
- package/dist/templates/vendor-risk.js +186 -0
- package/dist/templates/vendor-risk.js.map +1 -0
- 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
|