@made-by-moonlight/athene-core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +241 -0
- package/dist/activity-events.d.ts +42 -0
- package/dist/activity-events.d.ts.map +1 -0
- package/dist/activity-events.js +192 -0
- package/dist/activity-events.js.map +1 -0
- package/dist/activity-log.d.ts +71 -0
- package/dist/activity-log.d.ts.map +1 -0
- package/dist/activity-log.js +203 -0
- package/dist/activity-log.js.map +1 -0
- package/dist/activity-signal.d.ts +20 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +91 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/agent-report.d.ts +148 -0
- package/dist/agent-report.d.ts.map +1 -0
- package/dist/agent-report.js +516 -0
- package/dist/agent-report.js.map +1 -0
- package/dist/agent-selection.d.ts +31 -0
- package/dist/agent-selection.d.ts.map +1 -0
- package/dist/agent-selection.js +69 -0
- package/dist/agent-selection.js.map +1 -0
- package/dist/agent-workspace-hooks.d.ts +74 -0
- package/dist/agent-workspace-hooks.d.ts.map +1 -0
- package/dist/agent-workspace-hooks.js +988 -0
- package/dist/agent-workspace-hooks.js.map +1 -0
- package/dist/atomic-write.d.ts +6 -0
- package/dist/atomic-write.d.ts.map +1 -0
- package/dist/atomic-write.js +49 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/cleanup-stack.d.ts +37 -0
- package/dist/cleanup-stack.d.ts.map +1 -0
- package/dist/cleanup-stack.js +45 -0
- package/dist/cleanup-stack.js.map +1 -0
- package/dist/code-review-manager.d.ts +118 -0
- package/dist/code-review-manager.d.ts.map +1 -0
- package/dist/code-review-manager.js +719 -0
- package/dist/code-review-manager.js.map +1 -0
- package/dist/code-review-store.d.ts +114 -0
- package/dist/code-review-store.d.ts.map +1 -0
- package/dist/code-review-store.js +346 -0
- package/dist/code-review-store.js.map +1 -0
- package/dist/config-generator.d.ts +84 -0
- package/dist/config-generator.d.ts.map +1 -0
- package/dist/config-generator.js +295 -0
- package/dist/config-generator.js.map +1 -0
- package/dist/config.d.ts +55 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +852 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon-children.d.ts +55 -0
- package/dist/daemon-children.d.ts.map +1 -0
- package/dist/daemon-children.js +435 -0
- package/dist/daemon-children.js.map +1 -0
- package/dist/dashboard-notifications.d.ts +42 -0
- package/dist/dashboard-notifications.d.ts.map +1 -0
- package/dist/dashboard-notifications.js +123 -0
- package/dist/dashboard-notifications.js.map +1 -0
- package/dist/events-db.d.ts +39 -0
- package/dist/events-db.d.ts.map +1 -0
- package/dist/events-db.js +185 -0
- package/dist/events-db.js.map +1 -0
- package/dist/feature-flags.d.ts +2 -0
- package/dist/feature-flags.d.ts.map +1 -0
- package/dist/feature-flags.js +9 -0
- package/dist/feature-flags.js.map +1 -0
- package/dist/feedback-tools.d.ts +97 -0
- package/dist/feedback-tools.d.ts.map +1 -0
- package/dist/feedback-tools.js +161 -0
- package/dist/feedback-tools.js.map +1 -0
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.d.ts.map +1 -0
- package/dist/file-lock.js +59 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/format-automated-comments.d.ts +18 -0
- package/dist/format-automated-comments.d.ts.map +1 -0
- package/dist/gh-trace.d.ts +57 -0
- package/dist/gh-trace.d.ts.map +1 -0
- package/dist/gh-trace.js +320 -0
- package/dist/gh-trace.js.map +1 -0
- package/dist/git-activity.d.ts +10 -0
- package/dist/git-activity.d.ts.map +1 -0
- package/dist/git-activity.js +30 -0
- package/dist/git-activity.js.map +1 -0
- package/dist/global-config.d.ts +1085 -0
- package/dist/global-config.d.ts.map +1 -0
- package/dist/global-config.js +1067 -0
- package/dist/global-config.js.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +59 -0
- package/dist/index.js.map +1 -0
- package/dist/key-value.d.ts +7 -0
- package/dist/key-value.d.ts.map +1 -0
- package/dist/key-value.js +24 -0
- package/dist/key-value.js.map +1 -0
- package/dist/lifecycle-manager.d.ts +22 -0
- package/dist/lifecycle-manager.d.ts.map +1 -0
- package/dist/lifecycle-manager.js +2813 -0
- package/dist/lifecycle-manager.js.map +1 -0
- package/dist/lifecycle-state.d.ts +28 -0
- package/dist/lifecycle-state.d.ts.map +1 -0
- package/dist/lifecycle-state.js +446 -0
- package/dist/lifecycle-state.js.map +1 -0
- package/dist/lifecycle-status-decisions.d.ts +85 -0
- package/dist/lifecycle-status-decisions.d.ts.map +1 -0
- package/dist/lifecycle-status-decisions.js +262 -0
- package/dist/lifecycle-status-decisions.js.map +1 -0
- package/dist/lifecycle-transition.d.ts +81 -0
- package/dist/lifecycle-transition.d.ts.map +1 -0
- package/dist/lifecycle-transition.js +207 -0
- package/dist/lifecycle-transition.js.map +1 -0
- package/dist/metadata.d.ts +54 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +484 -0
- package/dist/metadata.js.map +1 -0
- package/dist/migration/storage-v2.d.ts +76 -0
- package/dist/migration/storage-v2.d.ts.map +1 -0
- package/dist/migration/storage-v2.js +1614 -0
- package/dist/migration/storage-v2.js.map +1 -0
- package/dist/notification-data.d.ts +135 -0
- package/dist/notification-data.d.ts.map +1 -0
- package/dist/notification-data.js +204 -0
- package/dist/notification-data.js.map +1 -0
- package/dist/notification-observability.d.ts +21 -0
- package/dist/notification-observability.d.ts.map +1 -0
- package/dist/notification-observability.js +154 -0
- package/dist/notification-observability.js.map +1 -0
- package/dist/notifier-resolution.d.ts +14 -0
- package/dist/notifier-resolution.d.ts.map +1 -0
- package/dist/notifier-resolution.js +23 -0
- package/dist/notifier-resolution.js.map +1 -0
- package/dist/observability.d.ts +100 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +535 -0
- package/dist/observability.js.map +1 -0
- package/dist/opencode-agents-md.d.ts +3 -0
- package/dist/opencode-agents-md.d.ts.map +1 -0
- package/dist/opencode-agents-md.js +40 -0
- package/dist/opencode-agents-md.js.map +1 -0
- package/dist/opencode-config.d.ts +2 -0
- package/dist/opencode-config.d.ts.map +1 -0
- package/dist/opencode-config.js +17 -0
- package/dist/opencode-config.js.map +1 -0
- package/dist/opencode-session-id.d.ts +2 -0
- package/dist/opencode-session-id.d.ts.map +1 -0
- package/dist/opencode-session-id.js +12 -0
- package/dist/opencode-session-id.js.map +1 -0
- package/dist/opencode-shared.d.ts +80 -0
- package/dist/opencode-shared.d.ts.map +1 -0
- package/dist/opencode-shared.js +202 -0
- package/dist/opencode-shared.js.map +1 -0
- package/dist/orchestrator-prompt.d.ts +19 -0
- package/dist/orchestrator-prompt.d.ts.map +1 -0
- package/dist/orchestrator-prompt.js +130 -0
- package/dist/orchestrator-prompt.js.map +1 -0
- package/dist/orchestrator-session-strategy.d.ts +5 -0
- package/dist/orchestrator-session-strategy.d.ts.map +1 -0
- package/dist/orchestrator-session-strategy.js +13 -0
- package/dist/orchestrator-session-strategy.js.map +1 -0
- package/dist/paths.d.ts +145 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +288 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform.d.ts +32 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +211 -0
- package/dist/platform.js.map +1 -0
- package/dist/plugin-registry.d.ts +15 -0
- package/dist/plugin-registry.d.ts.map +1 -0
- package/dist/plugin-registry.js +499 -0
- package/dist/plugin-registry.js.map +1 -0
- package/dist/portfolio-projects.d.ts +7 -0
- package/dist/portfolio-projects.d.ts.map +1 -0
- package/dist/portfolio-projects.js +65 -0
- package/dist/portfolio-projects.js.map +1 -0
- package/dist/portfolio-registry.d.ts +42 -0
- package/dist/portfolio-registry.d.ts.map +1 -0
- package/dist/portfolio-registry.js +311 -0
- package/dist/portfolio-registry.js.map +1 -0
- package/dist/portfolio-routing.d.ts +5 -0
- package/dist/portfolio-routing.d.ts.map +1 -0
- package/dist/portfolio-routing.js +24 -0
- package/dist/portfolio-routing.js.map +1 -0
- package/dist/portfolio-session-service.d.ts +15 -0
- package/dist/portfolio-session-service.d.ts.map +1 -0
- package/dist/portfolio-session-service.js +206 -0
- package/dist/portfolio-session-service.js.map +1 -0
- package/dist/process-cache.d.ts +32 -0
- package/dist/process-cache.d.ts.map +1 -0
- package/dist/process-cache.js +44 -0
- package/dist/process-cache.js.map +1 -0
- package/dist/project-resolver.d.ts +5 -0
- package/dist/project-resolver.d.ts.map +1 -0
- package/dist/project-resolver.js +20 -0
- package/dist/project-resolver.js.map +1 -0
- package/dist/prompt-builder.d.ts +42 -0
- package/dist/prompt-builder.d.ts.map +1 -0
- package/dist/prompt-builder.js +182 -0
- package/dist/prompt-builder.js.map +1 -0
- package/dist/prompts/orchestrator.md.js +4 -0
- package/dist/prompts/orchestrator.md.js.map +1 -0
- package/dist/query-activity-events.d.ts +42 -0
- package/dist/query-activity-events.d.ts.map +1 -0
- package/dist/query-activity-events.js +170 -0
- package/dist/query-activity-events.js.map +1 -0
- package/dist/recovery/actions.d.ts +7 -0
- package/dist/recovery/actions.d.ts.map +1 -0
- package/dist/recovery/index.d.ts +8 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/logger.d.ts +12 -0
- package/dist/recovery/logger.d.ts.map +1 -0
- package/dist/recovery/manager.d.ts +24 -0
- package/dist/recovery/manager.d.ts.map +1 -0
- package/dist/recovery/scanner.d.ts +11 -0
- package/dist/recovery/scanner.d.ts.map +1 -0
- package/dist/recovery/types.d.ts +170 -0
- package/dist/recovery/types.d.ts.map +1 -0
- package/dist/recovery/validator.d.ts +8 -0
- package/dist/recovery/validator.d.ts.map +1 -0
- package/dist/report-watcher.d.ts +93 -0
- package/dist/report-watcher.d.ts.map +1 -0
- package/dist/report-watcher.js +182 -0
- package/dist/report-watcher.js.map +1 -0
- package/dist/scm-webhook-utils.d.ts +6 -0
- package/dist/scm-webhook-utils.d.ts.map +1 -0
- package/dist/scm-webhook-utils.js +36 -0
- package/dist/scm-webhook-utils.js.map +1 -0
- package/dist/session-manager.d.ts +22 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +3077 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/spawn-target.d.ts +23 -0
- package/dist/spawn-target.d.ts.map +1 -0
- package/dist/spawn-target.js +39 -0
- package/dist/spawn-target.js.map +1 -0
- package/dist/storage-key.d.ts +9 -0
- package/dist/storage-key.d.ts.map +1 -0
- package/dist/storage-key.js +59 -0
- package/dist/storage-key.js.map +1 -0
- package/dist/tmux.d.ts +39 -0
- package/dist/tmux.d.ts.map +1 -0
- package/dist/tmux.js +141 -0
- package/dist/tmux.js.map +1 -0
- package/dist/types.d.ts +1496 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +215 -0
- package/dist/types.js.map +1 -0
- package/dist/update-cache.d.ts +59 -0
- package/dist/update-cache.d.ts.map +1 -0
- package/dist/update-cache.js +77 -0
- package/dist/update-cache.js.map +1 -0
- package/dist/utils/metadata-flatten.d.ts +3 -0
- package/dist/utils/metadata-flatten.d.ts.map +1 -0
- package/dist/utils/metadata-flatten.js +18 -0
- package/dist/utils/metadata-flatten.js.map +1 -0
- package/dist/utils/pr.d.ts +7 -0
- package/dist/utils/pr.d.ts.map +1 -0
- package/dist/utils/pr.js +97 -0
- package/dist/utils/pr.js.map +1 -0
- package/dist/utils/session-from-metadata.d.ts +16 -0
- package/dist/utils/session-from-metadata.d.ts.map +1 -0
- package/dist/utils/session-from-metadata.js +87 -0
- package/dist/utils/session-from-metadata.js.map +1 -0
- package/dist/utils/session-id.d.ts +4 -0
- package/dist/utils/session-id.d.ts.map +1 -0
- package/dist/utils/session-id.js +9 -0
- package/dist/utils/session-id.js.map +1 -0
- package/dist/utils/validation.d.ts +9 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +45 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils.d.ts +65 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +189 -0
- package/dist/utils.js.map +1 -0
- package/dist/version-compare.d.ts +27 -0
- package/dist/version-compare.d.ts.map +1 -0
- package/dist/version-compare.js +121 -0
- package/dist/version-compare.js.map +1 -0
- package/dist/windows-pty-registry.d.ts +27 -0
- package/dist/windows-pty-registry.d.ts.map +1 -0
- package/dist/windows-pty-registry.js +109 -0
- package/dist/windows-pty-registry.js.map +1 -0
- package/package.json +110 -0
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { resolve, basename, join, dirname, relative, sep } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { parse, stringify } from 'yaml';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { atomicWriteFileSync } from './atomic-write.js';
|
|
8
|
+
import { detectScmPlatform } from './config-generator.js';
|
|
9
|
+
import { withFileLockSync } from './file-lock.js';
|
|
10
|
+
import { ProjectResolveError } from './types.js';
|
|
11
|
+
import { generateSessionPrefix } from './paths.js';
|
|
12
|
+
import { normalizeOriginUrl } from './storage-key.js';
|
|
13
|
+
import { getDefaultRuntime } from './platform.js';
|
|
14
|
+
import { recordActivityEvent } from './activity-events.js';
|
|
15
|
+
|
|
16
|
+
function globalConfigLockPath(configPath) {
|
|
17
|
+
return `${configPath}.lock`;
|
|
18
|
+
}
|
|
19
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
20
|
+
const rel = relative(rootPath, candidatePath);
|
|
21
|
+
return rel === "" || (!rel.startsWith("..") && !rel.startsWith(`..${sep}`));
|
|
22
|
+
}
|
|
23
|
+
function normalizeRegistryProjectPath(projectId, rawPath) {
|
|
24
|
+
if (rawPath === "~") {
|
|
25
|
+
return homedir();
|
|
26
|
+
}
|
|
27
|
+
if (rawPath.startsWith("~/")) {
|
|
28
|
+
const homePath = homedir();
|
|
29
|
+
const resolvedPath = resolve(homePath, rawPath.slice(2));
|
|
30
|
+
if (!isWithinRoot(homePath, resolvedPath)) {
|
|
31
|
+
throw new ProjectResolveError(projectId, `Project path "${rawPath}" escapes the home directory and cannot be loaded from the global registry.`);
|
|
32
|
+
}
|
|
33
|
+
return resolvedPath;
|
|
34
|
+
}
|
|
35
|
+
return resolve(rawPath);
|
|
36
|
+
}
|
|
37
|
+
function normalizeRegisteredProjectPath(projectPath) {
|
|
38
|
+
return realpathSync(resolve(projectPath));
|
|
39
|
+
}
|
|
40
|
+
function generateExternalId(projectPath, originUrl) {
|
|
41
|
+
const resolvedProjectPath = resolve(projectPath);
|
|
42
|
+
const name = sanitizeBasename(basename(resolvedProjectPath));
|
|
43
|
+
const raw = `${resolvedProjectPath}:${originUrl ?? ""}`;
|
|
44
|
+
const hash = createHash("sha256").update(raw).digest("hex").slice(0, 10);
|
|
45
|
+
return `${name}_${hash}`;
|
|
46
|
+
}
|
|
47
|
+
function sanitizeBasename(name) {
|
|
48
|
+
return name
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9_-]/g, "-")
|
|
51
|
+
.replace(/^[^a-z0-9]/, "x")
|
|
52
|
+
.replace(/-+/g, "-")
|
|
53
|
+
.slice(0, 30);
|
|
54
|
+
}
|
|
55
|
+
// =============================================================================
|
|
56
|
+
// GLOBAL CONFIG PATH (XDG-aware)
|
|
57
|
+
// =============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Return the canonical path to the global config file.
|
|
60
|
+
*
|
|
61
|
+
* Priority:
|
|
62
|
+
* 1. AO_GLOBAL_CONFIG environment variable (explicit global config override)
|
|
63
|
+
* 2. $XDG_CONFIG_HOME/agent-orchestrator/config.yaml
|
|
64
|
+
* 3. ~/.agent-orchestrator/config.yaml (default)
|
|
65
|
+
*
|
|
66
|
+
* NOTE: This intentionally does NOT read AO_CONFIG_PATH. That env var is used
|
|
67
|
+
* by findConfigFile() to locate any config (including project-local ones).
|
|
68
|
+
* Using it here would risk overwriting a project-local config with global-format
|
|
69
|
+
* YAML when registry helpers call this function.
|
|
70
|
+
*/
|
|
71
|
+
function getGlobalConfigPath() {
|
|
72
|
+
if (process.env["AO_GLOBAL_CONFIG"]) {
|
|
73
|
+
return resolve(process.env["AO_GLOBAL_CONFIG"]);
|
|
74
|
+
}
|
|
75
|
+
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
|
|
76
|
+
if (xdgConfigHome) {
|
|
77
|
+
return join(xdgConfigHome, "agent-orchestrator", "config.yaml");
|
|
78
|
+
}
|
|
79
|
+
return join(homedir(), ".agent-orchestrator", "config.yaml");
|
|
80
|
+
}
|
|
81
|
+
function isCanonicalGlobalConfigPath(configPath) {
|
|
82
|
+
if (!configPath)
|
|
83
|
+
return false;
|
|
84
|
+
return resolve(configPath) === resolve(getGlobalConfigPath());
|
|
85
|
+
}
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// GLOBAL CONFIG SCHEMA
|
|
88
|
+
// =============================================================================
|
|
89
|
+
const GlobalRepoIdentitySchema = z.object({
|
|
90
|
+
owner: z.string(),
|
|
91
|
+
name: z.string(),
|
|
92
|
+
platform: z.enum(["github", "gitlab", "bitbucket"]),
|
|
93
|
+
originUrl: z.string(),
|
|
94
|
+
});
|
|
95
|
+
const GLOBAL_PROJECT_ENTRY_FIELDS = new Set([
|
|
96
|
+
"projectId",
|
|
97
|
+
"path",
|
|
98
|
+
"repo",
|
|
99
|
+
"defaultBranch",
|
|
100
|
+
"source",
|
|
101
|
+
"registeredAt",
|
|
102
|
+
"displayName",
|
|
103
|
+
"sessionPrefix",
|
|
104
|
+
"storageKey", // Preserved until `athene migrate-storage` strips it
|
|
105
|
+
]);
|
|
106
|
+
const LOCAL_CONFIG_FILENAMES = ["agent-orchestrator.yaml", "agent-orchestrator.yml"];
|
|
107
|
+
const LOCAL_IDENTITY_FIELDS = new Set([
|
|
108
|
+
"repo",
|
|
109
|
+
"defaultBranch",
|
|
110
|
+
"originUrl",
|
|
111
|
+
"projectId",
|
|
112
|
+
"path",
|
|
113
|
+
"storageKey",
|
|
114
|
+
]);
|
|
115
|
+
const GlobalProjectEntrySchema = z.object({
|
|
116
|
+
projectId: z.string().optional(),
|
|
117
|
+
path: z.string(),
|
|
118
|
+
repo: GlobalRepoIdentitySchema.optional(),
|
|
119
|
+
defaultBranch: z.string().optional(),
|
|
120
|
+
source: z.string().optional(),
|
|
121
|
+
registeredAt: z.number().optional(),
|
|
122
|
+
displayName: z.string().optional(),
|
|
123
|
+
sessionPrefix: z.string().optional(),
|
|
124
|
+
storageKey: z.string().optional(),
|
|
125
|
+
});
|
|
126
|
+
/**
|
|
127
|
+
* Global config schema.
|
|
128
|
+
* Operational settings + project registry with identity fields only.
|
|
129
|
+
*/
|
|
130
|
+
/**
|
|
131
|
+
* Update channel — controls which npm dist-tag the auto-updater tracks.
|
|
132
|
+
*
|
|
133
|
+
* stable — @latest (weekly Thursday releases). Auto-installs when run.
|
|
134
|
+
* nightly — @nightly (daily Fri–Tue cron). Auto-installs when run.
|
|
135
|
+
* manual — no checks, no notice, no install. User runs `athene update` manually.
|
|
136
|
+
*/
|
|
137
|
+
const UpdateChannelSchema = z.enum(["stable", "nightly", "manual"]);
|
|
138
|
+
/**
|
|
139
|
+
* Install-method override. When set, the auto-updater bypasses path-based
|
|
140
|
+
* detection and uses this value to pick the upgrade command. Useful for
|
|
141
|
+
* non-standard install layouts (custom prefixes, asdf, etc.).
|
|
142
|
+
*
|
|
143
|
+
* Mirrors `InstallMethod` from the CLI (kept as `string` here so the core
|
|
144
|
+
* package doesn't depend on the CLI).
|
|
145
|
+
*/
|
|
146
|
+
const InstallMethodOverrideSchema = z.enum([
|
|
147
|
+
"git",
|
|
148
|
+
"npm-global",
|
|
149
|
+
"pnpm-global",
|
|
150
|
+
"bun-global",
|
|
151
|
+
"homebrew",
|
|
152
|
+
"unknown",
|
|
153
|
+
]);
|
|
154
|
+
const GlobalConfigSchema = z
|
|
155
|
+
.object({
|
|
156
|
+
/** Web dashboard port. Default: 3000 */
|
|
157
|
+
port: z.number().default(3000),
|
|
158
|
+
terminalPort: z.number().optional(),
|
|
159
|
+
directTerminalPort: z.number().optional(),
|
|
160
|
+
/** Time before a "ready" session becomes "idle". Default: 300 000 ms (5 min). */
|
|
161
|
+
readyThresholdMs: z.number().nonnegative().default(300_000),
|
|
162
|
+
/**
|
|
163
|
+
* Auto-update channel preference.
|
|
164
|
+
*
|
|
165
|
+
* Default `manual` (resolved at read time) so users who upgrade across
|
|
166
|
+
* this change keep their existing behavior — no surprise auto-installs.
|
|
167
|
+
* The onboarding flow prompts new users on first `athene start` and persists
|
|
168
|
+
* the answer here.
|
|
169
|
+
*
|
|
170
|
+
* `.catch(undefined)` makes the schema tolerant of legacy / typo'd values
|
|
171
|
+
* in the on-disk config: a stray `updateChannel: foo` parses as
|
|
172
|
+
* "unset" rather than failing the whole config load. The user can fix it
|
|
173
|
+
* later via `athene config set updateChannel <stable|nightly|manual>`.
|
|
174
|
+
*/
|
|
175
|
+
updateChannel: UpdateChannelSchema.optional().catch(undefined),
|
|
176
|
+
/** Override path-based install detection. Optional. */
|
|
177
|
+
installMethod: InstallMethodOverrideSchema.optional().catch(undefined),
|
|
178
|
+
/** Structured observability defaults. Env vars still override at runtime. */
|
|
179
|
+
observability: z
|
|
180
|
+
.object({
|
|
181
|
+
logLevel: z.enum(["debug", "info", "warn", "error"]).default("warn"),
|
|
182
|
+
stderr: z.boolean().default(false),
|
|
183
|
+
})
|
|
184
|
+
.optional(),
|
|
185
|
+
/** Cross-project defaults — projects inherit when fields are omitted. */
|
|
186
|
+
defaults: z
|
|
187
|
+
.object({
|
|
188
|
+
runtime: z.string().default(() => getDefaultRuntime()),
|
|
189
|
+
agent: z.string().default("claude-code"),
|
|
190
|
+
workspace: z.string().default("worktree"),
|
|
191
|
+
notifiers: z.array(z.string()).default(["composio", "desktop"]),
|
|
192
|
+
orchestrator: z.object({ agent: z.string().optional() }).optional(),
|
|
193
|
+
worker: z.object({ agent: z.string().optional() }).optional(),
|
|
194
|
+
})
|
|
195
|
+
.default({}),
|
|
196
|
+
/** Project registry — map key is the canonical project ID. */
|
|
197
|
+
projects: z.record(GlobalProjectEntrySchema).default({}),
|
|
198
|
+
/** Optional explicit project ordering for sidebar / portfolio display. */
|
|
199
|
+
projectOrder: z.array(z.string()).optional(),
|
|
200
|
+
/** Notification channel configurations. */
|
|
201
|
+
notifiers: z.record(z.object({ plugin: z.string() }).passthrough()).default({}),
|
|
202
|
+
/** Maps priority levels to notifier channel IDs. */
|
|
203
|
+
notificationRouting: z.record(z.array(z.string())).default({
|
|
204
|
+
urgent: ["desktop", "composio"],
|
|
205
|
+
action: ["desktop", "composio"],
|
|
206
|
+
warning: ["composio"],
|
|
207
|
+
info: ["composio"],
|
|
208
|
+
}),
|
|
209
|
+
/** Reaction rules (default reactions merged at load time). */
|
|
210
|
+
reactions: z.record(z.object({}).passthrough()).default({}),
|
|
211
|
+
})
|
|
212
|
+
.passthrough();
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// LOCAL PROJECT CONFIG SCHEMA (flat, behavior-only)
|
|
215
|
+
// =============================================================================
|
|
216
|
+
/**
|
|
217
|
+
* Flat, behavior-only local project config.
|
|
218
|
+
* Lives at <project>/agent-orchestrator.yaml.
|
|
219
|
+
*
|
|
220
|
+
* Does NOT contain identity fields: projectId, path, repo,
|
|
221
|
+
* defaultBranch, source, registeredAt, displayName, sessionPrefix.
|
|
222
|
+
* Those are owned by the global registry.
|
|
223
|
+
*/
|
|
224
|
+
const LocalProjectConfigSchema = z
|
|
225
|
+
.object({
|
|
226
|
+
repo: z.string().optional(),
|
|
227
|
+
defaultBranch: z.string().optional(),
|
|
228
|
+
runtime: z.string().optional(),
|
|
229
|
+
agent: z.string().optional(),
|
|
230
|
+
workspace: z.string().optional(),
|
|
231
|
+
tracker: z.object({ plugin: z.string() }).passthrough().optional(),
|
|
232
|
+
scm: z
|
|
233
|
+
.object({
|
|
234
|
+
plugin: z.string(),
|
|
235
|
+
webhook: z
|
|
236
|
+
.object({
|
|
237
|
+
enabled: z.boolean().optional(),
|
|
238
|
+
path: z.string().optional(),
|
|
239
|
+
secretEnvVar: z.string().optional(),
|
|
240
|
+
signatureHeader: z.string().optional(),
|
|
241
|
+
eventHeader: z.string().optional(),
|
|
242
|
+
deliveryHeader: z.string().optional(),
|
|
243
|
+
maxBodyBytes: z.number().optional(),
|
|
244
|
+
})
|
|
245
|
+
.optional(),
|
|
246
|
+
})
|
|
247
|
+
.passthrough()
|
|
248
|
+
.optional(),
|
|
249
|
+
symlinks: z.array(z.string()).optional(),
|
|
250
|
+
postCreate: z.array(z.string()).optional(),
|
|
251
|
+
agentConfig: z
|
|
252
|
+
.object({
|
|
253
|
+
permissions: z
|
|
254
|
+
.enum(["permissionless", "default", "auto-edit", "suggest", "skip"])
|
|
255
|
+
.optional(),
|
|
256
|
+
model: z.string().optional(),
|
|
257
|
+
orchestratorModel: z.string().optional(),
|
|
258
|
+
})
|
|
259
|
+
.passthrough()
|
|
260
|
+
.optional(),
|
|
261
|
+
orchestrator: z
|
|
262
|
+
.object({ agent: z.string().optional(), agentConfig: z.object({}).passthrough().optional() })
|
|
263
|
+
.optional(),
|
|
264
|
+
worker: z
|
|
265
|
+
.object({ agent: z.string().optional(), agentConfig: z.object({}).passthrough().optional() })
|
|
266
|
+
.optional(),
|
|
267
|
+
reactions: z.record(z.object({}).passthrough()).optional(),
|
|
268
|
+
agentRules: z.string().optional(),
|
|
269
|
+
agentRulesFile: z.string().optional(),
|
|
270
|
+
orchestratorRules: z.string().optional(),
|
|
271
|
+
orchestratorSessionStrategy: z
|
|
272
|
+
.enum(["reuse", "delete", "ignore", "delete-new", "ignore-new", "kill-previous"])
|
|
273
|
+
.optional(),
|
|
274
|
+
opencodeIssueSessionStrategy: z.enum(["reuse", "delete", "ignore"]).optional(),
|
|
275
|
+
decomposer: z.object({}).passthrough().optional(),
|
|
276
|
+
})
|
|
277
|
+
.passthrough();
|
|
278
|
+
// =============================================================================
|
|
279
|
+
// LOAD / SAVE
|
|
280
|
+
// =============================================================================
|
|
281
|
+
/**
|
|
282
|
+
* Load and validate the global config.
|
|
283
|
+
* Returns null if the file does not exist (not an error — first run).
|
|
284
|
+
*/
|
|
285
|
+
function loadGlobalConfig(configPath, options = {}) {
|
|
286
|
+
const path = configPath ?? getGlobalConfigPath();
|
|
287
|
+
if (!existsSync(path))
|
|
288
|
+
return null;
|
|
289
|
+
const { parsed, migrationSummary } = migrateLegacyGlobalConfigOnLoad(path, options);
|
|
290
|
+
if (!parsed)
|
|
291
|
+
return null;
|
|
292
|
+
if (migrationSummary) {
|
|
293
|
+
// eslint-disable-next-line no-console -- required migration visibility for stale shadow stripping
|
|
294
|
+
console.info(migrationSummary);
|
|
295
|
+
recordActivityEvent({
|
|
296
|
+
source: "config",
|
|
297
|
+
kind: "config.migrated",
|
|
298
|
+
summary: "global config migrated",
|
|
299
|
+
data: { migrationSummary },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const config = GlobalConfigSchema.parse(parsed);
|
|
303
|
+
for (const [projectId, entry] of Object.entries(config.projects)) {
|
|
304
|
+
entry.path = normalizeRegistryProjectPath(projectId, entry.path);
|
|
305
|
+
}
|
|
306
|
+
return config;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Save the global config atomically (temp-file + rename).
|
|
310
|
+
* Creates parent directories if they don't exist.
|
|
311
|
+
*/
|
|
312
|
+
function saveGlobalConfig(config, configPath) {
|
|
313
|
+
const path = configPath ?? getGlobalConfigPath();
|
|
314
|
+
const dir = dirname(path);
|
|
315
|
+
mkdirSync(dir, { recursive: true });
|
|
316
|
+
atomicWriteFileSync(path, stringify(config, { indent: 2 }));
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Load a flat local project config from <projectPath>/agent-orchestrator.yaml.
|
|
320
|
+
*
|
|
321
|
+
* Returns null when:
|
|
322
|
+
* - No config file found at projectPath
|
|
323
|
+
* - File has a `projects:` wrapper (old format — use loadConfig() instead)
|
|
324
|
+
* - File is empty or malformed
|
|
325
|
+
*/
|
|
326
|
+
function loadLocalProjectConfig(projectPath) {
|
|
327
|
+
const result = loadLocalProjectConfigDetailed(projectPath);
|
|
328
|
+
return result.kind === "loaded" ? (result.config ?? null) : null;
|
|
329
|
+
}
|
|
330
|
+
function loadLocalProjectConfigDetailed(projectPath) {
|
|
331
|
+
const candidates = LOCAL_CONFIG_FILENAMES.map((filename) => join(projectPath, filename));
|
|
332
|
+
for (const path of candidates) {
|
|
333
|
+
if (!existsSync(path))
|
|
334
|
+
continue;
|
|
335
|
+
let parsed;
|
|
336
|
+
try {
|
|
337
|
+
const raw = readFileSync(path, "utf-8");
|
|
338
|
+
parsed = parse(raw);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
return {
|
|
342
|
+
kind: "malformed",
|
|
343
|
+
path,
|
|
344
|
+
error: `Failed to parse local config at ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (!parsed || typeof parsed !== "object") {
|
|
348
|
+
return {
|
|
349
|
+
kind: "invalid",
|
|
350
|
+
path,
|
|
351
|
+
error: `Local config at ${path} must parse to an object`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// Old format: has `projects:` wrapper → not a flat local config
|
|
355
|
+
if ("projects" in parsed) {
|
|
356
|
+
return {
|
|
357
|
+
kind: "old-format",
|
|
358
|
+
path,
|
|
359
|
+
error: `Local config at ${path} still uses a wrapped projects: format`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
return {
|
|
364
|
+
kind: "loaded",
|
|
365
|
+
path,
|
|
366
|
+
config: LocalProjectConfigSchema.parse(parsed),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
kind: "invalid",
|
|
372
|
+
path,
|
|
373
|
+
error: `Local config at ${path} failed validation: ${error instanceof Error ? error.message : String(error)}`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return { kind: "missing" };
|
|
378
|
+
}
|
|
379
|
+
function stripLocalIdentityFields(config) {
|
|
380
|
+
const next = { ...config };
|
|
381
|
+
for (const key of LOCAL_IDENTITY_FIELDS) {
|
|
382
|
+
Reflect.deleteProperty(next, key);
|
|
383
|
+
}
|
|
384
|
+
return next;
|
|
385
|
+
}
|
|
386
|
+
function getLocalProjectConfigPath(projectPath) {
|
|
387
|
+
for (const filename of LOCAL_CONFIG_FILENAMES) {
|
|
388
|
+
const candidate = join(projectPath, filename);
|
|
389
|
+
if (existsSync(candidate))
|
|
390
|
+
return candidate;
|
|
391
|
+
}
|
|
392
|
+
return join(projectPath, LOCAL_CONFIG_FILENAMES[0]);
|
|
393
|
+
}
|
|
394
|
+
function writeLocalProjectConfig(projectPath, config, configPath = getLocalProjectConfigPath(projectPath)) {
|
|
395
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
396
|
+
const validated = LocalProjectConfigSchema.parse(stripLocalIdentityFields(config));
|
|
397
|
+
atomicWriteFileSync(configPath, stringify(validated, { indent: 2 }));
|
|
398
|
+
return configPath;
|
|
399
|
+
}
|
|
400
|
+
function isRecord(value) {
|
|
401
|
+
return value !== null && value !== undefined && typeof value === "object" && !Array.isArray(value);
|
|
402
|
+
}
|
|
403
|
+
function mergeRoleBehavior(defaults, project, key) {
|
|
404
|
+
const defaultRole = isRecord(defaults[key]) ? defaults[key] : undefined;
|
|
405
|
+
const projectRole = isRecord(project[key]) ? project[key] : undefined;
|
|
406
|
+
const merged = {
|
|
407
|
+
...(defaultRole ?? {}),
|
|
408
|
+
...(projectRole ?? {}),
|
|
409
|
+
};
|
|
410
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
411
|
+
}
|
|
412
|
+
function buildRepairedLocalProjectConfig(parsed, project) {
|
|
413
|
+
const defaults = isRecord(parsed["defaults"]) ? parsed["defaults"] : {};
|
|
414
|
+
const defaultBehavior = {};
|
|
415
|
+
for (const key of ["runtime", "agent", "workspace"]) {
|
|
416
|
+
if (defaults[key] !== null && defaults[key] !== undefined) {
|
|
417
|
+
defaultBehavior[key] = defaults[key];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const { name: _name, path: _path, sessionPrefix: _sessionPrefix, projectId: _projectId, source: _source, registeredAt: _registeredAt, displayName: _displayName, orchestrator: _orchestrator, worker: _worker, ...projectBehavior } = project;
|
|
421
|
+
const behavior = {
|
|
422
|
+
...defaultBehavior,
|
|
423
|
+
...projectBehavior,
|
|
424
|
+
};
|
|
425
|
+
const orchestrator = mergeRoleBehavior(defaults, project, "orchestrator");
|
|
426
|
+
const worker = mergeRoleBehavior(defaults, project, "worker");
|
|
427
|
+
if (orchestrator)
|
|
428
|
+
behavior["orchestrator"] = orchestrator;
|
|
429
|
+
if (worker)
|
|
430
|
+
behavior["worker"] = worker;
|
|
431
|
+
return behavior;
|
|
432
|
+
}
|
|
433
|
+
function repairWrappedLocalProjectConfig(projectId, projectPath) {
|
|
434
|
+
const localConfigResult = loadLocalProjectConfigDetailed(projectPath);
|
|
435
|
+
if (localConfigResult.kind !== "old-format" || !localConfigResult.path) {
|
|
436
|
+
throw new Error(`No wrapped local config found for project "${projectId}" at ${projectPath}`);
|
|
437
|
+
}
|
|
438
|
+
const configPath = localConfigResult.path;
|
|
439
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
440
|
+
const parsed = parse(raw);
|
|
441
|
+
if (!parsed || typeof parsed !== "object" || !isOldConfigFormat(parsed)) {
|
|
442
|
+
throw new Error(`Local config at ${configPath} is not a wrapped old-format config.`);
|
|
443
|
+
}
|
|
444
|
+
const projects = (parsed["projects"] ?? {});
|
|
445
|
+
// Try the effective registered ID first, then fall back to any entry in the
|
|
446
|
+
// wrapped config (the old-format config may use a different key than the hashed ID).
|
|
447
|
+
let project = projects[projectId];
|
|
448
|
+
if (!project || typeof project !== "object") {
|
|
449
|
+
const entries = Object.values(projects).filter((v) => v !== null && v !== undefined && typeof v === "object");
|
|
450
|
+
project = entries.length === 1 ? entries[0] : undefined;
|
|
451
|
+
}
|
|
452
|
+
if (!project || typeof project !== "object") {
|
|
453
|
+
throw new Error(`Wrapped local config at ${configPath} does not contain project "${projectId}".`);
|
|
454
|
+
}
|
|
455
|
+
const behaviorFields = buildRepairedLocalProjectConfig(parsed, project);
|
|
456
|
+
writeLocalProjectConfig(projectPath, behaviorFields, configPath);
|
|
457
|
+
}
|
|
458
|
+
function resolveGitRoot(projectPath) {
|
|
459
|
+
let current = resolve(projectPath);
|
|
460
|
+
while (true) {
|
|
461
|
+
if (existsSync(join(current, ".git")))
|
|
462
|
+
return current;
|
|
463
|
+
const parent = dirname(current);
|
|
464
|
+
if (parent === current)
|
|
465
|
+
return resolve(projectPath);
|
|
466
|
+
current = parent;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function resolveGitConfigPath(gitRoot) {
|
|
470
|
+
const dotGitPath = join(gitRoot, ".git");
|
|
471
|
+
if (!existsSync(dotGitPath))
|
|
472
|
+
return null;
|
|
473
|
+
if (statSync(dotGitPath).isDirectory()) {
|
|
474
|
+
const configPath = join(dotGitPath, "config");
|
|
475
|
+
return existsSync(configPath) ? configPath : null;
|
|
476
|
+
}
|
|
477
|
+
const pointer = readFileSync(dotGitPath, "utf-8").trim();
|
|
478
|
+
const match = pointer.match(/^gitdir:\s*(.+)$/i);
|
|
479
|
+
if (!match)
|
|
480
|
+
return null;
|
|
481
|
+
const gitDir = resolve(gitRoot, match[1]);
|
|
482
|
+
const directConfig = join(gitDir, "config");
|
|
483
|
+
if (existsSync(directConfig))
|
|
484
|
+
return directConfig;
|
|
485
|
+
const commonDirPath = join(gitDir, "commondir");
|
|
486
|
+
if (!existsSync(commonDirPath))
|
|
487
|
+
return null;
|
|
488
|
+
const commonDir = resolve(gitDir, readFileSync(commonDirPath, "utf-8").trim());
|
|
489
|
+
const commonConfig = join(commonDir, "config");
|
|
490
|
+
return existsSync(commonConfig) ? commonConfig : null;
|
|
491
|
+
}
|
|
492
|
+
function readOriginUrlFromGitConfig(projectPath) {
|
|
493
|
+
const gitRoot = resolveGitRoot(projectPath);
|
|
494
|
+
const configPath = resolveGitConfigPath(gitRoot);
|
|
495
|
+
if (!configPath)
|
|
496
|
+
return null;
|
|
497
|
+
const lines = readFileSync(configPath, "utf-8").split(/\r?\n/);
|
|
498
|
+
let inOrigin = false;
|
|
499
|
+
for (const line of lines) {
|
|
500
|
+
const section = line.match(/^\s*\[(.+)\]\s*$/);
|
|
501
|
+
if (section) {
|
|
502
|
+
inOrigin = section[1] === 'remote "origin"';
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (!inOrigin)
|
|
506
|
+
continue;
|
|
507
|
+
const url = line.match(/^\s*url\s*=\s*(.+)\s*$/);
|
|
508
|
+
if (url?.[1])
|
|
509
|
+
return url[1].trim();
|
|
510
|
+
}
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
function normalizeRepoIdentity(originUrl) {
|
|
514
|
+
if (!originUrl)
|
|
515
|
+
return undefined;
|
|
516
|
+
const normalizedOriginUrl = normalizeOriginUrl(originUrl);
|
|
517
|
+
if (!normalizedOriginUrl.startsWith("https://"))
|
|
518
|
+
return undefined;
|
|
519
|
+
try {
|
|
520
|
+
const parsed = new URL(normalizedOriginUrl);
|
|
521
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
522
|
+
if (segments.length < 2)
|
|
523
|
+
return undefined;
|
|
524
|
+
const name = segments[segments.length - 1];
|
|
525
|
+
const owner = segments.slice(0, -1).join("/");
|
|
526
|
+
const platform = detectScmPlatform(parsed.host);
|
|
527
|
+
if (platform === "unknown")
|
|
528
|
+
return undefined;
|
|
529
|
+
return {
|
|
530
|
+
owner,
|
|
531
|
+
name,
|
|
532
|
+
platform,
|
|
533
|
+
originUrl: normalizedOriginUrl,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function normalizeLegacyRepoValue(repoValue) {
|
|
541
|
+
if (typeof repoValue !== "string")
|
|
542
|
+
return undefined;
|
|
543
|
+
const trimmed = repoValue.trim();
|
|
544
|
+
if (!trimmed)
|
|
545
|
+
return undefined;
|
|
546
|
+
if (trimmed.startsWith("http://") ||
|
|
547
|
+
trimmed.startsWith("https://") ||
|
|
548
|
+
trimmed.startsWith("git@")) {
|
|
549
|
+
return normalizeRepoIdentity(trimmed);
|
|
550
|
+
}
|
|
551
|
+
const segments = trimmed.split("/").filter(Boolean);
|
|
552
|
+
if (segments.length === 2) {
|
|
553
|
+
return normalizeRepoIdentity(`https://github.com/${segments[0]}/${segments[1]}`);
|
|
554
|
+
}
|
|
555
|
+
if (segments.length >= 3 && segments[0].includes(".")) {
|
|
556
|
+
const host = segments[0];
|
|
557
|
+
const platform = detectScmPlatform(host);
|
|
558
|
+
if (platform === "unknown")
|
|
559
|
+
return undefined;
|
|
560
|
+
const owner = segments.slice(1, -1).join("/");
|
|
561
|
+
const name = segments[segments.length - 1];
|
|
562
|
+
return normalizeRepoIdentity(`https://${host}/${owner}/${name}`);
|
|
563
|
+
}
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
function getRegisteredSessionPrefix(entry, projectId) {
|
|
567
|
+
return entry.sessionPrefix ?? generateSessionPrefix(basename(entry.path ?? projectId));
|
|
568
|
+
}
|
|
569
|
+
function findSessionPrefixOwner(globalConfig, sessionPrefix, excludeProjectId) {
|
|
570
|
+
for (const [projectId, entry] of Object.entries(globalConfig.projects)) {
|
|
571
|
+
if (projectId === excludeProjectId)
|
|
572
|
+
continue;
|
|
573
|
+
if (getRegisteredSessionPrefix(entry, projectId) === sessionPrefix) {
|
|
574
|
+
return projectId;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
function deriveAvailableSessionPrefix(requestedPrefix, globalConfig, excludeProjectId) {
|
|
580
|
+
if (!findSessionPrefixOwner(globalConfig, requestedPrefix, excludeProjectId)) {
|
|
581
|
+
return requestedPrefix;
|
|
582
|
+
}
|
|
583
|
+
for (let suffix = 1; suffix < 10_000; suffix += 1) {
|
|
584
|
+
const candidate = `${requestedPrefix}-${suffix}`;
|
|
585
|
+
if (!findSessionPrefixOwner(globalConfig, candidate, excludeProjectId)) {
|
|
586
|
+
return candidate;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
throw new Error(`Could not allocate a session prefix for "${requestedPrefix}" after 9999 attempts.`);
|
|
590
|
+
}
|
|
591
|
+
// =============================================================================
|
|
592
|
+
// REGISTRATION
|
|
593
|
+
// =============================================================================
|
|
594
|
+
/**
|
|
595
|
+
* Register or update a project in the global registry.
|
|
596
|
+
*
|
|
597
|
+
* - If the project already exists, identity fields are preserved and only
|
|
598
|
+
* updated if explicitly provided.
|
|
599
|
+
* - Local behavior is never written into the registry.
|
|
600
|
+
* - Write is atomic.
|
|
601
|
+
*/
|
|
602
|
+
function registerProjectInGlobalConfig(projectId, name, projectPath, localConfig, optionsOrGlobalConfigPath, globalConfigPath) {
|
|
603
|
+
const configPath = typeof optionsOrGlobalConfigPath === "string"
|
|
604
|
+
? optionsOrGlobalConfigPath
|
|
605
|
+
: (globalConfigPath ?? getGlobalConfigPath());
|
|
606
|
+
const requestedProjectPath = resolve(projectPath);
|
|
607
|
+
const normalizedProjectPath = normalizeRegisteredProjectPath(projectPath);
|
|
608
|
+
const originUrl = readOriginUrlFromGitConfig(normalizedProjectPath);
|
|
609
|
+
return withFileLockSync(globalConfigLockPath(configPath), () => {
|
|
610
|
+
const globalConfig = loadGlobalConfig(configPath, { alreadyLocked: true }) ?? makeEmptyGlobalConfig();
|
|
611
|
+
let effectiveProjectId = projectId;
|
|
612
|
+
let existing = globalConfig.projects[projectId];
|
|
613
|
+
if (!existing) {
|
|
614
|
+
const hashedId = generateExternalId(normalizedProjectPath, originUrl);
|
|
615
|
+
const hashedExisting = globalConfig.projects[hashedId];
|
|
616
|
+
if (hashedExisting?.path && resolve(hashedExisting.path) === normalizedProjectPath) {
|
|
617
|
+
effectiveProjectId = hashedId;
|
|
618
|
+
existing = hashedExisting;
|
|
619
|
+
}
|
|
620
|
+
else if (!hashedExisting) {
|
|
621
|
+
effectiveProjectId = hashedId;
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
throw new Error(`Project ID collision: "${hashedId}" already registered at a different path (${hashedExisting.path}). ` +
|
|
625
|
+
"This is extremely unlikely — please file a bug.");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (existing?.path && resolve(existing.path) !== normalizedProjectPath) {
|
|
629
|
+
throw new Error(`Project id "${effectiveProjectId}" is already registered for "${existing.path}". ` +
|
|
630
|
+
`Choose a different configProjectKey to add "${normalizedProjectPath}" as a separate project.`);
|
|
631
|
+
}
|
|
632
|
+
for (const [existingProjectId, entry] of Object.entries(globalConfig.projects)) {
|
|
633
|
+
if (existingProjectId === effectiveProjectId)
|
|
634
|
+
continue;
|
|
635
|
+
if (resolve(entry.path) === normalizedProjectPath) {
|
|
636
|
+
throw new Error(`Project "${existingProjectId}" is already registered at "${normalizedProjectPath}". ` +
|
|
637
|
+
`Choose a different project ID or path.`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const repoIdentity = existing?.repo
|
|
641
|
+
?? normalizeRepoIdentity(originUrl)
|
|
642
|
+
?? (localConfig?.repo ? normalizeLegacyRepoValue(localConfig.repo) : undefined);
|
|
643
|
+
const defaultBranch = existing?.defaultBranch ?? localConfig?.defaultBranch ?? "main";
|
|
644
|
+
const requestedSessionPrefix = existing?.sessionPrefix ??
|
|
645
|
+
localConfig?.sessionPrefix ??
|
|
646
|
+
generateSessionPrefix(basename(requestedProjectPath));
|
|
647
|
+
const source = existing?.source ?? (repoIdentity ? "ao-project-add" : "local");
|
|
648
|
+
const registeredAt = existing?.registeredAt ?? Math.floor(Date.now() / 1000);
|
|
649
|
+
const explicitSessionPrefix = !existing?.sessionPrefix && Boolean(localConfig?.sessionPrefix);
|
|
650
|
+
const prefixOwner = findSessionPrefixOwner(globalConfig, requestedSessionPrefix, effectiveProjectId);
|
|
651
|
+
if (prefixOwner && explicitSessionPrefix) {
|
|
652
|
+
throw new Error(`Duplicate session prefix detected: "${requestedSessionPrefix}"\n` +
|
|
653
|
+
`Projects "${prefixOwner}" and "${effectiveProjectId}" would generate the same prefix.\n\n` +
|
|
654
|
+
`Choose a different configProjectKey or add an explicit sessionPrefix before registering the project.`);
|
|
655
|
+
}
|
|
656
|
+
const sessionPrefix = prefixOwner
|
|
657
|
+
? deriveAvailableSessionPrefix(requestedSessionPrefix, globalConfig, effectiveProjectId)
|
|
658
|
+
: requestedSessionPrefix;
|
|
659
|
+
globalConfig.projects[effectiveProjectId] = {
|
|
660
|
+
projectId: effectiveProjectId,
|
|
661
|
+
path: normalizedProjectPath,
|
|
662
|
+
...(repoIdentity ? { repo: repoIdentity } : {}),
|
|
663
|
+
defaultBranch,
|
|
664
|
+
source,
|
|
665
|
+
registeredAt,
|
|
666
|
+
displayName: name,
|
|
667
|
+
sessionPrefix,
|
|
668
|
+
};
|
|
669
|
+
saveGlobalConfig(globalConfig, configPath);
|
|
670
|
+
return effectiveProjectId;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
// =============================================================================
|
|
674
|
+
// EFFECTIVE CONFIG BUILD
|
|
675
|
+
// =============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* Build effective project configuration by merging global registry identity
|
|
678
|
+
* with local behavior config.
|
|
679
|
+
*
|
|
680
|
+
* Load order:
|
|
681
|
+
* 1. Global entry supplies identity
|
|
682
|
+
* 2. Local flat config (if present) supplies behavior
|
|
683
|
+
* 3. Shared defaults supply missing required behavior when local config is absent
|
|
684
|
+
*
|
|
685
|
+
* Returns a plain object compatible with ProjectConfig from config.ts.
|
|
686
|
+
* Returns null if the project is not registered in the global config.
|
|
687
|
+
*/
|
|
688
|
+
function buildEffectiveProjectConfig(projectId, globalConfig, _globalConfigPath) {
|
|
689
|
+
const resolved = resolveProjectIdentity(projectId, globalConfig);
|
|
690
|
+
return resolved ?? null;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Resolve a single project from the canonical global registry.
|
|
694
|
+
*
|
|
695
|
+
* Behavior precedence:
|
|
696
|
+
* 1. Identity always comes from the global registry entry
|
|
697
|
+
* 2. Local flat config overrides shared defaults when it loads cleanly
|
|
698
|
+
* 3. Shared defaults are used when local config is missing
|
|
699
|
+
* 4. When local config is broken, resolveError is attached instead of throwing
|
|
700
|
+
*/
|
|
701
|
+
function resolveProjectIdentity(projectId, globalConfig, _globalConfigPath) {
|
|
702
|
+
const entry = globalConfig.projects[projectId];
|
|
703
|
+
if (!entry || !entry.path)
|
|
704
|
+
return null;
|
|
705
|
+
const projectPath = entry.path;
|
|
706
|
+
const name = entry.displayName ?? projectId;
|
|
707
|
+
const sessionPrefix = typeof entry.sessionPrefix === "string" && entry.sessionPrefix.length > 0
|
|
708
|
+
? entry.sessionPrefix
|
|
709
|
+
: generateSessionPrefix(basename(projectPath));
|
|
710
|
+
const defaultBranch = typeof entry.defaultBranch === "string" && entry.defaultBranch.length > 0
|
|
711
|
+
? entry.defaultBranch
|
|
712
|
+
: "main";
|
|
713
|
+
const repoString = entry.repo && typeof entry.repo.owner === "string" && typeof entry.repo.name === "string"
|
|
714
|
+
? `${entry.repo.owner}/${entry.repo.name}`
|
|
715
|
+
: undefined;
|
|
716
|
+
const identityFields = {
|
|
717
|
+
name,
|
|
718
|
+
path: projectPath,
|
|
719
|
+
...(repoString ? { repo: repoString } : {}),
|
|
720
|
+
sessionPrefix,
|
|
721
|
+
defaultBranch,
|
|
722
|
+
};
|
|
723
|
+
const applyBehaviorDefaults = (behavior) => {
|
|
724
|
+
const merged = { ...behavior };
|
|
725
|
+
const defaults = globalConfig.defaults ?? {};
|
|
726
|
+
if (merged["runtime"] === undefined)
|
|
727
|
+
merged["runtime"] = defaults.runtime;
|
|
728
|
+
if (merged["agent"] === undefined)
|
|
729
|
+
merged["agent"] = defaults.agent;
|
|
730
|
+
if (merged["workspace"] === undefined)
|
|
731
|
+
merged["workspace"] = defaults.workspace;
|
|
732
|
+
const orchestrator = {
|
|
733
|
+
...(defaults.orchestrator ?? {}),
|
|
734
|
+
...(merged["orchestrator"] ?? {}),
|
|
735
|
+
};
|
|
736
|
+
if (Object.keys(orchestrator).length > 0) {
|
|
737
|
+
merged["orchestrator"] = orchestrator;
|
|
738
|
+
}
|
|
739
|
+
const worker = {
|
|
740
|
+
...(defaults.worker ?? {}),
|
|
741
|
+
...(merged["worker"] ?? {}),
|
|
742
|
+
};
|
|
743
|
+
if (Object.keys(worker).length > 0) {
|
|
744
|
+
merged["worker"] = worker;
|
|
745
|
+
}
|
|
746
|
+
const missing = ["runtime", "agent", "workspace"].filter((field) => {
|
|
747
|
+
const value = merged[field];
|
|
748
|
+
return typeof value !== "string" || value.length === 0;
|
|
749
|
+
});
|
|
750
|
+
if (missing.length > 0) {
|
|
751
|
+
throw new ProjectResolveError(projectId, `Project "${projectId}" is missing required behavior fields with no defaults: ${missing.join(", ")}`);
|
|
752
|
+
}
|
|
753
|
+
return merged;
|
|
754
|
+
};
|
|
755
|
+
const localConfigResult = loadLocalProjectConfigDetailed(projectPath);
|
|
756
|
+
if (localConfigResult.kind === "loaded" && localConfigResult.config) {
|
|
757
|
+
return {
|
|
758
|
+
...applyBehaviorDefaults(stripLocalIdentityFields(localConfigResult.config)),
|
|
759
|
+
...identityFields,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (localConfigResult.kind === "malformed") {
|
|
763
|
+
recordActivityEvent({
|
|
764
|
+
projectId,
|
|
765
|
+
source: "config",
|
|
766
|
+
kind: "config.project_malformed",
|
|
767
|
+
level: "error",
|
|
768
|
+
summary: `local config for ${projectId} could not be parsed`,
|
|
769
|
+
data: {
|
|
770
|
+
path: localConfigResult.path,
|
|
771
|
+
error: localConfigResult.error,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
else if (localConfigResult.kind === "invalid") {
|
|
776
|
+
recordActivityEvent({
|
|
777
|
+
projectId,
|
|
778
|
+
source: "config",
|
|
779
|
+
kind: "config.project_invalid",
|
|
780
|
+
level: "error",
|
|
781
|
+
summary: `local config for ${projectId} failed validation`,
|
|
782
|
+
data: {
|
|
783
|
+
path: localConfigResult.path,
|
|
784
|
+
error: localConfigResult.error,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const resolveError = localConfigResult.kind !== "missing"
|
|
789
|
+
? (localConfigResult.error ?? "Failed to load local config")
|
|
790
|
+
: undefined;
|
|
791
|
+
const resolveErrorKind = localConfigResult.kind === "malformed" ||
|
|
792
|
+
localConfigResult.kind === "invalid" ||
|
|
793
|
+
localConfigResult.kind === "old-format"
|
|
794
|
+
? localConfigResult.kind
|
|
795
|
+
: undefined;
|
|
796
|
+
return {
|
|
797
|
+
...(resolveError ? {} : applyBehaviorDefaults({})),
|
|
798
|
+
...identityFields,
|
|
799
|
+
...(resolveError ? { resolveError } : {}),
|
|
800
|
+
...(resolveErrorKind ? { resolveErrorKind } : {}),
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
// =============================================================================
|
|
804
|
+
// MIGRATION
|
|
805
|
+
// =============================================================================
|
|
806
|
+
/**
|
|
807
|
+
* Detect if a raw parsed YAML object uses the old single-file config format.
|
|
808
|
+
* Old format: top-level `projects:` map where each entry has `path` + behavior.
|
|
809
|
+
*/
|
|
810
|
+
function isOldConfigFormat(raw) {
|
|
811
|
+
if (!raw || typeof raw !== "object")
|
|
812
|
+
return false;
|
|
813
|
+
const obj = raw;
|
|
814
|
+
if (!("projects" in obj) || typeof obj["projects"] !== "object")
|
|
815
|
+
return false;
|
|
816
|
+
// Confirm at least one project entry has `path` (old format)
|
|
817
|
+
const projects = obj["projects"];
|
|
818
|
+
return Object.values(projects).some((entry) => entry !== null &&
|
|
819
|
+
entry !== undefined &&
|
|
820
|
+
typeof entry === "object" &&
|
|
821
|
+
"path" in entry);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Migrate an old single-file config to the new hybrid format.
|
|
825
|
+
*
|
|
826
|
+
* What happens:
|
|
827
|
+
* 1. Read old config from oldConfigPath
|
|
828
|
+
* 2. Create global config at ~/.agent-orchestrator/config.yaml with:
|
|
829
|
+
* - Global settings (port, defaults, notifiers, reactions)
|
|
830
|
+
* - Project registry entries (identity only)
|
|
831
|
+
* 3. Rewrite local config at oldConfigPath to flat behavior-only format
|
|
832
|
+
* (removes name, path, sessionPrefix from each project entry, removes
|
|
833
|
+
* the `projects:` wrapper — only the first/matched project is written
|
|
834
|
+
* when the old config is inside the project directory)
|
|
835
|
+
* 4. Returns the new global config path
|
|
836
|
+
*
|
|
837
|
+
* @param oldConfigPath Absolute path to the old agent-orchestrator.yaml
|
|
838
|
+
* @param globalConfigPath Override for global config path (default: getGlobalConfigPath())
|
|
839
|
+
* @returns The global config path
|
|
840
|
+
*/
|
|
841
|
+
function migrateToGlobalConfig(oldConfigPath, globalConfigPath) {
|
|
842
|
+
const targetGlobalPath = globalConfigPath ?? getGlobalConfigPath();
|
|
843
|
+
const raw = readFileSync(oldConfigPath, "utf-8");
|
|
844
|
+
const parsed = parse(raw);
|
|
845
|
+
if (!isOldConfigFormat(parsed)) {
|
|
846
|
+
throw new Error(`File at ${oldConfigPath} is not an old-format config.`);
|
|
847
|
+
}
|
|
848
|
+
const oldProjects = (parsed["projects"] ?? {});
|
|
849
|
+
// Build new global config
|
|
850
|
+
const newGlobal = makeEmptyGlobalConfig();
|
|
851
|
+
// Preserve global operational settings
|
|
852
|
+
if (typeof parsed["port"] === "number")
|
|
853
|
+
newGlobal.port = parsed["port"];
|
|
854
|
+
if (parsed["terminalPort"] !== null && parsed["terminalPort"] !== undefined)
|
|
855
|
+
newGlobal.terminalPort = parsed["terminalPort"];
|
|
856
|
+
if (parsed["directTerminalPort"] !== null && parsed["directTerminalPort"] !== undefined)
|
|
857
|
+
newGlobal.directTerminalPort = parsed["directTerminalPort"];
|
|
858
|
+
if (parsed["readyThresholdMs"] !== null && parsed["readyThresholdMs"] !== undefined)
|
|
859
|
+
newGlobal.readyThresholdMs = parsed["readyThresholdMs"];
|
|
860
|
+
if (parsed["observability"] !== null && parsed["observability"] !== undefined)
|
|
861
|
+
newGlobal.observability = parsed["observability"];
|
|
862
|
+
if (parsed["defaults"] !== null && parsed["defaults"] !== undefined)
|
|
863
|
+
newGlobal.defaults = parsed["defaults"];
|
|
864
|
+
if (parsed["notifiers"] !== null && parsed["notifiers"] !== undefined)
|
|
865
|
+
newGlobal.notifiers = parsed["notifiers"];
|
|
866
|
+
if (parsed["notificationRouting"] !== null && parsed["notificationRouting"] !== undefined)
|
|
867
|
+
newGlobal.notificationRouting = parsed["notificationRouting"];
|
|
868
|
+
if (parsed["reactions"] !== null && parsed["reactions"] !== undefined)
|
|
869
|
+
newGlobal.reactions = parsed["reactions"];
|
|
870
|
+
// Build project registry entries
|
|
871
|
+
for (const [projectId, project] of Object.entries(oldProjects)) {
|
|
872
|
+
if (!project["path"])
|
|
873
|
+
continue;
|
|
874
|
+
const projectPath = typeof project["path"] === "string" && project["path"].startsWith("~/")
|
|
875
|
+
? join(homedir(), project["path"].slice(2))
|
|
876
|
+
: project["path"];
|
|
877
|
+
const repoIdentity = typeof project["originUrl"] === "string"
|
|
878
|
+
? normalizeRepoIdentity(project["originUrl"])
|
|
879
|
+
: undefined;
|
|
880
|
+
newGlobal.projects[projectId] = {
|
|
881
|
+
projectId,
|
|
882
|
+
path: projectPath,
|
|
883
|
+
...(repoIdentity ? { repo: repoIdentity } : {}),
|
|
884
|
+
...(typeof project["defaultBranch"] === "string"
|
|
885
|
+
? { defaultBranch: project["defaultBranch"] }
|
|
886
|
+
: {}),
|
|
887
|
+
source: "migrated",
|
|
888
|
+
registeredAt: Math.floor(Date.now() / 1000),
|
|
889
|
+
displayName: project["name"] ?? projectId,
|
|
890
|
+
...(typeof project["sessionPrefix"] === "string"
|
|
891
|
+
? { sessionPrefix: project["sessionPrefix"] }
|
|
892
|
+
: {}),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
// Write global config atomically
|
|
896
|
+
saveGlobalConfig(newGlobal, targetGlobalPath);
|
|
897
|
+
// Rewrite each old project's local config to flat format.
|
|
898
|
+
// Each old project had its config inside the multi-project file.
|
|
899
|
+
// For single-project configs at the project root: rewrite in place.
|
|
900
|
+
// For multi-project configs: write each project's local config to its path.
|
|
901
|
+
for (const [_projectId, project] of Object.entries(oldProjects)) {
|
|
902
|
+
if (!project["path"])
|
|
903
|
+
continue;
|
|
904
|
+
const projectPath = typeof project["path"] === "string" && project["path"].startsWith("~/")
|
|
905
|
+
? join(homedir(), project["path"].slice(2))
|
|
906
|
+
: project["path"];
|
|
907
|
+
const { name: _name, path: _path, sessionPrefix: _sessionPrefix, ...behaviorFields } = project;
|
|
908
|
+
const localBehaviorFields = behaviorFields;
|
|
909
|
+
// Write flat local config
|
|
910
|
+
const localConfigPath = join(projectPath, basename(oldConfigPath));
|
|
911
|
+
atomicWriteFileSync(localConfigPath, stringify(localBehaviorFields, { indent: 2 }));
|
|
912
|
+
}
|
|
913
|
+
return targetGlobalPath;
|
|
914
|
+
}
|
|
915
|
+
// =============================================================================
|
|
916
|
+
// HELPERS
|
|
917
|
+
// =============================================================================
|
|
918
|
+
/**
|
|
919
|
+
* Build a fresh GlobalConfig with all platform-aware defaults filled in.
|
|
920
|
+
*
|
|
921
|
+
* Single source of truth for "what does a brand-new global config look like?"
|
|
922
|
+
* — used by:
|
|
923
|
+
* - The internal initial-load path here in core (`makeEmptyGlobalConfig`).
|
|
924
|
+
* - `athene config set` (CLI) when no config file exists yet.
|
|
925
|
+
* - `maybePromptForUpdateChannel` (CLI) when persisting the user's channel
|
|
926
|
+
* pick on first run.
|
|
927
|
+
*
|
|
928
|
+
* Critically, `defaults.runtime` is platform-aware via `getDefaultRuntime()`
|
|
929
|
+
* (returns "process" on Windows, "tmux" elsewhere) — hardcoding "tmux" would
|
|
930
|
+
* lock Windows users into a non-functional config.
|
|
931
|
+
*/
|
|
932
|
+
function createDefaultGlobalConfig() {
|
|
933
|
+
return {
|
|
934
|
+
port: 3000,
|
|
935
|
+
readyThresholdMs: 300_000,
|
|
936
|
+
observability: {
|
|
937
|
+
logLevel: "warn",
|
|
938
|
+
stderr: false,
|
|
939
|
+
},
|
|
940
|
+
defaults: {
|
|
941
|
+
runtime: getDefaultRuntime(),
|
|
942
|
+
agent: "claude-code",
|
|
943
|
+
workspace: "worktree",
|
|
944
|
+
notifiers: ["composio", "desktop"],
|
|
945
|
+
},
|
|
946
|
+
projects: {},
|
|
947
|
+
notifiers: {},
|
|
948
|
+
notificationRouting: {
|
|
949
|
+
urgent: ["desktop", "composio"],
|
|
950
|
+
action: ["desktop", "composio"],
|
|
951
|
+
warning: ["composio"],
|
|
952
|
+
info: ["composio"],
|
|
953
|
+
},
|
|
954
|
+
reactions: {},
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/** Internal alias for back-compat with existing callers in this file. */
|
|
958
|
+
function makeEmptyGlobalConfig() {
|
|
959
|
+
return createDefaultGlobalConfig();
|
|
960
|
+
}
|
|
961
|
+
function sanitizeRawGlobalConfig(raw) {
|
|
962
|
+
const projects = raw["projects"];
|
|
963
|
+
if (!projects || typeof projects !== "object") {
|
|
964
|
+
return { changed: false, strippedProjects: [] };
|
|
965
|
+
}
|
|
966
|
+
let changed = false;
|
|
967
|
+
const strippedProjects = [];
|
|
968
|
+
for (const [projectId, value] of Object.entries(projects)) {
|
|
969
|
+
if (!value || typeof value !== "object")
|
|
970
|
+
continue;
|
|
971
|
+
const entry = value;
|
|
972
|
+
const hadLegacyAliases = entry["projectId"] !== projectId ||
|
|
973
|
+
(typeof entry["name"] === "string" && typeof entry["displayName"] !== "string") ||
|
|
974
|
+
typeof entry["repo"] === "string" ||
|
|
975
|
+
(typeof entry["originUrl"] === "string" && entry["repo"] === undefined);
|
|
976
|
+
const result = sanitizeRawGlobalProjectEntry(projectId, value);
|
|
977
|
+
if (result.strippedFieldCount > 0) {
|
|
978
|
+
strippedProjects.push({ projectId, strippedFieldCount: result.strippedFieldCount });
|
|
979
|
+
}
|
|
980
|
+
if (result.strippedFieldCount > 0 || hadLegacyAliases) {
|
|
981
|
+
changed = true;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return { changed, strippedProjects };
|
|
985
|
+
}
|
|
986
|
+
function migrateLegacyGlobalConfigOnLoad(configPath, options) {
|
|
987
|
+
let parsed = readRawGlobalConfig(configPath);
|
|
988
|
+
if (!parsed) {
|
|
989
|
+
return { parsed: null, migrationSummary: null };
|
|
990
|
+
}
|
|
991
|
+
const initialSanitization = sanitizeRawGlobalConfig(parsed);
|
|
992
|
+
if (!initialSanitization.changed) {
|
|
993
|
+
return { parsed, migrationSummary: null };
|
|
994
|
+
}
|
|
995
|
+
let migrationSummary = null;
|
|
996
|
+
const rewrite = () => {
|
|
997
|
+
const freshParsed = readRawGlobalConfig(configPath);
|
|
998
|
+
if (!freshParsed) {
|
|
999
|
+
parsed = null;
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const freshSanitization = sanitizeRawGlobalConfig(freshParsed);
|
|
1003
|
+
parsed = freshParsed;
|
|
1004
|
+
if (!freshSanitization.changed)
|
|
1005
|
+
return;
|
|
1006
|
+
migrationSummary = formatGlobalConfigMigrationLog(freshSanitization);
|
|
1007
|
+
saveGlobalConfig(GlobalConfigSchema.parse(freshParsed), configPath);
|
|
1008
|
+
};
|
|
1009
|
+
if (options.alreadyLocked) {
|
|
1010
|
+
rewrite();
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
withFileLockSync(globalConfigLockPath(configPath), rewrite);
|
|
1014
|
+
}
|
|
1015
|
+
return { parsed, migrationSummary };
|
|
1016
|
+
}
|
|
1017
|
+
function readRawGlobalConfig(configPath) {
|
|
1018
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
1019
|
+
const parsed = parse(raw);
|
|
1020
|
+
if (!parsed || typeof parsed !== "object")
|
|
1021
|
+
return null;
|
|
1022
|
+
return parsed;
|
|
1023
|
+
}
|
|
1024
|
+
function formatGlobalConfigMigrationLog(sanitization) {
|
|
1025
|
+
if (sanitization.strippedProjects.length === 0) {
|
|
1026
|
+
return "[ao] migrated legacy project registry fields in global config";
|
|
1027
|
+
}
|
|
1028
|
+
const totalFieldCount = sanitization.strippedProjects.reduce((sum, project) => sum + project.strippedFieldCount, 0);
|
|
1029
|
+
const projectSummary = sanitization.strippedProjects
|
|
1030
|
+
.map((project) => `${project.projectId} (${project.strippedFieldCount})`)
|
|
1031
|
+
.join(", ");
|
|
1032
|
+
return `[ao] stripped ${totalFieldCount} legacy project registry fields from ${sanitization.strippedProjects.length} project${sanitization.strippedProjects.length === 1 ? "" : "s"}: ${projectSummary}`;
|
|
1033
|
+
}
|
|
1034
|
+
function sanitizeRawGlobalProjectEntry(projectId, entry) {
|
|
1035
|
+
let strippedFieldCount = 0;
|
|
1036
|
+
entry["projectId"] = projectId;
|
|
1037
|
+
if (typeof entry["name"] === "string" && typeof entry["displayName"] !== "string") {
|
|
1038
|
+
entry["displayName"] = entry["name"];
|
|
1039
|
+
}
|
|
1040
|
+
if (typeof entry["originUrl"] === "string" && entry["repo"] === undefined) {
|
|
1041
|
+
const repoIdentity = normalizeRepoIdentity(entry["originUrl"]);
|
|
1042
|
+
if (repoIdentity) {
|
|
1043
|
+
entry["repo"] = repoIdentity;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof entry["repo"] === "string") {
|
|
1047
|
+
const repoIdentity = normalizeLegacyRepoValue(entry["repo"]);
|
|
1048
|
+
if (repoIdentity) {
|
|
1049
|
+
entry["repo"] = repoIdentity;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
delete entry["repo"];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
delete entry["name"];
|
|
1056
|
+
delete entry["originUrl"];
|
|
1057
|
+
for (const key of Object.keys(entry)) {
|
|
1058
|
+
if (GLOBAL_PROJECT_ENTRY_FIELDS.has(key))
|
|
1059
|
+
continue;
|
|
1060
|
+
Reflect.deleteProperty(entry, key);
|
|
1061
|
+
strippedFieldCount += 1;
|
|
1062
|
+
}
|
|
1063
|
+
return { strippedFieldCount };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export { GlobalConfigSchema, GlobalProjectEntrySchema, InstallMethodOverrideSchema, LocalProjectConfigSchema, UpdateChannelSchema, buildEffectiveProjectConfig, createDefaultGlobalConfig, generateExternalId, getGlobalConfigPath, getLocalProjectConfigPath, isCanonicalGlobalConfigPath, isOldConfigFormat, loadGlobalConfig, loadLocalProjectConfig, loadLocalProjectConfigDetailed, migrateToGlobalConfig, registerProjectInGlobalConfig, repairWrappedLocalProjectConfig, resolveProjectIdentity, saveGlobalConfig, writeLocalProjectConfig };
|
|
1067
|
+
//# sourceMappingURL=global-config.js.map
|