contextos-agents 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.agents/AGENTS.md +5 -0
  2. package/.agents/adapters/aider/export.js +4 -2
  3. package/.agents/adapters/copilot/export.js +4 -2
  4. package/.agents/adapters/cursor/export.js +48 -12
  5. package/.agents/adapters/zed/export.js +53 -14
  6. package/.agents/core/skills/adapters/skill.yaml +6 -0
  7. package/.agents/core/skills/architecture-diagrams/skill.yaml +4 -0
  8. package/.agents/core/skills/brutalist-design/skill.yaml +4 -0
  9. package/.agents/core/skills/context-manager/references/context-rules.md +59 -0
  10. package/.agents/core/skills/context-manager/skill.yaml +7 -0
  11. package/.agents/core/skills/context-os/skill.yaml +11 -0
  12. package/.agents/core/skills/database/skill.yaml +6 -0
  13. package/.agents/core/skills/ddd/skill.yaml +7 -0
  14. package/.agents/core/skills/decisions/skill.yaml +6 -0
  15. package/.agents/core/skills/docker/skill.yaml +6 -0
  16. package/.agents/core/skills/engineering-workflow/SKILL.md +1 -1
  17. package/.agents/core/skills/engineering-workflow/skill.yaml +6 -0
  18. package/.agents/core/skills/fastapi/skill.yaml +7 -0
  19. package/.agents/core/skills/gemini-precision/SKILL.md +4 -0
  20. package/.agents/core/skills/gemini-precision/skill.yaml +4 -0
  21. package/.agents/core/skills/generators/skill.yaml +15 -0
  22. package/.agents/core/skills/graphify/SKILL.md +2 -2
  23. package/.agents/core/skills/graphify/skill.yaml +6 -1
  24. package/.agents/core/skills/gstack-roles/SKILL.md +3 -1
  25. package/.agents/core/skills/gstack-roles/skill.yaml +6 -0
  26. package/.agents/core/skills/impeccable-design/skill.yaml +6 -0
  27. package/.agents/core/skills/interview-me/skill.yaml +4 -0
  28. package/.agents/core/skills/microservices/skill.yaml +7 -0
  29. package/.agents/core/skills/minimalist-design/skill.yaml +4 -0
  30. package/.agents/core/skills/nestjs/skill.yaml +7 -0
  31. package/.agents/core/skills/nextjs/skill.yaml +7 -0
  32. package/.agents/core/skills/node/skill.yaml +7 -0
  33. package/.agents/core/skills/performance/skill.yaml +7 -0
  34. package/.agents/core/skills/ponytail-mindset/skill.yaml +6 -0
  35. package/.agents/core/skills/react/skill.yaml +7 -0
  36. package/.agents/core/skills/react-best-practices/skill.yaml +4 -0
  37. package/.agents/core/skills/redesign-audit/skill.yaml +4 -0
  38. package/.agents/core/skills/security/skill.yaml +7 -0
  39. package/.agents/core/skills/soft-design/skill.yaml +4 -0
  40. package/.agents/core/skills/state-management/skill.yaml +6 -0
  41. package/.agents/core/skills/subagent-orchestrator/SKILL.md +17 -0
  42. package/.agents/core/skills/subagent-orchestrator/skill.yaml +4 -0
  43. package/.agents/core/skills/system-design/skill.yaml +7 -0
  44. package/.agents/core/skills/testing/skill.yaml +6 -0
  45. package/.agents/core/skills/typescript/skill.yaml +7 -0
  46. package/.agents/core/skills/ui-design/skill.yaml +7 -0
  47. package/.agents/core/skills/ui-ux-pro/skill.yaml +6 -0
  48. package/.agents/core/skills/ux-design/skill.yaml +7 -0
  49. package/.agents/core/skills/vercel-optimize/scripts/collect-signals.mjs +131 -0
  50. package/.agents/core/skills/vercel-optimize/scripts/gate-investigations.mjs +142 -0
  51. package/.agents/core/skills/vercel-optimize/scripts/merge-signals.mjs +143 -0
  52. package/.agents/core/skills/vercel-optimize/scripts/scan-codebase.mjs +174 -0
  53. package/.agents/core/skills/vercel-optimize/skill.yaml +8 -0
  54. package/.agents/core/skills/web-accessibility/skill.yaml +7 -0
  55. package/.agents/ctx.js +14 -1
  56. package/.agents/doctor.js +193 -4
  57. package/.agents/generated/claude/skills/engineering-workflow/SKILL.md +1 -1
  58. package/.agents/generated/claude/skills/gemini-precision/SKILL.md +4 -0
  59. package/.agents/generated/claude/skills/graphify/SKILL.md +1 -1
  60. package/.agents/generated/claude/skills/gstack-roles/SKILL.md +3 -1
  61. package/.agents/generated/claude/skills/subagent-orchestrator/SKILL.md +17 -0
  62. package/.agents/generated/gemini/skills/engineering-workflow/SKILL.md +1 -1
  63. package/.agents/generated/gemini/skills/gemini-precision/SKILL.md +4 -0
  64. package/.agents/generated/gemini/skills/graphify/SKILL.md +2 -2
  65. package/.agents/generated/gemini/skills/gstack-roles/SKILL.md +3 -1
  66. package/.agents/generated/gemini/skills/subagent-orchestrator/SKILL.md +17 -0
  67. package/.agents/plugins/contextos/hooks.json +25 -0
  68. package/.agents/plugins/contextos/plugin.json +19 -0
  69. package/.agents/plugins.js +180 -20
  70. package/.agents/profiles.js +26 -0
  71. package/.agents/resolver.js +150 -30
  72. package/.agents/stats.js +22 -9
  73. package/.agents/validate.js +99 -13
  74. package/bin/commands/uninstall.js +207 -0
  75. package/bin/commands/update.js +262 -0
  76. package/bin/commands.js +305 -0
  77. package/bin/index.js +203 -83
  78. package/bin/lib/detector.js +106 -0
  79. package/bin/lib/lockfile.js +251 -0
  80. package/bin/lib/safe-writer.js +259 -0
  81. package/package.json +5 -6
  82. package/registry.json +13 -5
  83. package/registry.schema.json +3 -1
  84. package/.agents/mcp/runtime.py +0 -470
  85. package/.agents/mcp/server.mjs +0 -189271
  86. package/benchmarks/gemini-issues.js +0 -533
