amalgm 0.1.138 → 0.1.139
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/bin/amalgm.js +73 -0
- package/lib/auth-store.js +16 -2
- package/lib/cli.js +48 -7
- package/lib/identity-adopt.js +43 -0
- package/lib/layout.js +17 -10
- package/lib/migrate-layout.js +1 -1
- package/lib/state-migration.js +23 -1
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/agents/store.js +8 -2
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +116 -11
- package/runtime/scripts/amalgm-mcp/browser/backend.js +5 -2
- package/runtime/scripts/amalgm-mcp/browser/cli.js +2 -1
- package/runtime/scripts/amalgm-mcp/config.js +26 -5
- package/runtime/scripts/amalgm-mcp/events/desktop-release-runner.js +119 -53
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +2 -2
- package/runtime/scripts/amalgm-mcp/events/npm-release-runner.js +4 -2
- package/runtime/scripts/amalgm-mcp/events/pr-check-runner.js +4 -2
- package/runtime/scripts/amalgm-mcp/fs/rest.js +20 -1
- package/runtime/scripts/amalgm-mcp/lib/layout.js +17 -10
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +2 -2
- package/runtime/scripts/amalgm-mcp/project-context/store.js +27 -5
- package/runtime/scripts/amalgm-mcp/tests/agents-source-of-truth.test.js +13 -0
- package/runtime/scripts/amalgm-mcp/tests/apps-supervisor.test.js +190 -0
- package/runtime/scripts/amalgm-mcp/tests/browser-routing.test.js +2 -3
- package/runtime/scripts/amalgm-mcp/tests/config-migration.test.js +37 -1
- package/runtime/scripts/amalgm-mcp/tests/desktop-release-runner.test.js +51 -0
- package/runtime/scripts/amalgm-mcp/tests/files-resource.test.js +25 -0
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +59 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +6 -1
- package/runtime/scripts/chat-core/contract.js +2 -2
- package/runtime/scripts/chat-core/credentials/store.js +2 -1
- package/runtime/scripts/chat-core/recorder.js +2 -1
- package/runtime/scripts/chat-core/tooling/native-binaries.js +2 -1
- package/runtime/scripts/chat-server/config.js +2 -2
- package/runtime/scripts/credential-adapter.js +2 -2
- package/runtime/scripts/lib/runtime-paths.js +61 -1
- package/runtime/scripts/local-gateway.js +2 -1
- package/runtime/scripts/proxy-token-store.js +7 -4
- package/runtime/scripts/proxy-token-store.test.js +112 -0
|
@@ -39,14 +39,17 @@ function pidIsRunning(pid) {
|
|
|
39
39
|
*/
|
|
40
40
|
function bridgeFileCandidates() {
|
|
41
41
|
const path = require('path');
|
|
42
|
-
const os = require('os');
|
|
43
42
|
const dirs = [];
|
|
44
43
|
if (process.env.AMALGM_RUNTIME_STATE_DIR) dirs.push(process.env.AMALGM_RUNTIME_STATE_DIR);
|
|
45
44
|
const amalgmDir = process.env.AMALGM_DIR;
|
|
46
45
|
const label = process.env.AMALGM_RUNTIME_LABEL || process.env.AMALGM_BRANCH;
|
|
47
46
|
if (amalgmDir && label) dirs.push(path.join(amalgmDir, 'runtimes', label));
|
|
48
47
|
if (amalgmDir) dirs.push(amalgmDir);
|
|
49
|
-
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
dirs.push(require('../../lib/runtime-paths').amalgmDir());
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
50
53
|
return [...new Set(dirs)].map((dir) => path.join(dir, 'electron-browser-bridge.json'));
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -20,12 +20,13 @@ const fs = require('fs');
|
|
|
20
20
|
const os = require('os');
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const { spawn } = require('child_process');
|
|
23
|
+
const { amalgmDir: resolveAmalgmDir } = require('../../lib/runtime-paths');
|
|
23
24
|
|
|
24
25
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
25
26
|
const DEFAULT_SESSION = 'default';
|
|
26
27
|
|
|
27
28
|
function amalgmDir() {
|
|
28
|
-
return
|
|
29
|
+
return resolveAmalgmDir();
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function ensureDir(dir) {
|
|
@@ -9,18 +9,21 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const os = require('os');
|
|
12
|
+
const runtimePaths = require('../lib/runtime-paths');
|
|
12
13
|
|
|
13
14
|
const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
|
|
14
15
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
15
16
|
const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
|
|
17
|
+
const WORKSPACES_BACKFILL_ID = 'workspaces-merge-v1';
|
|
18
|
+
const CONTEXT_BACKFILL_ID = 'context-merge-v1';
|
|
16
19
|
|
|
17
|
-
// Layout-aware home: visible ~/amalgm once initialized
|
|
18
|
-
//
|
|
20
|
+
// Layout-aware home: visible ~/amalgm once initialized. User storage resolves
|
|
21
|
+
// under users/<identity>; the root is only installation/device state.
|
|
19
22
|
const AMALGM_HOME = require('./lib/layout').resolveAmalgmHome(process.env);
|
|
20
|
-
const AMALGM_DIR = process.env.AMALGM_DIR ||
|
|
23
|
+
const AMALGM_DIR = process.env.AMALGM_DIR || runtimePaths.amalgmDir();
|
|
21
24
|
const AMALGM_RUNTIME_LABEL = normalizeRuntimeLabel(process.env.AMALGM_RUNTIME_LABEL || process.env.AMALGM_BRANCH);
|
|
22
25
|
const AMALGM_RUNTIME_STATE_DIR = process.env.AMALGM_RUNTIME_STATE_DIR
|
|
23
|
-
||
|
|
26
|
+
|| runtimePaths.runtimeStateDir();
|
|
24
27
|
|
|
25
28
|
// Storage file/dir layout under AMALGM_DIR.
|
|
26
29
|
const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
|
|
@@ -179,6 +182,8 @@ function migrateLegacyUserState() {
|
|
|
179
182
|
const hadMarker = Boolean(markerData);
|
|
180
183
|
const completedMigrations = markerCompletedMigrations(markerData);
|
|
181
184
|
const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
|
|
185
|
+
const needsWorkspacesBackfill = !completedMigrations.has(WORKSPACES_BACKFILL_ID);
|
|
186
|
+
const needsContextBackfill = !completedMigrations.has(CONTEXT_BACKFILL_ID);
|
|
182
187
|
fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
|
|
183
188
|
|
|
184
189
|
const copied = [];
|
|
@@ -208,7 +213,23 @@ function migrateLegacyUserState() {
|
|
|
208
213
|
completedMigrations.add(CLI_HOMES_BACKFILL_ID);
|
|
209
214
|
}
|
|
210
215
|
|
|
211
|
-
if (
|
|
216
|
+
if (needsWorkspacesBackfill) {
|
|
217
|
+
const workspacesSource = path.join(AMALGM_HOME, 'workspaces');
|
|
218
|
+
const workspacesTarget = path.join(AMALGM_DIR, 'workspaces');
|
|
219
|
+
if (mergeDirContents(workspacesSource, workspacesTarget)) copied.push('workspaces/');
|
|
220
|
+
completedMigrations.add(WORKSPACES_BACKFILL_ID);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (needsContextBackfill) {
|
|
224
|
+
// System context (user-authored system-instructions.md) is the singular
|
|
225
|
+
// context/ dir; earlier passes only migrated per-project state.
|
|
226
|
+
const contextSource = path.join(AMALGM_HOME, 'context');
|
|
227
|
+
const contextTarget = path.join(AMALGM_DIR, 'context');
|
|
228
|
+
if (mergeDirContents(contextSource, contextTarget)) copied.push('context/');
|
|
229
|
+
completedMigrations.add(CONTEXT_BACKFILL_ID);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!hadMarker || copied.length > 0 || needsCliHomesBackfill || needsWorkspacesBackfill || needsContextBackfill) {
|
|
212
233
|
fs.writeFileSync(marker, `${JSON.stringify({
|
|
213
234
|
migrated_at: markerData?.migrated_at || new Date().toISOString(),
|
|
214
235
|
source: AMALGM_HOME,
|
|
@@ -6,6 +6,7 @@ const os = require('os');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
const yaml = require('js-yaml');
|
|
9
|
+
const { resolveAmalgmHome } = require('../lib/layout');
|
|
9
10
|
|
|
10
11
|
const ENGINE_REPO_FULL_NAME = 'amalgm-inc/amalgm-engine';
|
|
11
12
|
const UI_REPO_FULL_NAME = 'amalgm-inc/amalgm-ui';
|
|
@@ -13,8 +14,9 @@ const DEFAULT_DESKTOP_RELEASE_REPO_FULL_NAME = 'amalgm-inc/amalgm-desktop-releas
|
|
|
13
14
|
const DEFAULT_DESKTOP_LEGACY_BRIDGE_REPO_FULL_NAME = ENGINE_REPO_FULL_NAME;
|
|
14
15
|
const DEFAULT_ENGINE_REPO_URL = 'https://github.com/amalgm-inc/amalgm-engine.git';
|
|
15
16
|
const DEFAULT_UI_REPO_URL = 'https://github.com/amalgm-inc/amalgm-ui.git';
|
|
16
|
-
const
|
|
17
|
-
const
|
|
17
|
+
const AMALGM_HOME = resolveAmalgmHome(process.env);
|
|
18
|
+
const DEFAULT_CHECKOUT_ROOT = path.join(AMALGM_HOME, 'workflow-checkouts', 'amalgm-desktop-release');
|
|
19
|
+
const DEFAULT_ENV_FILE = path.join(AMALGM_HOME, 'desktop-release.env');
|
|
18
20
|
const DEFAULT_CSC_NAME = 'amalgm, Inc. (C5S6UATV3L)';
|
|
19
21
|
const DEFAULT_DESKTOP_UPDATE_BASE_URL = 'https://amalgm-desktop-releases.fly.dev';
|
|
20
22
|
const DEFAULT_DESKTOP_RELEASE_FLY_APP = 'amalgm-desktop-releases';
|
|
@@ -119,6 +121,29 @@ function notarytoolCredentialArgs(env = process.env) {
|
|
|
119
121
|
throw new Error('Missing Apple notarization credentials for manual desktop release notarization.');
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function notarytoolSubmitTransportArgs(env = process.env) {
|
|
125
|
+
const raw = cleanString(env.AMALGM_DESKTOP_NOTARY_S3_ACCELERATION).toLowerCase();
|
|
126
|
+
if (!raw || ['0', 'false', 'no', 'off'].includes(raw)) return ['--no-s3-acceleration'];
|
|
127
|
+
if (['1', 'true', 'yes', 'on'].includes(raw)) return [];
|
|
128
|
+
throw new Error(`Unsupported AMALGM_DESKTOP_NOTARY_S3_ACCELERATION value: ${raw}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function prepareNotarySubmissionFile(filePath) {
|
|
132
|
+
const ext = path.extname(filePath) || '.dmg';
|
|
133
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-notary-submit-'));
|
|
134
|
+
const tempPath = path.join(
|
|
135
|
+
tempRoot,
|
|
136
|
+
`amalgm-notary-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString('hex')}${ext}`,
|
|
137
|
+
);
|
|
138
|
+
fs.copyFileSync(filePath, tempPath);
|
|
139
|
+
return {
|
|
140
|
+
filePath: tempPath,
|
|
141
|
+
cleanup() {
|
|
142
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
122
147
|
function parseRepoFullName(value) {
|
|
123
148
|
const raw = cleanString(value);
|
|
124
149
|
const match = /^([^/\s]+)\/([^/\s]+)$/.exec(raw);
|
|
@@ -684,6 +709,13 @@ function envForBuild(
|
|
|
684
709
|
DESKTOP_RELEASE_LANE: request.lane,
|
|
685
710
|
DESKTOP_RELEASE_CHANNEL: request.lane === 'preview' ? 'preview' : 'latest',
|
|
686
711
|
AMALGM_DESKTOP_THIN: desktopThinRuntimeBuildEnabled(baseEnv) ? '1' : '0',
|
|
712
|
+
AMALGM_DESKTOP_RENDERER_DELIVERY: desktopRendererDelivery(baseEnv),
|
|
713
|
+
AMALGM_DESKTOP_RUNTIME_DELIVERY: desktopRuntimeDelivery(baseEnv),
|
|
714
|
+
...(desktopRendererDelivery(baseEnv) === 'bundled-next' ? {
|
|
715
|
+
NEXT_IMAGE_UNOPTIMIZED: '1',
|
|
716
|
+
NEXT_PUBLIC_SUPABASE_URL: '',
|
|
717
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: '',
|
|
718
|
+
} : {}),
|
|
687
719
|
AMALGM_RUNTIME_LABEL: request.lane === 'preview' ? 'preview' : 'main',
|
|
688
720
|
AMALGM_BRANCH: request.lane,
|
|
689
721
|
ELECTRON_VERSION: version,
|
|
@@ -737,6 +769,26 @@ function desktopThinRuntimeBuildEnabled(env = process.env) {
|
|
|
737
769
|
throw new Error(`Unsupported AMALGM_DESKTOP_THIN value: ${raw}`);
|
|
738
770
|
}
|
|
739
771
|
|
|
772
|
+
function desktopRendererDelivery(env = process.env) {
|
|
773
|
+
const raw = cleanString(
|
|
774
|
+
env.AMALGM_DESKTOP_RENDERER_DELIVERY
|
|
775
|
+
|| env.AMALGM_DESKTOP_UI_DELIVERY
|
|
776
|
+
|| (cleanString(env.AMALGM_DESKTOP_UI_MODE).toLowerCase() === 'local' ? 'bundled-next' : ''),
|
|
777
|
+
).toLowerCase();
|
|
778
|
+
if (!raw) return desktopThinRuntimeBuildEnabled(env) ? 'hosted' : 'bundled-next';
|
|
779
|
+
if (['hosted', 'remote', 'web'].includes(raw)) return 'hosted';
|
|
780
|
+
if (['bundled-next', 'bundled', 'local', 'next', 'local-next'].includes(raw)) return 'bundled-next';
|
|
781
|
+
throw new Error(`Unsupported AMALGM_DESKTOP_RENDERER_DELIVERY value: ${raw}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function desktopRuntimeDelivery(env = process.env) {
|
|
785
|
+
const raw = cleanString(env.AMALGM_DESKTOP_RUNTIME_DELIVERY).toLowerCase();
|
|
786
|
+
if (!raw) return desktopThinRuntimeBuildEnabled(env) ? 'npm' : 'bundled-with-npm-handoff';
|
|
787
|
+
if (['npm', 'managed-npm'].includes(raw)) return 'npm';
|
|
788
|
+
if (['bundled-with-npm-handoff', 'bundled', 'local', 'embedded'].includes(raw)) return 'bundled-with-npm-handoff';
|
|
789
|
+
throw new Error(`Unsupported AMALGM_DESKTOP_RUNTIME_DELIVERY value: ${raw}`);
|
|
790
|
+
}
|
|
791
|
+
|
|
740
792
|
function appOriginForLane(lane, env = process.env) {
|
|
741
793
|
if (lane === 'preview') {
|
|
742
794
|
return cleanString(env.AMALGM_DESKTOP_PREVIEW_APP_ORIGIN)
|
|
@@ -749,14 +801,8 @@ function appOriginForLane(lane, env = process.env) {
|
|
|
749
801
|
}
|
|
750
802
|
|
|
751
803
|
function writeBundledWebEnv(engineDir, lane, env = process.env) {
|
|
752
|
-
const supabaseUrl = cleanString(env.NEXT_PUBLIC_SUPABASE_URL);
|
|
753
|
-
const supabaseAnonKey = cleanString(env.NEXT_PUBLIC_SUPABASE_ANON_KEY);
|
|
754
|
-
if (!supabaseUrl) throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL for bundled desktop web app.');
|
|
755
|
-
if (!supabaseAnonKey) throw new Error('Missing NEXT_PUBLIC_SUPABASE_ANON_KEY for bundled desktop web app.');
|
|
756
804
|
const appOrigin = appOriginForLane(lane, env);
|
|
757
805
|
fs.writeFileSync(path.join(engineDir, 'web', '.env'), [
|
|
758
|
-
`NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
|
|
759
|
-
`NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseAnonKey}`,
|
|
760
806
|
`NEXT_PUBLIC_SITE_URL=${appOrigin}`,
|
|
761
807
|
`NEXT_PUBLIC_APP_URL=${appOrigin}`,
|
|
762
808
|
`NEXT_PUBLIC_AMALGM_RUNTIME_LABEL=${lane === 'preview' ? 'preview' : 'main'}`,
|
|
@@ -1015,46 +1061,52 @@ function waitForNotaryDecision(submissionId, artifactName, cwd, env, authArgs) {
|
|
|
1015
1061
|
function submitArtifactForNotarization(filePath, cwd, env) {
|
|
1016
1062
|
const { args: authArgs, credentialType } = notarytoolCredentialArgs(env);
|
|
1017
1063
|
const startedAtMs = Date.now();
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
'
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1064
|
+
const submissionFile = prepareNotarySubmissionFile(filePath);
|
|
1065
|
+
const artifactName = path.basename(submissionFile.filePath);
|
|
1066
|
+
try {
|
|
1067
|
+
const result = runResult('xcrun', [
|
|
1068
|
+
'notarytool',
|
|
1069
|
+
'submit',
|
|
1070
|
+
submissionFile.filePath,
|
|
1071
|
+
...notarytoolSubmitTransportArgs(env),
|
|
1072
|
+
...authArgs,
|
|
1073
|
+
'--no-progress',
|
|
1074
|
+
'--output-format',
|
|
1075
|
+
'json',
|
|
1076
|
+
], {
|
|
1077
|
+
cwd,
|
|
1078
|
+
env,
|
|
1079
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
1080
|
+
});
|
|
1032
1081
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1082
|
+
let submissionId = '';
|
|
1083
|
+
if (cleanString(result.stdout)) {
|
|
1084
|
+
try {
|
|
1085
|
+
submissionId = parseNotarySubmissionId(parseJsonOutput(result.stdout, 'notarytool submit'));
|
|
1086
|
+
} catch {
|
|
1087
|
+
submissionId = '';
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (!submissionId) {
|
|
1091
|
+
submissionId = parseNotarySubmissionId(waitForNotarySubmission(artifactName, startedAtMs, cwd, env, authArgs));
|
|
1092
|
+
}
|
|
1093
|
+
if (!submissionId) {
|
|
1094
|
+
throw new Error(`Failed to resolve notary submission id for ${artifactName}.`);
|
|
1095
|
+
}
|
|
1096
|
+
if (result.status !== 0) {
|
|
1097
|
+
console.warn(`notarytool submit exited with code ${result.status} for ${artifactName}; continuing with submission ${submissionId}.`);
|
|
1039
1098
|
}
|
|
1040
|
-
}
|
|
1041
|
-
if (!submissionId) {
|
|
1042
|
-
submissionId = parseNotarySubmissionId(waitForNotarySubmission(artifactName, startedAtMs, cwd, env, authArgs));
|
|
1043
|
-
}
|
|
1044
|
-
if (!submissionId) {
|
|
1045
|
-
throw new Error(`Failed to resolve notary submission id for ${artifactName}.`);
|
|
1046
|
-
}
|
|
1047
|
-
if (result.status !== 0) {
|
|
1048
|
-
console.warn(`notarytool submit exited with code ${result.status} for ${artifactName}; continuing with submission ${submissionId}.`);
|
|
1049
|
-
}
|
|
1050
1099
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1100
|
+
const info = waitForNotaryDecision(submissionId, artifactName, cwd, env, authArgs);
|
|
1101
|
+
return {
|
|
1102
|
+
id: submissionId,
|
|
1103
|
+
status: parseNotaryStatus(info),
|
|
1104
|
+
credentialType,
|
|
1105
|
+
info,
|
|
1106
|
+
};
|
|
1107
|
+
} finally {
|
|
1108
|
+
submissionFile.cleanup();
|
|
1109
|
+
}
|
|
1058
1110
|
}
|
|
1059
1111
|
|
|
1060
1112
|
function manualNotarizeDesktopArtifacts({ engineDir, lane, env, artifactPaths, channelPath }) {
|
|
@@ -1701,18 +1753,30 @@ function publishDesktopRelease(request, options = {}) {
|
|
|
1701
1753
|
run('git', ['push', 'origin', `refs/tags/v${version}`, '--force'], { cwd: engineDir });
|
|
1702
1754
|
run('npm', ['version', '--no-git-tag-version', '--allow-same-version', version], { cwd: engineDir, env: releaseEnv });
|
|
1703
1755
|
run('npm', ['run', 'compile'], { cwd: engineDir, env: releaseEnv });
|
|
1704
|
-
const
|
|
1756
|
+
const rendererDelivery = desktopRendererDelivery(releaseEnv);
|
|
1757
|
+
const runtimeDelivery = desktopRuntimeDelivery(releaseEnv);
|
|
1758
|
+
const hostedRenderer = rendererDelivery === 'hosted';
|
|
1759
|
+
const npmRuntime = runtimeDelivery === 'npm';
|
|
1705
1760
|
let appOrigin = appOriginForLane(request.lane, releaseEnv);
|
|
1706
|
-
if (
|
|
1707
|
-
|
|
1708
|
-
throw new Error('Missing NEXT_PUBLIC_SUPABASE_URL for thin desktop auth metadata.');
|
|
1709
|
-
}
|
|
1710
|
-
console.log('Building thin hosted-UI desktop shell; skipping bundled runtime and Next.js renderer payload.');
|
|
1761
|
+
if (hostedRenderer) {
|
|
1762
|
+
console.log('Building hosted-UI desktop shell; skipping bundled Next.js renderer payload.');
|
|
1711
1763
|
} else {
|
|
1712
|
-
run('npm', ['run', 'npm:sync'], { cwd: engineDir, env: releaseEnv });
|
|
1713
1764
|
copyUiIntoEngine(uiDir, engineDir);
|
|
1714
1765
|
appOrigin = writeBundledWebEnv(engineDir, request.lane, releaseEnv);
|
|
1715
|
-
run('npm', ['run', 'build:next'], {
|
|
1766
|
+
run('npm', ['run', 'build:next'], {
|
|
1767
|
+
cwd: engineDir,
|
|
1768
|
+
env: {
|
|
1769
|
+
...releaseEnv,
|
|
1770
|
+
NEXT_IMAGE_UNOPTIMIZED: '1',
|
|
1771
|
+
NEXT_PUBLIC_SUPABASE_URL: '',
|
|
1772
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: '',
|
|
1773
|
+
},
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
if (npmRuntime) {
|
|
1777
|
+
console.log('Building desktop shell with managed npm runtime; skipping bundled engine runtime payload.');
|
|
1778
|
+
} else {
|
|
1779
|
+
run('npm', ['run', 'npm:sync'], { cwd: engineDir, env: releaseEnv });
|
|
1716
1780
|
}
|
|
1717
1781
|
if (request.lane === 'preview') run('npm', ['run', 'assets:preview-icon'], { cwd: engineDir, env: releaseEnv });
|
|
1718
1782
|
|
|
@@ -1826,7 +1890,9 @@ module.exports = {
|
|
|
1826
1890
|
envForBuild,
|
|
1827
1891
|
manualDesktopNotarizationEnabled,
|
|
1828
1892
|
notarytoolCredentialArgs,
|
|
1893
|
+
notarytoolSubmitTransportArgs,
|
|
1829
1894
|
parseEnvFile,
|
|
1895
|
+
prepareNotarySubmissionFile,
|
|
1830
1896
|
parseUploadOffsetMismatch,
|
|
1831
1897
|
publishLegacyGithubBridgeRelease,
|
|
1832
1898
|
publishDesktopRelease,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
4
|
const path = require('path');
|
|
5
|
+
const { resolveAmalgmHome } = require('../lib/layout');
|
|
6
6
|
|
|
7
7
|
function findEngineRepoPath() {
|
|
8
8
|
const explicit = process.env.AMALGM_ENGINE_REPO_PATH;
|
|
@@ -137,7 +137,7 @@ function engineNpmPublishWorkflow(repoPath) {
|
|
|
137
137
|
function internalWorkflowSeeds() {
|
|
138
138
|
if (!findEngineRepoPath()) return [];
|
|
139
139
|
const repoPath = process.env.AMALGM_ENGINE_PUBLISH_REPO_PATH
|
|
140
|
-
|| path.join(
|
|
140
|
+
|| path.join(resolveAmalgmHome(process.env), 'workflow-checkouts', 'amalgm-engine-publish');
|
|
141
141
|
|
|
142
142
|
return [
|
|
143
143
|
{
|
|
@@ -4,11 +4,13 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
|
+
const { resolveAmalgmHome } = require('../lib/layout');
|
|
7
8
|
|
|
8
9
|
const ENGINE_REPO_FULL_NAME = 'amalgm-inc/amalgm-engine';
|
|
9
10
|
const DEFAULT_ENGINE_REPO_URL = 'https://github.com/amalgm-inc/amalgm-engine.git';
|
|
10
|
-
const
|
|
11
|
-
const
|
|
11
|
+
const AMALGM_HOME = resolveAmalgmHome(process.env);
|
|
12
|
+
const DEFAULT_CHECKOUT_ROOT = path.join(AMALGM_HOME, 'workflow-checkouts', 'amalgm-npm-release');
|
|
13
|
+
const DEFAULT_ENV_FILE = path.join(AMALGM_HOME, 'npm-release.env');
|
|
12
14
|
|
|
13
15
|
function cleanString(value) {
|
|
14
16
|
return typeof value === 'string' ? value.trim() : '';
|
|
@@ -4,13 +4,15 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
|
+
const { resolveAmalgmHome } = require('../lib/layout');
|
|
7
8
|
|
|
8
9
|
const ENGINE_REPO_FULL_NAME = 'amalgm-inc/amalgm-engine';
|
|
9
10
|
const UI_REPO_FULL_NAME = 'amalgm-inc/amalgm-ui';
|
|
10
11
|
const DEFAULT_ENGINE_REPO_URL = 'https://github.com/amalgm-inc/amalgm-engine.git';
|
|
11
12
|
const DEFAULT_UI_REPO_URL = 'https://github.com/amalgm-inc/amalgm-ui.git';
|
|
12
|
-
const
|
|
13
|
-
const
|
|
13
|
+
const AMALGM_HOME = resolveAmalgmHome(process.env);
|
|
14
|
+
const DEFAULT_CHECKOUT_ROOT = path.join(AMALGM_HOME, 'workflow-checkouts', 'amalgm-pr-check');
|
|
15
|
+
const DEFAULT_ENV_FILE = path.join(AMALGM_HOME, 'pr-check.env');
|
|
14
16
|
const DEFAULT_STATUS_CONTEXT = 'amalgm/pr-check';
|
|
15
17
|
|
|
16
18
|
function cleanString(value) {
|
|
@@ -61,8 +61,26 @@ function isHiddenFilesystemEntry(name) {
|
|
|
61
61
|
|
|
62
62
|
function expandHome(targetPath) {
|
|
63
63
|
if (targetPath === '~') return os.homedir();
|
|
64
|
+
// Platform-data token: clients address the user's amalgm data dir without
|
|
65
|
+
// knowing the on-disk layout (email folder, machine differences). This is
|
|
66
|
+
// the contract for uploads and md-library paths from the UI.
|
|
67
|
+
if (targetPath === '~amalgm') return AMALGM_DIR;
|
|
68
|
+
if (typeof targetPath === 'string' && targetPath.startsWith('~amalgm/')) {
|
|
69
|
+
return path.join(AMALGM_DIR, targetPath.slice('~amalgm/'.length));
|
|
70
|
+
}
|
|
64
71
|
if (typeof targetPath === 'string' && targetPath.startsWith('~/')) {
|
|
65
|
-
|
|
72
|
+
const expanded = path.join(os.homedir(), targetPath.slice(2));
|
|
73
|
+
// Transitional wire-compat: UI builds deployed before 2026-07-03 address
|
|
74
|
+
// platform data via the old hidden layout (uploads, notes-files,
|
|
75
|
+
// browser-sessions, ...). Redirect the whole legacy prefix into the user
|
|
76
|
+
// data dir — the wire-level equivalent of the removed ~/.amalgm symlink —
|
|
77
|
+
// so the hidden dir is never recreated on disk. Remove once all deployed
|
|
78
|
+
// UIs send ~amalgm/... paths.
|
|
79
|
+
const legacyHiddenRoot = path.join(os.homedir(), '.amalgm');
|
|
80
|
+
if (expanded === legacyHiddenRoot || expanded.startsWith(legacyHiddenRoot + path.sep)) {
|
|
81
|
+
return path.join(AMALGM_DIR, path.relative(legacyHiddenRoot, expanded));
|
|
82
|
+
}
|
|
83
|
+
return expanded;
|
|
66
84
|
}
|
|
67
85
|
return targetPath;
|
|
68
86
|
}
|
|
@@ -772,6 +790,7 @@ module.exports = {
|
|
|
772
790
|
handleWrite,
|
|
773
791
|
_private: {
|
|
774
792
|
buildFilesResource,
|
|
793
|
+
expandHome,
|
|
775
794
|
buildUploadsResource,
|
|
776
795
|
configuredRootPaths,
|
|
777
796
|
filesResourceName,
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
* Rules:
|
|
17
17
|
* - The folder name is presentation; `.amalgm-user.json` is identity. If a
|
|
18
18
|
* folder is renamed, resolution recovers by scanning user manifests.
|
|
19
|
-
* -
|
|
20
|
-
* resolution
|
|
21
|
-
*
|
|
19
|
+
* - Hidden-root layouts are migrated to the visible root by startup preflight;
|
|
20
|
+
* resolution itself does not silently stay on ~/.amalgm.
|
|
21
|
+
* - UUID-named user folders are compatibility aliases only after email
|
|
22
|
+
* adoption. The runtime identity remains the userId in the manifest.
|
|
22
23
|
* - Explicit env (AMALGM_HOME / AMALGM_DIR) always wins — isolated runtimes
|
|
23
24
|
* and tests depend on that.
|
|
24
25
|
*/
|
|
@@ -101,16 +102,14 @@ function isAmalgmRoot(dir) {
|
|
|
101
102
|
/**
|
|
102
103
|
* Where Amalgm state lives on this machine.
|
|
103
104
|
*
|
|
104
|
-
* Order: explicit AMALGM_HOME →
|
|
105
|
-
*
|
|
106
|
-
*
|
|
105
|
+
* Order: explicit AMALGM_HOME → visible root. A migrated ~/.amalgm symlink is
|
|
106
|
+
* allowed only as a pointer back to the visible root; an old hidden directory
|
|
107
|
+
* is intentionally ignored so new process trees don't keep depending on it.
|
|
107
108
|
*/
|
|
108
109
|
function resolveAmalgmHome(env = process.env) {
|
|
109
110
|
if (env.AMALGM_HOME) return path.resolve(env.AMALGM_HOME);
|
|
110
111
|
|
|
111
112
|
const visible = visibleRootPath();
|
|
112
|
-
if (isAmalgmRoot(visible)) return visible;
|
|
113
|
-
|
|
114
113
|
const hidden = hiddenRootPath();
|
|
115
114
|
const hiddenStat = lstatOrNull(hidden);
|
|
116
115
|
if (hiddenStat?.isSymbolicLink()) {
|
|
@@ -121,7 +120,6 @@ function resolveAmalgmHome(env = process.env) {
|
|
|
121
120
|
return visible;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
|
-
if (hiddenStat) return hidden;
|
|
125
123
|
return visible;
|
|
126
124
|
}
|
|
127
125
|
|
|
@@ -186,6 +184,14 @@ function writeUserManifest(userDir, { userId, email }) {
|
|
|
186
184
|
return manifest;
|
|
187
185
|
}
|
|
188
186
|
|
|
187
|
+
function createCompatLink(linkPath, targetPath) {
|
|
188
|
+
if (process.platform === 'win32') {
|
|
189
|
+
fs.symlinkSync(targetPath, linkPath, 'junction');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
fs.symlinkSync(path.basename(targetPath), linkPath);
|
|
193
|
+
}
|
|
194
|
+
|
|
189
195
|
/** All user folders under <homeDir>/users. Real directories first, compat
|
|
190
196
|
* symlinks after, both sorted — so scans prefer canonical folders. */
|
|
191
197
|
function listUserEntries(homeDir) {
|
|
@@ -299,7 +305,7 @@ function adoptUserEmail(homeDir, { userId, email }) {
|
|
|
299
305
|
}
|
|
300
306
|
|
|
301
307
|
fs.renameSync(current, target);
|
|
302
|
-
|
|
308
|
+
createCompatLink(current, target);
|
|
303
309
|
indexUserFolder(homeDir, folderName, { userId: cleanId, email });
|
|
304
310
|
return { dir: target, renamed: true, from: current };
|
|
305
311
|
}
|
|
@@ -321,6 +327,7 @@ module.exports = {
|
|
|
321
327
|
emailFolderName,
|
|
322
328
|
ensureRootManifest,
|
|
323
329
|
ensureUserDir,
|
|
330
|
+
createCompatLink,
|
|
324
331
|
findUserDirByUserId,
|
|
325
332
|
hiddenRootPath,
|
|
326
333
|
isAmalgmRoot,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const os = require('os');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
|
|
5
4
|
const { buildSessionConfig, getMcpApp } = require('../../../lib/mcpApps/registry.js');
|
|
5
|
+
const { amalgmDir } = require('../../lib/runtime-paths');
|
|
6
6
|
|
|
7
|
-
const CONNECTIONS_FILE = path.join(
|
|
7
|
+
const CONNECTIONS_FILE = path.join(process.env.AMALGM_DIR || amalgmDir(), 'mcp-connections.json');
|
|
8
8
|
|
|
9
9
|
function readConnectionsFile() {
|
|
10
10
|
try {
|
|
@@ -155,15 +155,37 @@ function resolveProject(inputPath) {
|
|
|
155
155
|
|
|
156
156
|
function projectFromInput(input) {
|
|
157
157
|
if (input && typeof input === 'object' && cleanString(input.path)) {
|
|
158
|
+
// Workspace records must go through the same path canonicalization as
|
|
159
|
+
// string inputs. Boot/list passes feed raw row paths here; without this,
|
|
160
|
+
// a legacy managed-copy path and its canonical checkout fight over the
|
|
161
|
+
// same project row (each pass re-pins projectPath to its own view).
|
|
162
|
+
const canonical = canonicalProjectPath(input.path) || path.resolve(input.path);
|
|
158
163
|
return {
|
|
159
164
|
id: cleanString(input.id) || null,
|
|
160
|
-
path:
|
|
161
|
-
name: cleanString(input.name) || path.basename(
|
|
165
|
+
path: canonical,
|
|
166
|
+
name: cleanString(input.name) || path.basename(canonical),
|
|
162
167
|
};
|
|
163
168
|
}
|
|
164
169
|
return resolveProject(input);
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
function unregisteredProjectError(inputPath) {
|
|
173
|
+
const requested = cleanString(typeof inputPath === 'string' ? inputPath : inputPath?.path) || '(no path)';
|
|
174
|
+
let known = [];
|
|
175
|
+
try {
|
|
176
|
+
known = require('../workspace/store')
|
|
177
|
+
.listWorkspaces()
|
|
178
|
+
.map((workspace) => workspace?.path)
|
|
179
|
+
.filter(Boolean);
|
|
180
|
+
} catch {
|
|
181
|
+
// Listing is best-effort; the error is still actionable without it.
|
|
182
|
+
}
|
|
183
|
+
const hint = known.length
|
|
184
|
+
? ` Registered project roots: ${known.join(', ')}.`
|
|
185
|
+
: ' No workspaces are registered on this computer.';
|
|
186
|
+
return new Error(`No registered Amalgm project contains: ${requested}.${hint} Register the checkout as a workspace or pass projectPath explicitly.`);
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
function atomicWrite(filePath, content) {
|
|
168
190
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
169
191
|
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
@@ -481,7 +503,7 @@ function listProjectContexts(options = {}) {
|
|
|
481
503
|
function appendChangeLog(inputPath, body, options = {}) {
|
|
482
504
|
const project = projectFromInput(inputPath);
|
|
483
505
|
if (!project?.id) {
|
|
484
|
-
throw
|
|
506
|
+
throw unregisteredProjectError(inputPath);
|
|
485
507
|
}
|
|
486
508
|
if (isSystemProject(project)) {
|
|
487
509
|
throw new Error('The system context has system-instructions.md only — change logs are per-project.');
|
|
@@ -511,7 +533,7 @@ function appendChangeLog(inputPath, body, options = {}) {
|
|
|
511
533
|
function writeContextDoc(inputPath, docKey, content, options = {}) {
|
|
512
534
|
const project = projectFromInput(inputPath);
|
|
513
535
|
if (!project?.id) {
|
|
514
|
-
throw
|
|
536
|
+
throw unregisteredProjectError(inputPath);
|
|
515
537
|
}
|
|
516
538
|
if (isSystemProject(project) && docKey !== 'instructions') {
|
|
517
539
|
throw new Error('The system context has system-instructions.md only — project goals, project state, and change logs are per-project.');
|
|
@@ -599,7 +621,7 @@ function upsertNoteSection(current, title, body) {
|
|
|
599
621
|
function upsertProjectNote(inputPath, title, body, options = {}) {
|
|
600
622
|
const project = projectFromInput(inputPath);
|
|
601
623
|
if (!project?.id) {
|
|
602
|
-
throw
|
|
624
|
+
throw unregisteredProjectError(inputPath);
|
|
603
625
|
}
|
|
604
626
|
if (isSystemProject(project)) {
|
|
605
627
|
throw new Error('The system context has system-instructions.md only — project notes are per-project.');
|
|
@@ -120,3 +120,16 @@ test('agents create REST preserves caller id and creates per-agent config', asyn
|
|
|
120
120
|
assert.equal(response.payload.agent.config.runtimeHomeLayout, 'per-agent');
|
|
121
121
|
assert.equal(getAgentConfig(agentId).runtimeHomeLayout, 'per-agent');
|
|
122
122
|
});
|
|
123
|
+
|
|
124
|
+
test('freshly seeded builtin agents carry the default platform loadout including memories', () => {
|
|
125
|
+
const { DEFAULT_BUILTIN_TOOL_IDS } = require('../agents/store');
|
|
126
|
+
assert.ok(DEFAULT_BUILTIN_TOOL_IDS.includes('memories'));
|
|
127
|
+
for (const id of ['claude_code', 'codex']) {
|
|
128
|
+
const agent = getAllAgentsWithBuiltins().find((candidate) => candidate.id === id);
|
|
129
|
+
assert.ok(agent, `${id} should exist`);
|
|
130
|
+
assert.ok(
|
|
131
|
+
agent.loadout.toolIds.includes('memories'),
|
|
132
|
+
`${id} loadout should include memories, got: ${JSON.stringify(agent.loadout.toolIds)}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
});
|