@revoengine/cli 1.0.9 → 1.0.11
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 +464 -8
- package/dist/src/cli.js +65 -6
- package/dist/src/client.d.ts +366 -5
- package/dist/src/client.js +954 -13
- package/dist/src/commands/auth.js +4 -2
- package/dist/src/commands/component.js +839 -412
- package/dist/src/commands/database-schemas.d.ts +2 -0
- package/dist/src/commands/database-schemas.js +188 -0
- package/dist/src/commands/database-views.d.ts +2 -0
- package/dist/src/commands/database-views.js +123 -0
- package/dist/src/commands/endpoints.js +114 -0
- package/dist/src/commands/env.d.ts +2 -0
- package/dist/src/commands/env.js +380 -0
- package/dist/src/commands/events.d.ts +2 -0
- package/dist/src/commands/events.js +146 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +169 -0
- package/dist/src/commands/index.d.ts +10 -0
- package/dist/src/commands/index.js +10 -0
- package/dist/src/commands/job-templates.d.ts +2 -0
- package/dist/src/commands/job-templates.js +101 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +159 -0
- package/dist/src/commands/project.js +82 -1
- package/dist/src/commands/role-groups.d.ts +2 -0
- package/dist/src/commands/role-groups.js +152 -0
- package/dist/src/commands/schedules.d.ts +2 -0
- package/dist/src/commands/schedules.js +141 -0
- package/dist/src/commands/terminal-service.d.ts +38 -0
- package/dist/src/commands/terminal-service.js +210 -0
- package/dist/src/commands/terminal.d.ts +22 -0
- package/dist/src/commands/terminal.js +511 -0
- package/dist/src/component-lock.d.ts +126 -2
- package/dist/src/component-lock.js +378 -15
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +121 -6
- package/dist/src/database-schema-artifacts.d.ts +7 -0
- package/dist/src/database-schema-artifacts.js +8 -0
- package/dist/src/env-sync.d.ts +83 -0
- package/dist/src/env-sync.js +315 -0
- package/dist/src/metadata-backfill.d.ts +56 -0
- package/dist/src/metadata-backfill.js +1176 -0
- package/dist/src/project.d.ts +17 -9
- package/dist/src/project.js +87 -11
- package/dist/src/prompt.js +10 -18
- package/dist/src/resource-metadata.d.ts +25 -0
- package/dist/src/resource-metadata.js +132 -0
- package/dist/src/resource-syncs/database-schema-sync.d.ts +117 -0
- package/dist/src/resource-syncs/database-schema-sync.js +2289 -0
- package/dist/src/resource-syncs/database-view-sync.d.ts +124 -0
- package/dist/src/resource-syncs/database-view-sync.js +1317 -0
- package/dist/src/resource-syncs/endpoint-sync.d.ts +96 -0
- package/dist/src/resource-syncs/endpoint-sync.js +1283 -0
- package/dist/src/resource-syncs/event-sync.d.ts +99 -0
- package/dist/src/resource-syncs/event-sync.js +949 -0
- package/dist/src/resource-syncs/group-sync.d.ts +86 -0
- package/dist/src/resource-syncs/group-sync.js +882 -0
- package/dist/src/resource-syncs/job-template-sync.d.ts +85 -0
- package/dist/src/resource-syncs/job-template-sync.js +782 -0
- package/dist/src/resource-syncs/role-group-sync.d.ts +83 -0
- package/dist/src/resource-syncs/role-group-sync.js +597 -0
- package/dist/src/resource-syncs/schedule-sync.d.ts +111 -0
- package/dist/src/resource-syncs/schedule-sync.js +1302 -0
- package/dist/src/resource-syncs/util.d.ts +19 -0
- package/dist/src/resource-syncs/util.js +116 -0
- package/dist/src/runtime-view.d.ts +1 -0
- package/dist/src/runtime-view.js +6 -1
- package/dist/src/sync-output.d.ts +38 -0
- package/dist/src/sync-output.js +131 -0
- package/dist/src/tracked-resources.d.ts +7 -0
- package/dist/src/tracked-resources.js +61 -0
- package/dist/src/types.d.ts +227 -0
- package/dist/src/ui.d.ts +3 -0
- package/dist/src/ui.js +68 -10
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +64 -0
- package/dist/src/workspace-component.d.ts +2 -0
- package/dist/src/workspace-component.js +34 -0
- package/dist/src/workspace-resource.d.ts +2 -0
- package/dist/src/workspace-resource.js +52 -0
- package/package.json +8 -3
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { hashStable, readComponentLock, writeComponentLock } from "../component-lock.js";
|
|
3
|
+
import { NULL_CATEGORY_KEY, normalizeStableKeyValue, readConfiguredResourceMetadataStableKey, sanitizeUserMetadata } from "../resource-metadata.js";
|
|
4
|
+
import { readJsonFile, sanitizeSegment } from "../utils.js";
|
|
5
|
+
import { findManifestFiles, isRecord, localTargetPath, requireProjectLayout, stableKeySuffix, toPortableRelativePath, unwrapData, writePulledManifest, } from "./util.js";
|
|
6
|
+
export const EVENTS_DIRECTORY = 'Events';
|
|
7
|
+
export const EVENT_MANIFEST_FILE = 'event.json';
|
|
8
|
+
const EVENT_TRANSPORT_FIELDS = new Set([
|
|
9
|
+
'jobEventId', 'eventId', 'id', 'name', 'category', 'desc', 'description', 'active', 'type',
|
|
10
|
+
'targetType', 'targetId', 'targetVersion', 'targetConfig', 'filter', 'version', 'metadata',
|
|
11
|
+
'metaData', 'resourceMetadata', 'topic', 'deletedAt', 'deletedBy', 'createdAt', 'createdBy',
|
|
12
|
+
'lastUpdateAt', 'lastUpdateBy', 'updatedAt', 'updatedBy', 'history', 'jobEventHistory',
|
|
13
|
+
]);
|
|
14
|
+
function eventIdOf(record) {
|
|
15
|
+
return String(record.jobEventId || record.eventId || record.id || '');
|
|
16
|
+
}
|
|
17
|
+
function jobTemplateIdOf(record) {
|
|
18
|
+
return String(record.jobTemplateId || record.id || '');
|
|
19
|
+
}
|
|
20
|
+
function canonicalValue(value) {
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
return value.map((item) => canonicalValue(item));
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(value)) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
return Object.fromEntries(Object.keys(value).sort((left, right) => left.localeCompare(right))
|
|
28
|
+
.map((key) => [key, canonicalValue(value[key])]));
|
|
29
|
+
}
|
|
30
|
+
function canonicalMetadata(record, stableKeyName, stableKey) {
|
|
31
|
+
return canonicalValue({
|
|
32
|
+
...sanitizeUserMetadata(record.resourceMetadata),
|
|
33
|
+
...sanitizeUserMetadata(record.metaData),
|
|
34
|
+
...sanitizeUserMetadata(record.metadata),
|
|
35
|
+
[stableKeyName]: stableKey,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function extensionFields(record) {
|
|
39
|
+
return canonicalValue(Object.fromEntries(Object.entries(record)
|
|
40
|
+
.filter(([key, value]) => !EVENT_TRANSPORT_FIELDS.has(key) && value !== undefined)));
|
|
41
|
+
}
|
|
42
|
+
function eventExclusionReason(record) {
|
|
43
|
+
if (record.deletedAt || record.deletedBy) {
|
|
44
|
+
return 'deleted event is excluded';
|
|
45
|
+
}
|
|
46
|
+
if (record.targetType !== 'JOB_TEMPLATE') {
|
|
47
|
+
return `target type ${String(record.targetType || 'unknown')} is unsupported; only JOB_TEMPLATE is migrated`;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function eventPresentationKey(value) {
|
|
52
|
+
const category = typeof value.category === 'string' && value.category.trim()
|
|
53
|
+
? value.category
|
|
54
|
+
: NULL_CATEGORY_KEY;
|
|
55
|
+
return normalizeStableKeyValue(`${category}/${String(value.name || '')}`);
|
|
56
|
+
}
|
|
57
|
+
export function eventManifestPath(workspaceRoot, manifest) {
|
|
58
|
+
const category = typeof manifest.category === 'string' && manifest.category.trim()
|
|
59
|
+
? sanitizeSegment(manifest.category)
|
|
60
|
+
: 'Uncategorized';
|
|
61
|
+
return path.join(workspaceRoot, EVENTS_DIRECTORY, category, `${sanitizeSegment(manifest.name)}-${stableKeySuffix(manifest.stableKey)}`, EVENT_MANIFEST_FILE);
|
|
62
|
+
}
|
|
63
|
+
function portablePathIdentity(filePath) {
|
|
64
|
+
return path.resolve(filePath)
|
|
65
|
+
.normalize('NFC')
|
|
66
|
+
.split(path.sep)
|
|
67
|
+
.map((segment) => segment.replace(/[. ]+$/g, '').toLowerCase())
|
|
68
|
+
.join(path.sep);
|
|
69
|
+
}
|
|
70
|
+
function disambiguateManifestPath(filePath, stableKey) {
|
|
71
|
+
const directory = path.dirname(filePath);
|
|
72
|
+
return path.join(path.dirname(directory), `${path.basename(directory)}-${hashStable(stableKey).slice(0, 8)}`, path.basename(filePath));
|
|
73
|
+
}
|
|
74
|
+
function readWorkspaceEvents(cwd) {
|
|
75
|
+
const { workspaceRoot } = requireProjectLayout(cwd, 'Event');
|
|
76
|
+
return findManifestFiles(path.join(workspaceRoot, EVENTS_DIRECTORY), EVENT_MANIFEST_FILE).map((manifestPath) => ({
|
|
77
|
+
manifestPath,
|
|
78
|
+
manifest: readJsonFile(manifestPath),
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function nonEmptyString(value) {
|
|
82
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
83
|
+
}
|
|
84
|
+
function validManifest(value, stableKeyName) {
|
|
85
|
+
if (!isRecord(value) || value.kind !== 'event' || !nonEmptyString(value.stableKey) || !nonEmptyString(value.name)
|
|
86
|
+
|| !nonEmptyString(value.type) || !isRecord(value.metadata) || !isRecord(value.target)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (value.category !== undefined && value.category !== null && typeof value.category !== 'string') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (value.description !== undefined && typeof value.description !== 'string') {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (typeof value.active !== 'boolean' || value.target.type !== 'JOB_TEMPLATE' || !nonEmptyString(value.target.jobTemplateStableKey)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (value.filter !== undefined && value.filter !== null && !isRecord(value.filter)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if (value.extensions !== undefined && !isRecord(value.extensions)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if (value.extensions !== undefined && Object.keys(value.extensions).length > 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return readConfiguredResourceMetadataStableKey(value, stableKeyName) === value.stableKey;
|
|
108
|
+
}
|
|
109
|
+
function manifestFromRecord(input) {
|
|
110
|
+
const { record, stableKey, jobTemplateStableKey, stableKeyName } = input;
|
|
111
|
+
const extensions = extensionFields(record);
|
|
112
|
+
return {
|
|
113
|
+
kind: 'event',
|
|
114
|
+
stableKey,
|
|
115
|
+
name: String(record.name || ''),
|
|
116
|
+
...(record.category === undefined ? {} : { category: record.category }),
|
|
117
|
+
...(typeof record.desc === 'string' ? { description: record.desc } : typeof record.description === 'string' ? { description: record.description } : {}),
|
|
118
|
+
metadata: canonicalMetadata(record, stableKeyName, stableKey),
|
|
119
|
+
active: record.active === true,
|
|
120
|
+
type: String(record.type || ''),
|
|
121
|
+
...(record.filter === undefined ? {} : { filter: canonicalValue(record.filter) }),
|
|
122
|
+
target: { type: 'JOB_TEMPLATE', jobTemplateStableKey },
|
|
123
|
+
...(Object.keys(extensions).length > 0 ? { extensions } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export function hashEventContent(manifest) {
|
|
127
|
+
return hashStable({
|
|
128
|
+
kind: 'event', stableKey: manifest.stableKey, name: manifest.name, category: manifest.category ?? null,
|
|
129
|
+
description: manifest.description ?? '', metadata: canonicalValue(manifest.metadata), type: manifest.type,
|
|
130
|
+
filter: canonicalValue(manifest.filter ?? null), target: manifest.target, extensions: canonicalValue(manifest.extensions || {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
export function hashPortableEvent(manifest) {
|
|
134
|
+
return hashStable({ content: hashEventContent(manifest), active: manifest.active });
|
|
135
|
+
}
|
|
136
|
+
export function hashEventManifest(manifest) {
|
|
137
|
+
return hashStable(canonicalValue(manifest));
|
|
138
|
+
}
|
|
139
|
+
async function hydrate(records, idOf, get) {
|
|
140
|
+
return Promise.all(records.map(async (record) => {
|
|
141
|
+
const id = idOf(record);
|
|
142
|
+
return id ? unwrapData(await get(id)) : record;
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
async function loadRemoteEventInventory(client, stableKeyName) {
|
|
146
|
+
const [{ events: eventSummaries }, { jobTemplates: templateSummaries }] = await Promise.all([
|
|
147
|
+
client.listAllEvents({ includeDeleted: true }), client.listAllJobTemplates(),
|
|
148
|
+
]);
|
|
149
|
+
const [events, jobTemplates] = await Promise.all([
|
|
150
|
+
hydrate(eventSummaries, eventIdOf, (id) => client.getEvent(id)),
|
|
151
|
+
hydrate(templateSummaries, jobTemplateIdOf, (id) => client.getJobTemplate(id)),
|
|
152
|
+
]);
|
|
153
|
+
const eventsByStableKey = new Map();
|
|
154
|
+
const jobTemplatesByStableKey = new Map();
|
|
155
|
+
const jobTemplatesById = new Map();
|
|
156
|
+
for (const event of events) {
|
|
157
|
+
const stableKey = readConfiguredResourceMetadataStableKey(event, stableKeyName);
|
|
158
|
+
if (stableKey) {
|
|
159
|
+
eventsByStableKey.set(stableKey, [...(eventsByStableKey.get(stableKey) || []), event]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (const jobTemplate of jobTemplates) {
|
|
163
|
+
const id = jobTemplateIdOf(jobTemplate);
|
|
164
|
+
const stableKey = readConfiguredResourceMetadataStableKey(jobTemplate, stableKeyName);
|
|
165
|
+
if (id) {
|
|
166
|
+
jobTemplatesById.set(id, jobTemplate);
|
|
167
|
+
}
|
|
168
|
+
if (stableKey) {
|
|
169
|
+
jobTemplatesByStableKey.set(stableKey, [...(jobTemplatesByStableKey.get(stableKey) || []), jobTemplate]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { events, eventsByStableKey, jobTemplatesById, jobTemplatesByStableKey };
|
|
173
|
+
}
|
|
174
|
+
function emptyPlanCounts() {
|
|
175
|
+
return {
|
|
176
|
+
clean: 0,
|
|
177
|
+
create: 0,
|
|
178
|
+
'safe-update': 0,
|
|
179
|
+
'remote-changed': 0,
|
|
180
|
+
conflict: 0,
|
|
181
|
+
'missing-lock': 0,
|
|
182
|
+
'activation-drift': 0,
|
|
183
|
+
orphan: 0,
|
|
184
|
+
collision: 0,
|
|
185
|
+
skipped: 0,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function diffEvents(desired, current) {
|
|
189
|
+
const fields = [
|
|
190
|
+
['name', desired.name, current.name],
|
|
191
|
+
['category', desired.category ?? null, current.category ?? null],
|
|
192
|
+
['description', desired.description ?? '', current.description ?? ''],
|
|
193
|
+
['metadata', desired.metadata, current.metadata],
|
|
194
|
+
['type', desired.type, current.type],
|
|
195
|
+
['filter', desired.filter ?? null, current.filter ?? null],
|
|
196
|
+
['target.jobTemplateStableKey', desired.target.jobTemplateStableKey, current.target.jobTemplateStableKey],
|
|
197
|
+
['extensions', desired.extensions || {}, current.extensions || {}],
|
|
198
|
+
];
|
|
199
|
+
return fields
|
|
200
|
+
.filter(([, left, right]) => hashStable(left) !== hashStable(right))
|
|
201
|
+
.map(([field]) => field)
|
|
202
|
+
.sort();
|
|
203
|
+
}
|
|
204
|
+
function eventManifestFromTarget(record, templatesById, templatesByStableKey, stableKeyName) {
|
|
205
|
+
const stableKey = readConfiguredResourceMetadataStableKey(record, stableKeyName);
|
|
206
|
+
if (!stableKey) {
|
|
207
|
+
return { reason: `missing ${stableKeyName}; unmanaged` };
|
|
208
|
+
}
|
|
209
|
+
const exclusion = eventExclusionReason(record);
|
|
210
|
+
if (exclusion) {
|
|
211
|
+
return { stableKey, reason: exclusion };
|
|
212
|
+
}
|
|
213
|
+
const template = record.targetId ? templatesById.get(record.targetId) : undefined;
|
|
214
|
+
const jobTemplateStableKey = template
|
|
215
|
+
? readConfiguredResourceMetadataStableKey(template, stableKeyName)
|
|
216
|
+
: undefined;
|
|
217
|
+
if (!jobTemplateStableKey) {
|
|
218
|
+
return { stableKey, reason: 'target Job Template dependency is missing stableKey or unavailable', dependencyBlocked: true };
|
|
219
|
+
}
|
|
220
|
+
const matchingTemplates = templatesByStableKey.get(jobTemplateStableKey) || [];
|
|
221
|
+
if (matchingTemplates.length !== 1) {
|
|
222
|
+
return {
|
|
223
|
+
stableKey,
|
|
224
|
+
reason: `target Job Template dependency "${jobTemplateStableKey}" is ambiguous`,
|
|
225
|
+
dependencyBlocked: true,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return { stableKey, manifest: manifestFromRecord({ record, stableKey, jobTemplateStableKey, stableKeyName }) };
|
|
229
|
+
}
|
|
230
|
+
export async function buildEventsPlan(input) {
|
|
231
|
+
const { projectRoot } = requireProjectLayout(input.cwd, 'Event');
|
|
232
|
+
const remote = await loadRemoteEventInventory(input.client, input.stableKeyName);
|
|
233
|
+
const targetEvents = remote.events;
|
|
234
|
+
const templatesById = remote.jobTemplatesById;
|
|
235
|
+
const templatesByStableKey = remote.jobTemplatesByStableKey;
|
|
236
|
+
const blockers = [];
|
|
237
|
+
const exclusions = [];
|
|
238
|
+
const items = [];
|
|
239
|
+
const localByStableKey = new Map();
|
|
240
|
+
for (const entry of readWorkspaceEvents(input.cwd)) {
|
|
241
|
+
const manifest = entry.manifest;
|
|
242
|
+
if (!validManifest(manifest, input.stableKeyName)) {
|
|
243
|
+
const unsupportedExtensions = isRecord(manifest) && isRecord(manifest.extensions)
|
|
244
|
+
? Object.keys(manifest.extensions).sort()
|
|
245
|
+
: [];
|
|
246
|
+
const reason = unsupportedExtensions.length > 0
|
|
247
|
+
? `unsupported extension fields: ${unsupportedExtensions.join(', ')}`
|
|
248
|
+
: 'invalid manifest';
|
|
249
|
+
blockers.push(`Invalid Event manifest ${path.relative(projectRoot, entry.manifestPath)}: ${reason}.`);
|
|
250
|
+
items.push({ status: 'conflict', name: isRecord(manifest) && nonEmptyString(manifest.name) ? manifest.name : path.basename(entry.manifestPath), reason });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
localByStableKey.set(manifest.stableKey, [...(localByStableKey.get(manifest.stableKey) || []), { manifest }]);
|
|
254
|
+
}
|
|
255
|
+
for (const [stableKey, localEvents] of localByStableKey) {
|
|
256
|
+
if (localEvents.length > 1) {
|
|
257
|
+
blockers.push(`Duplicate local Event stableKey "${stableKey}".`);
|
|
258
|
+
for (const local of localEvents) {
|
|
259
|
+
items.push({ status: 'collision', stableKey, name: local.manifest.name, reason: 'duplicate workspace stableKey' });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const targetsByStableKey = new Map();
|
|
264
|
+
const targetIssuesByStableKey = new Map();
|
|
265
|
+
for (const record of targetEvents) {
|
|
266
|
+
const target = eventManifestFromTarget(record, templatesById, templatesByStableKey, input.stableKeyName);
|
|
267
|
+
if ('manifest' in target) {
|
|
268
|
+
const targetEvent = {
|
|
269
|
+
manifest: target.manifest,
|
|
270
|
+
hash: hashPortableEvent(target.manifest),
|
|
271
|
+
contentHash: hashEventContent(target.manifest),
|
|
272
|
+
};
|
|
273
|
+
targetsByStableKey.set(target.stableKey, [...(targetsByStableKey.get(target.stableKey) || []), targetEvent]);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const label = target.stableKey || String(record.name || eventIdOf(record) || 'unknown event');
|
|
277
|
+
if (target.dependencyBlocked) {
|
|
278
|
+
blockers.push(`Cannot plan Event "${label}": ${target.reason}.`);
|
|
279
|
+
items.push({ status: 'conflict', ...(target.stableKey ? { stableKey: target.stableKey } : {}), name: String(record.name || label), reason: target.reason });
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
exclusions.push(`${label}: ${target.reason}`);
|
|
283
|
+
items.push({ status: 'skipped', ...(target.stableKey ? { stableKey: target.stableKey } : {}), name: String(record.name || label), reason: target.reason });
|
|
284
|
+
}
|
|
285
|
+
if (target.stableKey) {
|
|
286
|
+
targetIssuesByStableKey.set(target.stableKey, target.reason);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const [stableKey, targets] of targetsByStableKey) {
|
|
290
|
+
if (targets.length <= 1) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
blockers.push(`Duplicate target Event stableKey "${stableKey}".`);
|
|
294
|
+
for (const target of targets) {
|
|
295
|
+
items.push({
|
|
296
|
+
status: 'collision',
|
|
297
|
+
stableKey,
|
|
298
|
+
name: target.manifest.name,
|
|
299
|
+
currentHash: target.hash,
|
|
300
|
+
reason: 'duplicate target stableKey',
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const lock = readComponentLock(projectRoot);
|
|
305
|
+
const consumedTargets = new Set();
|
|
306
|
+
for (const [stableKey, localEvents] of localByStableKey) {
|
|
307
|
+
if (localEvents.length !== 1) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const desired = localEvents[0].manifest;
|
|
311
|
+
const templates = templatesByStableKey.get(desired.target.jobTemplateStableKey) || [];
|
|
312
|
+
if (templates.length !== 1) {
|
|
313
|
+
const reason = `target Job Template dependency "${desired.target.jobTemplateStableKey}" is ${templates.length === 0 ? 'missing' : 'ambiguous'}`;
|
|
314
|
+
blockers.push(`Cannot plan Event "${stableKey}": ${reason}.`);
|
|
315
|
+
items.push({ status: 'conflict', stableKey, name: desired.name, reason, jobTemplateStableKey: desired.target.jobTemplateStableKey });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const targets = targetsByStableKey.get(stableKey) || [];
|
|
319
|
+
const common = {
|
|
320
|
+
stableKey,
|
|
321
|
+
name: desired.name,
|
|
322
|
+
desiredHash: hashPortableEvent(desired),
|
|
323
|
+
jobTemplateStableKey: desired.target.jobTemplateStableKey,
|
|
324
|
+
};
|
|
325
|
+
if (targets.length > 1) {
|
|
326
|
+
consumedTargets.add(stableKey);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (targets.length === 0) {
|
|
330
|
+
const targetIssue = targetIssuesByStableKey.get(stableKey);
|
|
331
|
+
if (targetIssue) {
|
|
332
|
+
blockers.push(`Cannot plan Event "${stableKey}": ${targetIssue}.`);
|
|
333
|
+
items.push({ status: 'conflict', ...common, reason: targetIssue });
|
|
334
|
+
}
|
|
335
|
+
else if (targetEvents.some((record) => !record.deletedAt
|
|
336
|
+
&& !record.deletedBy
|
|
337
|
+
&& !readConfiguredResourceMetadataStableKey(record, input.stableKeyName)
|
|
338
|
+
&& eventPresentationKey(record) === eventPresentationKey(desired))) {
|
|
339
|
+
const reason = `target Event matching category/name is missing ${input.stableKeyName}`;
|
|
340
|
+
blockers.push(`Cannot plan Event "${stableKey}": ${reason}.`);
|
|
341
|
+
items.push({ status: 'conflict', ...common, reason });
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
items.push({ status: 'create', ...common });
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
consumedTargets.add(stableKey);
|
|
349
|
+
const current = targets[0];
|
|
350
|
+
const currentHash = current.hash;
|
|
351
|
+
const baselineHash = lock.events[stableKey]?.environments[input.envName]?.remoteHash;
|
|
352
|
+
const currentContentHash = current.contentHash;
|
|
353
|
+
const desiredContentHash = hashEventContent(desired);
|
|
354
|
+
if (desiredContentHash === currentContentHash && desired.active !== current.manifest.active) {
|
|
355
|
+
items.push({ status: 'activation-drift', ...common, currentHash, baselineHash, reason: `source active=${desired.active}; target active=${current.manifest.active}` });
|
|
356
|
+
}
|
|
357
|
+
else if (common.desiredHash === currentHash) {
|
|
358
|
+
items.push({ status: 'clean', ...common, currentHash, ...(baselineHash ? { baselineHash } : {}) });
|
|
359
|
+
}
|
|
360
|
+
else if (!baselineHash) {
|
|
361
|
+
items.push({ status: 'missing-lock', ...common, currentHash, reason: `missing ${input.envName} lock baseline` });
|
|
362
|
+
}
|
|
363
|
+
else if (currentHash === baselineHash) {
|
|
364
|
+
items.push({ status: 'safe-update', ...common, currentHash, baselineHash, changedFields: diffEvents(desired, current.manifest), reason: 'local manifest changed; target matches lock baseline' });
|
|
365
|
+
}
|
|
366
|
+
else if (common.desiredHash === baselineHash) {
|
|
367
|
+
items.push({ status: 'remote-changed', ...common, currentHash, baselineHash, changedFields: diffEvents(desired, current.manifest), reason: 'target changed; local manifest matches lock baseline' });
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
items.push({ status: 'conflict', ...common, currentHash, baselineHash, changedFields: diffEvents(desired, current.manifest), reason: 'local manifest and target both changed since lock baseline' });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const [stableKey, targets] of targetsByStableKey) {
|
|
374
|
+
if (consumedTargets.has(stableKey) || localByStableKey.has(stableKey)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (targets.length === 1) {
|
|
378
|
+
items.push({ status: 'orphan', stableKey, name: targets[0].manifest.name, currentHash: targets[0].hash, reason: 'target-only Event' });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const rank = {
|
|
382
|
+
create: 0,
|
|
383
|
+
'safe-update': 1,
|
|
384
|
+
'remote-changed': 2,
|
|
385
|
+
conflict: 3,
|
|
386
|
+
'missing-lock': 4,
|
|
387
|
+
'activation-drift': 5,
|
|
388
|
+
clean: 6,
|
|
389
|
+
orphan: 7,
|
|
390
|
+
collision: 8,
|
|
391
|
+
skipped: 9,
|
|
392
|
+
};
|
|
393
|
+
items.sort((left, right) => rank[left.status] - rank[right.status] || (left.stableKey || left.name).localeCompare(right.stableKey || right.name));
|
|
394
|
+
const counts = emptyPlanCounts();
|
|
395
|
+
for (const item of items) {
|
|
396
|
+
counts[item.status] += 1;
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
resourceType: 'event',
|
|
400
|
+
env: input.envName,
|
|
401
|
+
counts,
|
|
402
|
+
blockers: [...new Set(blockers)].sort(),
|
|
403
|
+
items,
|
|
404
|
+
exclusions: [...new Set(exclusions)].sort(),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function buildEventCreatePayload(manifest, targetId) {
|
|
408
|
+
const extensions = writableEventExtensions(manifest.extensions);
|
|
409
|
+
return {
|
|
410
|
+
...extensions,
|
|
411
|
+
name: manifest.name,
|
|
412
|
+
...(manifest.category === undefined || manifest.category === null ? {} : { category: manifest.category }),
|
|
413
|
+
active: false,
|
|
414
|
+
...(manifest.description === undefined ? {} : { desc: manifest.description }),
|
|
415
|
+
metadata: canonicalValue(manifest.metadata),
|
|
416
|
+
type: manifest.type,
|
|
417
|
+
...(manifest.filter === undefined ? {} : { filter: canonicalValue(manifest.filter) }),
|
|
418
|
+
targetType: 'JOB_TEMPLATE',
|
|
419
|
+
targetId,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function buildEventUpdatePayload(manifest, targetId, version) {
|
|
423
|
+
const extensions = writableEventExtensions(manifest.extensions);
|
|
424
|
+
return {
|
|
425
|
+
...extensions,
|
|
426
|
+
name: manifest.name,
|
|
427
|
+
category: manifest.category ?? null,
|
|
428
|
+
desc: manifest.description ?? '',
|
|
429
|
+
metadata: canonicalValue(manifest.metadata),
|
|
430
|
+
type: manifest.type,
|
|
431
|
+
filter: canonicalValue(manifest.filter ?? null),
|
|
432
|
+
targetType: 'JOB_TEMPLATE',
|
|
433
|
+
targetId,
|
|
434
|
+
version,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function writableEventExtensions(extensions) {
|
|
438
|
+
return canonicalValue(Object.fromEntries(Object.entries(extensions || {})
|
|
439
|
+
.filter(([key]) => !EVENT_TRANSPORT_FIELDS.has(key))));
|
|
440
|
+
}
|
|
441
|
+
function workspaceEventsByStableKey(cwd, stableKeyName) {
|
|
442
|
+
const events = new Map();
|
|
443
|
+
for (const entry of readWorkspaceEvents(cwd)) {
|
|
444
|
+
if (!validManifest(entry.manifest, stableKeyName)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
events.set(entry.manifest.stableKey, { manifestPath: entry.manifestPath, manifest: entry.manifest });
|
|
448
|
+
}
|
|
449
|
+
return events;
|
|
450
|
+
}
|
|
451
|
+
function updateEventEnvironmentBaseline(input) {
|
|
452
|
+
const { projectRoot } = requireProjectLayout(input.cwd, 'Event');
|
|
453
|
+
const lock = readComponentLock(projectRoot);
|
|
454
|
+
const stableKey = input.manifest.stableKey;
|
|
455
|
+
const existingPair = Object.entries(lock.events).find(([, entry]) => entry.stableKey === stableKey);
|
|
456
|
+
const existing = existingPair?.[1];
|
|
457
|
+
if (existingPair && existingPair[0] !== stableKey) {
|
|
458
|
+
delete lock.events[existingPair[0]];
|
|
459
|
+
}
|
|
460
|
+
const previousBaseline = existing?.environments[input.envName];
|
|
461
|
+
lock.events[stableKey] = {
|
|
462
|
+
stableKey,
|
|
463
|
+
path: toPortableRelativePath(projectRoot, input.manifestPath),
|
|
464
|
+
sourceHash: hashEventManifest(input.manifest),
|
|
465
|
+
environments: {
|
|
466
|
+
...(existing?.environments || {}),
|
|
467
|
+
[input.envName]: previousBaseline?.remoteHash === input.remoteHash
|
|
468
|
+
? previousBaseline
|
|
469
|
+
: { remoteHash: input.remoteHash, syncedAt: new Date().toISOString() },
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
writeComponentLock(projectRoot, lock);
|
|
473
|
+
}
|
|
474
|
+
function eventPushError(error) {
|
|
475
|
+
return error instanceof Error ? error.message : String(error);
|
|
476
|
+
}
|
|
477
|
+
function assertEventsPushable(plan, force) {
|
|
478
|
+
if (plan.blockers.length > 0) {
|
|
479
|
+
throw new Error(`Event push blocked: ${plan.blockers.join(' ')}`);
|
|
480
|
+
}
|
|
481
|
+
const contentBlockers = plan.items.filter((item) => ['missing-lock', 'remote-changed', 'conflict'].includes(item.status));
|
|
482
|
+
if (contentBlockers.length > 0 && !force) {
|
|
483
|
+
const statuses = [...new Set(contentBlockers.map((item) => item.status))].sort().join(', ');
|
|
484
|
+
throw new Error(`Event push blocked by ${statuses}; review the plan and re-run with --force only to adopt or overwrite reviewed content drift.`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function resolveLiveEventWriteTarget(input) {
|
|
488
|
+
const remote = input.inventory || await loadRemoteEventInventory(input.client, input.stableKeyName);
|
|
489
|
+
const dependencies = remote.jobTemplatesByStableKey.get(input.manifest.target.jobTemplateStableKey) || [];
|
|
490
|
+
if (dependencies.length !== 1) {
|
|
491
|
+
const reason = dependencies.length === 0 ? 'is missing' : 'is ambiguous';
|
|
492
|
+
throw new Error(`target Job Template dependency "${input.manifest.target.jobTemplateStableKey}" ${reason}`);
|
|
493
|
+
}
|
|
494
|
+
const jobTemplateId = jobTemplateIdOf(dependencies[0]);
|
|
495
|
+
if (!jobTemplateId) {
|
|
496
|
+
throw new Error(`target Job Template dependency "${input.manifest.target.jobTemplateStableKey}" has no jobTemplateId`);
|
|
497
|
+
}
|
|
498
|
+
const matches = remote.eventsByStableKey.get(input.manifest.stableKey) || [];
|
|
499
|
+
if (input.action === 'create') {
|
|
500
|
+
if (matches.length > 0) {
|
|
501
|
+
throw new Error(`target Event "${input.manifest.stableKey}" appeared after planning`);
|
|
502
|
+
}
|
|
503
|
+
const nameMatches = remote.events.filter((event) => !event.deletedAt
|
|
504
|
+
&& !event.deletedBy
|
|
505
|
+
&& eventPresentationKey(event) === eventPresentationKey(input.manifest));
|
|
506
|
+
if (nameMatches.length > 0) {
|
|
507
|
+
throw new Error(`target Event matching category/name for "${input.manifest.stableKey}" appeared after planning`);
|
|
508
|
+
}
|
|
509
|
+
return { jobTemplateId };
|
|
510
|
+
}
|
|
511
|
+
if (matches.length !== 1) {
|
|
512
|
+
throw new Error(`expected one live target Event match, found ${matches.length}`);
|
|
513
|
+
}
|
|
514
|
+
const event = matches[0];
|
|
515
|
+
const exclusion = eventExclusionReason(event);
|
|
516
|
+
if (exclusion) {
|
|
517
|
+
throw new Error(`live target Event is excluded: ${exclusion}`);
|
|
518
|
+
}
|
|
519
|
+
const eventId = eventIdOf(event);
|
|
520
|
+
if (!eventId || typeof event.version !== 'number') {
|
|
521
|
+
throw new Error('live target Event did not include id and version');
|
|
522
|
+
}
|
|
523
|
+
const currentTemplate = event.targetId ? remote.jobTemplatesById.get(event.targetId) : undefined;
|
|
524
|
+
const currentTemplateStableKey = currentTemplate
|
|
525
|
+
? readConfiguredResourceMetadataStableKey(currentTemplate, input.stableKeyName)
|
|
526
|
+
: undefined;
|
|
527
|
+
if (!currentTemplateStableKey) {
|
|
528
|
+
throw new Error('live target Event dependency is missing or unmanaged');
|
|
529
|
+
}
|
|
530
|
+
const currentManifest = manifestFromRecord({
|
|
531
|
+
record: event,
|
|
532
|
+
stableKey: input.manifest.stableKey,
|
|
533
|
+
jobTemplateStableKey: currentTemplateStableKey,
|
|
534
|
+
stableKeyName: input.stableKeyName,
|
|
535
|
+
});
|
|
536
|
+
return {
|
|
537
|
+
jobTemplateId,
|
|
538
|
+
event,
|
|
539
|
+
eventId,
|
|
540
|
+
currentHash: hashPortableEvent(currentManifest),
|
|
541
|
+
active: currentManifest.active,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
async function verifyEventWrite(input) {
|
|
545
|
+
const event = unwrapData(await input.client.getEvent(input.eventId));
|
|
546
|
+
if (readConfiguredResourceMetadataStableKey(event, input.stableKeyName) !== input.manifest.stableKey) {
|
|
547
|
+
throw new Error(`verified Event is missing or changed ${input.stableKeyName}`);
|
|
548
|
+
}
|
|
549
|
+
if (event.targetType !== 'JOB_TEMPLATE' || event.targetId !== input.jobTemplateId) {
|
|
550
|
+
throw new Error('verified Event target does not match the live Job Template dependency');
|
|
551
|
+
}
|
|
552
|
+
let jobTemplate = input.jobTemplatesById?.get(input.jobTemplateId);
|
|
553
|
+
if (!jobTemplate) {
|
|
554
|
+
jobTemplate = unwrapData(await input.client.getJobTemplate(input.jobTemplateId));
|
|
555
|
+
input.jobTemplatesById?.set(input.jobTemplateId, jobTemplate);
|
|
556
|
+
}
|
|
557
|
+
if (readConfiguredResourceMetadataStableKey(jobTemplate, input.stableKeyName) !== input.manifest.target.jobTemplateStableKey) {
|
|
558
|
+
throw new Error('verified Event target Job Template does not match the desired stable key');
|
|
559
|
+
}
|
|
560
|
+
const verifiedManifest = manifestFromRecord({
|
|
561
|
+
record: event,
|
|
562
|
+
stableKey: input.manifest.stableKey,
|
|
563
|
+
jobTemplateStableKey: input.manifest.target.jobTemplateStableKey,
|
|
564
|
+
stableKeyName: input.stableKeyName,
|
|
565
|
+
});
|
|
566
|
+
if (hashEventContent(verifiedManifest) !== hashEventContent(input.manifest)) {
|
|
567
|
+
throw new Error('written Event did not match desired portable configuration');
|
|
568
|
+
}
|
|
569
|
+
if (verifiedManifest.active !== input.expectedActive) {
|
|
570
|
+
throw new Error('written Event did not preserve the expected target activation state');
|
|
571
|
+
}
|
|
572
|
+
return hashPortableEvent(verifiedManifest);
|
|
573
|
+
}
|
|
574
|
+
export async function pushEvents(input) {
|
|
575
|
+
const plan = await buildEventsPlan(input);
|
|
576
|
+
assertEventsPushable(plan, input.force === true);
|
|
577
|
+
const localByKey = workspaceEventsByStableKey(input.cwd, input.stableKeyName);
|
|
578
|
+
const results = [];
|
|
579
|
+
const verifiedActivationCandidates = [];
|
|
580
|
+
const writeCandidates = plan.items.filter((item) => (item.status === 'create'
|
|
581
|
+
|| item.status === 'safe-update'
|
|
582
|
+
|| item.status === 'activation-drift'
|
|
583
|
+
|| (input.force === true && ['missing-lock', 'remote-changed', 'conflict'].includes(item.status))));
|
|
584
|
+
const liveInventory = writeCandidates.length > 0
|
|
585
|
+
? await loadRemoteEventInventory(input.client, input.stableKeyName)
|
|
586
|
+
: undefined;
|
|
587
|
+
const processWrite = async (item, action) => {
|
|
588
|
+
const stableKey = item.stableKey || '';
|
|
589
|
+
const local = localByKey.get(stableKey);
|
|
590
|
+
const targetPath = local ? localTargetPath(input.cwd, local.manifestPath) : stableKey;
|
|
591
|
+
try {
|
|
592
|
+
if (!local) {
|
|
593
|
+
throw new Error('desired manifest disappeared after planning');
|
|
594
|
+
}
|
|
595
|
+
if (hashPortableEvent(local.manifest) !== item.desiredHash) {
|
|
596
|
+
throw new Error('desired manifest changed after planning');
|
|
597
|
+
}
|
|
598
|
+
const live = await resolveLiveEventWriteTarget({
|
|
599
|
+
client: input.client,
|
|
600
|
+
stableKeyName: input.stableKeyName,
|
|
601
|
+
manifest: local.manifest,
|
|
602
|
+
action,
|
|
603
|
+
inventory: liveInventory,
|
|
604
|
+
});
|
|
605
|
+
let eventId;
|
|
606
|
+
let expectedActive;
|
|
607
|
+
if (action === 'create') {
|
|
608
|
+
const created = unwrapData(await input.client.createEvent(buildEventCreatePayload(local.manifest, live.jobTemplateId)));
|
|
609
|
+
eventId = eventIdOf(created);
|
|
610
|
+
if (!eventId) {
|
|
611
|
+
const freshInventory = await loadRemoteEventInventory(input.client, input.stableKeyName);
|
|
612
|
+
const resolvedCreated = await resolveLiveEventWriteTarget({
|
|
613
|
+
client: input.client,
|
|
614
|
+
stableKeyName: input.stableKeyName,
|
|
615
|
+
manifest: local.manifest,
|
|
616
|
+
action: 'update',
|
|
617
|
+
inventory: freshInventory,
|
|
618
|
+
});
|
|
619
|
+
if (!resolvedCreated.eventId) {
|
|
620
|
+
throw new Error('created Event could not be resolved by stableKey');
|
|
621
|
+
}
|
|
622
|
+
eventId = resolvedCreated.eventId;
|
|
623
|
+
}
|
|
624
|
+
else if (liveInventory) {
|
|
625
|
+
liveInventory.events.push(created);
|
|
626
|
+
liveInventory.eventsByStableKey.set(local.manifest.stableKey, [created]);
|
|
627
|
+
}
|
|
628
|
+
expectedActive = false;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
if (!live.eventId || !live.event || typeof live.event.version !== 'number' || live.active === undefined) {
|
|
632
|
+
throw new Error('live target Event did not include id, version, and activation state');
|
|
633
|
+
}
|
|
634
|
+
if (live.currentHash !== item.currentHash) {
|
|
635
|
+
throw new Error('target Event changed after planning');
|
|
636
|
+
}
|
|
637
|
+
await input.client.updateEvent(live.eventId, buildEventUpdatePayload(local.manifest, live.jobTemplateId, live.event.version));
|
|
638
|
+
eventId = live.eventId;
|
|
639
|
+
expectedActive = live.active;
|
|
640
|
+
}
|
|
641
|
+
const remoteHash = await verifyEventWrite({
|
|
642
|
+
client: input.client,
|
|
643
|
+
stableKeyName: input.stableKeyName,
|
|
644
|
+
manifest: local.manifest,
|
|
645
|
+
eventId,
|
|
646
|
+
jobTemplateId: live.jobTemplateId,
|
|
647
|
+
expectedActive,
|
|
648
|
+
jobTemplatesById: liveInventory?.jobTemplatesById,
|
|
649
|
+
});
|
|
650
|
+
updateEventEnvironmentBaseline({
|
|
651
|
+
cwd: input.cwd,
|
|
652
|
+
envName: input.envName,
|
|
653
|
+
manifest: local.manifest,
|
|
654
|
+
manifestPath: local.manifestPath,
|
|
655
|
+
remoteHash,
|
|
656
|
+
});
|
|
657
|
+
results.push({ stableKey, action, status: 'deployed', targetPath });
|
|
658
|
+
if (local.manifest.active !== expectedActive) {
|
|
659
|
+
verifiedActivationCandidates.push({
|
|
660
|
+
stableKey,
|
|
661
|
+
eventId,
|
|
662
|
+
targetPath,
|
|
663
|
+
manifestPath: local.manifestPath,
|
|
664
|
+
manifest: local.manifest,
|
|
665
|
+
stableKeyName: input.stableKeyName,
|
|
666
|
+
cwd: input.cwd,
|
|
667
|
+
envName: input.envName,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
results.push({ stableKey, action, status: 'failed', targetPath, error: eventPushError(error) });
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
for (const item of plan.items.filter((candidate) => candidate.status === 'create')) {
|
|
676
|
+
await processWrite(item, 'create');
|
|
677
|
+
}
|
|
678
|
+
for (const item of plan.items.filter((candidate) => (candidate.status === 'safe-update'
|
|
679
|
+
|| (input.force === true && ['missing-lock', 'remote-changed', 'conflict'].includes(candidate.status))))) {
|
|
680
|
+
await processWrite(item, 'update');
|
|
681
|
+
}
|
|
682
|
+
for (const item of plan.items.filter((candidate) => candidate.status === 'clean')) {
|
|
683
|
+
const local = item.stableKey ? localByKey.get(item.stableKey) : undefined;
|
|
684
|
+
if (local && item.currentHash) {
|
|
685
|
+
updateEventEnvironmentBaseline({
|
|
686
|
+
cwd: input.cwd,
|
|
687
|
+
envName: input.envName,
|
|
688
|
+
manifest: local.manifest,
|
|
689
|
+
manifestPath: local.manifestPath,
|
|
690
|
+
remoteHash: item.currentHash,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
results.push({
|
|
694
|
+
stableKey: item.stableKey || item.name,
|
|
695
|
+
action: 'noop',
|
|
696
|
+
status: 'skipped',
|
|
697
|
+
targetPath: local ? localTargetPath(input.cwd, local.manifestPath) : item.stableKey || item.name,
|
|
698
|
+
reason: 'no local changes',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
for (const item of plan.items.filter((candidate) => candidate.status === 'activation-drift')) {
|
|
702
|
+
const local = item.stableKey ? localByKey.get(item.stableKey) : undefined;
|
|
703
|
+
if (local) {
|
|
704
|
+
try {
|
|
705
|
+
const live = await resolveLiveEventWriteTarget({
|
|
706
|
+
client: input.client,
|
|
707
|
+
stableKeyName: input.stableKeyName,
|
|
708
|
+
manifest: local.manifest,
|
|
709
|
+
action: 'update',
|
|
710
|
+
inventory: liveInventory,
|
|
711
|
+
});
|
|
712
|
+
if (!live.eventId || live.active === undefined) {
|
|
713
|
+
throw new Error('live target Event did not include id and activation state');
|
|
714
|
+
}
|
|
715
|
+
if (live.currentHash !== item.currentHash) {
|
|
716
|
+
throw new Error('target Event changed after planning');
|
|
717
|
+
}
|
|
718
|
+
if (live.active !== local.manifest.active) {
|
|
719
|
+
verifiedActivationCandidates.push({
|
|
720
|
+
stableKey: local.manifest.stableKey,
|
|
721
|
+
eventId: live.eventId,
|
|
722
|
+
targetPath: localTargetPath(input.cwd, local.manifestPath),
|
|
723
|
+
manifestPath: local.manifestPath,
|
|
724
|
+
manifest: local.manifest,
|
|
725
|
+
stableKeyName: input.stableKeyName,
|
|
726
|
+
cwd: input.cwd,
|
|
727
|
+
envName: input.envName,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
results.push({
|
|
733
|
+
stableKey: item.stableKey || item.name,
|
|
734
|
+
action: 'noop',
|
|
735
|
+
status: 'failed',
|
|
736
|
+
targetPath: localTargetPath(input.cwd, local.manifestPath),
|
|
737
|
+
error: eventPushError(error),
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
results.push({
|
|
743
|
+
stableKey: item.stableKey || item.name,
|
|
744
|
+
action: 'noop',
|
|
745
|
+
status: 'skipped',
|
|
746
|
+
targetPath: local ? localTargetPath(input.cwd, local.manifestPath) : item.stableKey || item.name,
|
|
747
|
+
reason: 'activation differs; re-run with --activate to align it with source',
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
for (const item of plan.items.filter((candidate) => candidate.status === 'orphan')) {
|
|
751
|
+
results.push({ stableKey: item.stableKey || item.name, action: 'orphan', status: 'skipped', targetPath: item.stableKey || item.name, reason: 'target-only orphan' });
|
|
752
|
+
}
|
|
753
|
+
for (const item of plan.items.filter((candidate) => candidate.status === 'skipped')) {
|
|
754
|
+
results.push({ stableKey: item.stableKey || item.name, action: 'skipped', status: 'skipped', targetPath: item.stableKey || item.name, reason: item.reason || 'unmanaged' });
|
|
755
|
+
}
|
|
756
|
+
return { plan, results, verifiedActivationCandidates };
|
|
757
|
+
}
|
|
758
|
+
export async function activateVerifiedEvents(input) {
|
|
759
|
+
const results = [];
|
|
760
|
+
const jobTemplatesCache = new Map();
|
|
761
|
+
for (const candidate of input.candidates) {
|
|
762
|
+
const action = candidate.manifest.active ? 'activate' : 'disable';
|
|
763
|
+
try {
|
|
764
|
+
const current = unwrapData(await input.client.getEvent(candidate.eventId));
|
|
765
|
+
if (readConfiguredResourceMetadataStableKey(current, candidate.stableKeyName) !== candidate.stableKey) {
|
|
766
|
+
throw new Error('target Event stable key changed after activation preflight');
|
|
767
|
+
}
|
|
768
|
+
if (current.active === candidate.manifest.active) {
|
|
769
|
+
results.push({
|
|
770
|
+
stableKey: candidate.stableKey,
|
|
771
|
+
action,
|
|
772
|
+
status: 'skipped',
|
|
773
|
+
targetPath: candidate.targetPath,
|
|
774
|
+
reason: 'already aligned',
|
|
775
|
+
});
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (candidate.manifest.active) {
|
|
779
|
+
await input.client.activateEvent(candidate.eventId);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
await input.client.disableEvent(candidate.eventId);
|
|
783
|
+
}
|
|
784
|
+
const remoteHash = await verifyEventWrite({
|
|
785
|
+
client: input.client,
|
|
786
|
+
stableKeyName: candidate.stableKeyName,
|
|
787
|
+
manifest: candidate.manifest,
|
|
788
|
+
eventId: candidate.eventId,
|
|
789
|
+
jobTemplateId: String(current.targetId || ''),
|
|
790
|
+
expectedActive: candidate.manifest.active,
|
|
791
|
+
jobTemplatesById: jobTemplatesCache,
|
|
792
|
+
});
|
|
793
|
+
updateEventEnvironmentBaseline({
|
|
794
|
+
cwd: candidate.cwd,
|
|
795
|
+
envName: candidate.envName,
|
|
796
|
+
manifest: candidate.manifest,
|
|
797
|
+
manifestPath: candidate.manifestPath,
|
|
798
|
+
remoteHash,
|
|
799
|
+
});
|
|
800
|
+
results.push({
|
|
801
|
+
stableKey: candidate.stableKey,
|
|
802
|
+
action,
|
|
803
|
+
status: 'deployed',
|
|
804
|
+
targetPath: candidate.targetPath,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
results.push({
|
|
809
|
+
stableKey: candidate.stableKey,
|
|
810
|
+
action,
|
|
811
|
+
status: 'failed',
|
|
812
|
+
targetPath: candidate.targetPath,
|
|
813
|
+
error: eventPushError(error),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return results;
|
|
818
|
+
}
|
|
819
|
+
export async function pullEventsToWorkspace(input) {
|
|
820
|
+
const { projectRoot, workspaceRoot } = requireProjectLayout(input.cwd, 'Event');
|
|
821
|
+
const [{ events: summaries }, { jobTemplates: templateSummaries }] = await Promise.all([
|
|
822
|
+
input.client.listAllEvents({ includeDeleted: true }), input.client.listAllJobTemplates(),
|
|
823
|
+
]);
|
|
824
|
+
const [events, templates] = await Promise.all([
|
|
825
|
+
hydrate(summaries, eventIdOf, (id) => input.client.getEvent(id)),
|
|
826
|
+
hydrate(templateSummaries, jobTemplateIdOf, (id) => input.client.getJobTemplate(id)),
|
|
827
|
+
]);
|
|
828
|
+
const templatesById = new Map(templates.map((template) => [jobTemplateIdOf(template), template]));
|
|
829
|
+
const manifests = [];
|
|
830
|
+
const warnings = [];
|
|
831
|
+
const seen = new Set();
|
|
832
|
+
for (const record of events) {
|
|
833
|
+
const stableKey = readConfiguredResourceMetadataStableKey(record, input.stableKeyName);
|
|
834
|
+
const label = stableKey || String(record.name || eventIdOf(record) || 'unknown event');
|
|
835
|
+
const exclusion = eventExclusionReason(record);
|
|
836
|
+
if (exclusion) {
|
|
837
|
+
warnings.push({ stableKey: label, reason: exclusion });
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (!stableKey) {
|
|
841
|
+
warnings.push({ stableKey: label, reason: `missing ${input.stableKeyName}; unmanaged` });
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const target = record.targetId ? templatesById.get(record.targetId) : undefined;
|
|
845
|
+
const jobTemplateStableKey = target ? readConfiguredResourceMetadataStableKey(target, input.stableKeyName) : undefined;
|
|
846
|
+
if (!jobTemplateStableKey) {
|
|
847
|
+
warnings.push({ stableKey, reason: 'target Job Template dependency is missing stableKey' });
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (seen.has(stableKey)) {
|
|
851
|
+
throw new Error(`Duplicate Event stableKey "${stableKey}" on source; no files were changed.`);
|
|
852
|
+
}
|
|
853
|
+
seen.add(stableKey);
|
|
854
|
+
const manifest = manifestFromRecord({ record, stableKey, jobTemplateStableKey, stableKeyName: input.stableKeyName });
|
|
855
|
+
manifests.push(manifest);
|
|
856
|
+
}
|
|
857
|
+
manifests.sort((left, right) => left.stableKey.localeCompare(right.stableKey));
|
|
858
|
+
warnings.sort((left, right) => left.stableKey.localeCompare(right.stableKey) || left.reason.localeCompare(right.reason));
|
|
859
|
+
const lock = readComponentLock(projectRoot);
|
|
860
|
+
const existingByKey = new Map();
|
|
861
|
+
const existingByPath = new Map();
|
|
862
|
+
for (const entry of readWorkspaceEvents(input.cwd)) {
|
|
863
|
+
const manifest = entry.manifest;
|
|
864
|
+
if (!validManifest(manifest, input.stableKeyName)) {
|
|
865
|
+
throw new Error(`Cannot pull over invalid Event manifest ${path.relative(projectRoot, entry.manifestPath)}.`);
|
|
866
|
+
}
|
|
867
|
+
if (existingByKey.has(manifest.stableKey)) {
|
|
868
|
+
throw new Error(`Duplicate local Event stableKey "${manifest.stableKey}"; no files were changed.`);
|
|
869
|
+
}
|
|
870
|
+
const localEntry = { manifestPath: entry.manifestPath, manifest };
|
|
871
|
+
existingByKey.set(manifest.stableKey, localEntry);
|
|
872
|
+
const pathIdentity = portablePathIdentity(entry.manifestPath);
|
|
873
|
+
existingByPath.set(pathIdentity, [...(existingByPath.get(pathIdentity) || []), localEntry]);
|
|
874
|
+
}
|
|
875
|
+
const writes = manifests.map((manifest) => {
|
|
876
|
+
const sourceHash = hashEventManifest(manifest);
|
|
877
|
+
const remoteHash = hashPortableEvent(manifest);
|
|
878
|
+
const existing = existingByKey.get(manifest.stableKey);
|
|
879
|
+
if (existing && !input.force) {
|
|
880
|
+
const localHash = hashEventManifest(existing.manifest);
|
|
881
|
+
const baseline = lock.events[manifest.stableKey];
|
|
882
|
+
if (localHash !== sourceHash && (!baseline || localHash !== baseline.sourceHash)) {
|
|
883
|
+
throw new Error(`Cannot pull Event "${manifest.stableKey}": local manifest changed since the lock baseline. Re-run with --force to overwrite.`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
manifest,
|
|
888
|
+
sourceHash,
|
|
889
|
+
remoteHash,
|
|
890
|
+
targetPath: eventManifestPath(workspaceRoot, manifest),
|
|
891
|
+
previousPath: existing?.manifestPath,
|
|
892
|
+
};
|
|
893
|
+
});
|
|
894
|
+
const candidatePathCounts = new Map();
|
|
895
|
+
for (const write of writes) {
|
|
896
|
+
const pathIdentity = portablePathIdentity(write.targetPath);
|
|
897
|
+
candidatePathCounts.set(pathIdentity, (candidatePathCounts.get(pathIdentity) || 0) + 1);
|
|
898
|
+
}
|
|
899
|
+
for (const write of writes) {
|
|
900
|
+
if ((candidatePathCounts.get(portablePathIdentity(write.targetPath)) || 0) > 1) {
|
|
901
|
+
write.targetPath = disambiguateManifestPath(write.targetPath, write.manifest.stableKey);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const paths = new Set();
|
|
905
|
+
for (const write of writes) {
|
|
906
|
+
const pathIdentity = portablePathIdentity(write.targetPath);
|
|
907
|
+
if (paths.has(pathIdentity)) {
|
|
908
|
+
throw new Error('Multiple Events resolve to the same workspace path; no files were changed.');
|
|
909
|
+
}
|
|
910
|
+
paths.add(pathIdentity);
|
|
911
|
+
const pathOwner = (existingByPath.get(pathIdentity) || [])
|
|
912
|
+
.find((entry) => entry.manifest.stableKey !== write.manifest.stableKey);
|
|
913
|
+
if (pathOwner) {
|
|
914
|
+
throw new Error(`Event workspace path "${path.relative(projectRoot, write.targetPath)}" is occupied by different stableKey "${pathOwner.manifest.stableKey}"; no files were changed.`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const root = path.join(workspaceRoot, EVENTS_DIRECTORY);
|
|
918
|
+
let lockChanged = false;
|
|
919
|
+
for (const write of writes) {
|
|
920
|
+
writePulledManifest({
|
|
921
|
+
filePath: write.targetPath,
|
|
922
|
+
manifest: write.manifest,
|
|
923
|
+
previousPath: write.previousPath,
|
|
924
|
+
root: root,
|
|
925
|
+
force: input.force,
|
|
926
|
+
});
|
|
927
|
+
const previous = lock.events[write.manifest.stableKey];
|
|
928
|
+
const previousBaseline = previous?.environments[input.envName];
|
|
929
|
+
const entry = {
|
|
930
|
+
stableKey: write.manifest.stableKey,
|
|
931
|
+
path: toPortableRelativePath(projectRoot, write.targetPath),
|
|
932
|
+
sourceHash: write.sourceHash,
|
|
933
|
+
environments: {
|
|
934
|
+
...(previous?.environments || {}),
|
|
935
|
+
[input.envName]: previousBaseline?.remoteHash === write.remoteHash
|
|
936
|
+
? previousBaseline
|
|
937
|
+
: { remoteHash: write.remoteHash, syncedAt: new Date().toISOString() },
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
if (!previous || hashStable(previous) !== hashStable(entry)) {
|
|
941
|
+
lockChanged = true;
|
|
942
|
+
}
|
|
943
|
+
lock.events[write.manifest.stableKey] = entry;
|
|
944
|
+
}
|
|
945
|
+
if (lockChanged) {
|
|
946
|
+
writeComponentLock(projectRoot, lock);
|
|
947
|
+
}
|
|
948
|
+
return { pulled: writes.map((write) => ({ manifest: write.manifest, targetPath: localTargetPath(input.cwd, write.targetPath) })), warnings };
|
|
949
|
+
}
|