amalgm 0.1.134 → 0.1.135

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.
@@ -10,6 +10,7 @@ const {
10
10
  } = require('./paths');
11
11
 
12
12
  const MARKER_FILE = path.join(AMALGM_DIR, '.user-state-migrated.json');
13
+ const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
13
14
 
14
15
  const DATA_FILES = [
15
16
  'amalgm.db',
@@ -75,34 +76,100 @@ function copyDirIfMissing(source, target) {
75
76
  return true;
76
77
  }
77
78
 
79
+ function copyEntryIfMissing(source, target) {
80
+ if (!fs.existsSync(source) || fs.existsSync(target)) return false;
81
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
82
+ fs.cpSync(source, target, {
83
+ recursive: fs.statSync(source).isDirectory(),
84
+ force: false,
85
+ errorOnExist: true,
86
+ dereference: false,
87
+ });
88
+ return true;
89
+ }
90
+
91
+ function mergeDirContents(source, target) {
92
+ if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) return false;
93
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
94
+
95
+ let changed = false;
96
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
97
+ const sourcePath = path.join(source, entry.name);
98
+ const targetPath = path.join(target, entry.name);
99
+
100
+ if (copyEntryIfMissing(sourcePath, targetPath)) {
101
+ changed = true;
102
+ continue;
103
+ }
104
+
105
+ if (entry.isDirectory() && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
106
+ if (mergeDirContents(sourcePath, targetPath)) changed = true;
107
+ }
108
+ }
109
+
110
+ return changed;
111
+ }
112
+
113
+ function markerCopiedEntries(marker) {
114
+ return Array.isArray(marker?.copied)
115
+ ? marker.copied.filter((entry) => typeof entry === 'string' && entry)
116
+ : [];
117
+ }
118
+
119
+ function markerCompletedMigrations(marker) {
120
+ return new Set(
121
+ Array.isArray(marker?.completed_migrations)
122
+ ? marker.completed_migrations.filter((entry) => typeof entry === 'string' && entry)
123
+ : [],
124
+ );
125
+ }
126
+
78
127
  function migrateLegacyUserState() {
79
128
  if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return { migrated: false, reason: 'legacy-root' };
80
129
  if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return { migrated: false, reason: 'temp-dir' };
81
- if (readJson(MARKER_FILE, null)) return { migrated: false, reason: 'already-migrated' };
130
+
131
+ const marker = readJson(MARKER_FILE, null);
132
+ const hadMarker = Boolean(marker);
133
+ const completedMigrations = markerCompletedMigrations(marker);
134
+ const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
82
135
 
83
136
  fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
84
137
 
85
138
  const copied = [];
86
- for (const fileName of DATA_FILES) {
87
- const source = path.join(AMALGM_HOME, fileName);
88
- const target = path.join(AMALGM_DIR, fileName);
89
- if (copyFileIfUseful(source, target)) copied.push(fileName);
139
+ if (!hadMarker) {
140
+ for (const fileName of DATA_FILES) {
141
+ const source = path.join(AMALGM_HOME, fileName);
142
+ const target = path.join(AMALGM_DIR, fileName);
143
+ if (copyFileIfUseful(source, target)) copied.push(fileName);
144
+ }
145
+
146
+ for (const dirName of DATA_DIRS) {
147
+ const source = path.join(AMALGM_HOME, dirName);
148
+ const target = path.join(AMALGM_DIR, dirName);
149
+ if (copyDirIfMissing(source, target)) copied.push(`${dirName}/`);
150
+ }
90
151
  }
91
152
 
92
- for (const dirName of DATA_DIRS) {
93
- const source = path.join(AMALGM_HOME, dirName);
94
- const target = path.join(AMALGM_DIR, dirName);
95
- if (copyDirIfMissing(source, target)) copied.push(`${dirName}/`);
153
+ if (needsCliHomesBackfill) {
154
+ const cliHomesSource = path.join(AMALGM_HOME, 'cli-homes');
155
+ const cliHomesTarget = path.join(AMALGM_DIR, 'cli-homes');
156
+ if (mergeDirContents(cliHomesSource, cliHomesTarget)) copied.push('cli-homes/');
157
+ completedMigrations.add(CLI_HOMES_BACKFILL_ID);
96
158
  }
97
159
 
98
- writeJson(MARKER_FILE, {
99
- migrated_at: new Date().toISOString(),
100
- source: AMALGM_HOME,
101
- target: AMALGM_DIR,
102
- copied,
103
- });
160
+ if (!hadMarker || copied.length > 0 || needsCliHomesBackfill) {
161
+ writeJson(MARKER_FILE, {
162
+ migrated_at: marker?.migrated_at || new Date().toISOString(),
163
+ source: AMALGM_HOME,
164
+ target: AMALGM_DIR,
165
+ copied: [...new Set([...markerCopiedEntries(marker), ...copied])],
166
+ completed_migrations: Array.from(completedMigrations).sort(),
167
+ });
168
+ }
104
169
 
105
- return { migrated: copied.length > 0, copied };
170
+ if (copied.length > 0) return { migrated: true, copied };
171
+ if (hadMarker) return { migrated: false, reason: 'already-migrated' };
172
+ return { migrated: false, copied: [] };
106
173
  }
107
174
 
108
175
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.134",
3
+ "version": "0.1.135",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -12,6 +12,7 @@ const os = require('os');
12
12
 
13
13
  const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
14
14
  const MCP_PROTOCOL_VERSION = '2024-11-05';
15
+ const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
15
16
 
16
17
  const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
17
18
  const AMALGM_HOME = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
@@ -112,25 +113,108 @@ function copyIfUseful(source, target) {
112
113
  fs.copyFileSync(source, target);
113
114
  }
114
115
 
116
+ function copyEntryIfMissing(source, target) {
117
+ if (!fs.existsSync(source) || fs.existsSync(target)) return false;
118
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
119
+ fs.cpSync(source, target, {
120
+ recursive: fs.statSync(source).isDirectory(),
121
+ force: false,
122
+ errorOnExist: true,
123
+ dereference: false,
124
+ });
125
+ return true;
126
+ }
127
+
128
+ function mergeDirContents(source, target) {
129
+ if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) return false;
130
+ fs.mkdirSync(target, { recursive: true, mode: 0o700 });
131
+
132
+ let changed = false;
133
+ for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
134
+ const sourcePath = path.join(source, entry.name);
135
+ const targetPath = path.join(target, entry.name);
136
+
137
+ if (copyEntryIfMissing(sourcePath, targetPath)) {
138
+ changed = true;
139
+ continue;
140
+ }
141
+
142
+ if (entry.isDirectory() && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
143
+ if (mergeDirContents(sourcePath, targetPath)) changed = true;
144
+ }
145
+ }
146
+
147
+ return changed;
148
+ }
149
+
150
+ function readJson(file, fallback = null) {
151
+ try {
152
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
153
+ } catch {
154
+ return fallback;
155
+ }
156
+ }
157
+
158
+ function markerCopiedEntries(marker) {
159
+ return Array.isArray(marker?.copied)
160
+ ? marker.copied.filter((entry) => typeof entry === 'string' && entry)
161
+ : [];
162
+ }
163
+
164
+ function markerCompletedMigrations(marker) {
165
+ return new Set(
166
+ Array.isArray(marker?.completed_migrations)
167
+ ? marker.completed_migrations.filter((entry) => typeof entry === 'string' && entry)
168
+ : [],
169
+ );
170
+ }
171
+
115
172
  function migrateLegacyUserState() {
116
173
  if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return;
117
174
  if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return;
118
175
  const marker = path.join(AMALGM_DIR, '.runtime-legacy-state-migrated.json');
119
- if (fs.existsSync(marker)) return;
176
+ const markerData = readJson(marker, null);
177
+ const hadMarker = Boolean(markerData);
178
+ const completedMigrations = markerCompletedMigrations(markerData);
179
+ const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
120
180
  fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
121
- for (const fileName of [
122
- 'amalgm.db',
123
- 'apps.json',
124
- 'artifacts.json',
125
- 'agents.json',
126
- 'tasks.json',
127
- 'event-triggers.json',
128
- 'workflows.json',
129
- 'mcp-connections.json',
130
- ]) {
131
- copyIfUseful(path.join(AMALGM_HOME, fileName), path.join(AMALGM_DIR, fileName));
181
+
182
+ const copied = [];
183
+ if (!hadMarker) {
184
+ for (const fileName of [
185
+ 'amalgm.db',
186
+ 'apps.json',
187
+ 'artifacts.json',
188
+ 'agents.json',
189
+ 'tasks.json',
190
+ 'event-triggers.json',
191
+ 'workflows.json',
192
+ 'mcp-connections.json',
193
+ ]) {
194
+ const source = path.join(AMALGM_HOME, fileName);
195
+ const target = path.join(AMALGM_DIR, fileName);
196
+ const targetExists = fs.existsSync(target);
197
+ copyIfUseful(source, target);
198
+ if (!targetExists && fs.existsSync(target)) copied.push(fileName);
199
+ }
200
+ }
201
+
202
+ if (needsCliHomesBackfill) {
203
+ const cliHomesSource = path.join(AMALGM_HOME, 'cli-homes');
204
+ const cliHomesTarget = path.join(AMALGM_DIR, 'cli-homes');
205
+ if (mergeDirContents(cliHomesSource, cliHomesTarget)) copied.push('cli-homes/');
206
+ completedMigrations.add(CLI_HOMES_BACKFILL_ID);
207
+ }
208
+
209
+ if (!hadMarker || copied.length > 0 || needsCliHomesBackfill) {
210
+ fs.writeFileSync(marker, `${JSON.stringify({
211
+ migrated_at: markerData?.migrated_at || new Date().toISOString(),
212
+ source: AMALGM_HOME,
213
+ target: AMALGM_DIR,
214
+ copied: [...new Set([...markerCopiedEntries(markerData), ...copied])],
215
+ completed_migrations: Array.from(completedMigrations).sort(),
216
+ }, null, 2)}\n`);
132
217
  }
133
- fs.writeFileSync(marker, `${JSON.stringify({ migrated_at: new Date().toISOString(), source: AMALGM_HOME }, null, 2)}\n`);
134
218
  }
135
219
 
136
220
  migrateLegacyUserState();
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const ORIGINAL_ENV = { ...process.env };
10
+ const SCRIPT_DIR = path.resolve(__dirname, '..');
11
+ const TEST_TMP_DIR = path.join(__dirname, '.tmp');
12
+
13
+ function resetEnv() {
14
+ for (const key of Object.keys(process.env)) {
15
+ if (key.startsWith('AMALGM_') || key === 'HOME') delete process.env[key];
16
+ }
17
+ Object.assign(process.env, ORIGINAL_ENV);
18
+ }
19
+
20
+ function clearScriptModules() {
21
+ for (const key of Object.keys(require.cache)) {
22
+ if (key.startsWith(SCRIPT_DIR)) delete require.cache[key];
23
+ }
24
+ }
25
+
26
+ function loadConfig(root, scope = 'user-a') {
27
+ resetEnv();
28
+ const home = path.join(root, '.amalgm');
29
+ const userDir = path.join(home, 'users', scope);
30
+ process.env.HOME = root;
31
+ process.env.AMALGM_HOME = home;
32
+ process.env.AMALGM_DIR = userDir;
33
+ clearScriptModules();
34
+ return require('../config');
35
+ }
36
+
37
+ function readJson(file) {
38
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
39
+ }
40
+
41
+ test.after(() => {
42
+ resetEnv();
43
+ clearScriptModules();
44
+ });
45
+
46
+ test('runtime config backfills legacy cli homes after the runtime marker already exists', () => {
47
+ fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
48
+ const root = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'amalgm-runtime-config-'));
49
+ try {
50
+ const home = path.join(root, '.amalgm');
51
+ const userDir = path.join(home, 'users', 'user-a');
52
+ const legacyRoot = path.join(home, 'cli-homes', 'claude_code', 'home');
53
+ const targetRoot = path.join(userDir, 'cli-homes', 'claude_code', 'home');
54
+ const markerPath = path.join(userDir, '.runtime-legacy-state-migrated.json');
55
+
56
+ const legacySession = path.join(legacyRoot, 'sessions', '2026', '06', '15', 'rollout-old.jsonl');
57
+ const targetSession = path.join(targetRoot, 'sessions', '2026', '06', '15', 'rollout-new.jsonl');
58
+ const legacySettings = path.join(legacyRoot, 'settings.json');
59
+ const targetSettings = path.join(targetRoot, 'settings.json');
60
+
61
+ fs.mkdirSync(path.dirname(legacySession), { recursive: true });
62
+ fs.mkdirSync(path.dirname(targetSession), { recursive: true });
63
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
64
+ fs.writeFileSync(legacySession, 'old session\n');
65
+ fs.writeFileSync(targetSession, 'new session\n');
66
+ fs.writeFileSync(legacySettings, '{"source":"legacy"}\n');
67
+ fs.writeFileSync(targetSettings, '{"source":"target"}\n');
68
+ fs.writeFileSync(markerPath, `${JSON.stringify({
69
+ migrated_at: '2026-05-26T02:00:41.477Z',
70
+ source: home,
71
+ }, null, 2)}\n`);
72
+
73
+ const config = loadConfig(root);
74
+
75
+ assert.equal(config.AMALGM_DIR, userDir);
76
+ assert.equal(
77
+ fs.readFileSync(path.join(targetRoot, 'sessions', '2026', '06', '15', 'rollout-old.jsonl'), 'utf8'),
78
+ 'old session\n',
79
+ );
80
+ assert.equal(fs.readFileSync(targetSession, 'utf8'), 'new session\n');
81
+ assert.equal(fs.readFileSync(targetSettings, 'utf8'), '{"source":"target"}\n');
82
+
83
+ const marker = readJson(markerPath);
84
+ assert.ok(marker.copied.includes('cli-homes/'));
85
+ assert.ok(marker.completed_migrations.includes('cli-homes-merge-v1'));
86
+ } finally {
87
+ fs.rmSync(root, { recursive: true, force: true });
88
+ }
89
+ });
@@ -109,9 +109,8 @@ function providerAuthIdentity(harness) {
109
109
  return `${harness || 'provider'}:provider-default`;
110
110
  }
