redo-cli 0.1.0__py3-none-any.whl
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.
- main.py +301 -0
- modules/__init__.py +1 -0
- modules/placeholders.py +134 -0
- modules/runner.py +278 -0
- modules/storage.py +515 -0
- modules/ui.py +433 -0
- redo_cli-0.1.0.dist-info/METADATA +231 -0
- redo_cli-0.1.0.dist-info/RECORD +11 -0
- redo_cli-0.1.0.dist-info/WHEEL +5 -0
- redo_cli-0.1.0.dist-info/entry_points.txt +2 -0
- redo_cli-0.1.0.dist-info/top_level.txt +2 -0
modules/storage.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
STATE_FILE_NAME = "redo_state.json"
|
|
9
|
+
RESERVED_WORKFLOW_NAMES = {
|
|
10
|
+
"autofix",
|
|
11
|
+
"clearhistory",
|
|
12
|
+
"copy",
|
|
13
|
+
"delete",
|
|
14
|
+
"doctor",
|
|
15
|
+
"export",
|
|
16
|
+
"guide",
|
|
17
|
+
"help",
|
|
18
|
+
"import",
|
|
19
|
+
"info",
|
|
20
|
+
"init",
|
|
21
|
+
"list",
|
|
22
|
+
"new",
|
|
23
|
+
"path",
|
|
24
|
+
"rename",
|
|
25
|
+
"run",
|
|
26
|
+
"search",
|
|
27
|
+
"show",
|
|
28
|
+
"stats",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _user_data_dir():
|
|
33
|
+
override = os.environ.get("REDO_DATA_DIR")
|
|
34
|
+
if override:
|
|
35
|
+
return Path(override).expanduser()
|
|
36
|
+
appdata = os.environ.get("APPDATA")
|
|
37
|
+
if appdata:
|
|
38
|
+
return Path(appdata) / "Redo"
|
|
39
|
+
return Path.home() / ".redo"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
DATA_DIR = _user_data_dir()
|
|
43
|
+
DATA_FILE = DATA_DIR / "workflows.json"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _result(code, status, message, data=None):
|
|
47
|
+
result = {
|
|
48
|
+
"code": code,
|
|
49
|
+
"status": status,
|
|
50
|
+
"message": message,
|
|
51
|
+
}
|
|
52
|
+
if data is not None:
|
|
53
|
+
result["data"] = data
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _write_text_file(path, content):
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
temp_path = None
|
|
60
|
+
try:
|
|
61
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as file:
|
|
62
|
+
temp_path = Path(file.name)
|
|
63
|
+
file.write(content)
|
|
64
|
+
file.flush()
|
|
65
|
+
os.fsync(file.fileno())
|
|
66
|
+
temp_path.replace(path)
|
|
67
|
+
except OSError:
|
|
68
|
+
if temp_path is not None:
|
|
69
|
+
temp_path.unlink(missing_ok=True)
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _write_json_file(path, data):
|
|
74
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
temp_path = None
|
|
76
|
+
try:
|
|
77
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as file:
|
|
78
|
+
temp_path = Path(file.name)
|
|
79
|
+
json.dump(data, file, indent=2)
|
|
80
|
+
file.write("\n")
|
|
81
|
+
file.flush()
|
|
82
|
+
os.fsync(file.fileno())
|
|
83
|
+
temp_path.replace(path)
|
|
84
|
+
except OSError:
|
|
85
|
+
if temp_path is not None:
|
|
86
|
+
temp_path.unlink(missing_ok=True)
|
|
87
|
+
raise
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _require_loaded_workflows(result, allow_warnings=False):
|
|
91
|
+
if result["code"] == 1:
|
|
92
|
+
return None, result
|
|
93
|
+
if result["code"] == 2 and not allow_warnings:
|
|
94
|
+
return None, _result(1, "error", "workflow storage is invalid; run `redo autofix` first")
|
|
95
|
+
return result.get("data", {}), None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_workflow_name(name):
|
|
99
|
+
clean_name = str(name).strip()
|
|
100
|
+
if not clean_name:
|
|
101
|
+
return None, _result(2, "warning", "workflow name cannot be blank")
|
|
102
|
+
if clean_name.lower() in RESERVED_WORKFLOW_NAMES:
|
|
103
|
+
return None, _result(2, "warning", "workflow name is reserved by a Redo command")
|
|
104
|
+
return clean_name, None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def validate_workflow_name(name):
|
|
108
|
+
_, error = _validate_workflow_name(name)
|
|
109
|
+
if error:
|
|
110
|
+
return error
|
|
111
|
+
return _result(0, "success", "workflow name is valid")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def initialize_file():
|
|
115
|
+
try:
|
|
116
|
+
if DATA_FILE.exists() and DATA_FILE.read_text(encoding="utf-8").strip():
|
|
117
|
+
return _result(2, "warning", "workflow file already exists")
|
|
118
|
+
_write_text_file(DATA_FILE, "{}\n")
|
|
119
|
+
return _result(0, "success", "workflow file initialized")
|
|
120
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
121
|
+
return _result(1, "error", f"could not initialize workflow file: {error}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _broken_backup_file():
|
|
125
|
+
candidate = DATA_FILE.with_name(f"{DATA_FILE.stem}.broken.json")
|
|
126
|
+
if not candidate.exists():
|
|
127
|
+
return candidate
|
|
128
|
+
index = 1
|
|
129
|
+
while True:
|
|
130
|
+
candidate = DATA_FILE.with_name(f"{DATA_FILE.stem}.broken.{index}.json")
|
|
131
|
+
if not candidate.exists():
|
|
132
|
+
return candidate
|
|
133
|
+
index += 1
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _state_file():
|
|
137
|
+
return DATA_FILE.parent / STATE_FILE_NAME
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _normalize_workflow(name, workflow):
|
|
141
|
+
if not isinstance(name, str) or not name.strip() or not isinstance(workflow, dict):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
commands = workflow.get("commands", [])
|
|
145
|
+
if isinstance(commands, str):
|
|
146
|
+
commands = [commands]
|
|
147
|
+
elif isinstance(commands, list):
|
|
148
|
+
commands = [str(command) for command in commands if str(command).strip()]
|
|
149
|
+
else:
|
|
150
|
+
commands = []
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
runs = int(workflow.get("runs", 0))
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
runs = 0
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"description": str(workflow.get("description", "")),
|
|
159
|
+
"commands": commands,
|
|
160
|
+
"runs": max(runs, 0),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _normalize_workflows(workflows):
|
|
165
|
+
if not isinstance(workflows, dict):
|
|
166
|
+
return {}
|
|
167
|
+
|
|
168
|
+
normalized = {}
|
|
169
|
+
for name, workflow in workflows.items():
|
|
170
|
+
clean_name, name_error = _validate_workflow_name(name)
|
|
171
|
+
if name_error:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
fixed_workflow = _normalize_workflow(clean_name, workflow)
|
|
175
|
+
if fixed_workflow is not None:
|
|
176
|
+
normalized[clean_name] = fixed_workflow
|
|
177
|
+
|
|
178
|
+
return normalized
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def autofix_storage():
|
|
182
|
+
fixes = []
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
except OSError as error:
|
|
187
|
+
return _result(1, "error", f"could not create Redo data directory: {error}")
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
if not DATA_FILE.exists():
|
|
191
|
+
_write_text_file(DATA_FILE, "{}\n")
|
|
192
|
+
fixes.append("created workflow file")
|
|
193
|
+
return _result(0, "success", "autofix completed", {"fixes": fixes})
|
|
194
|
+
|
|
195
|
+
raw_text = DATA_FILE.read_text(encoding="utf-8")
|
|
196
|
+
if not raw_text.strip():
|
|
197
|
+
_write_text_file(DATA_FILE, "{}\n")
|
|
198
|
+
fixes.append("reset blank workflow file")
|
|
199
|
+
return _result(0, "success", "autofix completed", {"fixes": fixes})
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
workflows = json.loads(raw_text)
|
|
203
|
+
except json.JSONDecodeError:
|
|
204
|
+
backup_file = _broken_backup_file()
|
|
205
|
+
_write_text_file(backup_file, raw_text)
|
|
206
|
+
_write_text_file(DATA_FILE, "{}\n")
|
|
207
|
+
fixes.append("backed up malformed workflow file")
|
|
208
|
+
fixes.append("reset workflow file")
|
|
209
|
+
return _result(0, "success", "autofix completed", {"fixes": fixes})
|
|
210
|
+
|
|
211
|
+
normalized = _normalize_workflows(workflows)
|
|
212
|
+
if normalized != workflows:
|
|
213
|
+
save_result = save_workflows(normalized)
|
|
214
|
+
if save_result["code"] != 0:
|
|
215
|
+
return save_result
|
|
216
|
+
fixes.append("normalized workflow entries")
|
|
217
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
218
|
+
return _result(1, "error", f"could not autofix workflow storage: {error}")
|
|
219
|
+
|
|
220
|
+
if not fixes:
|
|
221
|
+
fixes.append("no fixes needed")
|
|
222
|
+
|
|
223
|
+
return _result(0, "success", "autofix completed", {"fixes": fixes})
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def load_workflows():
|
|
227
|
+
try:
|
|
228
|
+
if not DATA_FILE.exists() or not DATA_FILE.read_text(encoding="utf-8").strip():
|
|
229
|
+
init_result = initialize_file()
|
|
230
|
+
if init_result["code"] == 1:
|
|
231
|
+
return {**init_result, "data": {}}
|
|
232
|
+
|
|
233
|
+
with DATA_FILE.open("r", encoding="utf-8") as file:
|
|
234
|
+
data = json.load(file)
|
|
235
|
+
except json.JSONDecodeError:
|
|
236
|
+
return _result(2, "warning", "workflow file is malformed", {})
|
|
237
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
238
|
+
return _result(1, "error", f"could not load workflows: {error}", {})
|
|
239
|
+
|
|
240
|
+
if not isinstance(data, dict):
|
|
241
|
+
return _result(2, "warning", "workflow file must contain a JSON object", {})
|
|
242
|
+
|
|
243
|
+
return _result(0, "success", "workflows loaded successfully", data)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def save_workflows(workflows):
|
|
247
|
+
if not isinstance(workflows, dict):
|
|
248
|
+
return _result(1, "error", "workflows must be a dictionary")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
_write_json_file(DATA_FILE, workflows)
|
|
252
|
+
except OSError as error:
|
|
253
|
+
return _result(1, "error", f"could not save workflows: {error}")
|
|
254
|
+
|
|
255
|
+
return _result(0, "success", "workflows saved successfully")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def clear_workflows():
|
|
259
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
260
|
+
if error:
|
|
261
|
+
return error
|
|
262
|
+
|
|
263
|
+
save_result = save_workflows({})
|
|
264
|
+
if save_result["code"] != 0:
|
|
265
|
+
return save_result
|
|
266
|
+
|
|
267
|
+
return _result(0, "success", "workflow history cleared")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def should_offer_first_run_guide():
|
|
271
|
+
state_file = _state_file()
|
|
272
|
+
if not state_file.exists():
|
|
273
|
+
return True
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
with state_file.open("r", encoding="utf-8") as file:
|
|
277
|
+
state = json.load(file)
|
|
278
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
return not bool(state.get("first_run_guide_seen", False))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def mark_first_run_guide_seen():
|
|
285
|
+
state_file = _state_file()
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
state = {}
|
|
289
|
+
if state_file.exists() and state_file.read_text(encoding="utf-8").strip():
|
|
290
|
+
with state_file.open("r", encoding="utf-8") as file:
|
|
291
|
+
state = json.load(file)
|
|
292
|
+
|
|
293
|
+
state["first_run_guide_seen"] = True
|
|
294
|
+
_write_json_file(state_file, state)
|
|
295
|
+
except (json.JSONDecodeError, OSError, UnicodeDecodeError) as error:
|
|
296
|
+
return _result(1, "error", f"could not update first-run guide state: {error}")
|
|
297
|
+
|
|
298
|
+
return _result(0, "success", "first-run guide state updated")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def add_workflow(name, description, commands):
|
|
302
|
+
name, name_error = _validate_workflow_name(name)
|
|
303
|
+
if name_error:
|
|
304
|
+
return name_error
|
|
305
|
+
|
|
306
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
307
|
+
if error:
|
|
308
|
+
return error
|
|
309
|
+
|
|
310
|
+
if name in workflows:
|
|
311
|
+
return _result(2, "warning", "workflow already exists")
|
|
312
|
+
|
|
313
|
+
workflows[name] = {
|
|
314
|
+
"description": description,
|
|
315
|
+
"commands": commands,
|
|
316
|
+
"runs": 0,
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
save_result = save_workflows(workflows)
|
|
320
|
+
if save_result["code"] != 0:
|
|
321
|
+
return save_result
|
|
322
|
+
|
|
323
|
+
return _result(0, "success", "workflow saved successfully")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_workflow(name):
|
|
327
|
+
name = str(name).strip()
|
|
328
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
329
|
+
if error:
|
|
330
|
+
return error
|
|
331
|
+
|
|
332
|
+
workflow = workflows.get(name)
|
|
333
|
+
if workflow is None:
|
|
334
|
+
return _result(2, "warning", "workflow not found", None)
|
|
335
|
+
|
|
336
|
+
return _result(0, "success", "workflow found", workflow)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def delete_workflow(name):
|
|
340
|
+
name = str(name).strip()
|
|
341
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
342
|
+
if error:
|
|
343
|
+
return error
|
|
344
|
+
|
|
345
|
+
if name not in workflows:
|
|
346
|
+
return _result(2, "warning", "workflow not found")
|
|
347
|
+
|
|
348
|
+
del workflows[name]
|
|
349
|
+
save_result = save_workflows(workflows)
|
|
350
|
+
if save_result["code"] != 0:
|
|
351
|
+
return save_result
|
|
352
|
+
|
|
353
|
+
return _result(0, "success", "workflow deleted successfully")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def increment_runs(name):
|
|
357
|
+
name = str(name).strip()
|
|
358
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
359
|
+
if error:
|
|
360
|
+
return error
|
|
361
|
+
|
|
362
|
+
if name not in workflows:
|
|
363
|
+
return _result(2, "warning", "workflow not found")
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
workflows[name]["runs"] = int(workflows[name].get("runs", 0)) + 1
|
|
367
|
+
except (TypeError, ValueError):
|
|
368
|
+
workflows[name]["runs"] = 1
|
|
369
|
+
|
|
370
|
+
save_result = save_workflows(workflows)
|
|
371
|
+
if save_result["code"] != 0:
|
|
372
|
+
return save_result
|
|
373
|
+
|
|
374
|
+
return _result(0, "success", "workflow run count updated")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def copy_workflow(source_name, target_name):
|
|
378
|
+
source_name = str(source_name).strip()
|
|
379
|
+
target_name, name_error = _validate_workflow_name(target_name)
|
|
380
|
+
if name_error:
|
|
381
|
+
return name_error
|
|
382
|
+
|
|
383
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
384
|
+
if error:
|
|
385
|
+
return error
|
|
386
|
+
|
|
387
|
+
if source_name not in workflows:
|
|
388
|
+
return _result(2, "warning", "source workflow not found")
|
|
389
|
+
if target_name in workflows:
|
|
390
|
+
return _result(2, "warning", "target workflow already exists")
|
|
391
|
+
|
|
392
|
+
workflows[target_name] = deepcopy(workflows[source_name])
|
|
393
|
+
workflows[target_name]["runs"] = 0
|
|
394
|
+
|
|
395
|
+
save_result = save_workflows(workflows)
|
|
396
|
+
if save_result["code"] != 0:
|
|
397
|
+
return save_result
|
|
398
|
+
|
|
399
|
+
return _result(0, "success", "workflow copied successfully")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def rename_workflow(old_name, new_name):
|
|
403
|
+
old_name = str(old_name).strip()
|
|
404
|
+
new_name, name_error = _validate_workflow_name(new_name)
|
|
405
|
+
if name_error:
|
|
406
|
+
return name_error
|
|
407
|
+
|
|
408
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
409
|
+
if error:
|
|
410
|
+
return error
|
|
411
|
+
|
|
412
|
+
if old_name not in workflows:
|
|
413
|
+
return _result(2, "warning", "workflow not found")
|
|
414
|
+
if new_name in workflows:
|
|
415
|
+
return _result(2, "warning", "target workflow already exists")
|
|
416
|
+
|
|
417
|
+
workflows[new_name] = workflows.pop(old_name)
|
|
418
|
+
|
|
419
|
+
save_result = save_workflows(workflows)
|
|
420
|
+
if save_result["code"] != 0:
|
|
421
|
+
return save_result
|
|
422
|
+
|
|
423
|
+
return _result(0, "success", "workflow renamed successfully")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def find_workflows(query):
|
|
427
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
428
|
+
if error:
|
|
429
|
+
return error
|
|
430
|
+
|
|
431
|
+
query = query.lower()
|
|
432
|
+
matches = {}
|
|
433
|
+
|
|
434
|
+
for name, workflow in workflows.items():
|
|
435
|
+
searchable_text = " ".join(
|
|
436
|
+
[
|
|
437
|
+
name,
|
|
438
|
+
workflow.get("description", ""),
|
|
439
|
+
" ".join(workflow.get("commands", [])),
|
|
440
|
+
]
|
|
441
|
+
).lower()
|
|
442
|
+
if query in searchable_text:
|
|
443
|
+
matches[name] = workflow
|
|
444
|
+
|
|
445
|
+
return _result(0, "success", "workflow search completed", matches)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def export_workflows(destination):
|
|
449
|
+
workflows, error = _require_loaded_workflows(load_workflows())
|
|
450
|
+
if error:
|
|
451
|
+
return error
|
|
452
|
+
|
|
453
|
+
destination = Path(destination)
|
|
454
|
+
try:
|
|
455
|
+
_write_json_file(destination, workflows)
|
|
456
|
+
except OSError as error:
|
|
457
|
+
return _result(1, "error", f"could not export workflows: {error}")
|
|
458
|
+
|
|
459
|
+
return _result(0, "success", f"workflows exported to {destination}")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def import_workflows(source, replace=False):
|
|
463
|
+
source = Path(source)
|
|
464
|
+
try:
|
|
465
|
+
with source.open("r", encoding="utf-8") as file:
|
|
466
|
+
imported = json.load(file)
|
|
467
|
+
except FileNotFoundError:
|
|
468
|
+
return _result(2, "warning", "import file not found")
|
|
469
|
+
except json.JSONDecodeError:
|
|
470
|
+
return _result(2, "warning", "import file is malformed")
|
|
471
|
+
except (OSError, UnicodeDecodeError) as error:
|
|
472
|
+
return _result(1, "error", f"could not import workflows: {error}")
|
|
473
|
+
|
|
474
|
+
if not isinstance(imported, dict):
|
|
475
|
+
return _result(2, "warning", "import file must contain a JSON object")
|
|
476
|
+
|
|
477
|
+
current_workflows, error = _require_loaded_workflows(load_workflows())
|
|
478
|
+
if error:
|
|
479
|
+
return error
|
|
480
|
+
|
|
481
|
+
workflows = {} if replace else current_workflows
|
|
482
|
+
imported_count = 0
|
|
483
|
+
skipped_count = 0
|
|
484
|
+
|
|
485
|
+
for name, workflow in imported.items():
|
|
486
|
+
clean_name, name_error = _validate_workflow_name(name)
|
|
487
|
+
if name_error:
|
|
488
|
+
skipped_count += 1
|
|
489
|
+
continue
|
|
490
|
+
if not isinstance(workflow, dict):
|
|
491
|
+
skipped_count += 1
|
|
492
|
+
continue
|
|
493
|
+
if not replace and clean_name in workflows:
|
|
494
|
+
skipped_count += 1
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
normalized_workflow = _normalize_workflow(clean_name, workflow)
|
|
498
|
+
if normalized_workflow is None:
|
|
499
|
+
skipped_count += 1
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
workflows[clean_name] = normalized_workflow
|
|
503
|
+
imported_count += 1
|
|
504
|
+
|
|
505
|
+
save_result = save_workflows(workflows)
|
|
506
|
+
if save_result["code"] != 0:
|
|
507
|
+
return save_result
|
|
508
|
+
|
|
509
|
+
message = f"imported {imported_count} workflow"
|
|
510
|
+
if imported_count != 1:
|
|
511
|
+
message += "s"
|
|
512
|
+
if skipped_count:
|
|
513
|
+
message += f", skipped {skipped_count}"
|
|
514
|
+
|
|
515
|
+
return _result(0, "success", message)
|