orbital-command 0.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/LICENSE +21 -0
- package/README.md +396 -0
- package/bin/orbital.js +362 -0
- package/dist/assets/WorkflowVisualizer-BZ21PIIF.js +84 -0
- package/dist/assets/WorkflowVisualizer-BZV40eAE.css +1 -0
- package/dist/assets/charts-D__PA1zp.js +72 -0
- package/dist/assets/index-D1G6i0nS.css +1 -0
- package/dist/assets/index-DpItvKpf.js +419 -0
- package/dist/assets/ui-BvF022GT.js +53 -0
- package/dist/assets/vendor-Dzv9lrRc.js +59 -0
- package/dist/index.html +19 -0
- package/dist/scanner-sweep.png +0 -0
- package/dist/server/server/adapters/index.js +34 -0
- package/dist/server/server/adapters/iterm2-adapter.js +29 -0
- package/dist/server/server/adapters/subprocess-adapter.js +21 -0
- package/dist/server/server/adapters/terminal-adapter.js +1 -0
- package/dist/server/server/config.js +156 -0
- package/dist/server/server/database.js +90 -0
- package/dist/server/server/index.js +372 -0
- package/dist/server/server/init.js +811 -0
- package/dist/server/server/parsers/event-parser.js +64 -0
- package/dist/server/server/parsers/scope-parser.js +188 -0
- package/dist/server/server/routes/config-routes.js +163 -0
- package/dist/server/server/routes/data-routes.js +461 -0
- package/dist/server/server/routes/dispatch-routes.js +215 -0
- package/dist/server/server/routes/git-routes.js +92 -0
- package/dist/server/server/routes/scope-routes.js +215 -0
- package/dist/server/server/routes/sprint-routes.js +116 -0
- package/dist/server/server/routes/version-routes.js +130 -0
- package/dist/server/server/routes/workflow-routes.js +185 -0
- package/dist/server/server/schema.js +90 -0
- package/dist/server/server/services/batch-orchestrator.js +253 -0
- package/dist/server/server/services/claude-session-service.js +352 -0
- package/dist/server/server/services/config-service.js +132 -0
- package/dist/server/server/services/deploy-service.js +51 -0
- package/dist/server/server/services/event-service.js +63 -0
- package/dist/server/server/services/gate-service.js +83 -0
- package/dist/server/server/services/git-service.js +309 -0
- package/dist/server/server/services/github-service.js +145 -0
- package/dist/server/server/services/readiness-service.js +184 -0
- package/dist/server/server/services/scope-cache.js +72 -0
- package/dist/server/server/services/scope-service.js +424 -0
- package/dist/server/server/services/sprint-orchestrator.js +312 -0
- package/dist/server/server/services/sprint-service.js +293 -0
- package/dist/server/server/services/workflow-service.js +397 -0
- package/dist/server/server/utils/cc-hooks-parser.js +49 -0
- package/dist/server/server/utils/dispatch-utils.js +305 -0
- package/dist/server/server/utils/logger.js +86 -0
- package/dist/server/server/utils/terminal-launcher.js +388 -0
- package/dist/server/server/utils/worktree-manager.js +98 -0
- package/dist/server/server/watchers/event-watcher.js +81 -0
- package/dist/server/server/watchers/scope-watcher.js +33 -0
- package/dist/server/shared/api-types.js +5 -0
- package/dist/server/shared/default-workflow.json +616 -0
- package/dist/server/shared/workflow-config.js +44 -0
- package/dist/server/shared/workflow-engine.js +353 -0
- package/index.html +15 -0
- package/package.json +110 -0
- package/postcss.config.js +6 -0
- package/schemas/orbital.config.schema.json +83 -0
- package/scripts/postinstall.js +24 -0
- package/scripts/start.sh +20 -0
- package/server/adapters/index.ts +41 -0
- package/server/adapters/iterm2-adapter.ts +37 -0
- package/server/adapters/subprocess-adapter.ts +25 -0
- package/server/adapters/terminal-adapter.ts +24 -0
- package/server/config.ts +234 -0
- package/server/database.ts +107 -0
- package/server/index.ts +452 -0
- package/server/init.ts +891 -0
- package/server/parsers/event-parser.ts +74 -0
- package/server/parsers/scope-parser.ts +240 -0
- package/server/routes/config-routes.ts +182 -0
- package/server/routes/data-routes.ts +548 -0
- package/server/routes/dispatch-routes.ts +275 -0
- package/server/routes/git-routes.ts +112 -0
- package/server/routes/scope-routes.ts +262 -0
- package/server/routes/sprint-routes.ts +142 -0
- package/server/routes/version-routes.ts +156 -0
- package/server/routes/workflow-routes.ts +198 -0
- package/server/schema.ts +90 -0
- package/server/services/batch-orchestrator.ts +286 -0
- package/server/services/claude-session-service.ts +441 -0
- package/server/services/config-service.ts +151 -0
- package/server/services/deploy-service.ts +98 -0
- package/server/services/event-service.ts +98 -0
- package/server/services/gate-service.ts +126 -0
- package/server/services/git-service.ts +391 -0
- package/server/services/github-service.ts +183 -0
- package/server/services/readiness-service.ts +250 -0
- package/server/services/scope-cache.ts +81 -0
- package/server/services/scope-service.ts +476 -0
- package/server/services/sprint-orchestrator.ts +361 -0
- package/server/services/sprint-service.ts +415 -0
- package/server/services/workflow-service.ts +461 -0
- package/server/utils/cc-hooks-parser.ts +70 -0
- package/server/utils/dispatch-utils.ts +395 -0
- package/server/utils/logger.ts +109 -0
- package/server/utils/terminal-launcher.ts +462 -0
- package/server/utils/worktree-manager.ts +104 -0
- package/server/watchers/event-watcher.ts +100 -0
- package/server/watchers/scope-watcher.ts +38 -0
- package/shared/api-types.ts +20 -0
- package/shared/default-workflow.json +616 -0
- package/shared/workflow-config.ts +170 -0
- package/shared/workflow-engine.ts +427 -0
- package/src/App.tsx +33 -0
- package/src/components/AgentBadge.tsx +40 -0
- package/src/components/BatchPreflightModal.tsx +115 -0
- package/src/components/CardDisplayToggle.tsx +74 -0
- package/src/components/ColumnHeaderActions.tsx +55 -0
- package/src/components/ColumnMenu.tsx +99 -0
- package/src/components/DeployHistory.tsx +141 -0
- package/src/components/DispatchModal.tsx +164 -0
- package/src/components/DispatchPopover.tsx +139 -0
- package/src/components/DragOverlay.tsx +25 -0
- package/src/components/DriftSidebar.tsx +140 -0
- package/src/components/EnvironmentStrip.tsx +88 -0
- package/src/components/ErrorBoundary.tsx +62 -0
- package/src/components/FilterChip.tsx +105 -0
- package/src/components/GateIndicator.tsx +33 -0
- package/src/components/IdeaDetailModal.tsx +190 -0
- package/src/components/IdeaFormDialog.tsx +113 -0
- package/src/components/KanbanColumn.tsx +201 -0
- package/src/components/MarkdownRenderer.tsx +114 -0
- package/src/components/NeonGrid.tsx +128 -0
- package/src/components/PromotionQueue.tsx +89 -0
- package/src/components/ScopeCard.tsx +234 -0
- package/src/components/ScopeDetailModal.tsx +255 -0
- package/src/components/ScopeFilterBar.tsx +152 -0
- package/src/components/SearchInput.tsx +102 -0
- package/src/components/SessionPanel.tsx +335 -0
- package/src/components/SprintContainer.tsx +303 -0
- package/src/components/SprintDependencyDialog.tsx +78 -0
- package/src/components/SprintPreflightModal.tsx +138 -0
- package/src/components/StatusBar.tsx +168 -0
- package/src/components/SwimCell.tsx +67 -0
- package/src/components/SwimLaneRow.tsx +94 -0
- package/src/components/SwimlaneBoardView.tsx +108 -0
- package/src/components/VersionBadge.tsx +139 -0
- package/src/components/ViewModeSelector.tsx +114 -0
- package/src/components/config/AgentChip.tsx +53 -0
- package/src/components/config/AgentCreateDialog.tsx +321 -0
- package/src/components/config/AgentEditor.tsx +175 -0
- package/src/components/config/DirectoryTree.tsx +582 -0
- package/src/components/config/FileEditor.tsx +550 -0
- package/src/components/config/HookChip.tsx +50 -0
- package/src/components/config/StageCard.tsx +198 -0
- package/src/components/config/TransitionZone.tsx +173 -0
- package/src/components/config/UnifiedWorkflowPipeline.tsx +216 -0
- package/src/components/config/WorkflowPipeline.tsx +161 -0
- package/src/components/source-control/BranchList.tsx +93 -0
- package/src/components/source-control/BranchPanel.tsx +105 -0
- package/src/components/source-control/CommitLog.tsx +100 -0
- package/src/components/source-control/CommitRow.tsx +47 -0
- package/src/components/source-control/GitHubPanel.tsx +110 -0
- package/src/components/source-control/GitHubSetupGuide.tsx +52 -0
- package/src/components/source-control/GitOverviewBar.tsx +101 -0
- package/src/components/source-control/PullRequestList.tsx +69 -0
- package/src/components/source-control/WorktreeList.tsx +80 -0
- package/src/components/ui/badge.tsx +41 -0
- package/src/components/ui/button.tsx +55 -0
- package/src/components/ui/card.tsx +78 -0
- package/src/components/ui/dialog.tsx +94 -0
- package/src/components/ui/popover.tsx +33 -0
- package/src/components/ui/scroll-area.tsx +54 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/toggle-switch.tsx +35 -0
- package/src/components/ui/tooltip.tsx +27 -0
- package/src/components/workflow/AddEdgeDialog.tsx +217 -0
- package/src/components/workflow/AddListDialog.tsx +201 -0
- package/src/components/workflow/ChecklistEditor.tsx +239 -0
- package/src/components/workflow/CommandPrefixManager.tsx +118 -0
- package/src/components/workflow/ConfigSettingsPanel.tsx +189 -0
- package/src/components/workflow/DirectionSelector.tsx +133 -0
- package/src/components/workflow/DispatchConfigPanel.tsx +180 -0
- package/src/components/workflow/EdgeDetailPanel.tsx +236 -0
- package/src/components/workflow/EdgePropertyEditor.tsx +251 -0
- package/src/components/workflow/EditToolbar.tsx +138 -0
- package/src/components/workflow/HookDetailPanel.tsx +250 -0
- package/src/components/workflow/HookExecutionLog.tsx +24 -0
- package/src/components/workflow/HookSourceModal.tsx +129 -0
- package/src/components/workflow/HooksDashboard.tsx +363 -0
- package/src/components/workflow/ListPropertyEditor.tsx +251 -0
- package/src/components/workflow/MigrationPreviewDialog.tsx +237 -0
- package/src/components/workflow/MovementRulesPanel.tsx +188 -0
- package/src/components/workflow/NodeDetailPanel.tsx +245 -0
- package/src/components/workflow/PresetSelector.tsx +414 -0
- package/src/components/workflow/SkillCommandBuilder.tsx +174 -0
- package/src/components/workflow/WorkflowEdgeComponent.tsx +145 -0
- package/src/components/workflow/WorkflowNode.tsx +147 -0
- package/src/components/workflow/graphLayout.ts +186 -0
- package/src/components/workflow/mergeHooks.ts +85 -0
- package/src/components/workflow/useEditHistory.ts +88 -0
- package/src/components/workflow/useWorkflowEditor.ts +262 -0
- package/src/components/workflow/validateConfig.ts +70 -0
- package/src/hooks/useActiveDispatches.ts +198 -0
- package/src/hooks/useBoardSettings.ts +170 -0
- package/src/hooks/useCardDisplay.ts +57 -0
- package/src/hooks/useCcHooks.ts +24 -0
- package/src/hooks/useConfigTree.ts +51 -0
- package/src/hooks/useEnforcementRules.ts +46 -0
- package/src/hooks/useEvents.ts +59 -0
- package/src/hooks/useFileEditor.ts +165 -0
- package/src/hooks/useGates.ts +57 -0
- package/src/hooks/useIdeaActions.ts +53 -0
- package/src/hooks/useKanbanDnd.ts +410 -0
- package/src/hooks/useOrbitalConfig.ts +54 -0
- package/src/hooks/usePipeline.ts +47 -0
- package/src/hooks/usePipelineData.ts +338 -0
- package/src/hooks/useReconnect.ts +25 -0
- package/src/hooks/useScopeFilters.ts +125 -0
- package/src/hooks/useScopeSessions.ts +44 -0
- package/src/hooks/useScopes.ts +67 -0
- package/src/hooks/useSearch.ts +67 -0
- package/src/hooks/useSettings.tsx +187 -0
- package/src/hooks/useSocket.ts +25 -0
- package/src/hooks/useSourceControl.ts +105 -0
- package/src/hooks/useSprintPreflight.ts +55 -0
- package/src/hooks/useSprints.ts +154 -0
- package/src/hooks/useStatusBarHighlight.ts +18 -0
- package/src/hooks/useSwimlaneBoardSettings.ts +104 -0
- package/src/hooks/useTheme.ts +9 -0
- package/src/hooks/useTransitionReadiness.ts +53 -0
- package/src/hooks/useVersion.ts +155 -0
- package/src/hooks/useViolations.ts +65 -0
- package/src/hooks/useWorkflow.tsx +125 -0
- package/src/hooks/useZoomModifier.ts +19 -0
- package/src/index.css +797 -0
- package/src/layouts/DashboardLayout.tsx +113 -0
- package/src/lib/collisionDetection.ts +20 -0
- package/src/lib/scope-fields.ts +61 -0
- package/src/lib/swimlane.ts +146 -0
- package/src/lib/utils.ts +15 -0
- package/src/main.tsx +19 -0
- package/src/socket.ts +11 -0
- package/src/types/index.ts +497 -0
- package/src/views/AgentFeed.tsx +339 -0
- package/src/views/DeployPipeline.tsx +59 -0
- package/src/views/EnforcementView.tsx +378 -0
- package/src/views/PrimitivesConfig.tsx +500 -0
- package/src/views/QualityGates.tsx +1012 -0
- package/src/views/ScopeBoard.tsx +454 -0
- package/src/views/SessionTimeline.tsx +516 -0
- package/src/views/Settings.tsx +183 -0
- package/src/views/SourceControl.tsx +95 -0
- package/src/views/WorkflowVisualizer.tsx +382 -0
- package/tailwind.config.js +161 -0
- package/templates/agents/AUTO-INVOKE.md +180 -0
- package/templates/agents/CONFLICT-RESOLUTION.md +128 -0
- package/templates/agents/QUICK-REFERENCE.md +122 -0
- package/templates/agents/README.md +188 -0
- package/templates/agents/SKILL-TRIGGERS.md +100 -0
- package/templates/agents/blue-team/frontend-designer.md +424 -0
- package/templates/agents/green-team/architect.md +526 -0
- package/templates/agents/green-team/rules-enforcer.md +131 -0
- package/templates/agents/red-team/attacker-learned.md +24 -0
- package/templates/agents/red-team/attacker.md +486 -0
- package/templates/agents/red-team/chaos.md +548 -0
- package/templates/agents/reference/component-registry.md +82 -0
- package/templates/agents/workflows/full-mode.md +218 -0
- package/templates/agents/workflows/quick-mode.md +118 -0
- package/templates/agents/workflows/security-mode.md +283 -0
- package/templates/anti-patterns/dangerous-shortcuts.md +427 -0
- package/templates/config/agent-triggers.json +92 -0
- package/templates/hooks/agent-team-gate.sh +31 -0
- package/templates/hooks/agent-trigger.sh +97 -0
- package/templates/hooks/block-push.sh +66 -0
- package/templates/hooks/block-workarounds.sh +61 -0
- package/templates/hooks/blocker-check.sh +28 -0
- package/templates/hooks/completion-checklist.sh +28 -0
- package/templates/hooks/decision-capture.sh +15 -0
- package/templates/hooks/dependency-check.sh +27 -0
- package/templates/hooks/end-session.sh +31 -0
- package/templates/hooks/exploration-logger.sh +37 -0
- package/templates/hooks/files-changed-summary.sh +37 -0
- package/templates/hooks/get-session-id.sh +49 -0
- package/templates/hooks/git-commit-guard.sh +34 -0
- package/templates/hooks/init-session.sh +93 -0
- package/templates/hooks/orbital-emit.sh +79 -0
- package/templates/hooks/orbital-report-deploy.sh +78 -0
- package/templates/hooks/orbital-report-gates.sh +40 -0
- package/templates/hooks/orbital-report-violation.sh +36 -0
- package/templates/hooks/orbital-scope-update.sh +53 -0
- package/templates/hooks/phase-verify-reminder.sh +26 -0
- package/templates/hooks/review-gate-check.sh +82 -0
- package/templates/hooks/scope-commit-logger.sh +37 -0
- package/templates/hooks/scope-create-cleanup.sh +36 -0
- package/templates/hooks/scope-create-gate.sh +80 -0
- package/templates/hooks/scope-create-tracker.sh +17 -0
- package/templates/hooks/scope-file-sync.sh +53 -0
- package/templates/hooks/scope-gate.sh +35 -0
- package/templates/hooks/scope-helpers.sh +188 -0
- package/templates/hooks/scope-lifecycle-gate.sh +139 -0
- package/templates/hooks/scope-prepare.sh +244 -0
- package/templates/hooks/scope-transition.sh +172 -0
- package/templates/hooks/session-enforcer.sh +143 -0
- package/templates/hooks/time-tracker.sh +33 -0
- package/templates/lessons-learned.md +15 -0
- package/templates/orbital.config.json +35 -0
- package/templates/presets/development.json +42 -0
- package/templates/presets/gitflow.json +712 -0
- package/templates/presets/minimal.json +23 -0
- package/templates/quick/rules.md +218 -0
- package/templates/scopes/_template.md +255 -0
- package/templates/settings-hooks.json +98 -0
- package/templates/skills/git-commit/SKILL.md +85 -0
- package/templates/skills/git-dev/SKILL.md +99 -0
- package/templates/skills/git-hotfix/SKILL.md +223 -0
- package/templates/skills/git-main/SKILL.md +84 -0
- package/templates/skills/git-production/SKILL.md +165 -0
- package/templates/skills/git-staging/SKILL.md +112 -0
- package/templates/skills/scope-create/SKILL.md +81 -0
- package/templates/skills/scope-fix-review/SKILL.md +168 -0
- package/templates/skills/scope-implement/SKILL.md +110 -0
- package/templates/skills/scope-post-review/SKILL.md +144 -0
- package/templates/skills/scope-pre-review/SKILL.md +211 -0
- package/templates/skills/scope-verify/SKILL.md +201 -0
- package/templates/skills/session-init/SKILL.md +62 -0
- package/templates/skills/session-resume/SKILL.md +201 -0
- package/templates/skills/test-checks/SKILL.md +171 -0
- package/templates/skills/test-code-review/SKILL.md +252 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +38 -0
package/server/init.ts
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared init logic — used by both the CLI (`orbital init`) and
|
|
3
|
+
* programmatic callers (e.g. tests).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
// Walk up from __dirname until we find the package root (identified by templates/).
|
|
14
|
+
// Handles both dev (server/ → 1 hop) and compiled (dist/server/server/ → 3 hops).
|
|
15
|
+
function resolvePackageRoot(startDir: string): string {
|
|
16
|
+
let dir = startDir;
|
|
17
|
+
for (let i = 0; i < 5; i++) {
|
|
18
|
+
if (fs.existsSync(path.join(dir, 'templates'))) return dir;
|
|
19
|
+
dir = path.resolve(dir, '..');
|
|
20
|
+
}
|
|
21
|
+
return path.resolve(startDir, '..');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PACKAGE_ROOT = resolvePackageRoot(__dirname);
|
|
25
|
+
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
|
|
26
|
+
|
|
27
|
+
// ─── Helpers ─────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function ensureDir(dirPath: string): boolean {
|
|
30
|
+
if (!fs.existsSync(dirPath)) {
|
|
31
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function copyDirSync(src: string, dest: string, opts: { overwrite?: boolean } = {}): { created: string[]; skipped: string[] } {
|
|
38
|
+
const created: string[] = [];
|
|
39
|
+
const skipped: string[] = [];
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(src)) return { created, skipped };
|
|
42
|
+
|
|
43
|
+
ensureDir(dest);
|
|
44
|
+
|
|
45
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
46
|
+
const srcPath = path.join(src, entry.name);
|
|
47
|
+
const destPath = path.join(dest, entry.name);
|
|
48
|
+
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
const sub = copyDirSync(srcPath, destPath, opts);
|
|
51
|
+
created.push(...sub.created);
|
|
52
|
+
skipped.push(...sub.skipped);
|
|
53
|
+
} else {
|
|
54
|
+
if (!opts.overwrite && fs.existsSync(destPath)) {
|
|
55
|
+
skipped.push(destPath);
|
|
56
|
+
} else {
|
|
57
|
+
ensureDir(path.dirname(destPath));
|
|
58
|
+
fs.copyFileSync(srcPath, destPath);
|
|
59
|
+
created.push(destPath);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { created, skipped };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function chmodScripts(dir: string): void {
|
|
67
|
+
if (!fs.existsSync(dir)) return;
|
|
68
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
69
|
+
const fullPath = path.join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
chmodScripts(fullPath);
|
|
72
|
+
} else if (entry.name.endsWith('.sh')) {
|
|
73
|
+
fs.chmodSync(fullPath, 0o755);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pruneStaleEntries(sourceDir: string, targetDir: string): number {
|
|
79
|
+
if (!fs.existsSync(targetDir) || !fs.existsSync(sourceDir)) return 0;
|
|
80
|
+
|
|
81
|
+
const sourceEntries = new Set(fs.readdirSync(sourceDir));
|
|
82
|
+
let removed = 0;
|
|
83
|
+
|
|
84
|
+
for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
85
|
+
if (!sourceEntries.has(entry.name)) {
|
|
86
|
+
const fullPath = path.join(targetDir, entry.name);
|
|
87
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
88
|
+
removed++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return removed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mergeSettingsHooks(targetPath: string, sourcePath: string): void {
|
|
95
|
+
let target: Record<string, unknown> = {};
|
|
96
|
+
if (fs.existsSync(targetPath)) {
|
|
97
|
+
try {
|
|
98
|
+
target = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
99
|
+
} catch {
|
|
100
|
+
console.warn(' Warning: existing settings.local.json is malformed — creating new one');
|
|
101
|
+
target = {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!fs.existsSync(sourcePath)) {
|
|
106
|
+
console.warn(' Warning: settings-hooks template not found, skipping hook registration');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
|
|
111
|
+
const sourceHooks = source.hooks || {};
|
|
112
|
+
|
|
113
|
+
if (!(target as Record<string, Record<string, unknown[]>>).hooks) {
|
|
114
|
+
(target as Record<string, unknown>).hooks = {};
|
|
115
|
+
}
|
|
116
|
+
const targetHooks = (target as Record<string, Record<string, unknown[]>>).hooks;
|
|
117
|
+
|
|
118
|
+
for (const [event, sourceGroups] of Object.entries(sourceHooks)) {
|
|
119
|
+
if (!targetHooks[event]) {
|
|
120
|
+
targetHooks[event] = tagOrbitalGroups(sourceGroups as HookGroup[]);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const sourceGroup of sourceGroups as HookGroup[]) {
|
|
125
|
+
const sourceMatcher = sourceGroup.matcher || '__none__';
|
|
126
|
+
const targetGroup = (targetHooks[event] as HookGroup[]).find(
|
|
127
|
+
(g) => (g.matcher || '__none__') === sourceMatcher
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (!targetGroup) {
|
|
131
|
+
(targetHooks[event] as HookGroup[]).push(tagOrbitalGroup(sourceGroup));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const hook of sourceGroup.hooks || []) {
|
|
136
|
+
const taggedHook = { ...hook, _orbital: true };
|
|
137
|
+
const alreadyPresent = (targetGroup.hooks || []).some(
|
|
138
|
+
(h) => h.command === hook.command
|
|
139
|
+
);
|
|
140
|
+
if (!alreadyPresent) {
|
|
141
|
+
if (!targetGroup.hooks) targetGroup.hooks = [];
|
|
142
|
+
targetGroup.hooks.push(taggedHook);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(targetPath, JSON.stringify(target, null, 2) + '\n', 'utf8');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface HookEntry {
|
|
152
|
+
command: string;
|
|
153
|
+
_orbital?: boolean;
|
|
154
|
+
[key: string]: unknown;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface HookGroup {
|
|
158
|
+
matcher?: string;
|
|
159
|
+
hooks?: HookEntry[];
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function tagOrbitalGroups(groups: HookGroup[]): HookGroup[] {
|
|
164
|
+
return groups.map(tagOrbitalGroup);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function tagOrbitalGroup(group: HookGroup): HookGroup {
|
|
168
|
+
return {
|
|
169
|
+
...group,
|
|
170
|
+
hooks: (group.hooks || []).map((h) => ({ ...h, _orbital: true })),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function updateGitignore(projectRoot: string): boolean {
|
|
175
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
176
|
+
const marker = '# Orbital Command';
|
|
177
|
+
const lines = [
|
|
178
|
+
'',
|
|
179
|
+
marker,
|
|
180
|
+
'scopes/',
|
|
181
|
+
'.claude/orbital/',
|
|
182
|
+
'.claude/orbital-events/',
|
|
183
|
+
'.claude/config/workflow-manifest.sh',
|
|
184
|
+
'',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
let existing = '';
|
|
188
|
+
if (fs.existsSync(gitignorePath)) {
|
|
189
|
+
existing = fs.readFileSync(gitignorePath, 'utf8');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (existing.includes(marker)) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.appendFileSync(gitignorePath, lines.join('\n'), 'utf8');
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function generateManifest(config: Record<string, unknown>): string {
|
|
201
|
+
const lines: string[] = [];
|
|
202
|
+
const lists = ((config.lists as Array<Record<string, unknown>>) || []).sort(
|
|
203
|
+
(a, b) => ((a.order as number) ?? 0) - ((b.order as number) ?? 0)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
lines.push('#!/bin/bash');
|
|
207
|
+
lines.push('# Auto-generated by WorkflowEngine — DO NOT EDIT');
|
|
208
|
+
lines.push(`# Generated: ${new Date().toISOString()}`);
|
|
209
|
+
lines.push(`# Workflow: "${config.name}" (version ${config.version})`);
|
|
210
|
+
lines.push('');
|
|
211
|
+
|
|
212
|
+
lines.push('# ─── Branching mode (trunk or worktree) ───');
|
|
213
|
+
lines.push(`WORKFLOW_BRANCHING_MODE="${(config.branchingMode as string) ?? 'trunk'}"`);
|
|
214
|
+
lines.push('');
|
|
215
|
+
|
|
216
|
+
lines.push('# ─── Valid statuses (space-separated) ───');
|
|
217
|
+
lines.push(`WORKFLOW_STATUSES="${lists.map((l) => l.id).join(' ')}"`);
|
|
218
|
+
lines.push('');
|
|
219
|
+
|
|
220
|
+
lines.push('# ─── Statuses that have a scopes/ subdirectory ───');
|
|
221
|
+
const dirStatuses = lists.filter((l) => l.hasDirectory).map((l) => l.id);
|
|
222
|
+
lines.push(`WORKFLOW_DIR_STATUSES="${dirStatuses.join(' ')}"`);
|
|
223
|
+
lines.push('');
|
|
224
|
+
|
|
225
|
+
lines.push('# ─── Terminal statuses ───');
|
|
226
|
+
const terminalStatuses = (config.terminalStatuses as string[]) || [];
|
|
227
|
+
lines.push(`WORKFLOW_TERMINAL_STATUSES="${terminalStatuses.join(' ')}"`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
|
|
230
|
+
lines.push('# ─── Entry point status ───');
|
|
231
|
+
lines.push(`WORKFLOW_ENTRY_STATUS="${(config.entryPoint as string) || (lists[0]?.id as string) || 'todo'}"`);
|
|
232
|
+
lines.push('');
|
|
233
|
+
|
|
234
|
+
const listMap = new Map(lists.map((l) => [l.id, l]));
|
|
235
|
+
|
|
236
|
+
lines.push('# ─── Transition edges (from:to:sessionKey) ───');
|
|
237
|
+
lines.push('WORKFLOW_EDGES=(');
|
|
238
|
+
for (const edge of (config.edges as Array<Record<string, unknown>>) || []) {
|
|
239
|
+
const targetList = listMap.get(edge.to as string);
|
|
240
|
+
const sessionKey = (targetList?.sessionKey as string) ?? '';
|
|
241
|
+
lines.push(` "${edge.from}:${edge.to}:${sessionKey}"`);
|
|
242
|
+
}
|
|
243
|
+
lines.push(')');
|
|
244
|
+
lines.push('');
|
|
245
|
+
|
|
246
|
+
lines.push('# ─── Branch-to-transition mapping (gitBranch:from:to:sessionKey) ───');
|
|
247
|
+
lines.push('WORKFLOW_BRANCH_MAP=(');
|
|
248
|
+
for (const edge of (config.edges as Array<Record<string, unknown>>) || []) {
|
|
249
|
+
const targetList = listMap.get(edge.to as string);
|
|
250
|
+
if (targetList?.gitBranch) {
|
|
251
|
+
const sessionKey = (targetList.sessionKey as string) ?? '';
|
|
252
|
+
lines.push(` "${targetList.gitBranch}:${edge.from}:${edge.to}:${sessionKey}"`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
lines.push(')');
|
|
256
|
+
lines.push('');
|
|
257
|
+
|
|
258
|
+
lines.push('# ─── Helper functions ──────────────────────────────');
|
|
259
|
+
lines.push('');
|
|
260
|
+
lines.push('status_to_dir() {');
|
|
261
|
+
lines.push(' local scope_status="$1"');
|
|
262
|
+
lines.push(' for s in $WORKFLOW_DIR_STATUSES; do');
|
|
263
|
+
lines.push(' [ "$s" = "$scope_status" ] && echo "$scope_status" && return 0');
|
|
264
|
+
lines.push(' done');
|
|
265
|
+
lines.push(' echo "$WORKFLOW_ENTRY_STATUS"');
|
|
266
|
+
lines.push('}');
|
|
267
|
+
lines.push('');
|
|
268
|
+
lines.push('status_to_branch() {');
|
|
269
|
+
lines.push(' local status="$1"');
|
|
270
|
+
lines.push(' for entry in "${WORKFLOW_BRANCH_MAP[@]}"; do');
|
|
271
|
+
lines.push(" IFS=':' read -r branch from to skey <<< \"$entry\"");
|
|
272
|
+
lines.push(' [ "$to" = "$status" ] && echo "$branch" && return 0');
|
|
273
|
+
lines.push(' done');
|
|
274
|
+
lines.push(' echo ""');
|
|
275
|
+
lines.push('}');
|
|
276
|
+
lines.push('');
|
|
277
|
+
lines.push('is_valid_status() {');
|
|
278
|
+
lines.push(' local status="$1"');
|
|
279
|
+
lines.push(' for s in $WORKFLOW_STATUSES; do');
|
|
280
|
+
lines.push(' [ "$s" = "$status" ] && return 0');
|
|
281
|
+
lines.push(' done');
|
|
282
|
+
lines.push(' return 1');
|
|
283
|
+
lines.push('}');
|
|
284
|
+
|
|
285
|
+
return lines.join('\n') + '\n';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function writeManifest(claudeDir: string): boolean {
|
|
289
|
+
const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
|
|
290
|
+
if (!fs.existsSync(workflowPath)) return false;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const config = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
|
|
294
|
+
const manifest = generateManifest(config);
|
|
295
|
+
const manifestPath = path.join(claudeDir, 'config', 'workflow-manifest.sh');
|
|
296
|
+
fs.writeFileSync(manifestPath, manifest, 'utf8');
|
|
297
|
+
fs.chmodSync(manifestPath, 0o755);
|
|
298
|
+
return true;
|
|
299
|
+
} catch {
|
|
300
|
+
console.warn(' Warning: could not generate workflow manifest');
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function generateIndexMd(projectRoot: string, claudeDir: string): string {
|
|
306
|
+
let projectName = path.basename(projectRoot);
|
|
307
|
+
const configPath = path.join(claudeDir, 'orbital.config.json');
|
|
308
|
+
if (fs.existsSync(configPath)) {
|
|
309
|
+
try {
|
|
310
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
311
|
+
if (cfg.projectName) projectName = cfg.projectName;
|
|
312
|
+
} catch { /* use fallback */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const skillsDir = path.join(claudeDir, 'skills');
|
|
316
|
+
const skills: string[] = [];
|
|
317
|
+
if (fs.existsSync(skillsDir)) {
|
|
318
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
319
|
+
if (entry.isDirectory()) skills.push(entry.name);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const workflowPath = path.join(claudeDir, 'config', 'workflow.json');
|
|
324
|
+
let stages: string[] = [];
|
|
325
|
+
if (fs.existsSync(workflowPath)) {
|
|
326
|
+
try {
|
|
327
|
+
const wf = JSON.parse(fs.readFileSync(workflowPath, 'utf8'));
|
|
328
|
+
stages = (wf.lists || [])
|
|
329
|
+
.sort((a: Record<string, unknown>, b: Record<string, unknown>) =>
|
|
330
|
+
((a.order as number) ?? 0) - ((b.order as number) ?? 0))
|
|
331
|
+
.map((l: Record<string, unknown>) => l.id as string);
|
|
332
|
+
} catch { /* skip */ }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const skillCategories: Record<string, string[]> = {
|
|
336
|
+
'Git': skills.filter((s) => s.startsWith('git-')),
|
|
337
|
+
'Scope': skills.filter((s) => s.startsWith('scope-')),
|
|
338
|
+
'Session': skills.filter((s) => s.startsWith('session-')),
|
|
339
|
+
'Quality': skills.filter((s) => s.startsWith('test-')),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
let skillTable = '';
|
|
343
|
+
for (const [cat, list] of Object.entries(skillCategories)) {
|
|
344
|
+
if (list.length > 0) {
|
|
345
|
+
skillTable += `| ${cat} | ${list.map((s) => '`/' + s + '`').join(', ')} |\n`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return `# ${projectName} — AI Agent Index
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
tokens: ~1K
|
|
353
|
+
load-when: Always load first
|
|
354
|
+
last-verified: ${new Date().toISOString().split('T')[0]}
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## 30-Second Orientation
|
|
358
|
+
|
|
359
|
+
**Project**: ${projectName}
|
|
360
|
+
**Managed by**: Orbital Command
|
|
361
|
+
|
|
362
|
+
### Critical Commands
|
|
363
|
+
|
|
364
|
+
\`\`\`bash
|
|
365
|
+
# Run configured quality gates (from orbital.config.json)
|
|
366
|
+
# Typical: type-check, lint, build, test
|
|
367
|
+
\`\`\`
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Decision Tree: Where Should I Look?
|
|
372
|
+
|
|
373
|
+
\`\`\`
|
|
374
|
+
What are you trying to do?
|
|
375
|
+
|
|
|
376
|
+
+-- "I want to IMPLEMENT a scope"
|
|
377
|
+
| +-- Create new scope -> /scope-create
|
|
378
|
+
| +-- Implement scope -> /scope-implement
|
|
379
|
+
| +-- Review scope -> /scope-pre-review
|
|
380
|
+
|
|
|
381
|
+
+-- "I want to COMMIT/DEPLOY"
|
|
382
|
+
| +-- Commit work -> /git-commit
|
|
383
|
+
| +-- Push to main -> /git-main
|
|
384
|
+
${stages.includes('dev') ? '| +-- Merge to dev -> /git-dev\n' : ''}${stages.includes('staging') ? '| +-- PR to staging -> /git-staging\n' : ''}${stages.includes('production') ? '| +-- PR to production -> /git-production\n' : ''}| +-- Emergency fix -> /git-hotfix
|
|
385
|
+
|
|
|
386
|
+
+-- "I want to RUN CHECKS"
|
|
387
|
+
| +-- Quality gates -> /test-checks
|
|
388
|
+
| +-- Code review -> /test-code-review
|
|
389
|
+
| +-- Post-impl review -> /scope-post-review
|
|
390
|
+
|
|
|
391
|
+
+-- "I need SESSION help"
|
|
392
|
+
| +-- Continue work -> /session-resume
|
|
393
|
+
|
|
|
394
|
+
+-- "What should I AVOID?"
|
|
395
|
+
| +-- anti-patterns/dangerous-shortcuts.md
|
|
396
|
+
|
|
|
397
|
+
+-- "QUICK REFERENCES"
|
|
398
|
+
+-- Rules -> quick/rules.md
|
|
399
|
+
+-- Lessons learned -> lessons-learned.md
|
|
400
|
+
\`\`\`
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Skills
|
|
405
|
+
|
|
406
|
+
| Category | Skills |
|
|
407
|
+
|----------|--------|
|
|
408
|
+
${skillTable || '| (none installed) | |\n'}
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Scope System (Three-Part Documents)
|
|
412
|
+
|
|
413
|
+
Scopes live in directories matching their pipeline stage.
|
|
414
|
+
|
|
415
|
+
\`\`\`
|
|
416
|
+
scopes/
|
|
417
|
+
+-- _template.md # Copy for new scopes
|
|
418
|
+
${stages.map((s) => `+-- ${s}/`).join('\n')}
|
|
419
|
+
\`\`\`
|
|
420
|
+
|
|
421
|
+
**Three-Part Structure**:
|
|
422
|
+
- **PART 1: DASHBOARD** — Quick status, progress table, recent activity
|
|
423
|
+
- **PART 2: SPECIFICATION** — Feature lock (locked after review, any agent can implement)
|
|
424
|
+
- **PART 3: PROCESS** — Working memory (exploration, decisions, uncertainties, impl log)
|
|
425
|
+
- **AGENT REVIEW** — Synthesized findings from agent team review
|
|
426
|
+
|
|
427
|
+
**Lifecycle**: ${stages.join(' → ')}
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## File Organization
|
|
432
|
+
|
|
433
|
+
\`\`\`
|
|
434
|
+
.claude/
|
|
435
|
+
+-- INDEX.md <- You are here
|
|
436
|
+
+-- lessons-learned.md # Institutional memory
|
|
437
|
+
+-- skills/ # Invokable skills
|
|
438
|
+
+-- quick/ # Quick reference docs
|
|
439
|
+
| +-- rules.md # Project rules with verify commands
|
|
440
|
+
+-- agents/ # Agent specifications
|
|
441
|
+
+-- anti-patterns/ # What NOT to do
|
|
442
|
+
+-- hooks/ # Claude Code lifecycle hooks
|
|
443
|
+
+-- config/ # Workflow config and presets
|
|
444
|
+
| +-- workflow.json # Active workflow
|
|
445
|
+
| +-- workflow-manifest.sh # Shell variables (auto-generated)
|
|
446
|
+
| +-- workflows/ # Available presets
|
|
447
|
+
+-- orbital.config.json # Project configuration
|
|
448
|
+
\`\`\`
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## When In Doubt
|
|
453
|
+
|
|
454
|
+
1. **Check rules**: \`quick/rules.md\`
|
|
455
|
+
2. **Follow existing patterns**: Look at similar code in codebase
|
|
456
|
+
3. **Ask**: Use clarifying questions before making assumptions
|
|
457
|
+
4. **Verify**: Run quality gates before committing
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Helpers used by CLI commands ────────────────────────────
|
|
462
|
+
|
|
463
|
+
function listTemplateFiles(templateSubdir: string, targetDir: string): string[] {
|
|
464
|
+
const files: string[] = [];
|
|
465
|
+
if (!fs.existsSync(templateSubdir)) return files;
|
|
466
|
+
|
|
467
|
+
for (const entry of fs.readdirSync(templateSubdir, { withFileTypes: true })) {
|
|
468
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
469
|
+
if (entry.isDirectory()) {
|
|
470
|
+
files.push(...listTemplateFiles(path.join(templateSubdir, entry.name), targetPath));
|
|
471
|
+
} else {
|
|
472
|
+
files.push(targetPath);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return files;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function cleanEmptyDirs(dir: string): void {
|
|
479
|
+
if (!fs.existsSync(dir)) return;
|
|
480
|
+
|
|
481
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
482
|
+
if (entry.isDirectory()) {
|
|
483
|
+
cleanEmptyDirs(path.join(dir, entry.name));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (fs.readdirSync(dir).length === 0) {
|
|
488
|
+
fs.rmdirSync(dir);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ─── Exports ─────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
export { TEMPLATES_DIR, ensureDir };
|
|
495
|
+
|
|
496
|
+
export interface InitOptions {
|
|
497
|
+
force?: boolean;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function runInit(projectRoot: string, options: InitOptions = {}): void {
|
|
501
|
+
const force = options.force ?? false;
|
|
502
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
503
|
+
|
|
504
|
+
console.log(`\nOrbital Command — init`);
|
|
505
|
+
console.log(`Project root: ${projectRoot}\n`);
|
|
506
|
+
|
|
507
|
+
// 1. Create directories
|
|
508
|
+
const dirs = [
|
|
509
|
+
path.join(claudeDir, 'orbital-events'),
|
|
510
|
+
path.join(claudeDir, 'orbital'),
|
|
511
|
+
path.join(claudeDir, 'config'),
|
|
512
|
+
path.join(claudeDir, 'review-verdicts'),
|
|
513
|
+
];
|
|
514
|
+
for (const dir of dirs) {
|
|
515
|
+
const wasCreated = ensureDir(dir);
|
|
516
|
+
console.log(` ${wasCreated ? 'Created' : 'Exists '} ${path.relative(projectRoot, dir)}/`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 1b. Create scopes/ subdirectories from the default workflow preset
|
|
520
|
+
const defaultPresetPath = path.join(TEMPLATES_DIR, 'presets', 'default.json');
|
|
521
|
+
let scopeDirs = ['icebox'];
|
|
522
|
+
try {
|
|
523
|
+
const preset = JSON.parse(fs.readFileSync(defaultPresetPath, 'utf8'));
|
|
524
|
+
if (preset.lists && Array.isArray(preset.lists)) {
|
|
525
|
+
scopeDirs = preset.lists.filter((l: Record<string, unknown>) => l.hasDirectory).map((l: Record<string, unknown>) => l.id as string);
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
console.warn(' Warning: could not load default preset, creating scopes/icebox/ only');
|
|
529
|
+
}
|
|
530
|
+
for (const dirId of scopeDirs) {
|
|
531
|
+
const scopeDir = path.join(projectRoot, 'scopes', dirId);
|
|
532
|
+
const wasCreated = ensureDir(scopeDir);
|
|
533
|
+
console.log(` ${wasCreated ? 'Created' : 'Exists '} scopes/${dirId}/`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 1c. Copy scope template
|
|
537
|
+
const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
|
|
538
|
+
const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
|
|
539
|
+
if (fs.existsSync(scopeTemplateSrc)) {
|
|
540
|
+
if (force || !fs.existsSync(scopeTemplateDest)) {
|
|
541
|
+
fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
|
|
542
|
+
console.log(` ${force ? 'Reset ' : 'Created'} scopes/_template.md`);
|
|
543
|
+
} else {
|
|
544
|
+
console.log(` Exists scopes/_template.md`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// 2. Copy orbital.config.json template
|
|
549
|
+
const configDest = path.join(claudeDir, 'orbital.config.json');
|
|
550
|
+
const configSrc = path.join(TEMPLATES_DIR, 'orbital.config.json');
|
|
551
|
+
const configIsNew = !fs.existsSync(configDest);
|
|
552
|
+
if (configIsNew) {
|
|
553
|
+
if (fs.existsSync(configSrc)) {
|
|
554
|
+
fs.copyFileSync(configSrc, configDest);
|
|
555
|
+
console.log(` Created .claude/orbital.config.json`);
|
|
556
|
+
} else {
|
|
557
|
+
const defaultConfig = {
|
|
558
|
+
serverPort: 4444,
|
|
559
|
+
clientPort: 4445,
|
|
560
|
+
projectName: path.basename(projectRoot),
|
|
561
|
+
};
|
|
562
|
+
fs.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + '\n', 'utf8');
|
|
563
|
+
console.log(` Created .claude/orbital.config.json (default)`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Auto-detect project commands from package.json
|
|
567
|
+
const pkgJsonPath = path.join(projectRoot, 'package.json');
|
|
568
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
569
|
+
try {
|
|
570
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
571
|
+
const scripts = pkg.scripts || {};
|
|
572
|
+
const config = JSON.parse(fs.readFileSync(configDest, 'utf8'));
|
|
573
|
+
if (!config.commands) config.commands = {};
|
|
574
|
+
let detected = 0;
|
|
575
|
+
|
|
576
|
+
if (scripts.typecheck || scripts['type-check']) {
|
|
577
|
+
config.commands.typeCheck = `npm run ${scripts.typecheck ? 'typecheck' : 'type-check'}`;
|
|
578
|
+
detected++;
|
|
579
|
+
}
|
|
580
|
+
if (scripts.lint) { config.commands.lint = 'npm run lint'; detected++; }
|
|
581
|
+
if (scripts.build) { config.commands.build = 'npm run build'; detected++; }
|
|
582
|
+
if (scripts.test) { config.commands.test = 'npm run test'; detected++; }
|
|
583
|
+
|
|
584
|
+
if (detected > 0) {
|
|
585
|
+
fs.writeFileSync(configDest, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
586
|
+
console.log(` Detected ${detected} project command(s) from package.json`);
|
|
587
|
+
}
|
|
588
|
+
} catch { /* leave defaults */ }
|
|
589
|
+
}
|
|
590
|
+
} else {
|
|
591
|
+
console.log(` Exists .claude/orbital.config.json`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 3. Copy hooks
|
|
595
|
+
console.log('');
|
|
596
|
+
const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
|
|
597
|
+
const hooksDest = path.join(claudeDir, 'hooks');
|
|
598
|
+
if (force) {
|
|
599
|
+
const pruned = pruneStaleEntries(hooksSrc, hooksDest);
|
|
600
|
+
if (pruned > 0) console.log(` Pruned ${pruned} stale hook entries`);
|
|
601
|
+
}
|
|
602
|
+
const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: force });
|
|
603
|
+
console.log(` Hooks ${hooksResult.created.length} copied, ${hooksResult.skipped.length} skipped`);
|
|
604
|
+
|
|
605
|
+
// 4. Copy skills
|
|
606
|
+
const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
|
|
607
|
+
const skillsDest = path.join(claudeDir, 'skills');
|
|
608
|
+
if (force) {
|
|
609
|
+
const pruned = pruneStaleEntries(skillsSrc, skillsDest);
|
|
610
|
+
if (pruned > 0) console.log(` Pruned ${pruned} stale skill entries`);
|
|
611
|
+
}
|
|
612
|
+
const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: force });
|
|
613
|
+
console.log(` Skills ${skillsResult.created.length} copied, ${skillsResult.skipped.length} skipped`);
|
|
614
|
+
|
|
615
|
+
// 5. Copy agents
|
|
616
|
+
const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
|
|
617
|
+
const agentsDest = path.join(claudeDir, 'agents');
|
|
618
|
+
if (force) {
|
|
619
|
+
const pruned = pruneStaleEntries(agentsSrc, agentsDest);
|
|
620
|
+
if (pruned > 0) console.log(` Pruned ${pruned} stale agent entries`);
|
|
621
|
+
}
|
|
622
|
+
const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: force });
|
|
623
|
+
console.log(` Agents ${agentsResult.created.length} copied, ${agentsResult.skipped.length} skipped`);
|
|
624
|
+
|
|
625
|
+
// 6. Copy workflow presets
|
|
626
|
+
const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
|
|
627
|
+
const presetsDest = path.join(claudeDir, 'config', 'workflows');
|
|
628
|
+
if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
|
|
629
|
+
if (force) {
|
|
630
|
+
const pruned = pruneStaleEntries(presetsSrc, presetsDest);
|
|
631
|
+
if (pruned > 0) console.log(` Pruned ${pruned} stale preset entries`);
|
|
632
|
+
}
|
|
633
|
+
const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: force });
|
|
634
|
+
console.log(` Presets ${presetsResult.created.length} copied, ${presetsResult.skipped.length} skipped`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// 6b. Reset active workflow config when --force, or create if missing
|
|
638
|
+
const activeWorkflowDest = path.join(claudeDir, 'config', 'workflow.json');
|
|
639
|
+
if (force) {
|
|
640
|
+
fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
|
|
641
|
+
console.log(` Reset .claude/config/workflow.json (default workflow)`);
|
|
642
|
+
} else if (!fs.existsSync(activeWorkflowDest)) {
|
|
643
|
+
fs.copyFileSync(defaultPresetPath, activeWorkflowDest);
|
|
644
|
+
console.log(` Created .claude/config/workflow.json`);
|
|
645
|
+
} else {
|
|
646
|
+
console.log(` Exists .claude/config/workflow.json`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 7. Copy agent-triggers.json
|
|
650
|
+
const triggersSrc = path.join(TEMPLATES_DIR, 'config', 'agent-triggers.json');
|
|
651
|
+
const triggersDest = path.join(claudeDir, 'config', 'agent-triggers.json');
|
|
652
|
+
if (fs.existsSync(triggersSrc)) {
|
|
653
|
+
if (force || !fs.existsSync(triggersDest)) {
|
|
654
|
+
fs.copyFileSync(triggersSrc, triggersDest);
|
|
655
|
+
console.log(` Created .claude/config/agent-triggers.json`);
|
|
656
|
+
} else {
|
|
657
|
+
console.log(` Exists .claude/config/agent-triggers.json`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 7b. Copy quick/ templates
|
|
662
|
+
const quickSrc = path.join(TEMPLATES_DIR, 'quick');
|
|
663
|
+
const quickDest = path.join(claudeDir, 'quick');
|
|
664
|
+
if (fs.existsSync(quickSrc)) {
|
|
665
|
+
const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: force });
|
|
666
|
+
console.log(` Quick ${quickResult.created.length} copied, ${quickResult.skipped.length} skipped`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 7c. Copy anti-patterns/ templates
|
|
670
|
+
const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
|
|
671
|
+
const antiDest = path.join(claudeDir, 'anti-patterns');
|
|
672
|
+
if (fs.existsSync(antiSrc)) {
|
|
673
|
+
const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: force });
|
|
674
|
+
console.log(` Anti-pat ${antiResult.created.length} copied, ${antiResult.skipped.length} skipped`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// 7d. Copy lessons-learned.md
|
|
678
|
+
const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
|
|
679
|
+
const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
|
|
680
|
+
if (fs.existsSync(lessonsSrc)) {
|
|
681
|
+
if (force || !fs.existsSync(lessonsDest)) {
|
|
682
|
+
fs.copyFileSync(lessonsSrc, lessonsDest);
|
|
683
|
+
console.log(` Created .claude/lessons-learned.md`);
|
|
684
|
+
} else {
|
|
685
|
+
console.log(` Exists .claude/lessons-learned.md`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 7e. Generate workflow manifest
|
|
690
|
+
const manifestOk = writeManifest(claudeDir);
|
|
691
|
+
console.log(` ${manifestOk ? 'Created' : 'Skipped'} .claude/config/workflow-manifest.sh`);
|
|
692
|
+
|
|
693
|
+
// 7f. Generate INDEX.md
|
|
694
|
+
const indexDest = path.join(claudeDir, 'INDEX.md');
|
|
695
|
+
if (force || !fs.existsSync(indexDest)) {
|
|
696
|
+
const indexContent = generateIndexMd(projectRoot, claudeDir);
|
|
697
|
+
fs.writeFileSync(indexDest, indexContent, 'utf8');
|
|
698
|
+
console.log(` ${force ? 'Reset ' : 'Created'} .claude/INDEX.md`);
|
|
699
|
+
} else {
|
|
700
|
+
console.log(` Exists .claude/INDEX.md`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// 8. Merge hook registrations into settings.local.json
|
|
704
|
+
console.log('');
|
|
705
|
+
const settingsTarget = path.join(claudeDir, 'settings.local.json');
|
|
706
|
+
const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
|
|
707
|
+
mergeSettingsHooks(settingsTarget, settingsSrc);
|
|
708
|
+
console.log(` Merged hook registrations into .claude/settings.local.json`);
|
|
709
|
+
|
|
710
|
+
// 9. Update .gitignore
|
|
711
|
+
const gitignoreUpdated = updateGitignore(projectRoot);
|
|
712
|
+
console.log(` ${gitignoreUpdated ? 'Updated' : 'Exists '} .gitignore (Orbital patterns)`);
|
|
713
|
+
|
|
714
|
+
// 10. Make hook scripts executable
|
|
715
|
+
chmodScripts(hooksDest);
|
|
716
|
+
console.log(` chmod hook scripts set to executable`);
|
|
717
|
+
|
|
718
|
+
// Summary
|
|
719
|
+
const totalCreated = hooksResult.created.length + skillsResult.created.length + agentsResult.created.length;
|
|
720
|
+
const totalSkipped = hooksResult.skipped.length + skillsResult.skipped.length + agentsResult.skipped.length;
|
|
721
|
+
console.log(`\nDone. ${totalCreated} files installed, ${totalSkipped} skipped (use --force to overwrite).`);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export function runUpdate(projectRoot: string): void {
|
|
725
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
726
|
+
|
|
727
|
+
console.log(`\nOrbital Command — update`);
|
|
728
|
+
console.log(`Project root: ${projectRoot}\n`);
|
|
729
|
+
|
|
730
|
+
// 1. Copy hooks (overwrite) — prune stale entries first
|
|
731
|
+
const hooksSrc = path.join(TEMPLATES_DIR, 'hooks');
|
|
732
|
+
const hooksDest = path.join(claudeDir, 'hooks');
|
|
733
|
+
const hooksPruned = pruneStaleEntries(hooksSrc, hooksDest);
|
|
734
|
+
if (hooksPruned > 0) console.log(` Pruned ${hooksPruned} stale hook entries`);
|
|
735
|
+
const hooksResult = copyDirSync(hooksSrc, hooksDest, { overwrite: true });
|
|
736
|
+
console.log(` Hooks ${hooksResult.created.length} updated`);
|
|
737
|
+
|
|
738
|
+
// 2. Copy skills (overwrite) — prune stale entries first
|
|
739
|
+
const skillsSrc = path.join(TEMPLATES_DIR, 'skills');
|
|
740
|
+
const skillsDest = path.join(claudeDir, 'skills');
|
|
741
|
+
const skillsPruned = pruneStaleEntries(skillsSrc, skillsDest);
|
|
742
|
+
if (skillsPruned > 0) console.log(` Pruned ${skillsPruned} stale skill entries`);
|
|
743
|
+
const skillsResult = copyDirSync(skillsSrc, skillsDest, { overwrite: true });
|
|
744
|
+
console.log(` Skills ${skillsResult.created.length} updated`);
|
|
745
|
+
|
|
746
|
+
// 3. Copy agents (overwrite) — prune stale entries first
|
|
747
|
+
const agentsSrc = path.join(TEMPLATES_DIR, 'agents');
|
|
748
|
+
const agentsDest = path.join(claudeDir, 'agents');
|
|
749
|
+
const agentsPruned = pruneStaleEntries(agentsSrc, agentsDest);
|
|
750
|
+
if (agentsPruned > 0) console.log(` Pruned ${agentsPruned} stale agent entries`);
|
|
751
|
+
const agentsResult = copyDirSync(agentsSrc, agentsDest, { overwrite: true });
|
|
752
|
+
console.log(` Agents ${agentsResult.created.length} updated`);
|
|
753
|
+
|
|
754
|
+
// 4. Update workflow presets — prune stale entries first
|
|
755
|
+
const presetsSrc = path.join(TEMPLATES_DIR, 'presets');
|
|
756
|
+
const presetsDest = path.join(claudeDir, 'config', 'workflows');
|
|
757
|
+
if (fs.existsSync(presetsSrc) && fs.readdirSync(presetsSrc).length > 0) {
|
|
758
|
+
const presetsPruned = pruneStaleEntries(presetsSrc, presetsDest);
|
|
759
|
+
if (presetsPruned > 0) console.log(` Pruned ${presetsPruned} stale preset entries`);
|
|
760
|
+
const presetsResult = copyDirSync(presetsSrc, presetsDest, { overwrite: true });
|
|
761
|
+
console.log(` Presets ${presetsResult.created.length} updated`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 5. Update quick/, anti-patterns/, lessons-learned, scope template
|
|
765
|
+
const quickSrc = path.join(TEMPLATES_DIR, 'quick');
|
|
766
|
+
const quickDest = path.join(claudeDir, 'quick');
|
|
767
|
+
if (fs.existsSync(quickSrc)) {
|
|
768
|
+
const quickResult = copyDirSync(quickSrc, quickDest, { overwrite: true });
|
|
769
|
+
console.log(` Quick ${quickResult.created.length} updated`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const antiSrc = path.join(TEMPLATES_DIR, 'anti-patterns');
|
|
773
|
+
const antiDest = path.join(claudeDir, 'anti-patterns');
|
|
774
|
+
if (fs.existsSync(antiSrc)) {
|
|
775
|
+
const antiResult = copyDirSync(antiSrc, antiDest, { overwrite: true });
|
|
776
|
+
console.log(` Anti-pat ${antiResult.created.length} updated`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const lessonsSrc = path.join(TEMPLATES_DIR, 'lessons-learned.md');
|
|
780
|
+
const lessonsDest = path.join(claudeDir, 'lessons-learned.md');
|
|
781
|
+
if (fs.existsSync(lessonsSrc) && !fs.existsSync(lessonsDest)) {
|
|
782
|
+
fs.copyFileSync(lessonsSrc, lessonsDest);
|
|
783
|
+
console.log(` Created .claude/lessons-learned.md`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const scopeTemplateSrc = path.join(TEMPLATES_DIR, 'scopes', '_template.md');
|
|
787
|
+
const scopeTemplateDest = path.join(projectRoot, 'scopes', '_template.md');
|
|
788
|
+
if (fs.existsSync(scopeTemplateSrc)) {
|
|
789
|
+
ensureDir(path.join(projectRoot, 'scopes'));
|
|
790
|
+
fs.copyFileSync(scopeTemplateSrc, scopeTemplateDest);
|
|
791
|
+
console.log(` Updated scopes/_template.md`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// 5b. Regenerate workflow manifest
|
|
795
|
+
const manifestOk = writeManifest(claudeDir);
|
|
796
|
+
console.log(` ${manifestOk ? 'Updated' : 'Skipped'} .claude/config/workflow-manifest.sh`);
|
|
797
|
+
|
|
798
|
+
// 6. Re-merge settings hooks
|
|
799
|
+
const settingsTarget = path.join(claudeDir, 'settings.local.json');
|
|
800
|
+
const settingsSrc = path.join(TEMPLATES_DIR, 'settings-hooks.json');
|
|
801
|
+
mergeSettingsHooks(settingsTarget, settingsSrc);
|
|
802
|
+
console.log(` Merged hook registrations into .claude/settings.local.json`);
|
|
803
|
+
|
|
804
|
+
// 7. Make hook scripts executable
|
|
805
|
+
chmodScripts(hooksDest);
|
|
806
|
+
console.log(` chmod hook scripts set to executable`);
|
|
807
|
+
|
|
808
|
+
console.log(`\nUpdate complete.\n`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function runUninstall(projectRoot: string): void {
|
|
812
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
813
|
+
|
|
814
|
+
console.log(`\nOrbital Command — uninstall`);
|
|
815
|
+
console.log(`Project root: ${projectRoot}\n`);
|
|
816
|
+
|
|
817
|
+
let removedCount = 0;
|
|
818
|
+
|
|
819
|
+
// 1. Remove orbital hooks from settings.local.json
|
|
820
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json');
|
|
821
|
+
if (fs.existsSync(settingsPath)) {
|
|
822
|
+
try {
|
|
823
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
824
|
+
if (settings.hooks) {
|
|
825
|
+
for (const [event] of Object.entries(settings.hooks)) {
|
|
826
|
+
for (const group of settings.hooks[event]) {
|
|
827
|
+
if (group.hooks) {
|
|
828
|
+
const before = group.hooks.length;
|
|
829
|
+
group.hooks = group.hooks.filter((h: HookEntry) => !h._orbital);
|
|
830
|
+
removedCount += before - group.hooks.length;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
settings.hooks[event] = settings.hooks[event].filter(
|
|
834
|
+
(g: HookGroup) => g.hooks && g.hooks.length > 0
|
|
835
|
+
);
|
|
836
|
+
if (settings.hooks[event].length === 0) {
|
|
837
|
+
delete settings.hooks[event];
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
841
|
+
delete settings.hooks;
|
|
842
|
+
}
|
|
843
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
844
|
+
console.log(` Removed ${removedCount} orbital hook registrations from settings.local.json`);
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
console.warn(' Warning: could not parse settings.local.json');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// 2. Delete hooks that came from templates
|
|
852
|
+
const hookFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'hooks'), path.join(claudeDir, 'hooks'));
|
|
853
|
+
let hooksRemoved = 0;
|
|
854
|
+
for (const f of hookFiles) {
|
|
855
|
+
if (fs.existsSync(f)) {
|
|
856
|
+
fs.unlinkSync(f);
|
|
857
|
+
hooksRemoved++;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
console.log(` Removed ${hooksRemoved} hook scripts`);
|
|
861
|
+
|
|
862
|
+
// 3. Delete skills that came from templates
|
|
863
|
+
const skillFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'skills'), path.join(claudeDir, 'skills'));
|
|
864
|
+
let skillsRemoved = 0;
|
|
865
|
+
for (const f of skillFiles) {
|
|
866
|
+
if (fs.existsSync(f)) {
|
|
867
|
+
fs.unlinkSync(f);
|
|
868
|
+
skillsRemoved++;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const skillsDest = path.join(claudeDir, 'skills');
|
|
872
|
+
if (fs.existsSync(skillsDest)) cleanEmptyDirs(skillsDest);
|
|
873
|
+
console.log(` Removed ${skillsRemoved} skill files`);
|
|
874
|
+
|
|
875
|
+
// 4. Delete agents that came from templates
|
|
876
|
+
const agentFiles = listTemplateFiles(path.join(TEMPLATES_DIR, 'agents'), path.join(claudeDir, 'agents'));
|
|
877
|
+
let agentsRemoved = 0;
|
|
878
|
+
for (const f of agentFiles) {
|
|
879
|
+
if (fs.existsSync(f)) {
|
|
880
|
+
fs.unlinkSync(f);
|
|
881
|
+
agentsRemoved++;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const agentsDest = path.join(claudeDir, 'agents');
|
|
885
|
+
if (fs.existsSync(agentsDest)) cleanEmptyDirs(agentsDest);
|
|
886
|
+
console.log(` Removed ${agentsRemoved} agent files`);
|
|
887
|
+
|
|
888
|
+
const total = removedCount + hooksRemoved + skillsRemoved + agentsRemoved;
|
|
889
|
+
console.log(`\nUninstall complete. ${total} items removed.`);
|
|
890
|
+
console.log(`Note: scopes/ and .claude/orbital-events/ were preserved.\n`);
|
|
891
|
+
}
|