windmill-cli 1.506.0 → 1.507.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/conf.js +47 -0
- package/esm/context.js +44 -5
- package/esm/flow.js +5 -1
- package/esm/gen/core/OpenAPI.js +1 -1
- package/esm/gitsync-settings.js +812 -0
- package/esm/instance.js +3 -0
- package/esm/main.js +112 -17
- package/esm/store.js +9 -4
- package/esm/sync.js +239 -13
- package/esm/types.js +1 -1
- package/esm/utils.js +31 -0
- package/esm/workspace.js +85 -16
- package/package.json +2 -6
- package/types/conf.d.ts +10 -0
- package/types/conf.d.ts.map +1 -1
- package/types/context.d.ts.map +1 -1
- package/types/flow.d.ts.map +1 -1
- package/types/gen/types.gen.d.ts +8 -3
- package/types/gen/types.gen.d.ts.map +1 -1
- package/types/gitsync-settings.d.ts +36 -0
- package/types/gitsync-settings.d.ts.map +1 -0
- package/types/instance.d.ts.map +1 -1
- package/types/main.d.ts +7 -2
- package/types/main.d.ts.map +1 -1
- package/types/store.d.ts +2 -2
- package/types/store.d.ts.map +1 -1
- package/types/sync.d.ts +26 -2
- package/types/sync.d.ts.map +1 -1
- package/types/types.d.ts +1 -0
- package/types/types.d.ts.map +1 -1
- package/types/utils.d.ts +4 -0
- package/types/utils.d.ts.map +1 -1
- package/types/workspace.d.ts +3 -3
- package/types/workspace.d.ts.map +1 -1
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
import * as dntShim from "./_dnt.shims.js";
|
|
2
|
+
import { colors, Command, log, yamlStringify } from "./deps.js";
|
|
3
|
+
import { requireLogin, resolveWorkspace } from "./context.js";
|
|
4
|
+
import * as wmill from "./gen/services.gen.js";
|
|
5
|
+
import { DEFAULT_SYNC_OPTIONS, getEffectiveSettings, readConfigFile, } from "./conf.js";
|
|
6
|
+
import { deepEqual, selectRepository } from "./utils.js";
|
|
7
|
+
// Constants for git-sync fields to avoid duplication
|
|
8
|
+
const GIT_SYNC_FIELDS = [
|
|
9
|
+
"includes",
|
|
10
|
+
"excludes",
|
|
11
|
+
"extraIncludes",
|
|
12
|
+
"skipScripts",
|
|
13
|
+
"skipFlows",
|
|
14
|
+
"skipApps",
|
|
15
|
+
"skipFolders",
|
|
16
|
+
"skipVariables",
|
|
17
|
+
"skipResources",
|
|
18
|
+
"skipResourceTypes",
|
|
19
|
+
"skipSecrets",
|
|
20
|
+
"includeSchedules",
|
|
21
|
+
"includeTriggers",
|
|
22
|
+
"includeUsers",
|
|
23
|
+
"includeGroups",
|
|
24
|
+
"includeSettings",
|
|
25
|
+
"includeKey",
|
|
26
|
+
];
|
|
27
|
+
// Helper to normalize repository path by removing $res: prefix
|
|
28
|
+
function normalizeRepoPath(path) {
|
|
29
|
+
return path.replace(/^\$res:/, "");
|
|
30
|
+
}
|
|
31
|
+
// Helper to get typed field value from SyncOptions
|
|
32
|
+
function getFieldValue(opts, field) {
|
|
33
|
+
return opts[field];
|
|
34
|
+
}
|
|
35
|
+
// Construct override key using the single format: baseUrl:workspaceId:repo
|
|
36
|
+
function constructOverrideKey(baseUrl, workspaceId, repoPath, workspaceLevel = false) {
|
|
37
|
+
// Validate that components don't contain colons to avoid key collisions
|
|
38
|
+
if (baseUrl.includes(':') && !baseUrl.startsWith('http')) {
|
|
39
|
+
throw new Error(`Invalid baseUrl contains colon: ${baseUrl}`);
|
|
40
|
+
}
|
|
41
|
+
if (workspaceId.includes(':')) {
|
|
42
|
+
throw new Error(`Invalid workspaceId contains colon: ${workspaceId}`);
|
|
43
|
+
}
|
|
44
|
+
if (repoPath.includes(':') && !repoPath.startsWith('$res:')) {
|
|
45
|
+
throw new Error(`Invalid repoPath contains colon: ${repoPath}`);
|
|
46
|
+
}
|
|
47
|
+
if (workspaceLevel) {
|
|
48
|
+
return `${baseUrl}:${workspaceId}:*`;
|
|
49
|
+
}
|
|
50
|
+
return `${baseUrl}:${workspaceId}:${repoPath}`;
|
|
51
|
+
}
|
|
52
|
+
// Helper to compare string arrays (used for includes/excludes/extraIncludes)
|
|
53
|
+
function arraysEqual(arr1, arr2) {
|
|
54
|
+
if (arr1.length !== arr2.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const sorted1 = [...arr1].sort();
|
|
58
|
+
const sorted2 = [...arr2].sort();
|
|
59
|
+
return sorted1.every((item, index) => item === sorted2[index]);
|
|
60
|
+
}
|
|
61
|
+
// Normalize SyncOptions for semantic comparison - treat undefined arrays as empty arrays
|
|
62
|
+
function normalizeSyncOptions(opts) {
|
|
63
|
+
return {
|
|
64
|
+
...opts,
|
|
65
|
+
includes: opts.includes || [],
|
|
66
|
+
excludes: opts.excludes || [],
|
|
67
|
+
extraIncludes: opts.extraIncludes || [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Extract only git-sync relevant fields for comparison
|
|
71
|
+
function extractGitSyncFields(opts) {
|
|
72
|
+
return {
|
|
73
|
+
includes: opts.includes || [],
|
|
74
|
+
excludes: opts.excludes || [],
|
|
75
|
+
extraIncludes: opts.extraIncludes || [],
|
|
76
|
+
skipScripts: opts.skipScripts,
|
|
77
|
+
skipFlows: opts.skipFlows,
|
|
78
|
+
skipApps: opts.skipApps,
|
|
79
|
+
skipFolders: opts.skipFolders,
|
|
80
|
+
skipVariables: opts.skipVariables,
|
|
81
|
+
skipResources: opts.skipResources,
|
|
82
|
+
skipResourceTypes: opts.skipResourceTypes,
|
|
83
|
+
skipSecrets: opts.skipSecrets,
|
|
84
|
+
includeSchedules: opts.includeSchedules,
|
|
85
|
+
includeTriggers: opts.includeTriggers,
|
|
86
|
+
includeUsers: opts.includeUsers,
|
|
87
|
+
includeGroups: opts.includeGroups,
|
|
88
|
+
includeSettings: opts.includeSettings,
|
|
89
|
+
includeKey: opts.includeKey,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Helper function to determine current settings based on write mode and conflicts
|
|
93
|
+
function getCurrentSettings(localConfig, writeMode, overrideKey) {
|
|
94
|
+
if (writeMode === "override" &&
|
|
95
|
+
overrideKey &&
|
|
96
|
+
localConfig.overrides?.[overrideKey]) {
|
|
97
|
+
return {
|
|
98
|
+
...DEFAULT_SYNC_OPTIONS,
|
|
99
|
+
...localConfig,
|
|
100
|
+
...localConfig.overrides[overrideKey],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// For "replace" mode, exclude overrides since they're never accessed
|
|
105
|
+
const { overrides, ...configWithoutOverrides } = localConfig;
|
|
106
|
+
return { ...DEFAULT_SYNC_OPTIONS, ...configWithoutOverrides };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Convert backend include_type array to SyncOptions boolean flags
|
|
110
|
+
function includeTypeToSyncOptions(includeTypes) {
|
|
111
|
+
return {
|
|
112
|
+
skipScripts: !includeTypes.includes("script"),
|
|
113
|
+
skipFlows: !includeTypes.includes("flow"),
|
|
114
|
+
skipApps: !includeTypes.includes("app"),
|
|
115
|
+
skipFolders: !includeTypes.includes("folder"),
|
|
116
|
+
skipVariables: !includeTypes.includes("variable"),
|
|
117
|
+
skipResources: !includeTypes.includes("resource"),
|
|
118
|
+
skipResourceTypes: !includeTypes.includes("resourcetype"),
|
|
119
|
+
skipSecrets: !includeTypes.includes("secret"),
|
|
120
|
+
includeSchedules: includeTypes.includes("schedule"),
|
|
121
|
+
includeTriggers: includeTypes.includes("trigger"),
|
|
122
|
+
includeUsers: includeTypes.includes("user"),
|
|
123
|
+
includeGroups: includeTypes.includes("group"),
|
|
124
|
+
includeSettings: includeTypes.includes("settings"),
|
|
125
|
+
includeKey: includeTypes.includes("key"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Convert SyncOptions boolean flags to backend include_type array
|
|
129
|
+
function syncOptionsToIncludeType(opts) {
|
|
130
|
+
const includeTypes = [];
|
|
131
|
+
if (!opts.skipScripts)
|
|
132
|
+
includeTypes.push("script");
|
|
133
|
+
if (!opts.skipFlows)
|
|
134
|
+
includeTypes.push("flow");
|
|
135
|
+
if (!opts.skipApps)
|
|
136
|
+
includeTypes.push("app");
|
|
137
|
+
if (!opts.skipFolders)
|
|
138
|
+
includeTypes.push("folder");
|
|
139
|
+
if (!opts.skipVariables)
|
|
140
|
+
includeTypes.push("variable");
|
|
141
|
+
if (!opts.skipResources)
|
|
142
|
+
includeTypes.push("resource");
|
|
143
|
+
if (!opts.skipResourceTypes)
|
|
144
|
+
includeTypes.push("resourcetype");
|
|
145
|
+
if (!opts.skipSecrets)
|
|
146
|
+
includeTypes.push("secret");
|
|
147
|
+
if (opts.includeSchedules)
|
|
148
|
+
includeTypes.push("schedule");
|
|
149
|
+
if (opts.includeTriggers)
|
|
150
|
+
includeTypes.push("trigger");
|
|
151
|
+
if (opts.includeUsers)
|
|
152
|
+
includeTypes.push("user");
|
|
153
|
+
if (opts.includeGroups)
|
|
154
|
+
includeTypes.push("group");
|
|
155
|
+
if (opts.includeSettings)
|
|
156
|
+
includeTypes.push("settings");
|
|
157
|
+
if (opts.includeKey)
|
|
158
|
+
includeTypes.push("key");
|
|
159
|
+
return includeTypes;
|
|
160
|
+
}
|
|
161
|
+
// Convert SyncOptions to backend format used by both Windmill backend and UI
|
|
162
|
+
function syncOptionsToBackendFormat(opts) {
|
|
163
|
+
return {
|
|
164
|
+
include_path: opts.includes || [],
|
|
165
|
+
exclude_path: opts.excludes || [],
|
|
166
|
+
extra_include_path: opts.extraIncludes || [],
|
|
167
|
+
include_type: syncOptionsToIncludeType(opts),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Select repository interactively if multiple exist
|
|
171
|
+
// Generate structured diff showing field changes
|
|
172
|
+
function generateStructuredDiff(current, backend) {
|
|
173
|
+
const diff = {};
|
|
174
|
+
// Get all unique keys from both objects
|
|
175
|
+
const allKeys = new Set([...Object.keys(current), ...Object.keys(backend)]);
|
|
176
|
+
for (const key of allKeys) {
|
|
177
|
+
const currentValue = current[key];
|
|
178
|
+
const backendValue = backend[key];
|
|
179
|
+
if (!deepEqual(currentValue, backendValue)) {
|
|
180
|
+
diff[key] = {
|
|
181
|
+
from: currentValue,
|
|
182
|
+
to: backendValue,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return diff;
|
|
187
|
+
}
|
|
188
|
+
// Helper to generate changes between two normalized SyncOptions objects
|
|
189
|
+
function generateChanges(normalizedCurrent, normalizedNew) {
|
|
190
|
+
const changes = {};
|
|
191
|
+
for (const field of GIT_SYNC_FIELDS) {
|
|
192
|
+
const currentValue = getFieldValue(normalizedCurrent, field);
|
|
193
|
+
const newValue = getFieldValue(normalizedNew, field);
|
|
194
|
+
if (!deepEqual(currentValue, newValue)) {
|
|
195
|
+
changes[field] = {
|
|
196
|
+
from: currentValue,
|
|
197
|
+
to: newValue,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return changes;
|
|
202
|
+
}
|
|
203
|
+
// Helper to display changes in human-readable format
|
|
204
|
+
function displayChanges(changes) {
|
|
205
|
+
for (const [field, change] of Object.entries(changes)) {
|
|
206
|
+
if (Array.isArray(change.from) ||
|
|
207
|
+
Array.isArray(change.to)) {
|
|
208
|
+
console.log(colors.yellow(` ${field}:`));
|
|
209
|
+
console.log(colors.red(` - ${JSON.stringify(change.from)}`));
|
|
210
|
+
console.log(colors.green(` + ${JSON.stringify(change.to)}`));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(colors.yellow(` ${field}: `) +
|
|
214
|
+
colors.red(`${change.from}`) +
|
|
215
|
+
" → " +
|
|
216
|
+
colors.green(`${change.to}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function pullGitSyncSettings(opts) {
|
|
221
|
+
const workspace = await resolveWorkspace(opts);
|
|
222
|
+
await requireLogin(opts);
|
|
223
|
+
try {
|
|
224
|
+
// Parse and validate --with-backend-settings if provided
|
|
225
|
+
let settings;
|
|
226
|
+
if (opts.withBackendSettings) {
|
|
227
|
+
try {
|
|
228
|
+
const parsedSettings = JSON.parse(opts.withBackendSettings);
|
|
229
|
+
// Validate the structure matches expected test format (raw settings object)
|
|
230
|
+
if (!parsedSettings.include_path ||
|
|
231
|
+
!Array.isArray(parsedSettings.include_path)) {
|
|
232
|
+
throw new Error("Invalid settings format. Expected include_path array");
|
|
233
|
+
}
|
|
234
|
+
if (!parsedSettings.include_type ||
|
|
235
|
+
!Array.isArray(parsedSettings.include_type)) {
|
|
236
|
+
throw new Error("Invalid settings format. Expected include_type array");
|
|
237
|
+
}
|
|
238
|
+
// Create mock backend response with single repository using provided settings
|
|
239
|
+
const mockRepositoryPath = opts.repository || "u/mock/repo";
|
|
240
|
+
settings = {
|
|
241
|
+
git_sync: {
|
|
242
|
+
repositories: [{
|
|
243
|
+
git_repo_resource_path: mockRepositoryPath,
|
|
244
|
+
settings: {
|
|
245
|
+
include_path: parsedSettings.include_path,
|
|
246
|
+
include_type: parsedSettings.include_type,
|
|
247
|
+
exclude_path: parsedSettings.exclude_path || [],
|
|
248
|
+
extra_include_path: parsedSettings.extra_include_path || [],
|
|
249
|
+
},
|
|
250
|
+
}],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (parseError) {
|
|
255
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
256
|
+
if (opts.jsonOutput) {
|
|
257
|
+
console.log(JSON.stringify({
|
|
258
|
+
success: false,
|
|
259
|
+
error: `Failed to parse --with-backend-settings JSON: ${errorMessage}`,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
log.error(colors.red(`Failed to parse --with-backend-settings JSON: ${errorMessage}`));
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Fetch workspace settings to get git-sync configuration
|
|
270
|
+
try {
|
|
271
|
+
settings = await wmill.getSettings({
|
|
272
|
+
workspace: workspace.workspaceId,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (apiError) {
|
|
276
|
+
const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
|
|
277
|
+
if (opts.jsonOutput) {
|
|
278
|
+
console.log(JSON.stringify({
|
|
279
|
+
success: false,
|
|
280
|
+
error: `Failed to fetch workspace settings: ${errorMessage}`,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
log.error(colors.red(`Failed to fetch workspace settings: ${errorMessage}`));
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (!settings.git_sync?.repositories ||
|
|
290
|
+
settings.git_sync.repositories.length === 0) {
|
|
291
|
+
if (opts.jsonOutput) {
|
|
292
|
+
console.log(JSON.stringify({
|
|
293
|
+
success: false,
|
|
294
|
+
error: "No git-sync repositories configured",
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
log.error(colors.red("No git-sync repositories configured in workspace"));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// Find the repository to work with
|
|
303
|
+
let selectedRepo;
|
|
304
|
+
if (opts.repository) {
|
|
305
|
+
const found = settings.git_sync.repositories.find((r) => r.git_repo_resource_path === opts.repository ||
|
|
306
|
+
r.git_repo_resource_path === `$res:${opts.repository}`);
|
|
307
|
+
if (!found) {
|
|
308
|
+
throw new Error(`Repository ${opts.repository} not found`);
|
|
309
|
+
}
|
|
310
|
+
selectedRepo = found;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
selectedRepo = await selectRepository(settings.git_sync.repositories);
|
|
314
|
+
}
|
|
315
|
+
// Convert backend settings to SyncOptions format
|
|
316
|
+
const backendSyncOptions = {
|
|
317
|
+
includes: selectedRepo.settings.include_path || [],
|
|
318
|
+
excludes: selectedRepo.settings.exclude_path || [],
|
|
319
|
+
extraIncludes: selectedRepo.settings.extra_include_path || [],
|
|
320
|
+
...includeTypeToSyncOptions(selectedRepo.settings.include_type || []),
|
|
321
|
+
};
|
|
322
|
+
// Check if wmill.yaml exists - require it for git-sync settings commands
|
|
323
|
+
try {
|
|
324
|
+
await dntShim.Deno.stat("wmill.yaml");
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
log.error(colors.red("No wmill.yaml file found. Please run 'wmill init' first to create the configuration file."));
|
|
328
|
+
dntShim.Deno.exit(1);
|
|
329
|
+
}
|
|
330
|
+
// Read current local configuration
|
|
331
|
+
const localConfig = await readConfigFile();
|
|
332
|
+
// Determine where to write the settings for diff display
|
|
333
|
+
let overrideKey;
|
|
334
|
+
let writeMode = "replace";
|
|
335
|
+
// For diff mode, determine what the write mode would be without interactive prompts
|
|
336
|
+
if (opts.default) {
|
|
337
|
+
writeMode = "replace";
|
|
338
|
+
}
|
|
339
|
+
else if (opts.replace) {
|
|
340
|
+
writeMode = "replace";
|
|
341
|
+
}
|
|
342
|
+
else if (opts.override || opts.workspaceLevel) {
|
|
343
|
+
writeMode = "override";
|
|
344
|
+
if (opts.workspaceLevel) {
|
|
345
|
+
overrideKey = constructOverrideKey(workspace.remote, workspace.workspaceId, "", true);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
349
|
+
overrideKey = constructOverrideKey(workspace.remote, workspace.workspaceId, repoPath);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Default behavior for existing files with no explicit flags
|
|
354
|
+
// Use same logic as diff to determine if there's a real conflict
|
|
355
|
+
const currentSettings = getCurrentSettings(localConfig, "replace", // Check against replace mode
|
|
356
|
+
undefined);
|
|
357
|
+
const gitSyncBackend = extractGitSyncFields(normalizeSyncOptions(backendSyncOptions));
|
|
358
|
+
const gitSyncCurrent = extractGitSyncFields(normalizeSyncOptions(currentSettings));
|
|
359
|
+
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
360
|
+
if (hasConflict) {
|
|
361
|
+
// For diff mode, show what override would look like
|
|
362
|
+
writeMode = "override";
|
|
363
|
+
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
364
|
+
overrideKey = constructOverrideKey(workspace.remote, workspace.workspaceId, repoPath);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
writeMode = "replace";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (opts.diff) {
|
|
371
|
+
// Show differences between local and backend
|
|
372
|
+
const currentSettings = getCurrentSettings(localConfig, writeMode, overrideKey);
|
|
373
|
+
const normalizedCurrent = normalizeSyncOptions(currentSettings);
|
|
374
|
+
const normalizedBackend = normalizeSyncOptions(backendSyncOptions);
|
|
375
|
+
const gitSyncCurrent = extractGitSyncFields(normalizedCurrent);
|
|
376
|
+
const gitSyncBackend = extractGitSyncFields(normalizedBackend);
|
|
377
|
+
const hasChanges = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
378
|
+
if (opts.jsonOutput) {
|
|
379
|
+
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
380
|
+
// Generate structured diff using the same normalized objects
|
|
381
|
+
const structuredDiff = hasChanges
|
|
382
|
+
? generateStructuredDiff(gitSyncCurrent, gitSyncBackend)
|
|
383
|
+
: {};
|
|
384
|
+
console.log(JSON.stringify({
|
|
385
|
+
success: true,
|
|
386
|
+
hasChanges,
|
|
387
|
+
local: syncOptionsToBackendFormat(normalizedCurrent),
|
|
388
|
+
backend: syncOptionsToBackendFormat(normalizedBackend),
|
|
389
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
390
|
+
diff: structuredDiff,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
if (hasChanges) {
|
|
395
|
+
log.info("Changes that would be made:");
|
|
396
|
+
const changes = generateChanges(normalizedCurrent, normalizedBackend);
|
|
397
|
+
if (Object.keys(changes).length === 0) {
|
|
398
|
+
log.info(colors.green("No differences found"));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
displayChanges(changes);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
log.info(colors.green("No differences found"));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// For non-diff mode, handle interactive logic if needed
|
|
411
|
+
// Only show interactive prompts for existing files with conflicts
|
|
412
|
+
if (!opts.diff &&
|
|
413
|
+
!opts.default &&
|
|
414
|
+
!opts.replace &&
|
|
415
|
+
!opts.override &&
|
|
416
|
+
!opts.workspaceLevel) {
|
|
417
|
+
// Use the same logic as diff to determine current settings
|
|
418
|
+
const currentSettings = getCurrentSettings(localConfig, writeMode, overrideKey);
|
|
419
|
+
const gitSyncBackend = extractGitSyncFields(normalizeSyncOptions(backendSyncOptions));
|
|
420
|
+
const gitSyncCurrent = extractGitSyncFields(normalizeSyncOptions(currentSettings));
|
|
421
|
+
const hasConflict = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
422
|
+
if (hasConflict && dntShim.Deno.stdin.isTerminal()) {
|
|
423
|
+
// Interactive mode - ask user
|
|
424
|
+
const { Select } = await import("./deps.js");
|
|
425
|
+
const choice = await Select.prompt({
|
|
426
|
+
message: "Settings conflict detected. How would you like to proceed?",
|
|
427
|
+
options: [
|
|
428
|
+
{
|
|
429
|
+
name: "Replace existing settings",
|
|
430
|
+
value: "replace",
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
name: "Add repository-specific override",
|
|
434
|
+
value: "override",
|
|
435
|
+
},
|
|
436
|
+
{ name: "Cancel", value: "cancel" },
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
if (choice === "cancel") {
|
|
440
|
+
log.info("Operation cancelled");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
writeMode = choice;
|
|
444
|
+
if (writeMode === "override") {
|
|
445
|
+
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
446
|
+
overrideKey = constructOverrideKey(workspace.remote, workspace.workspaceId, repoPath);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else if (hasConflict) {
|
|
450
|
+
// Non-interactive mode with conflicts - show message and exit
|
|
451
|
+
if (opts.jsonOutput) {
|
|
452
|
+
console.log(JSON.stringify({
|
|
453
|
+
success: false,
|
|
454
|
+
error: "Settings conflict detected. Use --replace or --override flags to resolve.",
|
|
455
|
+
hasConflict: true,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
log.error(colors.red("Settings conflict detected."));
|
|
460
|
+
log.info("Use --replace to overwrite existing settings or --override to add repository-specific override.");
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Check if there are actually any changes before writing
|
|
466
|
+
const currentSettingsForCheck = getCurrentSettings(localConfig, writeMode, overrideKey);
|
|
467
|
+
const gitSyncBackend = extractGitSyncFields(normalizeSyncOptions(backendSyncOptions));
|
|
468
|
+
const gitSyncCurrent = extractGitSyncFields(normalizeSyncOptions(currentSettingsForCheck));
|
|
469
|
+
const hasActualChanges = !deepEqual(gitSyncBackend, gitSyncCurrent);
|
|
470
|
+
if (!hasActualChanges) {
|
|
471
|
+
if (opts.jsonOutput) {
|
|
472
|
+
console.log(JSON.stringify({
|
|
473
|
+
success: true,
|
|
474
|
+
message: "No changes needed",
|
|
475
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
log.info(colors.green("No changes needed - settings are already up to date"));
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Apply the settings based on write mode
|
|
484
|
+
let updatedConfig;
|
|
485
|
+
if (writeMode === "replace") {
|
|
486
|
+
// Preserve existing local config and update only git-sync fields
|
|
487
|
+
updatedConfig = { ...localConfig };
|
|
488
|
+
// Remove overrides since we're in replace mode
|
|
489
|
+
delete updatedConfig.overrides;
|
|
490
|
+
// Update with backend git-sync settings
|
|
491
|
+
Object.assign(updatedConfig, backendSyncOptions);
|
|
492
|
+
}
|
|
493
|
+
else if (writeMode === "override" && overrideKey) {
|
|
494
|
+
// Add repository-specific override
|
|
495
|
+
updatedConfig = { ...localConfig };
|
|
496
|
+
if (!updatedConfig.overrides) {
|
|
497
|
+
updatedConfig.overrides = {};
|
|
498
|
+
}
|
|
499
|
+
// Only store the delta - settings that differ from current effective settings
|
|
500
|
+
const currentEffective = getCurrentSettings(localConfig, "replace");
|
|
501
|
+
const deltaSettings = {};
|
|
502
|
+
// Compare each setting and only include differences
|
|
503
|
+
for (const [key, value] of Object.entries(backendSyncOptions)) {
|
|
504
|
+
if (key === "overrides")
|
|
505
|
+
continue; // Skip overrides field
|
|
506
|
+
const currentValue = currentEffective[key];
|
|
507
|
+
const newValue = value;
|
|
508
|
+
// Compare arrays by content, primitives by value
|
|
509
|
+
const isDifferent = Array.isArray(currentValue) && Array.isArray(newValue)
|
|
510
|
+
? !arraysEqual(currentValue, newValue)
|
|
511
|
+
: currentValue !== newValue;
|
|
512
|
+
if (isDifferent) {
|
|
513
|
+
deltaSettings[key] = newValue;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
updatedConfig.overrides[overrideKey] = deltaSettings;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
// Replace top-level settings
|
|
520
|
+
updatedConfig = { ...localConfig };
|
|
521
|
+
// Copy all backend settings to top level, excluding overrides
|
|
522
|
+
const { overrides, ...topLevelSettings } = backendSyncOptions;
|
|
523
|
+
Object.assign(updatedConfig, topLevelSettings);
|
|
524
|
+
}
|
|
525
|
+
// Write updated configuration
|
|
526
|
+
await dntShim.Deno.writeTextFile("wmill.yaml", yamlStringify(updatedConfig));
|
|
527
|
+
if (opts.jsonOutput) {
|
|
528
|
+
console.log(JSON.stringify({
|
|
529
|
+
success: true,
|
|
530
|
+
message: `Git-sync settings pulled successfully`,
|
|
531
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
532
|
+
overrideKey,
|
|
533
|
+
}));
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
log.info(colors.green(`Git-sync settings pulled successfully from ${selectedRepo.git_repo_resource_path}`));
|
|
537
|
+
if (writeMode === "override" && overrideKey) {
|
|
538
|
+
log.info(colors.gray(`Settings written to override key: ${overrideKey}`));
|
|
539
|
+
}
|
|
540
|
+
else if (writeMode === "replace") {
|
|
541
|
+
log.info(colors.gray(`Settings written as simple configuration`));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
log.info(colors.gray(`Settings written to top-level defaults`));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
550
|
+
if (opts.jsonOutput) {
|
|
551
|
+
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
log.error(colors.red(`Failed to pull git-sync settings: ${errorMessage}`));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function pushGitSyncSettings(opts) {
|
|
559
|
+
const workspace = await resolveWorkspace(opts);
|
|
560
|
+
await requireLogin(opts);
|
|
561
|
+
try {
|
|
562
|
+
// Check if wmill.yaml exists - require it for git-sync settings commands
|
|
563
|
+
try {
|
|
564
|
+
await dntShim.Deno.stat("wmill.yaml");
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
log.error(colors.red("No wmill.yaml file found. Please run 'wmill init' first to create the configuration file."));
|
|
568
|
+
dntShim.Deno.exit(1);
|
|
569
|
+
}
|
|
570
|
+
// Read local configuration
|
|
571
|
+
const localConfig = await readConfigFile();
|
|
572
|
+
// Parse and validate --with-backend-settings if provided, otherwise fetch from backend
|
|
573
|
+
let settings;
|
|
574
|
+
if (opts.withBackendSettings) {
|
|
575
|
+
try {
|
|
576
|
+
const parsedSettings = JSON.parse(opts.withBackendSettings);
|
|
577
|
+
// Validate the structure matches expected test format (raw settings object)
|
|
578
|
+
if (!parsedSettings.include_path ||
|
|
579
|
+
!Array.isArray(parsedSettings.include_path)) {
|
|
580
|
+
throw new Error("Invalid settings format. Expected include_path array");
|
|
581
|
+
}
|
|
582
|
+
if (!parsedSettings.include_type ||
|
|
583
|
+
!Array.isArray(parsedSettings.include_type)) {
|
|
584
|
+
throw new Error("Invalid settings format. Expected include_type array");
|
|
585
|
+
}
|
|
586
|
+
// Create mock backend response with single repository using provided settings
|
|
587
|
+
const mockRepositoryPath = opts.repository || "u/mock/repo";
|
|
588
|
+
settings = {
|
|
589
|
+
git_sync: {
|
|
590
|
+
repositories: [{
|
|
591
|
+
git_repo_resource_path: mockRepositoryPath,
|
|
592
|
+
settings: {
|
|
593
|
+
include_path: parsedSettings.include_path,
|
|
594
|
+
include_type: parsedSettings.include_type,
|
|
595
|
+
exclude_path: parsedSettings.exclude_path || [],
|
|
596
|
+
extra_include_path: parsedSettings.extra_include_path || [],
|
|
597
|
+
},
|
|
598
|
+
}],
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
catch (parseError) {
|
|
603
|
+
const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
|
|
604
|
+
if (opts.jsonOutput) {
|
|
605
|
+
console.log(JSON.stringify({
|
|
606
|
+
success: false,
|
|
607
|
+
error: `Failed to parse --with-backend-settings JSON: ${errorMessage}`,
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
log.error(colors.red(`Failed to parse --with-backend-settings JSON: ${errorMessage}`));
|
|
612
|
+
}
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
// Fetch current backend settings
|
|
618
|
+
try {
|
|
619
|
+
settings = await wmill.getSettings({
|
|
620
|
+
workspace: workspace.workspaceId,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (apiError) {
|
|
624
|
+
const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
|
|
625
|
+
if (opts.jsonOutput) {
|
|
626
|
+
console.log(JSON.stringify({
|
|
627
|
+
success: false,
|
|
628
|
+
error: `Failed to fetch workspace settings: ${errorMessage}`,
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
log.error(colors.red(`Failed to fetch workspace settings: ${errorMessage}`));
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!settings.git_sync?.repositories ||
|
|
638
|
+
settings.git_sync.repositories.length === 0) {
|
|
639
|
+
if (opts.jsonOutput) {
|
|
640
|
+
console.log(JSON.stringify({
|
|
641
|
+
success: false,
|
|
642
|
+
error: "No git-sync repositories configured",
|
|
643
|
+
}));
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
log.error(colors.red("No git-sync repositories configured in workspace"));
|
|
647
|
+
}
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Find the repository to work with
|
|
651
|
+
let selectedRepo;
|
|
652
|
+
if (opts.repository) {
|
|
653
|
+
const found = settings.git_sync.repositories.find((r) => r.git_repo_resource_path === opts.repository ||
|
|
654
|
+
r.git_repo_resource_path === `$res:${opts.repository}`);
|
|
655
|
+
if (!found) {
|
|
656
|
+
throw new Error(`Repository ${opts.repository} not found`);
|
|
657
|
+
}
|
|
658
|
+
selectedRepo = found;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
selectedRepo = await selectRepository(settings.git_sync.repositories);
|
|
662
|
+
}
|
|
663
|
+
// Get effective settings for this workspace/repo
|
|
664
|
+
const repoPath = normalizeRepoPath(selectedRepo.git_repo_resource_path);
|
|
665
|
+
const effectiveSettings = getEffectiveSettings(localConfig, workspace.remote, workspace.workspaceId, repoPath);
|
|
666
|
+
// Convert to backend format
|
|
667
|
+
const backendFormat = {
|
|
668
|
+
include_path: effectiveSettings.includes || [],
|
|
669
|
+
include_type: syncOptionsToIncludeType(effectiveSettings),
|
|
670
|
+
exclude_path: effectiveSettings.excludes || [],
|
|
671
|
+
extra_include_path: effectiveSettings.extraIncludes || [],
|
|
672
|
+
};
|
|
673
|
+
if (opts.diff) {
|
|
674
|
+
// Show what would be pushed
|
|
675
|
+
const currentBackend = selectedRepo.settings;
|
|
676
|
+
// Convert current backend settings to SyncOptions for user-friendly display
|
|
677
|
+
const currentSyncOptions = {
|
|
678
|
+
includes: currentBackend.include_path || [],
|
|
679
|
+
excludes: currentBackend.exclude_path || [],
|
|
680
|
+
extraIncludes: currentBackend.extra_include_path || [],
|
|
681
|
+
...includeTypeToSyncOptions(currentBackend.include_type || []),
|
|
682
|
+
};
|
|
683
|
+
const normalizedCurrent = normalizeSyncOptions(currentSyncOptions);
|
|
684
|
+
const normalizedEffective = normalizeSyncOptions(effectiveSettings);
|
|
685
|
+
const gitSyncCurrent = extractGitSyncFields(normalizedCurrent);
|
|
686
|
+
const gitSyncEffective = extractGitSyncFields(normalizedEffective);
|
|
687
|
+
const hasChanges = !deepEqual(gitSyncEffective, gitSyncCurrent);
|
|
688
|
+
if (opts.jsonOutput) {
|
|
689
|
+
// Generate structured diff using the same normalized objects
|
|
690
|
+
const structuredDiff = hasChanges
|
|
691
|
+
? generateStructuredDiff(gitSyncCurrent, gitSyncEffective)
|
|
692
|
+
: {};
|
|
693
|
+
console.log(JSON.stringify({
|
|
694
|
+
success: true,
|
|
695
|
+
hasChanges,
|
|
696
|
+
local: syncOptionsToBackendFormat(normalizedEffective),
|
|
697
|
+
backend: syncOptionsToBackendFormat(normalizedCurrent),
|
|
698
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
699
|
+
diff: structuredDiff,
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
if (hasChanges) {
|
|
704
|
+
log.info("Changes that would be pushed:");
|
|
705
|
+
const changes = generateChanges(normalizedCurrent, normalizedEffective);
|
|
706
|
+
if (Object.keys(changes).length === 0) {
|
|
707
|
+
log.info(colors.green("No changes to push"));
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
displayChanges(changes);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
log.info(colors.green("No changes to push"));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (opts.withBackendSettings) {
|
|
720
|
+
// Skip backend update when using simulated settings
|
|
721
|
+
if (opts.jsonOutput) {
|
|
722
|
+
console.log(JSON.stringify({
|
|
723
|
+
success: true,
|
|
724
|
+
message: `Git-sync settings push simulated (--with-backend-settings used)`,
|
|
725
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
726
|
+
simulated: true,
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
log.info(colors.green(`Git-sync settings push simulated for ${selectedRepo.git_repo_resource_path} (--with-backend-settings used)`));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
// Update the specific repository settings
|
|
735
|
+
const updatedRepos = settings.git_sync.repositories.map((repo) => {
|
|
736
|
+
if (repo.git_repo_resource_path ===
|
|
737
|
+
selectedRepo.git_repo_resource_path) {
|
|
738
|
+
return {
|
|
739
|
+
...repo,
|
|
740
|
+
settings: backendFormat,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return repo;
|
|
744
|
+
});
|
|
745
|
+
// Push updated settings to backend
|
|
746
|
+
try {
|
|
747
|
+
await wmill.editWorkspaceGitSyncConfig({
|
|
748
|
+
workspace: workspace.workspaceId,
|
|
749
|
+
requestBody: {
|
|
750
|
+
git_sync_settings: {
|
|
751
|
+
repositories: updatedRepos,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
catch (apiError) {
|
|
757
|
+
const errorMessage = apiError instanceof Error ? apiError.message : String(apiError);
|
|
758
|
+
if (opts.jsonOutput) {
|
|
759
|
+
console.log(JSON.stringify({
|
|
760
|
+
success: false,
|
|
761
|
+
error: `Failed to update workspace git-sync config: ${errorMessage}`,
|
|
762
|
+
}));
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
log.error(colors.red(`Failed to update workspace git-sync config: ${errorMessage}`));
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (opts.jsonOutput) {
|
|
770
|
+
console.log(JSON.stringify({
|
|
771
|
+
success: true,
|
|
772
|
+
message: `Git-sync settings pushed successfully`,
|
|
773
|
+
repository: selectedRepo.git_repo_resource_path,
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
log.info(colors.green(`Git-sync settings pushed successfully to ${selectedRepo.git_repo_resource_path}`));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
783
|
+
if (opts.jsonOutput) {
|
|
784
|
+
console.log(JSON.stringify({ success: false, error: errorMessage }));
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
log.error(colors.red(`Failed to push git-sync settings: ${errorMessage}`));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const command = new Command()
|
|
792
|
+
.description("Manage git-sync settings between local wmill.yaml and Windmill backend")
|
|
793
|
+
.command("pull")
|
|
794
|
+
.description("Pull git-sync settings from Windmill backend to local wmill.yaml")
|
|
795
|
+
.option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo)")
|
|
796
|
+
.option("--workspace-level", "Write settings to workspace:* override instead of workspace:repo")
|
|
797
|
+
.option("--default", "Write settings to top-level defaults instead of overrides")
|
|
798
|
+
.option("--replace", "Replace existing settings (non-interactive mode)")
|
|
799
|
+
.option("--override", "Add repository-specific override (non-interactive mode)")
|
|
800
|
+
.option("--diff", "Show differences without applying changes")
|
|
801
|
+
.option("--json-output", "Output in JSON format")
|
|
802
|
+
.option("--with-backend-settings <json:string>", "Use provided JSON settings instead of querying backend (for testing)")
|
|
803
|
+
.action(pullGitSyncSettings)
|
|
804
|
+
.command("push")
|
|
805
|
+
.description("Push git-sync settings from local wmill.yaml to Windmill backend")
|
|
806
|
+
.option("--repository <repo:string>", "Specify repository path (e.g., u/user/repo)")
|
|
807
|
+
.option("--diff", "Show what would be pushed without applying changes")
|
|
808
|
+
.option("--json-output", "Output in JSON format")
|
|
809
|
+
.option("--with-backend-settings <json:string>", "Use provided JSON settings instead of querying backend (for testing)")
|
|
810
|
+
.action(pushGitSyncSettings);
|
|
811
|
+
export { pullGitSyncSettings, pushGitSyncSettings };
|
|
812
|
+
export default command;
|