scc-universal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function slugify(value, fallback = 'worker') {
|
|
8
|
+
const normalized = String(value || '')
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase()
|
|
11
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
12
|
+
.replace(/^-+|-+$/g, '');
|
|
13
|
+
return normalized || fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function renderTemplate(template, variables) {
|
|
17
|
+
if (typeof template !== 'string' || template.trim().length === 0) {
|
|
18
|
+
throw new Error('launcherCommand must be a non-empty string');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return template.replace(/\{([a-z_]+)\}/g, (match, key) => {
|
|
22
|
+
if (!(key in variables)) {
|
|
23
|
+
throw new Error(`Unknown template variable: ${key}`);
|
|
24
|
+
}
|
|
25
|
+
return String(variables[key]);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function shellQuote(value) {
|
|
30
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatCommand(program, args) {
|
|
34
|
+
return [program, ...args.map(shellQuote)].join(' ');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildTemplateVariables(values) {
|
|
38
|
+
return Object.entries(values).reduce((accumulator, [key, value]) => {
|
|
39
|
+
const stringValue = String(value);
|
|
40
|
+
const quotedValue = shellQuote(stringValue);
|
|
41
|
+
|
|
42
|
+
accumulator[key] = stringValue;
|
|
43
|
+
accumulator[`${key}_raw`] = stringValue;
|
|
44
|
+
accumulator[`${key}_sh`] = quotedValue;
|
|
45
|
+
return accumulator;
|
|
46
|
+
}, {});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildSessionBannerCommand(sessionName, coordinationDir) {
|
|
50
|
+
return `printf '%s\\n' ${shellQuote(`Session: ${sessionName}`)} ${shellQuote(`Coordination: ${coordinationDir}`)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSeedPaths(seedPaths, repoRoot) {
|
|
54
|
+
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
55
|
+
const entries = Array.isArray(seedPaths) ? seedPaths : [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
const normalized = [];
|
|
58
|
+
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (typeof entry !== 'string' || entry.trim().length === 0) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const absolutePath = path.resolve(resolvedRepoRoot, entry);
|
|
65
|
+
const relativePath = path.relative(resolvedRepoRoot, absolutePath);
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
relativePath.startsWith('..') ||
|
|
69
|
+
path.isAbsolute(relativePath)
|
|
70
|
+
) {
|
|
71
|
+
throw new Error(`seedPaths entries must stay inside repoRoot: ${entry}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const normalizedPath = relativePath.split(path.sep).join('/');
|
|
75
|
+
if (seen.has(normalizedPath)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
seen.add(normalizedPath);
|
|
80
|
+
normalized.push(normalizedPath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return normalized;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function overlaySeedPaths({ repoRoot, seedPaths, worktreePath }) {
|
|
87
|
+
const normalizedSeedPaths = normalizeSeedPaths(seedPaths, repoRoot);
|
|
88
|
+
|
|
89
|
+
for (const seedPath of normalizedSeedPaths) {
|
|
90
|
+
const sourcePath = path.join(repoRoot, seedPath);
|
|
91
|
+
const destinationPath = path.join(worktreePath, seedPath);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(sourcePath)) {
|
|
94
|
+
throw new Error(`Seed path does not exist in repoRoot: ${seedPath}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
98
|
+
fs.rmSync(destinationPath, { force: true, recursive: true });
|
|
99
|
+
fs.cpSync(sourcePath, destinationPath, {
|
|
100
|
+
dereference: false,
|
|
101
|
+
force: true,
|
|
102
|
+
preserveTimestamps: true,
|
|
103
|
+
recursive: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildWorkerArtifacts(workerPlan) {
|
|
109
|
+
const seededPathsSection = workerPlan.seedPaths.length > 0
|
|
110
|
+
? [
|
|
111
|
+
'',
|
|
112
|
+
'## Seeded Local Overlays',
|
|
113
|
+
...workerPlan.seedPaths.map(seedPath => `- \`${seedPath}\``)
|
|
114
|
+
]
|
|
115
|
+
: [];
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
dir: workerPlan.coordinationDir,
|
|
119
|
+
files: [
|
|
120
|
+
{
|
|
121
|
+
path: workerPlan.taskFilePath,
|
|
122
|
+
content: [
|
|
123
|
+
`# Worker Task: ${workerPlan.workerName}`,
|
|
124
|
+
'',
|
|
125
|
+
`- Session: \`${workerPlan.sessionName}\``,
|
|
126
|
+
`- Repo root: \`${workerPlan.repoRoot}\``,
|
|
127
|
+
`- Worktree: \`${workerPlan.worktreePath}\``,
|
|
128
|
+
`- Branch: \`${workerPlan.branchName}\``,
|
|
129
|
+
`- Launcher status file: \`${workerPlan.statusFilePath}\``,
|
|
130
|
+
`- Launcher handoff file: \`${workerPlan.handoffFilePath}\``,
|
|
131
|
+
...seededPathsSection,
|
|
132
|
+
'',
|
|
133
|
+
'## Objective',
|
|
134
|
+
workerPlan.task,
|
|
135
|
+
'',
|
|
136
|
+
'## Completion',
|
|
137
|
+
'Do not spawn subagents or external agents for this task.',
|
|
138
|
+
'Report results in your final response.',
|
|
139
|
+
`The worker launcher captures your response in \`${workerPlan.handoffFilePath}\` automatically.`,
|
|
140
|
+
`The worker launcher updates \`${workerPlan.statusFilePath}\` automatically.`
|
|
141
|
+
].join('\n')
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
path: workerPlan.handoffFilePath,
|
|
145
|
+
content: [
|
|
146
|
+
`# Handoff: ${workerPlan.workerName}`,
|
|
147
|
+
'',
|
|
148
|
+
'## Summary',
|
|
149
|
+
'- Pending',
|
|
150
|
+
'',
|
|
151
|
+
'## Files Changed',
|
|
152
|
+
'- Pending',
|
|
153
|
+
'',
|
|
154
|
+
'## Tests / Verification',
|
|
155
|
+
'- Pending',
|
|
156
|
+
'',
|
|
157
|
+
'## Follow-ups',
|
|
158
|
+
'- Pending'
|
|
159
|
+
].join('\n')
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: workerPlan.statusFilePath,
|
|
163
|
+
content: [
|
|
164
|
+
`# Status: ${workerPlan.workerName}`,
|
|
165
|
+
'',
|
|
166
|
+
'- State: not started',
|
|
167
|
+
`- Worktree: \`${workerPlan.worktreePath}\``,
|
|
168
|
+
`- Branch: \`${workerPlan.branchName}\``
|
|
169
|
+
].join('\n')
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function buildOrchestrationPlan(config = {}) {
|
|
176
|
+
const repoRoot = path.resolve(config.repoRoot || process.cwd());
|
|
177
|
+
const repoName = path.basename(repoRoot);
|
|
178
|
+
const workers = Array.isArray(config.workers) ? config.workers : [];
|
|
179
|
+
const globalSeedPaths = normalizeSeedPaths(config.seedPaths, repoRoot);
|
|
180
|
+
const sessionName = slugify(config.sessionName || repoName, 'session');
|
|
181
|
+
const worktreeRoot = path.resolve(config.worktreeRoot || path.dirname(repoRoot));
|
|
182
|
+
const coordinationRoot = path.resolve(
|
|
183
|
+
config.coordinationRoot || path.join(repoRoot, '.orchestration')
|
|
184
|
+
);
|
|
185
|
+
const coordinationDir = path.join(coordinationRoot, sessionName);
|
|
186
|
+
const baseRef = config.baseRef || 'HEAD';
|
|
187
|
+
const defaultLauncher = config.launcherCommand || '';
|
|
188
|
+
|
|
189
|
+
if (workers.length === 0) {
|
|
190
|
+
throw new Error('buildOrchestrationPlan requires at least one worker');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const seenSlugs = new Set();
|
|
194
|
+
const workerPlans = workers.map((worker, index) => {
|
|
195
|
+
if (!worker || typeof worker.task !== 'string' || worker.task.trim().length === 0) {
|
|
196
|
+
throw new Error(`Worker ${index + 1} is missing a task`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const workerName = worker.name || `worker-${index + 1}`;
|
|
200
|
+
const workerSlug = slugify(workerName, `worker-${index + 1}`);
|
|
201
|
+
|
|
202
|
+
if (seenSlugs.has(workerSlug)) {
|
|
203
|
+
throw new Error(`Workers must have unique slugs — duplicate: ${workerSlug}`);
|
|
204
|
+
}
|
|
205
|
+
seenSlugs.add(workerSlug);
|
|
206
|
+
|
|
207
|
+
const branchName = `orchestrator-${sessionName}-${workerSlug}`;
|
|
208
|
+
const worktreePath = path.join(worktreeRoot, `${repoName}-${sessionName}-${workerSlug}`);
|
|
209
|
+
const workerCoordinationDir = path.join(coordinationDir, workerSlug);
|
|
210
|
+
const taskFilePath = path.join(workerCoordinationDir, 'task.md');
|
|
211
|
+
const handoffFilePath = path.join(workerCoordinationDir, 'handoff.md');
|
|
212
|
+
const statusFilePath = path.join(workerCoordinationDir, 'status.md');
|
|
213
|
+
const launcherCommand = worker.launcherCommand || defaultLauncher;
|
|
214
|
+
const workerSeedPaths = normalizeSeedPaths(worker.seedPaths, repoRoot);
|
|
215
|
+
const seedPaths = normalizeSeedPaths([...globalSeedPaths, ...workerSeedPaths], repoRoot);
|
|
216
|
+
const templateVariables = buildTemplateVariables({
|
|
217
|
+
branch_name: branchName,
|
|
218
|
+
handoff_file: handoffFilePath,
|
|
219
|
+
repo_root: repoRoot,
|
|
220
|
+
session_name: sessionName,
|
|
221
|
+
status_file: statusFilePath,
|
|
222
|
+
task_file: taskFilePath,
|
|
223
|
+
worker_name: workerName,
|
|
224
|
+
worker_slug: workerSlug,
|
|
225
|
+
worktree_path: worktreePath
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (!launcherCommand) {
|
|
229
|
+
throw new Error(`Worker ${workerName} is missing a launcherCommand`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const gitArgs = ['worktree', 'add', '-b', branchName, worktreePath, baseRef];
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
branchName,
|
|
236
|
+
coordinationDir: workerCoordinationDir,
|
|
237
|
+
gitArgs,
|
|
238
|
+
gitCommand: formatCommand('git', gitArgs),
|
|
239
|
+
handoffFilePath,
|
|
240
|
+
launchCommand: renderTemplate(launcherCommand, templateVariables),
|
|
241
|
+
repoRoot,
|
|
242
|
+
sessionName,
|
|
243
|
+
seedPaths,
|
|
244
|
+
statusFilePath,
|
|
245
|
+
task: worker.task.trim(),
|
|
246
|
+
taskFilePath,
|
|
247
|
+
workerName,
|
|
248
|
+
workerSlug,
|
|
249
|
+
worktreePath
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const tmuxCommands = [
|
|
254
|
+
{
|
|
255
|
+
cmd: 'tmux',
|
|
256
|
+
args: ['new-session', '-d', '-s', sessionName, '-n', 'orchestrator', '-c', repoRoot],
|
|
257
|
+
description: 'Create detached tmux session'
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
cmd: 'tmux',
|
|
261
|
+
args: [
|
|
262
|
+
'send-keys',
|
|
263
|
+
'-t',
|
|
264
|
+
sessionName,
|
|
265
|
+
buildSessionBannerCommand(sessionName, coordinationDir),
|
|
266
|
+
'C-m'
|
|
267
|
+
],
|
|
268
|
+
description: 'Print orchestrator session details'
|
|
269
|
+
}
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
for (const workerPlan of workerPlans) {
|
|
273
|
+
tmuxCommands.push(
|
|
274
|
+
{
|
|
275
|
+
cmd: 'tmux',
|
|
276
|
+
args: ['split-window', '-d', '-t', sessionName, '-c', workerPlan.worktreePath],
|
|
277
|
+
description: `Create pane for ${workerPlan.workerName}`
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
cmd: 'tmux',
|
|
281
|
+
args: ['select-layout', '-t', sessionName, 'tiled'],
|
|
282
|
+
description: 'Arrange panes in tiled layout'
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
cmd: 'tmux',
|
|
286
|
+
args: ['select-pane', '-t', '<pane-id>', '-T', workerPlan.workerSlug],
|
|
287
|
+
description: `Label pane ${workerPlan.workerSlug}`
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
cmd: 'tmux',
|
|
291
|
+
args: [
|
|
292
|
+
'send-keys',
|
|
293
|
+
'-t',
|
|
294
|
+
'<pane-id>',
|
|
295
|
+
`cd ${shellQuote(workerPlan.worktreePath)} && ${workerPlan.launchCommand}`,
|
|
296
|
+
'C-m'
|
|
297
|
+
],
|
|
298
|
+
description: `Launch worker ${workerPlan.workerName}`
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
baseRef,
|
|
305
|
+
coordinationDir,
|
|
306
|
+
replaceExisting: Boolean(config.replaceExisting),
|
|
307
|
+
repoRoot,
|
|
308
|
+
sessionName,
|
|
309
|
+
tmuxCommands,
|
|
310
|
+
workerPlans
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function materializePlan(plan) {
|
|
315
|
+
for (const workerPlan of plan.workerPlans) {
|
|
316
|
+
const artifacts = buildWorkerArtifacts(workerPlan);
|
|
317
|
+
fs.mkdirSync(artifacts.dir, { recursive: true });
|
|
318
|
+
for (const file of artifacts.files) {
|
|
319
|
+
fs.writeFileSync(file.path, file.content + '\n', 'utf8');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function runCommand(program, args, options = {}) {
|
|
325
|
+
const result = spawnSync(program, args, {
|
|
326
|
+
cwd: options.cwd,
|
|
327
|
+
encoding: 'utf8',
|
|
328
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (result.error) {
|
|
332
|
+
throw result.error;
|
|
333
|
+
}
|
|
334
|
+
if (result.status !== 0) {
|
|
335
|
+
const stderr = (result.stderr || '').trim();
|
|
336
|
+
throw new Error(`${program} ${args.join(' ')} failed${stderr ? `: ${stderr}` : ''}`);
|
|
337
|
+
}
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function commandSucceeds(program, args, options = {}) {
|
|
342
|
+
const result = spawnSync(program, args, {
|
|
343
|
+
cwd: options.cwd,
|
|
344
|
+
encoding: 'utf8',
|
|
345
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
346
|
+
});
|
|
347
|
+
return result.status === 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function canonicalizePath(targetPath) {
|
|
351
|
+
const resolvedPath = path.resolve(targetPath);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
return fs.realpathSync.native(resolvedPath);
|
|
355
|
+
} catch (_error) {
|
|
356
|
+
const parentPath = path.dirname(resolvedPath);
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
return path.join(fs.realpathSync.native(parentPath), path.basename(resolvedPath));
|
|
360
|
+
} catch (_parentError) {
|
|
361
|
+
return resolvedPath;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function branchExists(repoRoot, branchName) {
|
|
367
|
+
return commandSucceeds('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branchName}`], {
|
|
368
|
+
cwd: repoRoot
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function listWorktrees(repoRoot) {
|
|
373
|
+
const listed = runCommand('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot });
|
|
374
|
+
const lines = (listed.stdout || '').split('\n');
|
|
375
|
+
const worktrees = [];
|
|
376
|
+
|
|
377
|
+
for (const line of lines) {
|
|
378
|
+
if (line.startsWith('worktree ')) {
|
|
379
|
+
const listedPath = line.slice('worktree '.length).trim();
|
|
380
|
+
worktrees.push({
|
|
381
|
+
listedPath,
|
|
382
|
+
canonicalPath: canonicalizePath(listedPath)
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return worktrees;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function cleanupExisting(plan) {
|
|
391
|
+
runCommand('git', ['worktree', 'prune', '--expire', 'now'], { cwd: plan.repoRoot });
|
|
392
|
+
|
|
393
|
+
const hasSession = spawnSync('tmux', ['has-session', '-t', plan.sessionName], {
|
|
394
|
+
encoding: 'utf8',
|
|
395
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (hasSession.status === 0) {
|
|
399
|
+
runCommand('tmux', ['kill-session', '-t', plan.sessionName], { cwd: plan.repoRoot });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const workerPlan of plan.workerPlans) {
|
|
403
|
+
const expectedWorktreePath = canonicalizePath(workerPlan.worktreePath);
|
|
404
|
+
const existingWorktree = listWorktrees(plan.repoRoot).find(
|
|
405
|
+
worktree => worktree.canonicalPath === expectedWorktreePath
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
if (existingWorktree) {
|
|
409
|
+
runCommand('git', ['worktree', 'remove', '--force', existingWorktree.listedPath], {
|
|
410
|
+
cwd: plan.repoRoot
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (fs.existsSync(workerPlan.worktreePath)) {
|
|
415
|
+
fs.rmSync(workerPlan.worktreePath, { force: true, recursive: true });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
runCommand('git', ['worktree', 'prune', '--expire', 'now'], { cwd: plan.repoRoot });
|
|
419
|
+
|
|
420
|
+
if (branchExists(plan.repoRoot, workerPlan.branchName)) {
|
|
421
|
+
runCommand('git', ['branch', '-D', workerPlan.branchName], { cwd: plan.repoRoot });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function rollbackCreatedResources(plan, createdState, runtime = {}) {
|
|
427
|
+
const runCommandImpl = runtime.runCommand || runCommand;
|
|
428
|
+
const listWorktreesImpl = runtime.listWorktrees || listWorktrees;
|
|
429
|
+
const branchExistsImpl = runtime.branchExists || branchExists;
|
|
430
|
+
const errors = [];
|
|
431
|
+
|
|
432
|
+
if (createdState.sessionCreated) {
|
|
433
|
+
try {
|
|
434
|
+
runCommandImpl('tmux', ['kill-session', '-t', plan.sessionName], { cwd: plan.repoRoot });
|
|
435
|
+
} catch (error) {
|
|
436
|
+
errors.push(error.message);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const workerPlan of [...createdState.workerPlans].reverse()) {
|
|
441
|
+
const expectedWorktreePath = canonicalizePath(workerPlan.worktreePath);
|
|
442
|
+
const existingWorktree = listWorktreesImpl(plan.repoRoot).find(
|
|
443
|
+
worktree => worktree.canonicalPath === expectedWorktreePath
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (existingWorktree) {
|
|
447
|
+
try {
|
|
448
|
+
runCommandImpl('git', ['worktree', 'remove', '--force', existingWorktree.listedPath], {
|
|
449
|
+
cwd: plan.repoRoot
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
errors.push(error.message);
|
|
453
|
+
}
|
|
454
|
+
} else if (fs.existsSync(workerPlan.worktreePath)) {
|
|
455
|
+
fs.rmSync(workerPlan.worktreePath, { force: true, recursive: true });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
runCommandImpl('git', ['worktree', 'prune', '--expire', 'now'], { cwd: plan.repoRoot });
|
|
460
|
+
} catch (error) {
|
|
461
|
+
errors.push(error.message);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (branchExistsImpl(plan.repoRoot, workerPlan.branchName)) {
|
|
465
|
+
try {
|
|
466
|
+
runCommandImpl('git', ['branch', '-D', workerPlan.branchName], { cwd: plan.repoRoot });
|
|
467
|
+
} catch (error) {
|
|
468
|
+
errors.push(error.message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (createdState.removeCoordinationDir && fs.existsSync(plan.coordinationDir)) {
|
|
474
|
+
fs.rmSync(plan.coordinationDir, { force: true, recursive: true });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (errors.length > 0) {
|
|
478
|
+
throw new Error(`rollback failed: ${errors.join('; ')}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function executePlan(plan, runtime = {}) {
|
|
483
|
+
const spawnSyncImpl = runtime.spawnSync || spawnSync;
|
|
484
|
+
const runCommandImpl = runtime.runCommand || runCommand;
|
|
485
|
+
const materializePlanImpl = runtime.materializePlan || materializePlan;
|
|
486
|
+
const overlaySeedPathsImpl = runtime.overlaySeedPaths || overlaySeedPaths;
|
|
487
|
+
const cleanupExistingImpl = runtime.cleanupExisting || cleanupExisting;
|
|
488
|
+
const rollbackCreatedResourcesImpl = runtime.rollbackCreatedResources || rollbackCreatedResources;
|
|
489
|
+
const createdState = {
|
|
490
|
+
workerPlans: [],
|
|
491
|
+
sessionCreated: false,
|
|
492
|
+
removeCoordinationDir: !fs.existsSync(plan.coordinationDir)
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
runCommandImpl('git', ['rev-parse', '--is-inside-work-tree'], { cwd: plan.repoRoot });
|
|
496
|
+
runCommandImpl('tmux', ['-V']);
|
|
497
|
+
|
|
498
|
+
if (plan.replaceExisting) {
|
|
499
|
+
cleanupExistingImpl(plan);
|
|
500
|
+
} else {
|
|
501
|
+
const hasSession = spawnSyncImpl('tmux', ['has-session', '-t', plan.sessionName], {
|
|
502
|
+
encoding: 'utf8',
|
|
503
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
504
|
+
});
|
|
505
|
+
if (hasSession.status === 0) {
|
|
506
|
+
throw new Error(`tmux session already exists: ${plan.sessionName}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
materializePlanImpl(plan);
|
|
512
|
+
|
|
513
|
+
for (const workerPlan of plan.workerPlans) {
|
|
514
|
+
runCommandImpl('git', workerPlan.gitArgs, { cwd: plan.repoRoot });
|
|
515
|
+
createdState.workerPlans.push(workerPlan);
|
|
516
|
+
overlaySeedPathsImpl({
|
|
517
|
+
repoRoot: plan.repoRoot,
|
|
518
|
+
seedPaths: workerPlan.seedPaths,
|
|
519
|
+
worktreePath: workerPlan.worktreePath
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
runCommandImpl(
|
|
524
|
+
'tmux',
|
|
525
|
+
['new-session', '-d', '-s', plan.sessionName, '-n', 'orchestrator', '-c', plan.repoRoot],
|
|
526
|
+
{ cwd: plan.repoRoot }
|
|
527
|
+
);
|
|
528
|
+
createdState.sessionCreated = true;
|
|
529
|
+
runCommandImpl(
|
|
530
|
+
'tmux',
|
|
531
|
+
[
|
|
532
|
+
'send-keys',
|
|
533
|
+
'-t',
|
|
534
|
+
plan.sessionName,
|
|
535
|
+
buildSessionBannerCommand(plan.sessionName, plan.coordinationDir),
|
|
536
|
+
'C-m'
|
|
537
|
+
],
|
|
538
|
+
{ cwd: plan.repoRoot }
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
for (const workerPlan of plan.workerPlans) {
|
|
542
|
+
const splitResult = runCommandImpl(
|
|
543
|
+
'tmux',
|
|
544
|
+
['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', plan.sessionName, '-c', workerPlan.worktreePath],
|
|
545
|
+
{ cwd: plan.repoRoot }
|
|
546
|
+
);
|
|
547
|
+
const paneId = splitResult.stdout.trim();
|
|
548
|
+
|
|
549
|
+
if (!paneId) {
|
|
550
|
+
throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
runCommandImpl('tmux', ['select-layout', '-t', plan.sessionName, 'tiled'], { cwd: plan.repoRoot });
|
|
554
|
+
runCommandImpl('tmux', ['select-pane', '-t', paneId, '-T', workerPlan.workerSlug], {
|
|
555
|
+
cwd: plan.repoRoot
|
|
556
|
+
});
|
|
557
|
+
runCommandImpl(
|
|
558
|
+
'tmux',
|
|
559
|
+
[
|
|
560
|
+
'send-keys',
|
|
561
|
+
'-t',
|
|
562
|
+
paneId,
|
|
563
|
+
`cd ${shellQuote(workerPlan.worktreePath)} && ${workerPlan.launchCommand}`,
|
|
564
|
+
'C-m'
|
|
565
|
+
],
|
|
566
|
+
{ cwd: plan.repoRoot }
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
} catch (error) {
|
|
570
|
+
try {
|
|
571
|
+
rollbackCreatedResourcesImpl(plan, createdState, {
|
|
572
|
+
branchExists: runtime.branchExists,
|
|
573
|
+
listWorktrees: runtime.listWorktrees,
|
|
574
|
+
runCommand: runCommandImpl
|
|
575
|
+
});
|
|
576
|
+
} catch (cleanupError) {
|
|
577
|
+
error.message = `${error.message}; cleanup failed: ${cleanupError.message}`;
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
coordinationDir: plan.coordinationDir,
|
|
584
|
+
sessionName: plan.sessionName,
|
|
585
|
+
workerCount: plan.workerPlans.length
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
buildOrchestrationPlan,
|
|
591
|
+
executePlan,
|
|
592
|
+
materializePlan,
|
|
593
|
+
normalizeSeedPaths,
|
|
594
|
+
overlaySeedPaths,
|
|
595
|
+
rollbackCreatedResources,
|
|
596
|
+
renderTemplate,
|
|
597
|
+
slugify
|
|
598
|
+
};
|