@@ -0,0 +1,207 @@
1
+ /**
2
+ * bin/commands/uninstall.js
3
+ * ContextOS CLI — Safe Uninstall Command
4
+ *
5
+ * Removes ContextOS artifacts safely:
6
+ * - Only deletes clean managed files (content matches installation hash)
7
+ * - Never deletes user-modified managed files (USER_MODIFIED_PRESERVED)
8
+ * - Never deletes user-created custom skills or unmanaged files
9
+ * - Preserves .agents/ directory if user files remain
10
+ * - Removes .agents/ cleanly if no user files remain
11
+ * - Supports --dry-run
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const {
17
+ loadLockfile,
18
+ migrateExistingInstallation,
19
+ } = require('../lib/lockfile.js');
20
+ const {
21
+ planFileDelete,
22
+ executeFileDelete,
23
+ } = require('../lib/safe-writer.js');
24
+
25
+ /**
26
+ * Recursively count all files remaining in a directory (excluding specified skip names).
27
+ */
28
+ function countFiles(dir, skipNames = new Set()) {
29
+ let count = 0;
30
+ if (!fs.existsSync(dir)) return 0;
31
+
32
+ try {
33
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
34
+ if (skipNames.has(entry.name)) continue;
35
+ const full = path.join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ count += countFiles(full, skipNames);
38
+ } else if (entry.isFile()) {
39
+ count++;
40
+ }
41
+ }
42
+ } catch {
43
+ return 0;
44
+ }
45
+ return count;
46
+ }
47
+
48
+ /**
49
+ * Plan uninstallation for a target project.
50
+ */
51
+ function planUninstall(projectDir, options = {}) {
52
+ const targetAgentsDir = path.join(projectDir, '.agents');
53
+ if (!fs.existsSync(targetAgentsDir)) {
54
+ return {
55
+ ok: false,
56
+ error: 'NO_AGENTS_DIR',
57
+ message: 'No .agents/ directory found in the target project. Nothing to uninstall.',
58
+ plans: [],
59
+ summary: { delete: 0, skipModified: 0, skipUnmanaged: 0, missing: 0 },
60
+ };
61
+ }
62
+
63
+ const sourceAgentsDir = options.sourceAgentsDir || path.join(__dirname, '..', '..', '.agents');
64
+
65
+ // Load existing lockfile or build migration plan
66
+ let lockData = loadLockfile(projectDir);
67
+ if (!lockData) {
68
+ lockData = migrateExistingInstallation(projectDir, sourceAgentsDir, {
69
+ dryRun: true,
70
+ inMemory: true,
71
+ });
72
+ }
73
+
74
+ const plans = [];
75
+ if (lockData && lockData.managedFiles) {
76
+ for (const relPath of Object.keys(lockData.managedFiles)) {
77
+ const plan = planFileDelete({
78
+ projectDir,
79
+ relPath,
80
+ lockData,
81
+ });
82
+ plans.push(plan);
83
+ }
84
+ }
85
+
86
+ // Count unmanaged files in .agents that are not in lockData
87
+ const managedSet = new Set(Object.keys(lockData?.managedFiles || {}));
88
+ let unmanagedFilesCount = 0;
89
+
90
+ function findUnmanaged(currentDir) {
91
+ let entries;
92
+ try {
93
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
94
+ } catch {
95
+ return;
96
+ }
97
+
98
+ for (const entry of entries) {
99
+ const full = path.join(currentDir, entry.name);
100
+ if (entry.isDirectory()) {
101
+ findUnmanaged(full);
102
+ } else if (entry.isFile()) {
103
+ if (entry.name === 'contextos.lock.json') continue;
104
+ const rel = path.relative(projectDir, full).replace(/\\/g, '/');
105
+ if (!managedSet.has(rel)) {
106
+ unmanagedFilesCount++;
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ findUnmanaged(targetAgentsDir);
113
+
114
+ const summary = {
115
+ delete: plans.filter(p => p.action === 'DELETE').length,
116
+ skipModified: plans.filter(p => p.action === 'SKIP' && p.reason === 'USER_MODIFIED_PRESERVED').length,
117
+ missing: plans.filter(p => p.action === 'SKIP' && p.reason === 'ALREADY_MISSING').length,
118
+ skipUnmanaged: unmanagedFilesCount,
119
+ };
120
+
121
+ return {
122
+ ok: true,
123
+ plans,
124
+ summary,
125
+ lockData,
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Run safe uninstall against a target project.
131
+ */
132
+ function runUninstall(projectDir = process.cwd(), options = {}) {
133
+ const dryRun = Boolean(options.dryRun);
134
+
135
+ console.log(`\nContextOS Safe Uninstall ${dryRun ? '[DRY-RUN]' : ''}`);
136
+ console.log(`Target: ${projectDir}`);
137
+
138
+ const planned = planUninstall(projectDir, options);
139
+ if (!planned.ok) {
140
+ console.error(`\n[ERROR] ${planned.message}`);
141
+ return planned;
142
+ }
143
+
144
+ const { lockData, plans, summary } = planned;
145
+
146
+ console.log('\nPlanned Actions:');
147
+ for (const item of plans) {
148
+ if (item.action === 'DELETE') {
149
+ console.log(` - DELETE ${item.relPath}`);
150
+ } else if (item.reason === 'USER_MODIFIED_PRESERVED') {
151
+ console.log(` = PRESERVE ${item.relPath} (modified by user — will NOT delete)`);
152
+ }
153
+ }
154
+
155
+ if (summary.skipUnmanaged > 0) {
156
+ console.log(` = PRESERVE ${summary.skipUnmanaged} unmanaged user file(s)/skill(s) in .agents/`);
157
+ }
158
+
159
+ console.log('\nSummary:');
160
+ console.log(` To delete: ${summary.delete}`);
161
+ console.log(` Preserved: ${summary.skipModified} (user-modified) + ${summary.skipUnmanaged} (user-created)`);
162
+
163
+ if (dryRun) {
164
+ console.log('\n[DRY-RUN] No files were deleted.');
165
+ return { ...planned, executed: false };
166
+ }
167
+
168
+ // Execute deletions
169
+ let deletedCount = 0;
170
+ for (const item of plans) {
171
+ if (item.action === 'DELETE') {
172
+ const res = executeFileDelete(item, lockData, { dryRun: false });
173
+ if (res.executed) deletedCount++;
174
+ }
175
+ }
176
+
177
+ const targetAgentsDir = path.join(projectDir, '.agents');
178
+ const lockfilePath = path.join(targetAgentsDir, 'contextos.lock.json');
179
+
180
+ // Count remaining files in .agents (excluding lockfile)
181
+ const remainingFiles = countFiles(targetAgentsDir, new Set(['contextos.lock.json']));
182
+
183
+ if (remainingFiles === 0) {
184
+ // Completely clean, remove lockfile and .agents directory
185
+ try {
186
+ if (fs.existsSync(lockfilePath)) fs.unlinkSync(lockfilePath);
187
+ fs.rmSync(targetAgentsDir, { recursive: true, force: true });
188
+ console.log('\n[OK] ContextOS completely uninstalled. .agents/ removed.');
189
+ } catch (err) {
190
+ console.warn(`[WARN] Could not remove .agents directory: ${err.message}`);
191
+ }
192
+ } else {
193
+ // User files or modified files remain — remove lockfile or keep remaining?
194
+ try {
195
+ if (fs.existsSync(lockfilePath)) fs.unlinkSync(lockfilePath);
196
+ } catch {}
197
+ console.log(`\n[OK] Clean ContextOS artifacts uninstalled (${deletedCount} removed).`);
198
+ console.log(`[INFO] .agents/ preserved because ${remainingFiles} user-modified or custom file(s) remain.`);
199
+ }
200
+
201
+ return { ...planned, executed: true, deletedCount, remainingFiles };
202
+ }
203
+
204
+ module.exports = {
205
+ planUninstall,
206
+ runUninstall,
207
+ };
@@ -0,0 +1,262 @@
1
+ /**
2
+ * bin/commands/update.js
3
+ * ContextOS CLI — Safe Update Command
4
+ *
5
+ * Updates ContextOS installation non-destructively:
6
+ * - Scans distribution package against target project .agents directory
7
+ * - Plans all mutations: CREATE, UPDATE, CONFLICT, SKIP, DELETE
8
+ * - Never overwrites user-customized files (generates .contextos.new conflicts)
9
+ * - Never touches or deletes unmanaged user files or custom skills
10
+ * - Supports --dry-run to preview changes without modifying filesystem
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const {
16
+ loadLockfile,
17
+ saveLockfile,
18
+ migrateExistingInstallation,
19
+ createLockfileData,
20
+ } = require('../lib/lockfile.js');
21
+ const {
22
+ planFileWrite,
23
+ executeFileWrite,
24
+ planFileDelete,
25
+ executeFileDelete,
26
+ } = require('../lib/safe-writer.js');
27
+
28
+ const pkg = require('../../package.json');
29
+
30
+ /**
31
+ * Collect all distributable files from the source .agents directory.
32
+ */
33
+ function collectDistributionFiles(sourceAgentsDir, options = {}) {
34
+ const files = [];
35
+ const excludedSkills = new Set(options.excludedSkills || []);
36
+
37
+ function walk(currentDir) {
38
+ let entries;
39
+ try {
40
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
41
+ } catch {
42
+ return;
43
+ }
44
+
45
+ for (const entry of entries) {
46
+ const full = path.join(currentDir, entry.name);
47
+ if (entry.isDirectory()) {
48
+ // Skip .git or temp dirs if any
49
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
50
+
51
+ // Skip excluded skills by profile
52
+ const relToAgents = path.relative(sourceAgentsDir, full).replace(/\\/g, '/');
53
+ const coreMatch = relToAgents.match(/^core\/skills\/([^/]+)$/);
54
+ if (coreMatch && excludedSkills.has(coreMatch[1])) {
55
+ continue;
56
+ }
57
+ const genMatch = relToAgents.match(/^generated\/[^/]+\/skills\/([^/]+)$/);
58
+ if (genMatch && excludedSkills.has(genMatch[1])) {
59
+ continue;
60
+ }
61
+
62
+ walk(full);
63
+ } else if (entry.isFile()) {
64
+ // Do not distribute lockfile itself or backup files
65
+ if (entry.name === 'contextos.lock.json' || entry.name.endsWith('.bak')) continue;
66
+
67
+ const relToAgents = path.relative(sourceAgentsDir, full).replace(/\\/g, '/');
68
+ files.push({
69
+ relPath: `.agents/${relToAgents}`,
70
+ fullSourcePath: full,
71
+ });
72
+ }
73
+ }
74
+ }
75
+
76
+ walk(sourceAgentsDir);
77
+ return files;
78
+ }
79
+
80
+ /**
81
+ * Plan the update operations for a project.
82
+ */
83
+ function planUpdate(projectDir, sourceAgentsDir, options = {}) {
84
+ const targetAgentsDir = path.join(projectDir, '.agents');
85
+ if (!fs.existsSync(targetAgentsDir)) {
86
+ return {
87
+ ok: false,
88
+ error: 'NO_AGENTS_DIR',
89
+ message: 'No .agents/ directory found in the target project. Run `contextos` to install first.',
90
+ plans: [],
91
+ summary: { create: 0, update: 0, conflict: 0, skip: 0, delete: 0 },
92
+ };
93
+ }
94
+
95
+ // Load existing lockfile or safely migrate
96
+ let lockData = loadLockfile(projectDir);
97
+ let migrated = false;
98
+ if (!lockData) {
99
+ lockData = migrateExistingInstallation(projectDir, sourceAgentsDir, {
100
+ dryRun: Boolean(options.dryRun),
101
+ version: pkg.version,
102
+ });
103
+ migrated = true;
104
+ }
105
+
106
+ // Determine active profile exclusions if available
107
+ let excludedSkills = [];
108
+ try {
109
+ const profilesPath = path.join(projectDir, '.agents', 'profiles.js');
110
+ if (fs.existsSync(profilesPath)) {
111
+ const profilesModule = require(profilesPath);
112
+ const active = profilesModule.getActiveProfile(projectDir);
113
+ if (active && Array.isArray(active.exclude_skills)) {
114
+ excludedSkills = active.exclude_skills;
115
+ }
116
+ }
117
+ } catch {}
118
+
119
+ const distFiles = collectDistributionFiles(sourceAgentsDir, { excludedSkills });
120
+ const distRelPaths = new Set(distFiles.map(f => f.relPath));
121
+
122
+ const plans = [];
123
+
124
+ // 1. Plan writes for distribution files
125
+ for (const item of distFiles) {
126
+ try {
127
+ const content = fs.readFileSync(item.fullSourcePath, 'utf8');
128
+ const plan = planFileWrite({
129
+ projectDir,
130
+ relPath: item.relPath,
131
+ content,
132
+ lockData,
133
+ });
134
+ plans.push(plan);
135
+ } catch {}
136
+ }
137
+
138
+ // 2. Plan deletions for stale managed files that no longer exist in distribution
139
+ if (lockData && lockData.managedFiles) {
140
+ for (const managedRel of Object.keys(lockData.managedFiles)) {
141
+ if (managedRel.startsWith('.agents/') && !distRelPaths.has(managedRel)) {
142
+ const delPlan = planFileDelete({
143
+ projectDir,
144
+ relPath: managedRel,
145
+ lockData,
146
+ });
147
+ plans.push(delPlan);
148
+ }
149
+ }
150
+ }
151
+
152
+ const summary = {
153
+ create: plans.filter(p => p.action === 'CREATE').length,
154
+ update: plans.filter(p => p.action === 'UPDATE').length,
155
+ conflict: plans.filter(p => p.action === 'CONFLICT').length,
156
+ skip: plans.filter(p => p.action === 'SKIP').length,
157
+ delete: plans.filter(p => p.action === 'DELETE').length,
158
+ };
159
+
160
+ return {
161
+ ok: true,
162
+ lockData,
163
+ migrated,
164
+ plans,
165
+ summary,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Run the update command against a target project.
171
+ */
172
+ function runUpdate(projectDir = process.cwd(), options = {}) {
173
+ const sourceAgentsDir = options.sourceAgentsDir || path.join(__dirname, '..', '..', '.agents');
174
+ const dryRun = Boolean(options.dryRun);
175
+
176
+ console.log(`\nContextOS Safe Update ${dryRun ? '[DRY-RUN]' : ''}`);
177
+ console.log(`Target: ${projectDir}`);
178
+
179
+ const planned = planUpdate(projectDir, sourceAgentsDir, options);
180
+ if (!planned.ok) {
181
+ console.error(`\n[ERROR] ${planned.message}`);
182
+ return planned;
183
+ }
184
+
185
+ const { lockData, plans, summary } = planned;
186
+
187
+ // Print classified plan preview
188
+ console.log('\nPlanned Actions:');
189
+ for (const item of plans) {
190
+ if (item.action === 'CREATE') {
191
+ console.log(` + CREATE ${item.relPath}`);
192
+ } else if (item.action === 'UPDATE') {
193
+ console.log(` ~ UPDATE ${item.relPath}`);
194
+ } else if (item.action === 'CONFLICT') {
195
+ console.log(` ! CONFLICT ${item.relPath} → ${item.conflictPath || item.relPath + '.contextos.new'} (customized by user)`);
196
+ } else if (item.action === 'DELETE') {
197
+ console.log(` - DELETE ${item.relPath} (stale managed file)`);
198
+ } else if (item.action === 'SKIP' && item.reason === 'UNMANAGED_USER_FILE') {
199
+ console.log(` = PRESERVE ${item.relPath} (unmanaged user file)`);
200
+ }
201
+ }
202
+
203
+ console.log('\nSummary:');
204
+ console.log(` Created: ${summary.create}`);
205
+ console.log(` Updated: ${summary.update}`);
206
+ console.log(` Conflicts: ${summary.conflict} (written as .contextos.new)`);
207
+ console.log(` Deleted: ${summary.delete}`);
208
+ console.log(` Skipped: ${summary.skip}`);
209
+
210
+ if (dryRun) {
211
+ console.log('\n[DRY-RUN] No changes were written to disk.');
212
+ return { ...planned, executed: false };
213
+ }
214
+
215
+ // Execute plans
216
+ let executedCount = 0;
217
+ for (const item of plans) {
218
+ if (item.action === 'CREATE' || item.action === 'UPDATE' || item.action === 'CONFLICT') {
219
+ executeFileWrite(item, lockData, { dryRun: false });
220
+ if (item.action !== 'SKIP') executedCount++;
221
+ } else if (item.action === 'DELETE') {
222
+ executeFileDelete(item, lockData, { dryRun: false });
223
+ executedCount++;
224
+ }
225
+ }
226
+
227
+ // Update lockfile metadata
228
+ lockData.version = options.packageVersion || pkg.version;
229
+ lockData.updatedAt = new Date().toISOString();
230
+ saveLockfile(projectDir, lockData);
231
+
232
+ console.log(`\n[OK] ContextOS successfully updated to v${lockData.version}!`);
233
+ if (summary.conflict > 0) {
234
+ console.warn(`[WARN] ${summary.conflict} customized file(s) had upstream changes.`);
235
+ console.warn(' Inspect the .contextos.new file(s) to merge upstream updates.');
236
+ }
237
+
238
+ // Re-compile if ctx.js exists and not skipped
239
+ if (!options.skipCompile) {
240
+ const ctxPath = path.join(projectDir, '.agents', 'ctx.js');
241
+ if (fs.existsSync(ctxPath)) {
242
+ try {
243
+ const { execFileSync } = require('child_process');
244
+ console.log('\nRecompiling skills...');
245
+ execFileSync(process.execPath, [ctxPath, 'export', 'gemini'], {
246
+ cwd: projectDir,
247
+ stdio: 'inherit',
248
+ });
249
+ } catch (err) {
250
+ console.warn(`[WARN] Post-update compilation failed: ${err.message}`);
251
+ }
252
+ }
253
+ }
254
+
255
+ return { ...planned, executed: true, executedCount };
256
+ }
257
+
258
+ module.exports = {
259
+ planUpdate,
260
+ runUpdate,
261
+ collectDistributionFiles,
262
+ };