spawnfile 0.1.2 → 0.1.4
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 +3 -0
- package/dist/cli/index.js +0 -0
- package/dist/cli/runCli.d.ts +18 -2
- package/dist/cli/runCli.js +75 -62
- package/dist/cli/viewCommand.d.ts +3 -0
- package/dist/cli/viewCommand.js +87 -0
- package/dist/compiler/buildCompilePlan.js +2 -0
- package/dist/compiler/containerEntrypointRender.js +3 -3
- package/dist/compiler/index.d.ts +2 -0
- package/dist/compiler/index.js +2 -0
- package/dist/compiler/moltnetArtifacts.js +7 -3
- package/dist/compiler/moltnetResolution.js +29 -48
- package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
- package/dist/compiler/moltnetRoomMemberships.js +140 -0
- package/dist/compiler/runProject.d.ts +2 -0
- package/dist/compiler/runProject.js +20 -6
- package/dist/compiler/syncProjectAuth.js +53 -19
- package/dist/compiler/types.d.ts +18 -0
- package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
- package/dist/compiler/view/buildOrganizationView.js +180 -0
- package/dist/compiler/view/index.d.ts +4 -0
- package/dist/compiler/view/index.js +4 -0
- package/dist/compiler/view/renderNetworks.d.ts +2 -0
- package/dist/compiler/view/renderNetworks.js +93 -0
- package/dist/compiler/view/renderTree.d.ts +2 -0
- package/dist/compiler/view/renderTree.js +59 -0
- package/dist/compiler/view/sourcePaths.d.ts +2 -0
- package/dist/compiler/view/sourcePaths.js +19 -0
- package/dist/compiler/view/types.d.ts +80 -0
- package/dist/compiler/view/types.js +1 -0
- package/dist/runtime/picoclaw/adapter.js +7 -0
- package/dist/runtime/picoclaw/runAuth.js +5 -41
- package/package.json +2 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { SpawnfileError } from "../shared/index.js";
|
|
2
|
+
import { resolveTeamRepresentatives } from "./moltnetRepresentativeResolution.js";
|
|
3
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
4
|
+
const findAgentBySource = (plan, source) => {
|
|
5
|
+
const node = plan.nodes.find((entry) => entry.kind === "agent" && entry.value.source === source);
|
|
6
|
+
if (!node || node.value.kind !== "agent") {
|
|
7
|
+
throw new SpawnfileError("compile_error", `Unable to find agent node at ${source}`);
|
|
8
|
+
}
|
|
9
|
+
return node.value;
|
|
10
|
+
};
|
|
11
|
+
const findTeamBySource = (plan, source) => {
|
|
12
|
+
const node = plan.nodes.find((entry) => entry.kind === "team" && entry.value.source === source);
|
|
13
|
+
if (!node || node.value.kind !== "team") {
|
|
14
|
+
throw new SpawnfileError("compile_error", `Unable to find team node at ${source}`);
|
|
15
|
+
}
|
|
16
|
+
return node.value;
|
|
17
|
+
};
|
|
18
|
+
const clonePolicy = (policy) => ({
|
|
19
|
+
...(policy.read ? { read: policy.read } : {}),
|
|
20
|
+
...(policy.reply ? { reply: policy.reply } : {})
|
|
21
|
+
});
|
|
22
|
+
const findDirectRoomPolicy = (plan, teamNode, agentSource, memberId, networkId, roomId) => {
|
|
23
|
+
const agentNode = findAgentBySource(plan, agentSource);
|
|
24
|
+
for (const attachment of agentNode.surfaces?.moltnet ?? []) {
|
|
25
|
+
if (attachment.network !== networkId) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (attachment.teamSource && attachment.teamSource !== teamNode.source) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (attachment.memberId && attachment.memberId !== memberId) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!attachment.rooms || !hasOwn(attachment.rooms, roomId)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
return clonePolicy(attachment.rooms[roomId] ?? {});
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
const compareRoomMemberships = (left, right) => [
|
|
42
|
+
left.declaringTeamSource.localeCompare(right.declaringTeamSource),
|
|
43
|
+
left.networkId.localeCompare(right.networkId),
|
|
44
|
+
left.roomId.localeCompare(right.roomId),
|
|
45
|
+
left.declaredSlot.localeCompare(right.declaredSlot),
|
|
46
|
+
(left.representativePath ?? []).join("/").localeCompare((right.representativePath ?? []).join("/")),
|
|
47
|
+
left.concreteMemberId.localeCompare(right.concreteMemberId),
|
|
48
|
+
left.agentSource.localeCompare(right.agentSource)
|
|
49
|
+
].find((result) => result !== 0) ?? 0;
|
|
50
|
+
export const listConcreteMoltnetRoomMemberIds = (plan, teamNode, networkId, room, memberships = plan.moltnetRoomMemberships) => {
|
|
51
|
+
if (memberships) {
|
|
52
|
+
return [
|
|
53
|
+
...new Set(memberships
|
|
54
|
+
.filter((membership) => membership.declaringTeamSource === teamNode.source
|
|
55
|
+
&& membership.networkId === networkId
|
|
56
|
+
&& membership.roomId === room.id)
|
|
57
|
+
.map((membership) => membership.concreteMemberId))
|
|
58
|
+
].sort();
|
|
59
|
+
}
|
|
60
|
+
const concreteMembers = [];
|
|
61
|
+
for (const declaredSlot of room.members) {
|
|
62
|
+
const member = teamNode.members.find((entry) => entry.id === declaredSlot);
|
|
63
|
+
if (!member) {
|
|
64
|
+
throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${declaredSlot}`);
|
|
65
|
+
}
|
|
66
|
+
if (member.kind === "agent") {
|
|
67
|
+
concreteMembers.push(member.id);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const childTeam = findTeamBySource(plan, member.nodeSource);
|
|
71
|
+
const representatives = resolveTeamRepresentatives(plan, childTeam);
|
|
72
|
+
if (representatives.length === 0) {
|
|
73
|
+
throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
|
|
74
|
+
}
|
|
75
|
+
concreteMembers.push(...representatives.map((representative) => representative.memberId));
|
|
76
|
+
}
|
|
77
|
+
return [...new Set(concreteMembers)].sort();
|
|
78
|
+
};
|
|
79
|
+
export const resolveMoltnetRoomMemberships = (plan) => {
|
|
80
|
+
const memberships = [];
|
|
81
|
+
for (const node of plan.nodes) {
|
|
82
|
+
if (node.value.kind !== "team") {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const teamNode = node.value;
|
|
86
|
+
for (const network of teamNode.networks ?? []) {
|
|
87
|
+
for (const room of network.rooms) {
|
|
88
|
+
for (const declaredSlot of room.members) {
|
|
89
|
+
const declaredMember = teamNode.members.find((member) => member.id === declaredSlot);
|
|
90
|
+
if (!declaredMember) {
|
|
91
|
+
throw new SpawnfileError("validation_error", `Team ${teamNode.name} Moltnet room ${room.id} references unknown member ${declaredSlot}`);
|
|
92
|
+
}
|
|
93
|
+
if (declaredMember.kind === "agent") {
|
|
94
|
+
const agentNode = findAgentBySource(plan, declaredMember.nodeSource);
|
|
95
|
+
const policy = findDirectRoomPolicy(plan, teamNode, agentNode.source, declaredSlot, network.id, room.id);
|
|
96
|
+
memberships.push({
|
|
97
|
+
agentName: agentNode.name,
|
|
98
|
+
agentSource: agentNode.source,
|
|
99
|
+
concreteMemberId: declaredSlot,
|
|
100
|
+
declaredSlot,
|
|
101
|
+
declaringTeamName: teamNode.name,
|
|
102
|
+
declaringTeamSource: teamNode.source,
|
|
103
|
+
directTeamName: teamNode.name,
|
|
104
|
+
directTeamSource: teamNode.source,
|
|
105
|
+
networkId: network.id,
|
|
106
|
+
...(policy ? { policy } : {}),
|
|
107
|
+
roomId: room.id
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const childTeam = findTeamBySource(plan, declaredMember.nodeSource);
|
|
112
|
+
const representatives = resolveTeamRepresentatives(plan, childTeam);
|
|
113
|
+
if (representatives.length === 0) {
|
|
114
|
+
throw new SpawnfileError("validation_error", `Team ${childTeam.name} has no concrete representative for Moltnet room ${room.id} on ${teamNode.name}`);
|
|
115
|
+
}
|
|
116
|
+
for (const representative of representatives) {
|
|
117
|
+
const agentNode = findAgentBySource(plan, representative.agentSource);
|
|
118
|
+
memberships.push({
|
|
119
|
+
agentName: agentNode.name,
|
|
120
|
+
agentSource: agentNode.source,
|
|
121
|
+
concreteMemberId: representative.memberId,
|
|
122
|
+
declaredSlot,
|
|
123
|
+
declaringTeamName: teamNode.name,
|
|
124
|
+
declaringTeamSource: teamNode.source,
|
|
125
|
+
directTeamName: representative.sourceTeamName,
|
|
126
|
+
directTeamSource: representative.sourceTeamSource,
|
|
127
|
+
networkId: network.id,
|
|
128
|
+
representedSlot: declaredSlot,
|
|
129
|
+
representedTeamName: childTeam.name,
|
|
130
|
+
representedTeamSource: childTeam.source,
|
|
131
|
+
representativePath: [declaredSlot, ...representative.path],
|
|
132
|
+
roomId: room.id
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return memberships.sort(compareRoomMemberships);
|
|
140
|
+
};
|
|
@@ -16,6 +16,7 @@ export interface RunProjectOptions extends CompileProjectOptions {
|
|
|
16
16
|
containerName?: string;
|
|
17
17
|
detach?: boolean;
|
|
18
18
|
dockerCommand?: string;
|
|
19
|
+
envFilePath?: string;
|
|
19
20
|
imageTag?: string;
|
|
20
21
|
runRunner?: DockerRunRunner;
|
|
21
22
|
}
|
|
@@ -29,6 +30,7 @@ export declare const createDockerRunInvocation: (compileResult: CompileProjectRe
|
|
|
29
30
|
containerName?: string;
|
|
30
31
|
detach?: boolean;
|
|
31
32
|
dockerCommand?: string;
|
|
33
|
+
envFilePath?: string;
|
|
32
34
|
}) => Promise<DockerRunInvocation>;
|
|
33
35
|
export declare const runDockerContainer: DockerRunRunner;
|
|
34
36
|
export declare const runProject: (inputPath: string, options?: RunProjectOptions) => Promise<RunProjectResult>;
|
|
@@ -3,8 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import { mkdtemp } from "node:fs/promises";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { requireAuthProfile } from "../auth/index.js";
|
|
7
|
-
import { ensureDirectory, fileExists, removeDirectory, writeUtf8File } from "../filesystem/index.js";
|
|
6
|
+
import { parseEnvFile, requireAuthProfile } from "../auth/index.js";
|
|
7
|
+
import { ensureDirectory, fileExists, readUtf8File, removeDirectory, writeUtf8File } from "../filesystem/index.js";
|
|
8
8
|
import { SpawnfileError } from "../shared/index.js";
|
|
9
9
|
import { compileProject } from "./compileProject.js";
|
|
10
10
|
import { createDefaultImageTag } from "./buildProject.js";
|
|
@@ -47,9 +47,10 @@ const collectMissingRequiredSecrets = (containerReport, env, coveredModelSecrets
|
|
|
47
47
|
}
|
|
48
48
|
return [...missing].sort();
|
|
49
49
|
};
|
|
50
|
-
const resolveRunEnvironment = (containerReport, authProfile) => {
|
|
50
|
+
const resolveRunEnvironment = (containerReport, authProfile, envFileEnv = {}) => {
|
|
51
51
|
const env = {
|
|
52
|
-
...(authProfile?.env ?? {})
|
|
52
|
+
...(authProfile?.env ?? {}),
|
|
53
|
+
...envFileEnv
|
|
53
54
|
};
|
|
54
55
|
for (const name of new Set([...Object.keys(env), ...containerReport.secrets_required])) {
|
|
55
56
|
const processValue = process.env[name];
|
|
@@ -83,6 +84,18 @@ const renderDockerEnvFile = (env) => `${Object.entries(env)
|
|
|
83
84
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
84
85
|
.map(([name, value]) => `${name}=${value}`)
|
|
85
86
|
.join("\n")}\n`;
|
|
87
|
+
const readRunEnvFile = async (envFilePath) => {
|
|
88
|
+
if (!envFilePath) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return parseEnvFile(await readUtf8File(envFilePath));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw new SpawnfileError("validation_error", `Unable to read env file ${envFilePath}: ${reason}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
86
99
|
const resolveAuthMountArgs = async (containerReport, authProfile) => {
|
|
87
100
|
if (!authProfile || containerReport.runtime_homes.length === 0) {
|
|
88
101
|
return [];
|
|
@@ -110,7 +123,7 @@ export const createDockerRunInvocation = async (compileResult, imageTag, options
|
|
|
110
123
|
const envFilePath = path.join(supportDirectory, "run.env");
|
|
111
124
|
try {
|
|
112
125
|
assertDeclaredModelAuthSatisfied(containerReport, options.authProfile ?? null);
|
|
113
|
-
const env = resolveRunEnvironment(containerReport, options.authProfile ?? null);
|
|
126
|
+
const env = resolveRunEnvironment(containerReport, options.authProfile ?? null, await readRunEnvFile(options.envFilePath));
|
|
114
127
|
const preparedRuntimeAuth = await prepareRuntimeAuthMounts(compileResult.outputDirectory, containerReport, options.authProfile ?? null, env, supportDirectory);
|
|
115
128
|
assertRunEnvironmentSatisfied(containerReport, env, preparedRuntimeAuth.coveredModelSecrets);
|
|
116
129
|
await ensureDirectory(supportDirectory);
|
|
@@ -178,7 +191,8 @@ export const runProject = async (inputPath, options = {}) => {
|
|
|
178
191
|
authProfile,
|
|
179
192
|
containerName: options.containerName,
|
|
180
193
|
detach: options.detach,
|
|
181
|
-
dockerCommand: options.dockerCommand
|
|
194
|
+
dockerCommand: options.dockerCommand,
|
|
195
|
+
envFilePath: options.envFilePath
|
|
182
196
|
});
|
|
183
197
|
try {
|
|
184
198
|
await (options.runRunner ?? runDockerContainer)(invocation);
|
|
@@ -7,39 +7,64 @@ import { listExecutionModelSecretNames, resolveExecutionModelAuthMethods } from
|
|
|
7
7
|
const resolveAuthRequirements = async (inputPath) => {
|
|
8
8
|
const plan = await buildCompilePlan(inputPath);
|
|
9
9
|
const methods = new Set();
|
|
10
|
-
const
|
|
10
|
+
const optionalEnvNames = new Set();
|
|
11
|
+
const requiredEnvNames = new Set();
|
|
12
|
+
const addProjectSecret = (secret) => {
|
|
13
|
+
if (secret.required) {
|
|
14
|
+
requiredEnvNames.add(secret.name);
|
|
15
|
+
optionalEnvNames.delete(secret.name);
|
|
16
|
+
}
|
|
17
|
+
else if (!requiredEnvNames.has(secret.name)) {
|
|
18
|
+
optionalEnvNames.add(secret.name);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
11
21
|
for (const node of plan.nodes) {
|
|
12
22
|
if (node.value.kind !== "agent") {
|
|
23
|
+
for (const secret of node.value.shared.secrets) {
|
|
24
|
+
addProjectSecret(secret);
|
|
25
|
+
}
|
|
13
26
|
continue;
|
|
14
27
|
}
|
|
28
|
+
for (const secret of node.value.secrets) {
|
|
29
|
+
addProjectSecret(secret);
|
|
30
|
+
}
|
|
15
31
|
for (const method of Object.values(resolveExecutionModelAuthMethods(node.value.execution))) {
|
|
16
32
|
methods.add(method);
|
|
17
33
|
}
|
|
18
34
|
for (const envName of listExecutionModelSecretNames(node.value.execution)) {
|
|
19
|
-
|
|
35
|
+
requiredEnvNames.add(envName);
|
|
36
|
+
optionalEnvNames.delete(envName);
|
|
20
37
|
}
|
|
21
38
|
for (const envName of listAgentSurfaceSecretNames(node.value.surfaces)) {
|
|
22
|
-
|
|
39
|
+
requiredEnvNames.add(envName);
|
|
40
|
+
optionalEnvNames.delete(envName);
|
|
23
41
|
}
|
|
24
42
|
}
|
|
25
|
-
return {
|
|
43
|
+
return { methods, optionalEnvNames, requiredEnvNames };
|
|
44
|
+
};
|
|
45
|
+
const readEnvFile = async (envFilePath) => envFilePath ? parseEnvFile(await readUtf8File(envFilePath)) : {};
|
|
46
|
+
const resolveEnvValue = (envName, fileEnv) => {
|
|
47
|
+
const processValue = process.env[envName];
|
|
48
|
+
if (typeof processValue === "string" && processValue.length > 0) {
|
|
49
|
+
return processValue;
|
|
50
|
+
}
|
|
51
|
+
const fileValue = fileEnv[envName];
|
|
52
|
+
if (typeof fileValue === "string" && fileValue.length > 0) {
|
|
53
|
+
return fileValue;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
26
56
|
};
|
|
27
|
-
const resolveRequiredEnv = async (
|
|
28
|
-
if (
|
|
57
|
+
const resolveRequiredEnv = async (requiredEnvNames, optionalEnvNames, envFilePath) => {
|
|
58
|
+
if (requiredEnvNames.size === 0 && optionalEnvNames.size === 0) {
|
|
29
59
|
return {};
|
|
30
60
|
}
|
|
31
|
-
const fileEnv =
|
|
61
|
+
const fileEnv = await readEnvFile(envFilePath);
|
|
32
62
|
const resolvedEnv = {};
|
|
33
63
|
const missingEnv = [];
|
|
34
|
-
for (const envName of [...
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
resolvedEnv[envName] =
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const fileValue = fileEnv[envName];
|
|
41
|
-
if (typeof fileValue === "string" && fileValue.length > 0) {
|
|
42
|
-
resolvedEnv[envName] = fileValue;
|
|
64
|
+
for (const envName of [...requiredEnvNames].sort()) {
|
|
65
|
+
const value = resolveEnvValue(envName, fileEnv);
|
|
66
|
+
if (value !== null) {
|
|
67
|
+
resolvedEnv[envName] = value;
|
|
43
68
|
continue;
|
|
44
69
|
}
|
|
45
70
|
missingEnv.push(envName);
|
|
@@ -47,10 +72,19 @@ const resolveRequiredEnv = async (envNames, envFilePath) => {
|
|
|
47
72
|
if (missingEnv.length > 0) {
|
|
48
73
|
throw new SpawnfileError("validation_error", `Missing required auth env: ${missingEnv.join(", ")}`);
|
|
49
74
|
}
|
|
75
|
+
for (const envName of [...optionalEnvNames].sort()) {
|
|
76
|
+
if (requiredEnvNames.has(envName)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const value = resolveEnvValue(envName, fileEnv);
|
|
80
|
+
if (value !== null) {
|
|
81
|
+
resolvedEnv[envName] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
50
84
|
return resolvedEnv;
|
|
51
85
|
};
|
|
52
86
|
export const syncProjectAuth = async (inputPath, options) => {
|
|
53
|
-
const {
|
|
87
|
+
const { methods, optionalEnvNames, requiredEnvNames } = await resolveAuthRequirements(inputPath);
|
|
54
88
|
await ensureAuthProfile(options.profileName);
|
|
55
89
|
if (methods.has("codex")) {
|
|
56
90
|
await importCodexAuth(options.profileName, options.codexDirectory);
|
|
@@ -58,8 +92,8 @@ export const syncProjectAuth = async (inputPath, options) => {
|
|
|
58
92
|
if (methods.has("claude-code")) {
|
|
59
93
|
await importClaudeCodeAuth(options.profileName, options.claudeCodeDirectory);
|
|
60
94
|
}
|
|
61
|
-
if (
|
|
62
|
-
await setAuthProfileEnv(options.profileName, await resolveRequiredEnv(
|
|
95
|
+
if (requiredEnvNames.size > 0 || optionalEnvNames.size > 0) {
|
|
96
|
+
await setAuthProfileEnv(options.profileName, await resolveRequiredEnv(requiredEnvNames, optionalEnvNames, options.envFilePath));
|
|
63
97
|
}
|
|
64
98
|
return requireAuthProfile(options.profileName);
|
|
65
99
|
};
|
package/dist/compiler/types.d.ts
CHANGED
|
@@ -189,9 +189,27 @@ export interface ResolvedTeamMembershipContext {
|
|
|
189
189
|
teamName: string;
|
|
190
190
|
teamSource: string;
|
|
191
191
|
}
|
|
192
|
+
export interface ResolvedMoltnetRoomMembership {
|
|
193
|
+
agentName: string;
|
|
194
|
+
agentSource: string;
|
|
195
|
+
concreteMemberId: string;
|
|
196
|
+
declaredSlot: string;
|
|
197
|
+
declaringTeamName: string;
|
|
198
|
+
declaringTeamSource: string;
|
|
199
|
+
directTeamName: string;
|
|
200
|
+
directTeamSource: string;
|
|
201
|
+
networkId: string;
|
|
202
|
+
policy?: ResolvedMoltnetRoomPolicy;
|
|
203
|
+
representedSlot?: string;
|
|
204
|
+
representedTeamName?: string;
|
|
205
|
+
representedTeamSource?: string;
|
|
206
|
+
representativePath?: string[];
|
|
207
|
+
roomId: string;
|
|
208
|
+
}
|
|
192
209
|
export interface CompilePlan {
|
|
193
210
|
edges: CompilePlanEdge[];
|
|
194
211
|
memberships?: ResolvedTeamMembershipContext[];
|
|
212
|
+
moltnetRoomMemberships?: ResolvedMoltnetRoomMembership[];
|
|
195
213
|
nodes: CompilePlanNode[];
|
|
196
214
|
root: string;
|
|
197
215
|
runtimes: Record<string, {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { SpawnfileError } from "../../shared/index.js";
|
|
3
|
+
import { buildCompilePlan } from "../buildCompilePlan.js";
|
|
4
|
+
import { resolveMoltnetRoomMemberships } from "../moltnetRoomMemberships.js";
|
|
5
|
+
const createNameCounts = (plan) => {
|
|
6
|
+
const counts = new Map();
|
|
7
|
+
for (const node of plan.nodes) {
|
|
8
|
+
const key = `${node.kind}:${node.value.name}`;
|
|
9
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
10
|
+
}
|
|
11
|
+
return counts;
|
|
12
|
+
};
|
|
13
|
+
const formatDisplayName = (node, nameCounts) => {
|
|
14
|
+
const key = `${node.kind}:${node.value.name}`;
|
|
15
|
+
return (nameCounts.get(key) ?? 0) > 1
|
|
16
|
+
? `${node.value.name} [${node.id}]`
|
|
17
|
+
: node.value.name;
|
|
18
|
+
};
|
|
19
|
+
const groupEdgesBySource = (edges) => {
|
|
20
|
+
const groups = new Map();
|
|
21
|
+
for (const edge of edges) {
|
|
22
|
+
const group = groups.get(edge.from) ?? [];
|
|
23
|
+
group.push(edge);
|
|
24
|
+
groups.set(edge.from, group);
|
|
25
|
+
}
|
|
26
|
+
return groups;
|
|
27
|
+
};
|
|
28
|
+
const buildTreeNetworkSummaries = (node) => {
|
|
29
|
+
if (node.value.kind !== "team") {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
return (node.value.networks ?? []).map((network) => ({
|
|
33
|
+
expose: network.expose ?? false,
|
|
34
|
+
id: network.id,
|
|
35
|
+
name: network.name,
|
|
36
|
+
provider: network.provider,
|
|
37
|
+
rooms: network.rooms.map((room) => ({
|
|
38
|
+
declaredMembers: [...room.members],
|
|
39
|
+
id: room.id
|
|
40
|
+
}))
|
|
41
|
+
}));
|
|
42
|
+
};
|
|
43
|
+
const buildTreeNode = (node, nodeById, edgesBySource, nameCounts, ancestors = []) => {
|
|
44
|
+
if (ancestors.includes(node.id)) {
|
|
45
|
+
throw new SpawnfileError("compile_error", `Cycle detected while building view tree for ${node.id}`);
|
|
46
|
+
}
|
|
47
|
+
const children = (edgesBySource.get(node.id) ?? []).map((edge) => {
|
|
48
|
+
const child = nodeById.get(edge.to);
|
|
49
|
+
if (!child) {
|
|
50
|
+
throw new SpawnfileError("compile_error", `Unable to find view node ${edge.to}`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
label: edge.label,
|
|
54
|
+
node: buildTreeNode(child, nodeById, edgesBySource, nameCounts, [...ancestors, node.id]),
|
|
55
|
+
relation: edge.kind
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
children,
|
|
60
|
+
displayName: formatDisplayName(node, nameCounts),
|
|
61
|
+
...(node.value.kind === "team"
|
|
62
|
+
? {
|
|
63
|
+
external: [...node.value.external],
|
|
64
|
+
lead: node.value.lead,
|
|
65
|
+
mode: node.value.mode
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
68
|
+
id: node.id,
|
|
69
|
+
kind: node.kind,
|
|
70
|
+
name: node.value.name,
|
|
71
|
+
networks: buildTreeNetworkSummaries(node),
|
|
72
|
+
runtimeName: node.runtimeName,
|
|
73
|
+
source: node.value.source
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const sortNetworkMembers = (declaredMembers, members) => {
|
|
77
|
+
const declaredOrder = new Map(declaredMembers.map((member, index) => [member, index]));
|
|
78
|
+
return [...members].sort((left, right) => (declaredOrder.get(left.declaredSlot) ?? Number.MAX_SAFE_INTEGER)
|
|
79
|
+
- (declaredOrder.get(right.declaredSlot) ?? Number.MAX_SAFE_INTEGER)
|
|
80
|
+
|| (left.representativePath ?? []).join("/").localeCompare((right.representativePath ?? []).join("/"))
|
|
81
|
+
|| left.concreteMemberId.localeCompare(right.concreteMemberId));
|
|
82
|
+
};
|
|
83
|
+
const toNetworkMemberView = (membership) => ({
|
|
84
|
+
agentName: membership.agentName,
|
|
85
|
+
agentSource: membership.agentSource,
|
|
86
|
+
concreteMemberId: membership.concreteMemberId,
|
|
87
|
+
declaredSlot: membership.declaredSlot,
|
|
88
|
+
directTeamName: membership.directTeamName,
|
|
89
|
+
directTeamSource: membership.directTeamSource,
|
|
90
|
+
...(membership.policy ? { policy: { ...membership.policy } } : {}),
|
|
91
|
+
...(membership.representedSlot ? { representedSlot: membership.representedSlot } : {}),
|
|
92
|
+
...(membership.representedTeamName
|
|
93
|
+
? { representedTeamName: membership.representedTeamName }
|
|
94
|
+
: {}),
|
|
95
|
+
...(membership.representedTeamSource
|
|
96
|
+
? { representedTeamSource: membership.representedTeamSource }
|
|
97
|
+
: {}),
|
|
98
|
+
...(membership.representativePath
|
|
99
|
+
? { representativePath: [...membership.representativePath] }
|
|
100
|
+
: {})
|
|
101
|
+
});
|
|
102
|
+
const createNetworkKey = (provider, networkId) => `${provider}::${networkId}`;
|
|
103
|
+
const buildNetworkDeclaration = (teamNode, network, roomMemberships) => ({
|
|
104
|
+
declaringTeamName: teamNode.name,
|
|
105
|
+
declaringTeamSource: teamNode.source,
|
|
106
|
+
expose: network.expose ?? false,
|
|
107
|
+
name: network.name,
|
|
108
|
+
rooms: network.rooms.map((room) => {
|
|
109
|
+
const members = roomMemberships
|
|
110
|
+
.filter((membership) => membership.declaringTeamSource === teamNode.source
|
|
111
|
+
&& membership.networkId === network.id
|
|
112
|
+
&& membership.roomId === room.id)
|
|
113
|
+
.map(toNetworkMemberView);
|
|
114
|
+
return {
|
|
115
|
+
declaredMembers: [...room.members],
|
|
116
|
+
id: room.id,
|
|
117
|
+
members: sortNetworkMembers(room.members, members)
|
|
118
|
+
};
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
const buildNetworks = (plan) => {
|
|
122
|
+
const roomMemberships = plan.moltnetRoomMemberships
|
|
123
|
+
?? resolveMoltnetRoomMemberships(plan);
|
|
124
|
+
const groups = new Map();
|
|
125
|
+
for (const node of plan.nodes) {
|
|
126
|
+
if (node.value.kind !== "team") {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const teamNode = node.value;
|
|
130
|
+
for (const network of teamNode.networks ?? []) {
|
|
131
|
+
const key = createNetworkKey(network.provider, network.id);
|
|
132
|
+
const declaration = buildNetworkDeclaration(teamNode, network, roomMemberships);
|
|
133
|
+
const existing = groups.get(key);
|
|
134
|
+
if (existing) {
|
|
135
|
+
existing.declarations = [...(existing.declarations ?? []), declaration];
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
groups.set(key, {
|
|
139
|
+
declaringTeamName: declaration.declaringTeamName,
|
|
140
|
+
declaringTeamSource: declaration.declaringTeamSource,
|
|
141
|
+
declarations: [declaration],
|
|
142
|
+
expose: declaration.expose,
|
|
143
|
+
id: network.id,
|
|
144
|
+
name: declaration.name,
|
|
145
|
+
provider: network.provider,
|
|
146
|
+
rooms: declaration.rooms
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const networks = [...groups.values()];
|
|
151
|
+
for (const network of networks) {
|
|
152
|
+
const firstDeclaration = network.declarations?.[0];
|
|
153
|
+
if (firstDeclaration) {
|
|
154
|
+
network.declaringTeamName = firstDeclaration.declaringTeamName;
|
|
155
|
+
network.declaringTeamSource = firstDeclaration.declaringTeamSource;
|
|
156
|
+
network.expose = firstDeclaration.expose;
|
|
157
|
+
network.name = firstDeclaration.name;
|
|
158
|
+
network.rooms = firstDeclaration.rooms;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return networks;
|
|
162
|
+
};
|
|
163
|
+
export const buildOrganizationView = async (inputPath) => {
|
|
164
|
+
const plan = await buildCompilePlan(inputPath);
|
|
165
|
+
const rootNode = plan.nodes.find((node) => node.value.source === plan.root);
|
|
166
|
+
if (!rootNode) {
|
|
167
|
+
throw new SpawnfileError("compile_error", `Unable to find root view node for ${plan.root}`);
|
|
168
|
+
}
|
|
169
|
+
const nodeById = new Map(plan.nodes.map((node) => [node.id, node]));
|
|
170
|
+
const root = buildTreeNode(rootNode, nodeById, groupEdgesBySource(plan.edges), createNameCounts(plan));
|
|
171
|
+
return {
|
|
172
|
+
contexts: [],
|
|
173
|
+
diagnostics: [],
|
|
174
|
+
inputPath,
|
|
175
|
+
networks: buildNetworks(plan),
|
|
176
|
+
projectRoot: path.dirname(plan.root),
|
|
177
|
+
root,
|
|
178
|
+
runtimes: []
|
|
179
|
+
};
|
|
180
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { formatSourceMeta } from "./sourcePaths.js";
|
|
2
|
+
const color = (value, code, options) => options.color ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
3
|
+
const glyphsFor = (options) => options.ascii
|
|
4
|
+
? { branch: "|-- ", last: "`-- ", pipe: "| ", space: " " }
|
|
5
|
+
: { branch: "├── ", last: "└── ", pipe: "│ ", space: " " };
|
|
6
|
+
const formatPolicy = (member) => {
|
|
7
|
+
if (!member.policy) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return [
|
|
11
|
+
member.policy.read ? `read=${member.policy.read}` : undefined,
|
|
12
|
+
member.policy.reply ? `reply=${member.policy.reply}` : undefined
|
|
13
|
+
].filter((entry) => entry !== undefined);
|
|
14
|
+
};
|
|
15
|
+
const formatMember = (member, options, projectRoot) => {
|
|
16
|
+
const metadata = member.representedSlot
|
|
17
|
+
? [
|
|
18
|
+
`represents=${member.representedSlot}`,
|
|
19
|
+
member.representedTeamName && member.representedTeamName !== member.representedSlot
|
|
20
|
+
? `team=${member.representedTeamName}`
|
|
21
|
+
: undefined,
|
|
22
|
+
`member=${member.concreteMemberId}`
|
|
23
|
+
]
|
|
24
|
+
: [
|
|
25
|
+
`team=${member.directTeamName}`,
|
|
26
|
+
`member=${member.concreteMemberId}`
|
|
27
|
+
];
|
|
28
|
+
const source = options.paths
|
|
29
|
+
? formatSourceMeta("source", member.agentSource, projectRoot)
|
|
30
|
+
: "";
|
|
31
|
+
const details = [
|
|
32
|
+
...metadata,
|
|
33
|
+
...formatPolicy(member)
|
|
34
|
+
].filter((entry) => entry !== undefined);
|
|
35
|
+
return `${member.agentName} ${details.join(" ")}${source}`;
|
|
36
|
+
};
|
|
37
|
+
const formatNetwork = (network, options) => {
|
|
38
|
+
const id = color(network.id, "36", options);
|
|
39
|
+
return `${network.provider} ${id}`;
|
|
40
|
+
};
|
|
41
|
+
const getDeclarations = (network) => network.declarations ?? [
|
|
42
|
+
{
|
|
43
|
+
declaringTeamName: network.declaringTeamName,
|
|
44
|
+
declaringTeamSource: network.declaringTeamSource,
|
|
45
|
+
expose: network.expose,
|
|
46
|
+
name: network.name,
|
|
47
|
+
rooms: network.rooms
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
const formatDeclaration = (network, declaration, options, projectRoot) => {
|
|
51
|
+
const exposed = declaration.expose ? " exposed" : "";
|
|
52
|
+
const source = options.paths
|
|
53
|
+
? formatSourceMeta("declared_source", declaration.declaringTeamSource, projectRoot)
|
|
54
|
+
: "";
|
|
55
|
+
return `${network.id} "${declaration.name}" on ${declaration.declaringTeamName}${exposed}${source}`;
|
|
56
|
+
};
|
|
57
|
+
export const renderOrganizationNetworks = (view, options = {}) => {
|
|
58
|
+
if (view.networks.length === 0) {
|
|
59
|
+
return "No Moltnet networks.";
|
|
60
|
+
}
|
|
61
|
+
const glyphs = glyphsFor(options);
|
|
62
|
+
const lines = ["Moltnet networks"];
|
|
63
|
+
view.networks.forEach((network, networkIndex) => {
|
|
64
|
+
const networkLast = networkIndex === view.networks.length - 1;
|
|
65
|
+
const networkPrefix = networkLast ? glyphs.space : glyphs.pipe;
|
|
66
|
+
lines.push(`${networkLast ? glyphs.last : glyphs.branch}${formatNetwork(network, options)}`);
|
|
67
|
+
const declarations = getDeclarations(network);
|
|
68
|
+
declarations.forEach((declaration, declarationIndex) => {
|
|
69
|
+
const declarationLast = declarationIndex === declarations.length - 1;
|
|
70
|
+
const declarationPrefix = `${networkPrefix}${declarationLast ? glyphs.space : glyphs.pipe}`;
|
|
71
|
+
lines.push(`${networkPrefix}${declarationLast ? glyphs.last : glyphs.branch}${formatDeclaration(network, declaration, options, view.projectRoot)}`);
|
|
72
|
+
declaration.rooms.forEach((room, roomIndex) => {
|
|
73
|
+
const roomLast = roomIndex === declaration.rooms.length - 1;
|
|
74
|
+
const roomPrefix = `${declarationPrefix}${roomLast ? glyphs.space : glyphs.pipe}`;
|
|
75
|
+
const legacyDeclared = options.declared && !view.projectRoot
|
|
76
|
+
? ` room ${room.id} declared [${room.declaredMembers.join(", ")}]`
|
|
77
|
+
: "";
|
|
78
|
+
lines.push(`${declarationPrefix}${roomLast ? glyphs.last : glyphs.branch}#${room.id}${legacyDeclared}`);
|
|
79
|
+
if (options.declared) {
|
|
80
|
+
const declaredMembers = room.declaredMembers.length > 0
|
|
81
|
+
? room.declaredMembers.join(", ")
|
|
82
|
+
: "(none)";
|
|
83
|
+
lines.push(`${roomPrefix}${glyphs.branch}declared members: ${declaredMembers}`);
|
|
84
|
+
}
|
|
85
|
+
room.members.forEach((member, memberIndex) => {
|
|
86
|
+
const memberLast = memberIndex === room.members.length - 1;
|
|
87
|
+
lines.push(`${roomPrefix}${memberLast ? glyphs.last : glyphs.branch}${formatMember(member, options, view.projectRoot)}`);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
};
|