@google-cloud/kcmd 0.0.0-stage → 0.1.0
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/LICENSE +203 -0
- package/README.md +266 -2
- package/build/ts/kcmd/cli.js +19578 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +16 -0
- package/build/ts/kcmd/gcp/biglake.js +50 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +63 -0
- package/build/ts/kcmd/gcp/spanner.js +123 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +344 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
- package/build/ts/kcmd/layouts/semantic-model.js +542 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +104 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +115 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +82 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +525 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +97 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +129 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +34 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
- package/build/ts/kcmd/semantic/import_model.js +1275 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +31 -0
- package/build/ts/kcmd/semantic/kc_converter.js +824 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +828 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +103 -0
- package/build/ts/kcmd/semantic/kc_ids.js +191 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
- package/build/ts/kcmd/semantic/loader.d.ts +262 -0
- package/build/ts/kcmd/semantic/loader.js +3440 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +911 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
- package/build/ts/kcmd/semantic/parameters.js +152 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
- package/build/ts/kcmd/semantic/pull_kc.js +346 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +94 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +318 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +10 -0
- package/build/ts/kcmd/semantic/source_dialect.js +261 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +282 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +17 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +44 -0
- package/build/ts/kcmd/semantic/sql_parser.js +378 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
- package/build/ts/kcmd/semantic/transpile.js +563 -0
- package/build/ts/kcmd/semantic/validate.d.ts +35 -0
- package/build/ts/kcmd/semantic/validate.js +1899 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +33 -0
- package/build/ts/kcmd/snapshot.js +379 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +697 -0
- package/package.json +62 -4
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Implements the SemanticModel layout.
|
|
3
|
+
//
|
|
4
|
+
// The whole model definition is a single Apache Ossie document per model,
|
|
5
|
+
// located at `catalog/EntryGroups/<entryGroupId>/<model>.yaml`, with its
|
|
6
|
+
// profile files beside it. Each Knowledge Catalog entry of the model may have
|
|
7
|
+
// an overview sidecar at its entry id under the same directory:
|
|
8
|
+
// `<model>.overview.md` for the model and, for example,
|
|
9
|
+
// `<model>/entities/<entity>.overview.md` for an entity.
|
|
10
|
+
//
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.SemanticModelLayout = void 0;
|
|
46
|
+
exports.profileFilesBeside = profileFilesBeside;
|
|
47
|
+
exports.fileNameOf = fileNameOf;
|
|
48
|
+
exports.parseOverviewSidecar = parseOverviewSidecar;
|
|
49
|
+
exports.formatOverviewSidecar = formatOverviewSidecar;
|
|
50
|
+
const glob = __importStar(require("glob"));
|
|
51
|
+
const fs = __importStar(require("node:fs"));
|
|
52
|
+
const path = __importStar(require("node:path"));
|
|
53
|
+
const yaml = __importStar(require("yaml"));
|
|
54
|
+
const loader_1 = require("../semantic/loader");
|
|
55
|
+
const names_1 = require("../semantic/names");
|
|
56
|
+
const yaml_options_1 = require("../semantic/yaml_options");
|
|
57
|
+
const documents_1 = require("./documents");
|
|
58
|
+
// Sidecar suffixes that are NOT model documents.
|
|
59
|
+
const SIDECAR_SUFFIXES = ['.aspects.yaml', '.overview.yaml'];
|
|
60
|
+
// A binding-profile file sits beside its model as `<model>.profile.<name>.yaml`.
|
|
61
|
+
// `<model>.profile.yaml`, with no name, is not a model either.
|
|
62
|
+
const PROFILE_FILE = /^(.+)\.profile\.([^.]+)\.yaml$/;
|
|
63
|
+
// Any file shaped like a profile file, including a nameless one and one whose
|
|
64
|
+
// profile name has a dot, which is never a model either.
|
|
65
|
+
const ANY_PROFILE_FILE = /\.profile\.(.*\.)?yaml$/;
|
|
66
|
+
// The model and profile a `<model>.profile.<name>.yaml` file name names, or
|
|
67
|
+
// undefined when `fileName` is not one.
|
|
68
|
+
function profileFileName(fileName) {
|
|
69
|
+
const m = fileName.match(PROFILE_FILE);
|
|
70
|
+
return m ? { model: m[1], profile: m[2] } : undefined;
|
|
71
|
+
}
|
|
72
|
+
// Each `<model>.profile.<name>.yaml` in `dir`, by profile name.
|
|
73
|
+
function profileFilesBeside(dir, model) {
|
|
74
|
+
if (!fs.existsSync(dir))
|
|
75
|
+
return [];
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const f of fs.readdirSync(dir)) {
|
|
78
|
+
const named = profileFileName(f);
|
|
79
|
+
if (named?.model === model) {
|
|
80
|
+
out.push({ name: named.profile, path: path.join(dir, f) });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
}
|
|
85
|
+
// The suffix of an entry's overview sidecar.
|
|
86
|
+
const OVERVIEW_SIDECAR = '.overview.md';
|
|
87
|
+
// The profile name reserved for the inline bindings in the model file.
|
|
88
|
+
const DEFAULT_PROFILE_NAME = 'default';
|
|
89
|
+
// The flavor that keeps its profiles inline, in a GOOGLE block, and so has no
|
|
90
|
+
// sibling profile files.
|
|
91
|
+
const VANILLA_VERSION = '0.2.0.dev0';
|
|
92
|
+
// The error for a sibling profile file beside a vanilla model, which keeps its
|
|
93
|
+
// profiles in its GOOGLE block.
|
|
94
|
+
function vanillaSiblingError(file, model) {
|
|
95
|
+
return `Profile file '${file}' sits beside '${model}.yaml', which is a ` +
|
|
96
|
+
`${VANILLA_VERSION} document; sibling profile files are Google-flavor ` +
|
|
97
|
+
`only, and a ${VANILLA_VERSION} model keeps its profiles in its GOOGLE ` +
|
|
98
|
+
`block.`;
|
|
99
|
+
}
|
|
100
|
+
class SemanticModelLayout {
|
|
101
|
+
_catalogPath;
|
|
102
|
+
_entryGroup;
|
|
103
|
+
// Maps a model handle (the document's file basename) to its absolute path.
|
|
104
|
+
_index = new Map();
|
|
105
|
+
constructor(catalogPath, entryGroup) {
|
|
106
|
+
this._catalogPath = catalogPath;
|
|
107
|
+
this._entryGroup = entryGroup;
|
|
108
|
+
}
|
|
109
|
+
async init() {
|
|
110
|
+
this._index.clear();
|
|
111
|
+
if (!fs.existsSync(this._catalogPath)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// A model document is a top-level `<model>.yaml` under the configured
|
|
115
|
+
// EntryGroup dir, excluding the `.aspects.yaml` / `.overview.yaml`
|
|
116
|
+
// sidecars. Scoping to the manifest's entryGroup keeps unrelated group
|
|
117
|
+
// directories out of the deploy set and avoids the basename collision that
|
|
118
|
+
// a cross-group `EntryGroups/*/*.yaml` glob would produce (same file name
|
|
119
|
+
// in two groups). Fall back to all groups only when no scope was provided.
|
|
120
|
+
const pattern = this._entryGroup ?
|
|
121
|
+
`EntryGroups/${this._entryGroup}/*.yaml` :
|
|
122
|
+
'EntryGroups/*/*.yaml';
|
|
123
|
+
const matches = await glob.glob(pattern, {
|
|
124
|
+
cwd: this._catalogPath,
|
|
125
|
+
absolute: true,
|
|
126
|
+
nodir: true,
|
|
127
|
+
});
|
|
128
|
+
// glob returns files in directory order, which differs between
|
|
129
|
+
// filesystems. Sorted, push and `kcmd profiles` visit models in the same
|
|
130
|
+
// order everywhere.
|
|
131
|
+
for (const localPath of matches.sort()) {
|
|
132
|
+
if (SIDECAR_SUFFIXES.some(s => localPath.endsWith(s))) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const base = path.basename(localPath);
|
|
136
|
+
if (ANY_PROFILE_FILE.test(base))
|
|
137
|
+
continue;
|
|
138
|
+
const name = path.basename(localPath, '.yaml');
|
|
139
|
+
this._index.set(name, localPath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Consistent with listEntries(): this push-only layout exposes no per-entry
|
|
143
|
+
// Knowledge Catalog files, so no entry "exists" at the KC layer. Answering
|
|
144
|
+
// from _index (which holds model-document handles) would report an entry that
|
|
145
|
+
// listEntries() won't return and that loadEntry()/saveEntry() reject. The
|
|
146
|
+
// model documents are surfaced via modelDocuments(), not as entries.
|
|
147
|
+
entryExists(_name) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
// This is a push-only layout: it exposes no per-entry Knowledge Catalog
|
|
151
|
+
// files, so it lists no entries. Returning model handles here would make
|
|
152
|
+
// callers that pair listEntries() with loadEntry() -- e.g. the MCP server --
|
|
153
|
+
// list a model and then throw on read. modelDocuments() is the sole accessor
|
|
154
|
+
// for the authored model documents.
|
|
155
|
+
listEntries() {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
// Reads the raw Ossie document text for each discovered model. This is the
|
|
159
|
+
// seam the push path consumes; the Ossie text is parsed to the semantic IR by
|
|
160
|
+
// the loader, not mapped to a Knowledge Catalog entry here.
|
|
161
|
+
modelDocuments() {
|
|
162
|
+
const docs = [];
|
|
163
|
+
for (const [name, localPath] of this._index) {
|
|
164
|
+
docs.push({ name, text: fs.readFileSync(localPath, 'utf8') });
|
|
165
|
+
}
|
|
166
|
+
return docs;
|
|
167
|
+
}
|
|
168
|
+
// Every local profile file of a model, for pull to find the ones the
|
|
169
|
+
// catalog did not return: each sibling file whose name is a valid profile
|
|
170
|
+
// name, with that name, then each file in the older `<model>.profiles/`
|
|
171
|
+
// directory, which push no longer reads. It applies no flavor check. A
|
|
172
|
+
// sibling file with an invalid profile name is left out; push rejects it,
|
|
173
|
+
// naming it.
|
|
174
|
+
localProfileFiles(model) {
|
|
175
|
+
return [
|
|
176
|
+
...this._siblingProfilePaths(model)
|
|
177
|
+
.filter(p => !(0, names_1.nameProblem)('profile', p.name))
|
|
178
|
+
.map(p => ({ path: p.path, profileName: p.name })),
|
|
179
|
+
...this._legacyProfilePaths(model).map(p => ({ path: p.path })),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
// The overview sidecar of each entry, by entry id. Throws naming a sidecar
|
|
183
|
+
// parseOverviewSidecar rejects.
|
|
184
|
+
overviewSidecars() {
|
|
185
|
+
return new Map(this.overviewSidecarPaths().map(({ entryId, path: p }) => [
|
|
186
|
+
entryId,
|
|
187
|
+
parseOverviewSidecar(fs.readFileSync(p, 'utf8'), path.relative(this._groupDir(), p)),
|
|
188
|
+
]));
|
|
189
|
+
}
|
|
190
|
+
// Each overview sidecar on disk at a model, entity, metric or relationship
|
|
191
|
+
// path, with the entry id its path names, in path order.
|
|
192
|
+
overviewSidecarPaths() {
|
|
193
|
+
if (!this._entryGroup)
|
|
194
|
+
return [];
|
|
195
|
+
const dir = this._groupDir();
|
|
196
|
+
const files = glob.globSync([
|
|
197
|
+
`*${OVERVIEW_SIDECAR}`,
|
|
198
|
+
`*/{entities,metrics,relationships}/*${OVERVIEW_SIDECAR}`,
|
|
199
|
+
], { cwd: dir, nodir: true, posix: true });
|
|
200
|
+
return files.sort().map(f => ({
|
|
201
|
+
entryId: f.slice(0, -OVERVIEW_SIDECAR.length),
|
|
202
|
+
path: path.join(dir, f),
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
// The text and path of each profile file beside a model, by profile name.
|
|
206
|
+
// Sibling files are checked first, and the first problem found with any of
|
|
207
|
+
// them throws, naming the file:
|
|
208
|
+
// - a `<model>.profile.yaml` names no profile;
|
|
209
|
+
// - the file's `name:` must equal its filename suffix, because `pull`
|
|
210
|
+
// writes a profile back to the file its name implies;
|
|
211
|
+
// - `default` names the model's inline bindings, so no profile may use it;
|
|
212
|
+
// - a vanilla `0.2.0.dev0` model keeps its profiles in its GOOGLE block, so
|
|
213
|
+
// it must have no sibling profile files;
|
|
214
|
+
// - the files are named after the model, so the model file must declare
|
|
215
|
+
// that name, which `pull` uses to name both.
|
|
216
|
+
profileDocuments(model) {
|
|
217
|
+
const nameless = this._entryGroup ?
|
|
218
|
+
path.join(this._groupDir(), `${model}.profile.yaml`) :
|
|
219
|
+
undefined;
|
|
220
|
+
if (nameless && fs.existsSync(nameless)) {
|
|
221
|
+
throw new Error(`Profile file '${path.basename(nameless)}' names no profile; ` +
|
|
222
|
+
`rename it '${model}.profile.<name>.yaml'.`);
|
|
223
|
+
}
|
|
224
|
+
const prefix = `${model}.profile.`;
|
|
225
|
+
const badName = this._entryGroup && fs.existsSync(this._groupDir()) ?
|
|
226
|
+
fs.readdirSync(this._groupDir()).find(f => {
|
|
227
|
+
if (!f.startsWith(prefix) || !f.endsWith('.yaml'))
|
|
228
|
+
return false;
|
|
229
|
+
const n = f.slice(prefix.length, -'.yaml'.length);
|
|
230
|
+
return f !== `${model}.profile.yaml` &&
|
|
231
|
+
(0, names_1.nameProblem)('profile', n) !== undefined;
|
|
232
|
+
}) :
|
|
233
|
+
undefined;
|
|
234
|
+
if (badName) {
|
|
235
|
+
throw new Error(`Profile file '${badName}' has profile name '${badName.slice(prefix.length, -'.yaml'.length)}'; a profile ` +
|
|
236
|
+
`name is a letter followed by letters, digits, underscores and ` +
|
|
237
|
+
`hyphens.`);
|
|
238
|
+
}
|
|
239
|
+
const siblings = this._siblingProfilePaths(model);
|
|
240
|
+
if (!siblings.length)
|
|
241
|
+
return [];
|
|
242
|
+
const modelFile = `${model}.yaml`;
|
|
243
|
+
const header = this._modelHeader(model);
|
|
244
|
+
if (header.name !== undefined && header.name !== model) {
|
|
245
|
+
// A name the loader rejects cannot name the files either.
|
|
246
|
+
const problem = (0, names_1.nameProblem)('model', header.name);
|
|
247
|
+
if (problem)
|
|
248
|
+
throw new Error(`Model file '${modelFile}': ${problem}`);
|
|
249
|
+
throw new Error(`Profile file '${path.basename(siblings[0].path)}' is named after ` +
|
|
250
|
+
`'${modelFile}', which declares model '${header.name}'; a profile ` +
|
|
251
|
+
`file is named after its model, so name the model file and its ` +
|
|
252
|
+
`profile files after '${header.name}'.`);
|
|
253
|
+
}
|
|
254
|
+
const version = header.version;
|
|
255
|
+
const docs = [];
|
|
256
|
+
// Profile names are unique ignoring case.
|
|
257
|
+
const byLowerName = new Map();
|
|
258
|
+
for (const { name, path: p } of siblings) {
|
|
259
|
+
const file = path.basename(p);
|
|
260
|
+
const twin = byLowerName.get(name.toLowerCase());
|
|
261
|
+
if (twin) {
|
|
262
|
+
throw new Error(`Profile files '${twin}' and '${file}' name the same profile; ` +
|
|
263
|
+
`profile names are unique ignoring case.`);
|
|
264
|
+
}
|
|
265
|
+
byLowerName.set(name.toLowerCase(), file);
|
|
266
|
+
if (!fs.statSync(p).isFile()) {
|
|
267
|
+
throw new Error(`Profile file '${file}' is not a file.`);
|
|
268
|
+
}
|
|
269
|
+
if (version === VANILLA_VERSION) {
|
|
270
|
+
throw new Error(vanillaSiblingError(file, model));
|
|
271
|
+
}
|
|
272
|
+
const text = fs.readFileSync(p, 'utf8');
|
|
273
|
+
const declared = parseProfileName(text, file, name);
|
|
274
|
+
const reserved = (n) => n?.toLowerCase() === DEFAULT_PROFILE_NAME;
|
|
275
|
+
if (reserved(name)) {
|
|
276
|
+
throw new Error(`Profile name '${DEFAULT_PROFILE_NAME}' is reserved for the ` +
|
|
277
|
+
`inline bindings in '${modelFile}'; remove '${file}'.`);
|
|
278
|
+
}
|
|
279
|
+
if (reserved(declared)) {
|
|
280
|
+
throw new Error(`Profile name '${DEFAULT_PROFILE_NAME}' is reserved for the ` +
|
|
281
|
+
`inline bindings in '${modelFile}'; change the name in '${file}' to '${name}'.`);
|
|
282
|
+
}
|
|
283
|
+
if (declared === undefined) {
|
|
284
|
+
throw new Error(`Profile file '${file}' declares no name; add 'name: ${name}'.`);
|
|
285
|
+
}
|
|
286
|
+
if (declared !== name) {
|
|
287
|
+
throw new Error(`Profile file '${file}' declares name '${declared}', which ` +
|
|
288
|
+
`does not match filename suffix '${name}'.`);
|
|
289
|
+
}
|
|
290
|
+
docs.push({ name, text, path: p });
|
|
291
|
+
}
|
|
292
|
+
return docs;
|
|
293
|
+
}
|
|
294
|
+
// The text of one profile file beside a model, or undefined when the model
|
|
295
|
+
// has no profile of that name. The rules `profileDocuments` enforces apply.
|
|
296
|
+
profileDocument(model, profileName) {
|
|
297
|
+
return this.profileDocuments(model).find(d => d.name === profileName)
|
|
298
|
+
?.text;
|
|
299
|
+
}
|
|
300
|
+
// The path a profile file for this model and profile name maps to:
|
|
301
|
+
// `<catalog>/EntryGroups/<entryGroup>/<model>.profile.<name>.yaml`. Throws
|
|
302
|
+
// when `profileName` breaks the profile naming rule or `model` holds a path
|
|
303
|
+
// separator, so a name read from elsewhere cannot point outside the entry
|
|
304
|
+
// group.
|
|
305
|
+
profilePath(model, profileName) {
|
|
306
|
+
if (/[\\/]/.test(model)) {
|
|
307
|
+
throw new Error(`Model name '${model}' is not valid in a file name.`);
|
|
308
|
+
}
|
|
309
|
+
if ((0, names_1.nameProblem)('profile', profileName)) {
|
|
310
|
+
throw new Error(`Profile name '${profileName}' is not valid; a profile name is a ` +
|
|
311
|
+
`letter, then letters, digits, underscores or hyphens.`);
|
|
312
|
+
}
|
|
313
|
+
return path.join(this._groupDir(), `${model}.profile.${profileName}.yaml`);
|
|
314
|
+
}
|
|
315
|
+
// Writes a profile file beside its model, creating the EntryGroup directory
|
|
316
|
+
// if needed, for `pull` to write a catalog profile through.
|
|
317
|
+
writeProfileDocument(model, profileName, text) {
|
|
318
|
+
const localPath = this.profilePath(model, profileName);
|
|
319
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
320
|
+
fs.writeFileSync(localPath, text);
|
|
321
|
+
}
|
|
322
|
+
// Deletes a profile file, for `pull --force-remove` to drop a local profile
|
|
323
|
+
// the catalog no longer has.
|
|
324
|
+
removeProfileDocument(model, profileName) {
|
|
325
|
+
const localPath = this.profilePath(model, profileName);
|
|
326
|
+
if (fs.existsSync(localPath))
|
|
327
|
+
fs.rmSync(localPath);
|
|
328
|
+
}
|
|
329
|
+
// Profile files with no model beside them, for push, `kcmd profiles` and
|
|
330
|
+
// `kcmd skills-generate` to reject: any
|
|
331
|
+
// `<prefix>.profile.<name>.yaml`, or nameless `<prefix>.profile.yaml`, where
|
|
332
|
+
// `<prefix>.yaml` does not exist.
|
|
333
|
+
orphanProfilePaths() {
|
|
334
|
+
const dir = this._entryGroup ? this._groupDir() : undefined;
|
|
335
|
+
if (!dir || !fs.existsSync(dir))
|
|
336
|
+
return [];
|
|
337
|
+
return fs.readdirSync(dir)
|
|
338
|
+
.filter(f => {
|
|
339
|
+
if (f.startsWith('.') || !ANY_PROFILE_FILE.test(f))
|
|
340
|
+
return false;
|
|
341
|
+
const prefix = f.slice(0, f.indexOf('.profile.'));
|
|
342
|
+
return !fs.existsSync(path.join(dir, `${prefix}.yaml`));
|
|
343
|
+
})
|
|
344
|
+
.sort()
|
|
345
|
+
.map(f => path.join(dir, f));
|
|
346
|
+
}
|
|
347
|
+
// The older `<model>.profiles/` directories in the EntryGroup dir that still
|
|
348
|
+
// hold a profile file, which push, `kcmd profiles` and `kcmd skills-generate`
|
|
349
|
+
// reject.
|
|
350
|
+
legacyProfileDirs() {
|
|
351
|
+
const dir = this._entryGroup ? this._groupDir() : undefined;
|
|
352
|
+
if (!dir || !fs.existsSync(dir))
|
|
353
|
+
return [];
|
|
354
|
+
return fs.readdirSync(dir)
|
|
355
|
+
.filter(f => f.endsWith('.profiles') &&
|
|
356
|
+
fs.statSync(path.join(dir, f), { throwIfNoEntry: false })
|
|
357
|
+
?.isDirectory() &&
|
|
358
|
+
this._legacyProfilePaths(f.slice(0, -'.profiles'.length))
|
|
359
|
+
.length > 0)
|
|
360
|
+
.sort()
|
|
361
|
+
.map(f => path.join(dir, f));
|
|
362
|
+
}
|
|
363
|
+
_groupDir() {
|
|
364
|
+
if (!this._entryGroup) {
|
|
365
|
+
throw new Error('SemanticModel layout has no entry group; cannot resolve a profile path.');
|
|
366
|
+
}
|
|
367
|
+
return path.join(this._catalogPath, 'EntryGroups', this._entryGroup);
|
|
368
|
+
}
|
|
369
|
+
_siblingProfilePaths(model) {
|
|
370
|
+
return this._entryGroup ? profileFilesBeside(this._groupDir(), model) : [];
|
|
371
|
+
}
|
|
372
|
+
// `<model>.profiles/*.yaml`, each named by its file basename, for pull to
|
|
373
|
+
// find the stale ones (see localProfileFiles). A model name
|
|
374
|
+
// that pull read from the catalog may hold a path separator, and with it a
|
|
375
|
+
// `..` segment, so such a name has no directory here.
|
|
376
|
+
_legacyProfilePaths(model) {
|
|
377
|
+
if (!this._entryGroup || /[\\/]/.test(model))
|
|
378
|
+
return [];
|
|
379
|
+
const dir = path.join(this._groupDir(), `${model}.profiles`);
|
|
380
|
+
if (!fs.existsSync(dir))
|
|
381
|
+
return [];
|
|
382
|
+
const out = [];
|
|
383
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
384
|
+
if (!entry.endsWith('.yaml'))
|
|
385
|
+
continue;
|
|
386
|
+
if (SIDECAR_SUFFIXES.some(s => entry.endsWith(s)))
|
|
387
|
+
continue;
|
|
388
|
+
out.push({ name: path.basename(entry, '.yaml'), path: path.join(dir, entry) });
|
|
389
|
+
}
|
|
390
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
391
|
+
}
|
|
392
|
+
// The `version` and model `name` a model document declares, each undefined
|
|
393
|
+
// when the document has none. In a document with a YAML error, the version
|
|
394
|
+
// is read as the lookup of its GOOGLE block profiles reads it, and is
|
|
395
|
+
// undefined when the error may change it. The name is undefined then, since
|
|
396
|
+
// the error can change the name the text seems to declare. The loader
|
|
397
|
+
// reports the error.
|
|
398
|
+
_modelHeader(model) {
|
|
399
|
+
const localPath = this._index.get(model);
|
|
400
|
+
if (!localPath)
|
|
401
|
+
return {};
|
|
402
|
+
try {
|
|
403
|
+
const text = fs.readFileSync(localPath, 'utf8');
|
|
404
|
+
let doc;
|
|
405
|
+
try {
|
|
406
|
+
doc = yaml.parse(text, yaml_options_1.YAML_OPTIONS);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// A YAML error; the name stays undefined.
|
|
410
|
+
}
|
|
411
|
+
const name = Array.isArray(doc?.semantic_model) ?
|
|
412
|
+
doc.semantic_model[0]?.name :
|
|
413
|
+
undefined;
|
|
414
|
+
return {
|
|
415
|
+
version: (0, loader_1.modelDocumentVersion)(text),
|
|
416
|
+
name: typeof name === 'string' ? name : undefined,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// True when a model document with this handle already exists on disk. `pull`
|
|
424
|
+
// uses it to report which files it would overwrite vs. create.
|
|
425
|
+
hasModel(name) {
|
|
426
|
+
return fs.existsSync(this.modelPath(name));
|
|
427
|
+
}
|
|
428
|
+
// The absolute path a model document with this handle maps to:
|
|
429
|
+
// `<catalog>/EntryGroups/<entryGroup>/<name>.yaml`. Path separators in the
|
|
430
|
+
// model name are replaced so a name still yields a single flat file. Requires
|
|
431
|
+
// the layout to be scoped to an entry group (the semantic-model source always
|
|
432
|
+
// is).
|
|
433
|
+
modelPath(name) {
|
|
434
|
+
if (!this._entryGroup) {
|
|
435
|
+
throw new Error('SemanticModel layout has no entry group; cannot resolve a model path.');
|
|
436
|
+
}
|
|
437
|
+
const file = `${name.replace(/[/\\]/g, '_')}.yaml`;
|
|
438
|
+
return path.join(this._catalogPath, 'EntryGroups', this._entryGroup, file);
|
|
439
|
+
}
|
|
440
|
+
// Writes a model's serialized document to its path, creating the EntryGroup
|
|
441
|
+
// directory if needed, and indexes it so a later modelDocuments() sees it.
|
|
442
|
+
// This is the sink `pull` writes reconstructed models to.
|
|
443
|
+
writeModelDocument(name, text) {
|
|
444
|
+
const localPath = this.modelPath(name);
|
|
445
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
446
|
+
fs.writeFileSync(localPath, text);
|
|
447
|
+
this._index.set(name, localPath);
|
|
448
|
+
}
|
|
449
|
+
// Deletes a model document from disk and the index. `pull --force-remove`
|
|
450
|
+
// uses it to drop a local model the catalog no longer names before writing
|
|
451
|
+
// the catalog's, so the entry group is never left holding two models.
|
|
452
|
+
removeModelDocument(name) {
|
|
453
|
+
const localPath = this.modelPath(name);
|
|
454
|
+
if (fs.existsSync(localPath)) {
|
|
455
|
+
fs.rmSync(localPath);
|
|
456
|
+
}
|
|
457
|
+
this._index.delete(name);
|
|
458
|
+
}
|
|
459
|
+
// The Knowledge Catalog entry-level members are not applicable to this
|
|
460
|
+
// push-only layout; the model is authored as a single Ossie document, not as
|
|
461
|
+
// per-entry Knowledge Catalog files. These are wired when KC-resource emit
|
|
462
|
+
// and semantic-model `pull` land.
|
|
463
|
+
async loadEntry(_name) {
|
|
464
|
+
throw new Error('The SemanticModel layout does not expose per-entry Knowledge Catalog files yet.');
|
|
465
|
+
}
|
|
466
|
+
async saveEntry(_name, _entry) {
|
|
467
|
+
throw new Error('The SemanticModel layout does not support writing per-entry files yet.');
|
|
468
|
+
}
|
|
469
|
+
async deleteEntry(_name) {
|
|
470
|
+
throw new Error('The SemanticModel layout does not support deleting per-entry files yet.');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
exports.SemanticModelLayout = SemanticModelLayout;
|
|
474
|
+
// The name push gives a model file or profile file of the model `model`: its
|
|
475
|
+
// path relative to the entry group directory, with `/` between segments.
|
|
476
|
+
function fileNameOf(layout, model, localPath) {
|
|
477
|
+
return path.relative(path.dirname(layout.modelPath(model)), localPath)
|
|
478
|
+
.split(path.sep)
|
|
479
|
+
.join('/');
|
|
480
|
+
}
|
|
481
|
+
// Reads the overview sidecar `text`. Throws naming `file` when its frontmatter
|
|
482
|
+
// does not parse, holds a key other than contentType and links, gives a
|
|
483
|
+
// contentType other than MARKDOWN or HTML, or a link with no url.
|
|
484
|
+
function parseOverviewSidecar(text, file) {
|
|
485
|
+
const fail = (problem) => new Error(`Overview sidecar '${file}': ${problem}`);
|
|
486
|
+
let split;
|
|
487
|
+
try {
|
|
488
|
+
split = (0, documents_1.splitFrontmatter)(text);
|
|
489
|
+
}
|
|
490
|
+
catch (err) {
|
|
491
|
+
throw fail(`its frontmatter does not parse: ${err?.message ?? err}`);
|
|
492
|
+
}
|
|
493
|
+
const metadata = split.metadata ?? {};
|
|
494
|
+
const extra = typeof metadata === 'object' && !Array.isArray(metadata) ?
|
|
495
|
+
Object.keys(metadata).find(k => k !== 'contentType' && k !== 'links') :
|
|
496
|
+
'';
|
|
497
|
+
if (extra !== undefined) {
|
|
498
|
+
throw fail(`its frontmatter holds only contentType and links` +
|
|
499
|
+
(extra ? `, not '${extra}'.` : '.'));
|
|
500
|
+
}
|
|
501
|
+
const { contentType, links } = metadata;
|
|
502
|
+
if (contentType !== undefined && contentType !== 'MARKDOWN' &&
|
|
503
|
+
contentType !== 'HTML') {
|
|
504
|
+
throw fail(`contentType is MARKDOWN or HTML, not '${contentType}'.`);
|
|
505
|
+
}
|
|
506
|
+
const isLink = (l) => typeof l?.url === 'string' &&
|
|
507
|
+
['string', 'undefined'].includes(typeof l.title) &&
|
|
508
|
+
Object.keys(l).every(k => k === 'url' || k === 'title');
|
|
509
|
+
if (links !== undefined && !(Array.isArray(links) && links.every(isLink))) {
|
|
510
|
+
throw fail('links is a list of entries, each a url and an optional title.');
|
|
511
|
+
}
|
|
512
|
+
return { content: split.body.trim() || undefined, contentType, links };
|
|
513
|
+
}
|
|
514
|
+
// The text of the overview sidecar that holds `overview`.
|
|
515
|
+
function formatOverviewSidecar({ content = '', ...rest }) {
|
|
516
|
+
// The body ends with a newline, which push trims.
|
|
517
|
+
const body = content && !content.endsWith('\n') ? `${content}\n` : content;
|
|
518
|
+
// Content whose first line is `---` would otherwise read back as frontmatter.
|
|
519
|
+
return Object.keys(rest).length || body.split(/\r?\n/, 1)[0] === '---' ?
|
|
520
|
+
(0, documents_1.joinFrontmatter)(rest, body) :
|
|
521
|
+
body;
|
|
522
|
+
}
|
|
523
|
+
// The `name` a profile file declares at its top level, or undefined when it
|
|
524
|
+
// declares none. A file that does not parse or that uses the legacy
|
|
525
|
+
// `semantic_model:` wrapper throws, naming the file.
|
|
526
|
+
function parseProfileName(text, file, name) {
|
|
527
|
+
let doc;
|
|
528
|
+
try {
|
|
529
|
+
doc = yaml.parse(text, yaml_options_1.YAML_OPTIONS);
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
throw new Error(`Profile file '${file}' does not parse: ${err?.message ?? err}`);
|
|
533
|
+
}
|
|
534
|
+
if (doc?.semantic_model !== undefined) {
|
|
535
|
+
throw new Error(`Profile file '${file}' uses the legacy 'semantic_model:' wrapper, ` +
|
|
536
|
+
`which is no longer supported; write its entities, relationships ` +
|
|
537
|
+
`and actions at the top level under 'name: ${name}', and move any ` +
|
|
538
|
+
`'deployment_target' to an entry under 'deployments:' in the model ` +
|
|
539
|
+
`file with 'profile: ${name}'.`);
|
|
540
|
+
}
|
|
541
|
+
return typeof doc?.name === 'string' ? doc.name : undefined;
|
|
542
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CatalogLayout } from '../layout';
|
|
2
|
+
import * as md from '../metadata';
|
|
3
|
+
export declare class StandardLayout implements CatalogLayout {
|
|
4
|
+
private readonly _catalogPath;
|
|
5
|
+
private readonly _index;
|
|
6
|
+
constructor(catalogPath: string);
|
|
7
|
+
init(): Promise<void>;
|
|
8
|
+
entryExists(name: string): boolean;
|
|
9
|
+
listEntries(): string[];
|
|
10
|
+
loadEntry(name: string): Promise<md.Entry>;
|
|
11
|
+
saveEntry(name: string, entry: md.Entry): Promise<void>;
|
|
12
|
+
deleteEntry(name: string): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Implements the standard layout (yaml files in directory)
|
|
3
|
+
//
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
+
var ownKeys = function(o) {
|
|
22
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
+
var ar = [];
|
|
24
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
+
return ar;
|
|
26
|
+
};
|
|
27
|
+
return ownKeys(o);
|
|
28
|
+
};
|
|
29
|
+
return function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
36
|
+
})();
|
|
37
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
+
exports.StandardLayout = void 0;
|
|
39
|
+
const fs = __importStar(require("node:fs"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const glob = __importStar(require("glob"));
|
|
42
|
+
const yaml = __importStar(require("yaml"));
|
|
43
|
+
class StandardLayout {
|
|
44
|
+
_catalogPath;
|
|
45
|
+
_index = new Map();
|
|
46
|
+
constructor(catalogPath) {
|
|
47
|
+
this._catalogPath = catalogPath;
|
|
48
|
+
}
|
|
49
|
+
async init() {
|
|
50
|
+
this._index.clear();
|
|
51
|
+
if (!fs.existsSync(this._catalogPath)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const matches = await glob.glob('**/*.yaml', {
|
|
55
|
+
cwd: this._catalogPath,
|
|
56
|
+
absolute: true,
|
|
57
|
+
nodir: true,
|
|
58
|
+
});
|
|
59
|
+
for (const localPath of matches) {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.promises.readFile(localPath, 'utf8');
|
|
62
|
+
const metadata = yaml.parse(content);
|
|
63
|
+
if (metadata && metadata.name) {
|
|
64
|
+
this._index.set(metadata.name, localPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// A file that fails to read or parse is left out of the index, so it is
|
|
69
|
+
// never listed and never pushed. Warn rather than drop it silently:
|
|
70
|
+
// push otherwise makes the destination match the directory, and a file
|
|
71
|
+
// vanishing from that set is easy to miss.
|
|
72
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const relPath = path.relative(this._catalogPath, localPath);
|
|
74
|
+
console.warn(`Skipping unreadable catalog file '${relPath}': ${reason}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
entryExists(name) {
|
|
79
|
+
const entryPath = this._index.get(name);
|
|
80
|
+
return !!entryPath && fs.existsSync(entryPath);
|
|
81
|
+
}
|
|
82
|
+
listEntries() {
|
|
83
|
+
return Array.from(this._index.keys());
|
|
84
|
+
}
|
|
85
|
+
async loadEntry(name) {
|
|
86
|
+
const entryPath = this._index.get(name);
|
|
87
|
+
if (!entryPath || !fs.existsSync(entryPath)) {
|
|
88
|
+
throw new Error(`Entry not found: ${name}`);
|
|
89
|
+
}
|
|
90
|
+
const content = await fs.promises.readFile(entryPath, 'utf8');
|
|
91
|
+
return yaml.parse(content);
|
|
92
|
+
}
|
|
93
|
+
async saveEntry(name, entry) {
|
|
94
|
+
const entryPath = path.join(this._catalogPath, `${name}.yaml`);
|
|
95
|
+
await fs.promises.mkdir(path.dirname(entryPath), { recursive: true });
|
|
96
|
+
await fs.promises.writeFile(entryPath, yaml.stringify(entry), 'utf8');
|
|
97
|
+
this._index.set(name, entryPath);
|
|
98
|
+
}
|
|
99
|
+
async deleteEntry(name) {
|
|
100
|
+
const entryPath = this._index.get(name);
|
|
101
|
+
if (!entryPath || !fs.existsSync(entryPath)) {
|
|
102
|
+
throw new Error(`Entry not found: ${name}`);
|
|
103
|
+
}
|
|
104
|
+
await fs.promises.unlink(entryPath);
|
|
105
|
+
this._index.delete(name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.StandardLayout = StandardLayout;
|