111
111
 
112
- function runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout }) {
112
+ function runtimeHome({ amalgmDir, harness, agentConfigId }) {
113
113
  const harnessHome = path.join(amalgmDir, 'cli-homes', safePathSegment(harness));
114
- if (runtimeHomeLayout === 'legacy') return path.join(harnessHome, 'home');
115
114
  return path.join(
116
115
  harnessHome,
117
116
  'agents',
@@ -172,7 +171,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
172
171
  } else if (method === 'provider_auth') {
173
172
  tokenFingerprint = providerAuthFingerprint(harness);
174
173
  }
175
- const runtimeHomePath = runtimeHome({ amalgmDir, harness, agentConfigId, runtimeHomeLayout });
174
+ const runtimeHomePath = runtimeHome({ amalgmDir, harness, agentConfigId });
176
175
  return {
177
176
  method,
178
177
  baseUrl,
@@ -141,6 +141,19 @@ test('provided CLI home path cannot move an agent off its canonical home', () =>
141
141
  assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/agents/codex/home');
142
142
  });
143
143
 
144
+ test('legacy runtime-home requests still resolve to the canonical per-agent home', () => {
145
+ const envelope = authEnvelope({
146
+ harness: 'codex',
147
+ authMethod: 'provider_auth',
148
+ sessionId: 'session-test',
149
+ amalgmDir: '/tmp/amalgm-test',
150
+ agentConfigId: 'legacy-agent',
151
+ runtimeHomeLayout: 'legacy',
152
+ });
153
+
154
+ assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/agents/legacy-agent/home');
155
+ });
156
+
144
157
  test('per-agent auth envelope exposes legacy homes as match sources', () => {
145
158
  const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-home-sources-'));
146
159
  try {
@@ -57,7 +57,7 @@ function agentConfig(instructions) {
57
57
  return {
58
58
  version: 1,
59
59
  agentId: 'codex',
60
- runtimeHomeLayout: 'legacy',
60
+ runtimeHomeLayout: 'per-agent',
61
61
  instructions,
62
62
  skills: [],
63
63
  loadout: { toolIds: [] },