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.
Files changed (33) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/index.js +0 -0
  3. package/dist/cli/runCli.d.ts +18 -2
  4. package/dist/cli/runCli.js +75 -62
  5. package/dist/cli/viewCommand.d.ts +3 -0
  6. package/dist/cli/viewCommand.js +87 -0
  7. package/dist/compiler/buildCompilePlan.js +2 -0
  8. package/dist/compiler/containerEntrypointRender.js +3 -3
  9. package/dist/compiler/index.d.ts +2 -0
  10. package/dist/compiler/index.js +2 -0
  11. package/dist/compiler/moltnetArtifacts.js +7 -3
  12. package/dist/compiler/moltnetResolution.js +29 -48
  13. package/dist/compiler/moltnetRoomMemberships.d.ts +3 -0
  14. package/dist/compiler/moltnetRoomMemberships.js +140 -0
  15. package/dist/compiler/runProject.d.ts +2 -0
  16. package/dist/compiler/runProject.js +20 -6
  17. package/dist/compiler/syncProjectAuth.js +53 -19
  18. package/dist/compiler/types.d.ts +18 -0
  19. package/dist/compiler/view/buildOrganizationView.d.ts +2 -0
  20. package/dist/compiler/view/buildOrganizationView.js +180 -0
  21. package/dist/compiler/view/index.d.ts +4 -0
  22. package/dist/compiler/view/index.js +4 -0
  23. package/dist/compiler/view/renderNetworks.d.ts +2 -0
  24. package/dist/compiler/view/renderNetworks.js +93 -0
  25. package/dist/compiler/view/renderTree.d.ts +2 -0
  26. package/dist/compiler/view/renderTree.js +59 -0
  27. package/dist/compiler/view/sourcePaths.d.ts +2 -0
  28. package/dist/compiler/view/sourcePaths.js +19 -0
  29. package/dist/compiler/view/types.d.ts +80 -0
  30. package/dist/compiler/view/types.js +1 -0
  31. package/dist/runtime/picoclaw/adapter.js +7 -0
  32. package/dist/runtime/picoclaw/runAuth.js +5 -41
  33. 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 envNames = new Set();
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
- envNames.add(envName);
35
+ requiredEnvNames.add(envName);
36
+ optionalEnvNames.delete(envName);
20
37
  }
21
38
  for (const envName of listAgentSurfaceSecretNames(node.value.surfaces)) {
22
- envNames.add(envName);
39
+ requiredEnvNames.add(envName);
40
+ optionalEnvNames.delete(envName);
23
41
  }
24
42
  }
25
- return { envNames, methods };
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 (envNames, envFilePath) => {
28
- if (envNames.size === 0) {
57
+ const resolveRequiredEnv = async (requiredEnvNames, optionalEnvNames, envFilePath) => {
58
+ if (requiredEnvNames.size === 0 && optionalEnvNames.size === 0) {
29
59
  return {};
30
60
  }
31
- const fileEnv = envFilePath ? parseEnvFile(await readUtf8File(envFilePath)) : {};
61
+ const fileEnv = await readEnvFile(envFilePath);
32
62
  const resolvedEnv = {};
33
63
  const missingEnv = [];
34
- for (const envName of [...envNames].sort()) {
35
- const processValue = process.env[envName];
36
- if (typeof processValue === "string" && processValue.length > 0) {
37
- resolvedEnv[envName] = processValue;
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 { envNames, methods } = await resolveAuthRequirements(inputPath);
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 (envNames.size > 0) {
62
- await setAuthProfileEnv(options.profileName, await resolveRequiredEnv(envNames, options.envFilePath));
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
  };
@@ -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,2 @@
1
+ import type { OrganizationView } from "./types.js";
2
+ export declare const buildOrganizationView: (inputPath: string) => Promise<OrganizationView>;
@@ -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,4 @@
1
+ export * from "./buildOrganizationView.js";
2
+ export * from "./renderNetworks.js";
3
+ export * from "./renderTree.js";
4
+ export * from "./types.js";
@@ -0,0 +1,4 @@
1
+ export * from "./buildOrganizationView.js";
2
+ export * from "./renderNetworks.js";
3
+ export * from "./renderTree.js";
4
+ export * from "./types.js";
@@ -0,0 +1,2 @@
1
+ import type { OrganizationView, RenderOrganizationViewOptions } from "./types.js";
2
+ export declare const renderOrganizationNetworks: (view: OrganizationView, options?: RenderOrganizationViewOptions) => string;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { OrganizationView, RenderOrganizationViewOptions } from "./types.js";
2
+ export declare const renderOrganizationTree: (view: OrganizationView, options?: RenderOrganizationViewOptions) => string;