@ericsanchezok/meta-synergy 1.1.26 → 1.2.18
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/README.md +47 -0
- package/install +444 -0
- package/package.json +7 -6
- package/script/build.ts +93 -0
- package/src/cli-backend.ts +118 -16
- package/src/cli.ts +178 -33
- package/src/control/schema.ts +25 -0
- package/src/display.ts +70 -0
- package/src/exec/process-registry.ts +14 -5
- package/src/holos/auth.ts +183 -0
- package/src/holos/login.ts +148 -8
- package/src/inbound/handler.ts +20 -9
- package/src/index.ts +1 -0
- package/src/migration/index.ts +22 -0
- package/src/migration/types.ts +5 -0
- package/src/owner-registry.ts +162 -0
- package/src/runtime.ts +283 -33
- package/src/service.ts +169 -77
- package/src/state/migration.ts +19 -0
- package/src/state/store.ts +53 -7
- package/src/types.ts +8 -0
- package/test/cli-backend-auth.test.ts +86 -0
- package/test/cli-backend-mode.test.ts +49 -0
- package/test/control-socket.test.ts +123 -0
- package/test/holos-auth.test.ts +117 -0
- package/test/migration.test.ts +58 -0
- package/test/runtime-managed-mode.test.ts +111 -0
- package/script/publish.ts +0 -38
package/src/service.ts
CHANGED
|
@@ -3,38 +3,37 @@ import { closeSync, openSync } from "node:fs"
|
|
|
3
3
|
import { MetaSynergyControlClient } from "./control/client"
|
|
4
4
|
import type { MetaSynergyLogsPayload, MetaSynergyServiceSnapshot } from "./control/schema"
|
|
5
5
|
import { Platform } from "./platform"
|
|
6
|
-
import { MetaSynergyStore } from "./state/store"
|
|
6
|
+
import { MetaSynergyStore, type MetaSynergyState } from "./state/store"
|
|
7
7
|
import { MetaSynergyLocalService } from "./service/local"
|
|
8
8
|
|
|
9
|
+
interface MetaSynergyLaunchContext {
|
|
10
|
+
launcherPath: string
|
|
11
|
+
invocationEntry?: string
|
|
12
|
+
printLogs?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PersistedServiceStateUpdate {
|
|
16
|
+
desiredState?: MetaSynergyState["service"]["desiredState"]
|
|
17
|
+
runtimeStatus?: MetaSynergyState["service"]["runtimeStatus"]
|
|
18
|
+
pid?: number
|
|
19
|
+
printLogs?: boolean
|
|
20
|
+
startedAt?: number
|
|
21
|
+
stoppedAt?: number
|
|
22
|
+
lastExitAt?: number
|
|
23
|
+
logPath?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
export namespace MetaSynergyService {
|
|
10
27
|
export async function status(): Promise<MetaSynergyServiceSnapshot> {
|
|
11
28
|
if (await MetaSynergyControlClient.isAvailable()) {
|
|
12
29
|
return await MetaSynergyControlClient.request({ action: "service.status" })
|
|
13
30
|
}
|
|
14
31
|
|
|
15
|
-
const state = await
|
|
16
|
-
|
|
17
|
-
if (state.service.runtimeStatus !== (running ? "running" : "stopped") || (state.service.pid && !running)) {
|
|
18
|
-
state.service.runtimeStatus = running ? "running" : "stopped"
|
|
19
|
-
if (!running) {
|
|
20
|
-
state.service.pid = undefined
|
|
21
|
-
}
|
|
22
|
-
await MetaSynergyStore.saveState(state)
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
desiredState: state.service.desiredState,
|
|
26
|
-
runtimeStatus: state.service.runtimeStatus,
|
|
27
|
-
running,
|
|
28
|
-
pid: state.service.pid,
|
|
29
|
-
startedAt: state.service.startedAt,
|
|
30
|
-
stoppedAt: state.service.stoppedAt,
|
|
31
|
-
lastExitAt: state.service.lastExitAt,
|
|
32
|
-
printLogs: state.service.printLogs,
|
|
33
|
-
logPath: state.logs.filePath || MetaSynergyStore.logsPath(),
|
|
34
|
-
}
|
|
32
|
+
const { state, running } = await loadReconciledState()
|
|
33
|
+
return snapshotFromState(state, running)
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
export async function start(input:
|
|
36
|
+
export async function start(input: MetaSynergyLaunchContext) {
|
|
38
37
|
if (await MetaSynergyControlClient.isAvailable()) {
|
|
39
38
|
return {
|
|
40
39
|
changed: false,
|
|
@@ -43,12 +42,12 @@ export namespace MetaSynergyService {
|
|
|
43
42
|
}
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
const state = await
|
|
47
|
-
const currentRunning = Boolean(state.service.pid && MetaSynergyLocalService.isPidRunning(state.service.pid))
|
|
45
|
+
const { state, running: currentRunning } = await loadReconciledState()
|
|
48
46
|
if (currentRunning) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
await updatePersistedServiceState({
|
|
48
|
+
desiredState: "running",
|
|
49
|
+
runtimeStatus: "running",
|
|
50
|
+
})
|
|
52
51
|
return {
|
|
53
52
|
changed: false,
|
|
54
53
|
alreadyRunning: true,
|
|
@@ -60,13 +59,14 @@ export namespace MetaSynergyService {
|
|
|
60
59
|
await MetaSynergyLocalService.removeSocketFile(MetaSynergyStore.controlSocketPath())
|
|
61
60
|
const outputPath = MetaSynergyStore.logsPath()
|
|
62
61
|
const stdout = openSync(outputPath, "a")
|
|
63
|
-
const command =
|
|
62
|
+
const command = resolveServerLaunchCommand(input)
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
await updatePersistedServiceState({
|
|
65
|
+
desiredState: "running",
|
|
66
|
+
runtimeStatus: "starting",
|
|
67
|
+
printLogs: input.printLogs ?? false,
|
|
68
|
+
logPath: outputPath,
|
|
69
|
+
})
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
const child = spawn(command.file, command.args, {
|
|
@@ -76,19 +76,19 @@ export namespace MetaSynergyService {
|
|
|
76
76
|
})
|
|
77
77
|
child.unref()
|
|
78
78
|
await waitForControlPlane(2_500)
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
const controlPlaneReady = await MetaSynergyControlClient.isAvailable()
|
|
80
|
+
await updatePersistedServiceState((currentState) => ({
|
|
81
|
+
desiredState: controlPlaneReady ? "running" : "stopped",
|
|
82
|
+
runtimeStatus: controlPlaneReady ? "running" : "stopped",
|
|
83
|
+
pid: controlPlaneReady ? child.pid : undefined,
|
|
84
|
+
startedAt: controlPlaneReady ? Date.now() : currentState.service.startedAt,
|
|
85
|
+
stoppedAt: controlPlaneReady ? undefined : Date.now(),
|
|
86
|
+
lastExitAt: controlPlaneReady ? currentState.service.lastExitAt : Date.now(),
|
|
87
|
+
printLogs: input.printLogs ?? false,
|
|
88
|
+
logPath: outputPath,
|
|
89
|
+
}))
|
|
90
90
|
return {
|
|
91
|
-
changed:
|
|
91
|
+
changed: controlPlaneReady,
|
|
92
92
|
alreadyRunning: false,
|
|
93
93
|
...(await status()),
|
|
94
94
|
}
|
|
@@ -102,13 +102,14 @@ export namespace MetaSynergyService {
|
|
|
102
102
|
const snapshot = await status()
|
|
103
103
|
await MetaSynergyControlClient.request({ action: "service.stop" }).catch(() => undefined)
|
|
104
104
|
await waitForControlPlaneShutdown(2_500)
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
105
|
+
const stoppedAt = Date.now()
|
|
106
|
+
await updatePersistedServiceState({
|
|
107
|
+
desiredState: "stopped",
|
|
108
|
+
runtimeStatus: "stopped",
|
|
109
|
+
pid: undefined,
|
|
110
|
+
stoppedAt,
|
|
111
|
+
lastExitAt: stoppedAt,
|
|
112
|
+
})
|
|
112
113
|
return {
|
|
113
114
|
changed: snapshot.running,
|
|
114
115
|
alreadyStopped: !snapshot.running,
|
|
@@ -116,19 +117,20 @@ export namespace MetaSynergyService {
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const state = await
|
|
120
|
+
const { state, running } = await loadReconciledState()
|
|
120
121
|
const pid = state.service.pid
|
|
121
|
-
const running = Boolean(pid && MetaSynergyLocalService.isPidRunning(pid))
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
await updatePersistedServiceState({
|
|
124
|
+
desiredState: "stopped",
|
|
125
|
+
runtimeStatus: running ? "stopping" : "stopped",
|
|
126
|
+
})
|
|
126
127
|
|
|
127
128
|
if (!pid || !running) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
await updatePersistedServiceState({
|
|
130
|
+
pid: undefined,
|
|
131
|
+
runtimeStatus: "stopped",
|
|
132
|
+
stoppedAt: Date.now(),
|
|
133
|
+
})
|
|
132
134
|
return {
|
|
133
135
|
changed: false,
|
|
134
136
|
alreadyStopped: true,
|
|
@@ -138,13 +140,14 @@ export namespace MetaSynergyService {
|
|
|
138
140
|
|
|
139
141
|
await MetaSynergyLocalService.terminatePid(pid)
|
|
140
142
|
await MetaSynergyLocalService.removeSocketFile(MetaSynergyStore.controlSocketPath())
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
const stoppedAt = Date.now()
|
|
144
|
+
await updatePersistedServiceState({
|
|
145
|
+
desiredState: "stopped",
|
|
146
|
+
runtimeStatus: "stopped",
|
|
147
|
+
pid: undefined,
|
|
148
|
+
stoppedAt,
|
|
149
|
+
lastExitAt: stoppedAt,
|
|
150
|
+
})
|
|
148
151
|
return {
|
|
149
152
|
changed: true,
|
|
150
153
|
alreadyStopped: false,
|
|
@@ -152,7 +155,7 @@ export namespace MetaSynergyService {
|
|
|
152
155
|
}
|
|
153
156
|
}
|
|
154
157
|
|
|
155
|
-
export async function restart(input:
|
|
158
|
+
export async function restart(input: MetaSynergyLaunchContext) {
|
|
156
159
|
const stopped = await stop()
|
|
157
160
|
const started = await start(input)
|
|
158
161
|
return { stopped, started }
|
|
@@ -192,16 +195,105 @@ export namespace MetaSynergyService {
|
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
function resolveServerLaunchCommand(input: MetaSynergyLaunchContext) {
|
|
199
|
+
const serverArgs = ["server"]
|
|
200
|
+
if (input.printLogs) serverArgs.push("--print-logs")
|
|
201
|
+
|
|
202
|
+
const invocationEntry = input.invocationEntry
|
|
203
|
+
if (!invocationEntry || invocationEntry === input.launcherPath) {
|
|
204
|
+
return { file: input.launcherPath, args: serverArgs }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isBunVirtualEntrypoint = invocationEntry.startsWith("/$bunfs/") || invocationEntry.endsWith("/cli.js")
|
|
208
|
+
if (isBunVirtualEntrypoint) {
|
|
209
|
+
return { file: input.launcherPath, args: serverArgs }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { file: input.launcherPath, args: [invocationEntry, ...serverArgs] }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function snapshotFromState(state: MetaSynergyState, running: boolean): MetaSynergyServiceSnapshot {
|
|
216
|
+
return {
|
|
217
|
+
desiredState: state.service.desiredState,
|
|
218
|
+
runtimeStatus: state.service.runtimeStatus,
|
|
219
|
+
running,
|
|
220
|
+
pid: state.service.pid,
|
|
221
|
+
startedAt: state.service.startedAt,
|
|
222
|
+
stoppedAt: state.service.stoppedAt,
|
|
223
|
+
lastExitAt: state.service.lastExitAt,
|
|
224
|
+
printLogs: state.service.printLogs,
|
|
225
|
+
logPath: state.logs.filePath || MetaSynergyStore.logsPath(),
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function loadReconciledState(): Promise<{ state: MetaSynergyState; running: boolean }> {
|
|
230
|
+
const state = await MetaSynergyStore.loadState()
|
|
231
|
+
const running = Boolean(state.service.pid && MetaSynergyLocalService.isPidRunning(state.service.pid))
|
|
232
|
+
if (reconcileObservedRuntimeState(state, running)) {
|
|
233
|
+
await MetaSynergyStore.saveState(state)
|
|
234
|
+
}
|
|
235
|
+
return { state, running }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function updatePersistedServiceState(
|
|
239
|
+
update: PersistedServiceStateUpdate | ((state: MetaSynergyState) => PersistedServiceStateUpdate),
|
|
240
|
+
): Promise<MetaSynergyState> {
|
|
241
|
+
const state = await MetaSynergyStore.loadState()
|
|
242
|
+
const nextUpdate = typeof update === "function" ? update(state) : update
|
|
243
|
+
if (applyPersistedServiceStateUpdate(state, nextUpdate)) {
|
|
244
|
+
await MetaSynergyStore.saveState(state)
|
|
245
|
+
}
|
|
246
|
+
return state
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function reconcileObservedRuntimeState(state: MetaSynergyState, running: boolean): boolean {
|
|
250
|
+
const expectedRuntimeStatus = running ? "running" : "stopped"
|
|
251
|
+
return applyPersistedServiceStateUpdate(state, {
|
|
252
|
+
runtimeStatus:
|
|
253
|
+
state.service.runtimeStatus === expectedRuntimeStatus && !(state.service.pid && !running)
|
|
254
|
+
? undefined
|
|
255
|
+
: expectedRuntimeStatus,
|
|
256
|
+
pid: state.service.pid && !running ? undefined : state.service.pid,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function applyPersistedServiceStateUpdate(state: MetaSynergyState, update: PersistedServiceStateUpdate): boolean {
|
|
261
|
+
let changed = false
|
|
262
|
+
|
|
263
|
+
if (update.desiredState !== undefined && state.service.desiredState !== update.desiredState) {
|
|
264
|
+
state.service.desiredState = update.desiredState
|
|
265
|
+
changed = true
|
|
266
|
+
}
|
|
267
|
+
if (update.runtimeStatus !== undefined && state.service.runtimeStatus !== update.runtimeStatus) {
|
|
268
|
+
state.service.runtimeStatus = update.runtimeStatus
|
|
269
|
+
changed = true
|
|
270
|
+
}
|
|
271
|
+
if (Object.hasOwn(update, "pid") && state.service.pid !== update.pid) {
|
|
272
|
+
state.service.pid = update.pid
|
|
273
|
+
changed = true
|
|
274
|
+
}
|
|
275
|
+
if (update.printLogs !== undefined && state.service.printLogs !== update.printLogs) {
|
|
276
|
+
state.service.printLogs = update.printLogs
|
|
277
|
+
changed = true
|
|
278
|
+
}
|
|
279
|
+
if (Object.hasOwn(update, "startedAt") && state.service.startedAt !== update.startedAt) {
|
|
280
|
+
state.service.startedAt = update.startedAt
|
|
281
|
+
changed = true
|
|
282
|
+
}
|
|
283
|
+
if (Object.hasOwn(update, "stoppedAt") && state.service.stoppedAt !== update.stoppedAt) {
|
|
284
|
+
state.service.stoppedAt = update.stoppedAt
|
|
285
|
+
changed = true
|
|
286
|
+
}
|
|
287
|
+
if (Object.hasOwn(update, "lastExitAt") && state.service.lastExitAt !== update.lastExitAt) {
|
|
288
|
+
state.service.lastExitAt = update.lastExitAt
|
|
289
|
+
changed = true
|
|
290
|
+
}
|
|
291
|
+
if (update.logPath !== undefined && state.logs.filePath !== update.logPath) {
|
|
292
|
+
state.logs.filePath = update.logPath
|
|
293
|
+
changed = true
|
|
200
294
|
}
|
|
201
295
|
|
|
202
|
-
|
|
203
|
-
if (input.printLogs) args.push("--print-logs")
|
|
204
|
-
return { file: input.execPath, args }
|
|
296
|
+
return changed
|
|
205
297
|
}
|
|
206
298
|
|
|
207
299
|
async function waitForControlPlane(timeoutMs: number) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises"
|
|
2
|
+
import { MetaSynergyStore } from "./store"
|
|
3
|
+
import type { MetaSynergyMigration } from "../migration/types"
|
|
4
|
+
|
|
5
|
+
export namespace MetaSynergyStateMigrations {
|
|
6
|
+
export const migrations: MetaSynergyMigration[] = [
|
|
7
|
+
{
|
|
8
|
+
id: "20260408-normalize-state",
|
|
9
|
+
description: "Normalize persisted meta-synergy state",
|
|
10
|
+
async run() {
|
|
11
|
+
const raw = await readFile(MetaSynergyStore.statePath(), "utf8").catch(() => undefined)
|
|
12
|
+
if (!raw) return
|
|
13
|
+
const parsed = JSON.parse(raw) as unknown
|
|
14
|
+
const next = MetaSynergyStore.hydrateStateForMigration(parsed)
|
|
15
|
+
await MetaSynergyStore.saveState(next)
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
]
|
|
19
|
+
}
|
package/src/state/store.ts
CHANGED
|
@@ -2,10 +2,12 @@ import os from "node:os"
|
|
|
2
2
|
import path from "node:path"
|
|
3
3
|
import process from "node:process"
|
|
4
4
|
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises"
|
|
5
|
+
import { MetaSynergyOwnerRegistry, type MetaSynergyOwnerRegistryState } from "../owner-registry"
|
|
5
6
|
|
|
6
7
|
export type MetaSynergyApprovalMode = "auto" | "manual" | "trusted-only"
|
|
7
8
|
export type MetaSynergyPendingRequestStatus = "pending" | "approved" | "denied"
|
|
8
9
|
export type MetaSynergyConnectionStatus = "disconnected" | "connecting" | "connected"
|
|
10
|
+
export type MetaSynergyRuntimeMode = "managed" | "standalone"
|
|
9
11
|
export type MetaSynergyServiceDesiredState = "running" | "stopped"
|
|
10
12
|
export type MetaSynergyServiceRuntimeStatus = "starting" | "running" | "stopping" | "stopped"
|
|
11
13
|
|
|
@@ -59,6 +61,8 @@ export interface MetaSynergyState {
|
|
|
59
61
|
envID?: string
|
|
60
62
|
hostSessionID?: string
|
|
61
63
|
label?: string
|
|
64
|
+
runtimeMode: MetaSynergyRuntimeMode
|
|
65
|
+
ownerRegistry: MetaSynergyOwnerRegistryState
|
|
62
66
|
collaborationEnabled: boolean
|
|
63
67
|
approvalMode: MetaSynergyApprovalMode
|
|
64
68
|
trusted: MetaSynergyTrustedIdentityState
|
|
@@ -72,6 +76,8 @@ export interface MetaSynergyState {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
const DEFAULT_STATE: MetaSynergyState = {
|
|
79
|
+
runtimeMode: "standalone",
|
|
80
|
+
ownerRegistry: MetaSynergyOwnerRegistry.defaultRegistry(),
|
|
75
81
|
collaborationEnabled: true,
|
|
76
82
|
approvalMode: "manual",
|
|
77
83
|
trusted: {
|
|
@@ -97,7 +103,7 @@ export namespace MetaSynergyStore {
|
|
|
97
103
|
return process.env.META_SYNERGY_HOME || path.join(os.homedir(), ".meta-synergy")
|
|
98
104
|
}
|
|
99
105
|
|
|
100
|
-
export function
|
|
106
|
+
export function legacyAuthPath(): string {
|
|
101
107
|
return path.join(root(), "auth.json")
|
|
102
108
|
}
|
|
103
109
|
|
|
@@ -105,6 +111,14 @@ export namespace MetaSynergyStore {
|
|
|
105
111
|
return path.join(root(), "state.json")
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
export function migrationLogPath(): string {
|
|
115
|
+
return path.join(root(), "migrations.json")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function ownerRegistryPath(): string {
|
|
119
|
+
return path.join(root(), "owner.json")
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
export function logsPath(): string {
|
|
109
123
|
return path.join(root(), "logs", "runtime.log")
|
|
110
124
|
}
|
|
@@ -119,21 +133,21 @@ export namespace MetaSynergyStore {
|
|
|
119
133
|
await mkdir(path.dirname(controlSocketPath()), { recursive: true })
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
export async function
|
|
136
|
+
export async function loadLegacyAuth(): Promise<MetaSynergyAuthState | undefined> {
|
|
123
137
|
try {
|
|
124
|
-
return JSON.parse(await readFile(
|
|
138
|
+
return JSON.parse(await readFile(legacyAuthPath(), "utf8")) as MetaSynergyAuthState
|
|
125
139
|
} catch {
|
|
126
140
|
return undefined
|
|
127
141
|
}
|
|
128
142
|
}
|
|
129
143
|
|
|
130
|
-
export async function
|
|
144
|
+
export async function saveLegacyAuth(auth: MetaSynergyAuthState): Promise<void> {
|
|
131
145
|
await ensureRoot()
|
|
132
|
-
await writeFile(
|
|
146
|
+
await writeFile(legacyAuthPath(), JSON.stringify(auth, null, 2) + "\n")
|
|
133
147
|
}
|
|
134
148
|
|
|
135
|
-
export async function
|
|
136
|
-
await unlink(
|
|
149
|
+
export async function clearLegacyAuth(): Promise<void> {
|
|
150
|
+
await unlink(legacyAuthPath()).catch(() => undefined)
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
export async function loadState(): Promise<MetaSynergyState> {
|
|
@@ -149,6 +163,28 @@ export namespace MetaSynergyStore {
|
|
|
149
163
|
await ensureRoot()
|
|
150
164
|
await writeFile(statePath(), JSON.stringify(hydrateState(state), null, 2) + "\n")
|
|
151
165
|
}
|
|
166
|
+
|
|
167
|
+
export async function loadMigrationLog(): Promise<Record<string, number>> {
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(await readFile(migrationLogPath(), "utf8")) as Record<string, unknown>
|
|
170
|
+
return Object.fromEntries(
|
|
171
|
+
Object.entries(parsed).filter(
|
|
172
|
+
(entry): entry is [string, number] => typeof entry[0] === "string" && typeof entry[1] === "number",
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
} catch {
|
|
176
|
+
return {}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function saveMigrationLog(log: Record<string, number>): Promise<void> {
|
|
181
|
+
await ensureRoot()
|
|
182
|
+
await writeFile(migrationLogPath(), JSON.stringify(log, null, 2) + "\n")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function hydrateStateForMigration(parsed: unknown): MetaSynergyState {
|
|
186
|
+
return hydrateState(isPartialState(parsed) ? parsed : undefined)
|
|
187
|
+
}
|
|
152
188
|
}
|
|
153
189
|
|
|
154
190
|
function hydrateState(parsed: Partial<MetaSynergyState> | undefined): MetaSynergyState {
|
|
@@ -156,6 +192,8 @@ function hydrateState(parsed: Partial<MetaSynergyState> | undefined): MetaSynerg
|
|
|
156
192
|
envID: parsed?.envID,
|
|
157
193
|
hostSessionID: parsed?.hostSessionID,
|
|
158
194
|
label: parsed?.label,
|
|
195
|
+
runtimeMode: parseRuntimeMode(parsed?.runtimeMode),
|
|
196
|
+
ownerRegistry: MetaSynergyOwnerRegistry.hydrate(parsed?.ownerRegistry),
|
|
159
197
|
collaborationEnabled: parsed?.collaborationEnabled ?? DEFAULT_STATE.collaborationEnabled,
|
|
160
198
|
approvalMode: parseApprovalMode(parsed?.approvalMode),
|
|
161
199
|
trusted: {
|
|
@@ -235,6 +273,10 @@ function hydratePendingRequests(input: MetaSynergyState["pendingRequests"] | und
|
|
|
235
273
|
return requests
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
function parseRuntimeMode(input: unknown): MetaSynergyRuntimeMode {
|
|
277
|
+
return input === "managed" || input === "standalone" ? input : DEFAULT_STATE.runtimeMode
|
|
278
|
+
}
|
|
279
|
+
|
|
238
280
|
function parseApprovalMode(input: unknown): MetaSynergyApprovalMode {
|
|
239
281
|
return input === "auto" || input === "trusted-only" || input === "manual" ? input : DEFAULT_STATE.approvalMode
|
|
240
282
|
}
|
|
@@ -268,3 +310,7 @@ function uniqueNumbers(values: number[] | undefined): number[] {
|
|
|
268
310
|
...new Set((values ?? []).filter((value): value is number => typeof value === "number" && Number.isFinite(value))),
|
|
269
311
|
]
|
|
270
312
|
}
|
|
313
|
+
|
|
314
|
+
function isPartialState(value: unknown): value is Partial<MetaSynergyState> {
|
|
315
|
+
return typeof value === "object" && value !== null
|
|
316
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import z from "zod"
|
|
1
2
|
import type { MetaProtocolEnv, MetaProtocolHost } from "@ericsanchezok/meta-protocol"
|
|
2
3
|
|
|
3
4
|
export type EnvID = MetaProtocolEnv.EnvID
|
|
@@ -10,6 +11,13 @@ export interface RemoteHostIdentity {
|
|
|
10
11
|
label?: string
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export const HolosCallerSchema = z.object({
|
|
15
|
+
type: z.string(),
|
|
16
|
+
agentID: z.string(),
|
|
17
|
+
ownerUserID: z.number(),
|
|
18
|
+
profile: z.record(z.string(), z.unknown()).optional(),
|
|
19
|
+
})
|
|
20
|
+
|
|
13
21
|
export interface HolosCaller {
|
|
14
22
|
type: string
|
|
15
23
|
agentID: string
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import process from "node:process"
|
|
6
|
+
import { MetaSynergyCLIBackend } from "../src/cli-backend"
|
|
7
|
+
import { MetaSynergyHolosAuth } from "../src/holos/auth"
|
|
8
|
+
import { MetaSynergyStore } from "../src/state/store"
|
|
9
|
+
|
|
10
|
+
const originalMetaHome = process.env.META_SYNERGY_HOME
|
|
11
|
+
const originalSynergyHome = process.env.SYNERGY_TEST_HOME
|
|
12
|
+
const originalFetch = globalThis.fetch
|
|
13
|
+
const tempRoots: string[] = []
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
const metaRoot = await mkdtemp(path.join(os.tmpdir(), "meta-synergy-cli-auth-meta-"))
|
|
17
|
+
const synergyHome = await mkdtemp(path.join(os.tmpdir(), "meta-synergy-cli-auth-synergy-"))
|
|
18
|
+
tempRoots.push(metaRoot, synergyHome)
|
|
19
|
+
process.env.META_SYNERGY_HOME = metaRoot
|
|
20
|
+
process.env.SYNERGY_TEST_HOME = synergyHome
|
|
21
|
+
globalThis.fetch = (async () =>
|
|
22
|
+
new Response(JSON.stringify({ code: 0, data: { ws_token: "token", expires_in: 60 } }), {
|
|
23
|
+
status: 200,
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
})) as typeof fetch
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterAll(async () => {
|
|
29
|
+
globalThis.fetch = originalFetch
|
|
30
|
+
if (originalMetaHome === undefined) delete process.env.META_SYNERGY_HOME
|
|
31
|
+
else process.env.META_SYNERGY_HOME = originalMetaHome
|
|
32
|
+
|
|
33
|
+
if (originalSynergyHome === undefined) delete process.env.SYNERGY_TEST_HOME
|
|
34
|
+
else process.env.SYNERGY_TEST_HOME = originalSynergyHome
|
|
35
|
+
|
|
36
|
+
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe("meta-synergy cli backend auth payloads", () => {
|
|
40
|
+
test("whoami reports shared auth source", async () => {
|
|
41
|
+
const sharedPath = MetaSynergyHolosAuth.sharedAuthPath()
|
|
42
|
+
await mkdir(path.dirname(sharedPath), { recursive: true })
|
|
43
|
+
await writeFile(
|
|
44
|
+
sharedPath,
|
|
45
|
+
JSON.stringify({ holos: { type: "holos", agentId: "agent_shared", agentSecret: "secret_shared" } }, null, 2),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const result = await MetaSynergyCLIBackend.whoami()
|
|
49
|
+
expect(result.auth).toEqual({
|
|
50
|
+
loggedIn: true,
|
|
51
|
+
agentID: "agent_shared",
|
|
52
|
+
source: "shared",
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("doctor reports migrated legacy auth source in payload and checks", async () => {
|
|
57
|
+
await MetaSynergyStore.saveLegacyAuth({ agentID: "agent_legacy", agentSecret: "secret_legacy" })
|
|
58
|
+
|
|
59
|
+
const result = await MetaSynergyCLIBackend.doctor()
|
|
60
|
+
expect(result.auth).toEqual({
|
|
61
|
+
loggedIn: true,
|
|
62
|
+
agentID: "agent_legacy",
|
|
63
|
+
source: "legacy-migrated",
|
|
64
|
+
})
|
|
65
|
+
expect(result.checks.find((check) => check.name === "auth")).toEqual({
|
|
66
|
+
name: "auth",
|
|
67
|
+
ok: true,
|
|
68
|
+
detail: "agent agent_legacy (legacy-migrated)",
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("logout clears auth and reports logged-out source state", async () => {
|
|
73
|
+
await MetaSynergyStore.saveLegacyAuth({ agentID: "agent_clear", agentSecret: "secret_clear" })
|
|
74
|
+
await MetaSynergyHolosAuth.save({ agentID: "agent_clear", agentSecret: "secret_clear" })
|
|
75
|
+
|
|
76
|
+
const result = await MetaSynergyCLIBackend.logout()
|
|
77
|
+
expect(result.authCleared).toBe(true)
|
|
78
|
+
|
|
79
|
+
const whoami = await MetaSynergyCLIBackend.whoami()
|
|
80
|
+
expect(whoami.auth).toEqual({
|
|
81
|
+
loggedIn: false,
|
|
82
|
+
agentID: null,
|
|
83
|
+
source: null,
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import process from "node:process"
|
|
6
|
+
import { MetaSynergyCLIBackend } from "../src/cli-backend"
|
|
7
|
+
import { MetaSynergyStore } from "../src/state/store"
|
|
8
|
+
|
|
9
|
+
const originalHome = process.env.META_SYNERGY_HOME
|
|
10
|
+
const tempRoots: string[] = []
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "meta-synergy-cli-mode-"))
|
|
14
|
+
tempRoots.push(root)
|
|
15
|
+
process.env.META_SYNERGY_HOME = root
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
if (originalHome === undefined) {
|
|
20
|
+
delete process.env.META_SYNERGY_HOME
|
|
21
|
+
} else {
|
|
22
|
+
process.env.META_SYNERGY_HOME = originalHome
|
|
23
|
+
}
|
|
24
|
+
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("meta-synergy cli backend mode transitions", () => {
|
|
28
|
+
test("standalone mode clears managed ownership when service is offline", async () => {
|
|
29
|
+
const state = await MetaSynergyStore.loadState()
|
|
30
|
+
state.runtimeMode = "managed"
|
|
31
|
+
state.ownerRegistry.local.ownerIDs = ["synergy:test"]
|
|
32
|
+
state.ownerRegistry.local.activeOwnerID = "synergy:test"
|
|
33
|
+
state.ownerRegistry.local.leaseExpiresAt = Date.now() + 10_000
|
|
34
|
+
await MetaSynergyStore.saveState(state)
|
|
35
|
+
|
|
36
|
+
const result = (await MetaSynergyCLIBackend.enterStandaloneMode()) as {
|
|
37
|
+
mode: string
|
|
38
|
+
ownership: { local: { owned: boolean } }
|
|
39
|
+
connectionStatus: string
|
|
40
|
+
}
|
|
41
|
+
expect(result.mode).toBe("standalone")
|
|
42
|
+
expect(result.ownership.local.owned).toBe(false)
|
|
43
|
+
expect(result.connectionStatus).toBe("disconnected")
|
|
44
|
+
|
|
45
|
+
const next = await MetaSynergyStore.loadState()
|
|
46
|
+
expect(next.runtimeMode).toBe("standalone")
|
|
47
|
+
expect(next.ownerRegistry.local.activeOwnerID).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
})
|