@volter/supercode-teams 0.2.20

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 (79) hide show
  1. package/README.md +119 -0
  2. package/attention.mjs +191 -0
  3. package/bin/fleet.mjs +151 -0
  4. package/bin/teams.mjs +291 -0
  5. package/bin/workspace/args.mjs +66 -0
  6. package/bin/workspace/files.mjs +110 -0
  7. package/bin/workspace/lifecycle.mjs +113 -0
  8. package/bin/workspace/volter-device.mjs +42 -0
  9. package/bin/workspace/volter-login.mjs +185 -0
  10. package/bin/workspace.mjs +726 -0
  11. package/browser-attach.mjs +90 -0
  12. package/client.mjs +212 -0
  13. package/connector/effects.mjs +95 -0
  14. package/connector/index.d.ts +69 -0
  15. package/connector/index.mjs +174 -0
  16. package/connector/native-continuation.d.ts +8 -0
  17. package/connector/native-continuation.mjs +50 -0
  18. package/connector/native-effects.mjs +164 -0
  19. package/context.mjs +34 -0
  20. package/contracts/adapter-map.json +16 -0
  21. package/contracts/index.d.ts +299 -0
  22. package/contracts/index.mjs +37 -0
  23. package/execution.mjs +206 -0
  24. package/fleet.mjs +78 -0
  25. package/index.mjs +12 -0
  26. package/machine-door.mjs +36 -0
  27. package/machine.mjs +1398 -0
  28. package/package.json +80 -0
  29. package/porthole-hook.cjs +137 -0
  30. package/remote/job-exec.py +146 -0
  31. package/remote/wire.py +23 -0
  32. package/server/arc2-transactions.mjs +125 -0
  33. package/server/attachment-relay.mjs +149 -0
  34. package/server/catalog.mjs +7 -0
  35. package/server/continuations.mjs +56 -0
  36. package/server/correlation.d.ts +4 -0
  37. package/server/correlation.mjs +17 -0
  38. package/server/effects-transactions.mjs +119 -0
  39. package/server/effects.mjs +97 -0
  40. package/server/event-routes.mjs +208 -0
  41. package/server/exports.mjs +46 -0
  42. package/server/harness-routes.mjs +248 -0
  43. package/server/index.d.ts +31 -0
  44. package/server/index.mjs +52 -0
  45. package/server/ingest.mjs +24 -0
  46. package/server/node-upgrade.mjs +39 -0
  47. package/server/otlp/collector-config.d.ts +2 -0
  48. package/server/otlp/collector-config.mjs +26 -0
  49. package/server/otlp/decoders/index.d.ts +63 -0
  50. package/server/otlp/decoders/index.mjs +311 -0
  51. package/server/otlp/index.d.ts +4 -0
  52. package/server/otlp/index.mjs +4 -0
  53. package/server/otlp/projection.d.ts +5 -0
  54. package/server/otlp/projection.mjs +17 -0
  55. package/server/otlp/receiver.d.ts +3 -0
  56. package/server/otlp/receiver.mjs +36 -0
  57. package/server/otlp/schema/google/protobuf/any.proto +162 -0
  58. package/server/otlp/schema/google/rpc/status.proto +48 -0
  59. package/server/otlp/schema/manifest.json +22 -0
  60. package/server/otlp/schema/opentelemetry/proto/collector/logs/v1/logs_service.proto +77 -0
  61. package/server/otlp/schema/opentelemetry/proto/collector/metrics/v1/metrics_service.proto +77 -0
  62. package/server/otlp/schema/opentelemetry/proto/collector/trace/v1/trace_service.proto +77 -0
  63. package/server/otlp/schema/opentelemetry/proto/common/v1/common.proto +154 -0
  64. package/server/otlp/schema/opentelemetry/proto/logs/v1/logs.proto +226 -0
  65. package/server/otlp/schema/opentelemetry/proto/metrics/v1/metrics.proto +735 -0
  66. package/server/otlp/schema/opentelemetry/proto/resource/v1/resource.proto +45 -0
  67. package/server/otlp/schema/opentelemetry/proto/trace/v1/trace.proto +359 -0
  68. package/server/otlp/transport.d.ts +10 -0
  69. package/server/otlp/transport.mjs +424 -0
  70. package/server/otlp/wire-codec.mjs +153 -0
  71. package/server/store-core.mjs +139 -0
  72. package/server/store-worker.mjs +23 -0
  73. package/worker/fetch-adapter.mjs +49 -0
  74. package/worker/index.mjs +127 -0
  75. package/worker/websocket-duplex.mjs +29 -0
  76. package/worker/wrangler.toml +38 -0
  77. package/workspace-client.d.ts +75 -0
  78. package/workspace-client.mjs +171 -0
  79. package/workspace-move.mjs +38 -0
@@ -0,0 +1,185 @@
1
+ // `supercode teams login` with Volter and no --team: signing in is never refused for being on several teams. The
2
+ // person signs in once; the teams server says which teams that identity may use; the person chooses which of them
3
+ // this computer signs into (in the browser tab they signed in from, else at the terminal), and each becomes a
4
+ // workspace context. Commands act on the current context; `supercode teams context use NAME` switches it.
5
+ import { hostname } from 'node:os';
6
+ import { createInterface } from 'node:readline/promises';
7
+ import { WorkspaceContexts, contextNameOf, loginContext } from '../../context.mjs';
8
+ import { TeamsClient } from '../../workspace-client.mjs';
9
+
10
+ /** The teams an identity may sign in to: `[{ id, name, role, joined }]`, or null from a server that cannot say. */
11
+ export async function teamsForIdentity({ origin, identityToken, fetch = globalThis.fetch }) {
12
+ const response = await fetch(new URL('/api/v1/login/volter/teams', origin), { method: 'POST', redirect: 'manual', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ access_token: identityToken }) });
13
+ // A server from before the listing answers 404, or 401 since its other routes all need a credential. A token that
14
+ // does not verify fails again, saying so, at the sign-in that follows.
15
+ if (response.status === 404 || response.status === 401) return null;
16
+ if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) throw new Error('cross-origin redirects are refused');
17
+ const value = await response.json().catch(() => null);
18
+ if (!response.ok) throw new Error(`sign-in failed: ${value?.error?.message ?? `HTTP ${response.status}`}`);
19
+ return value.teams;
20
+ }
21
+
22
+ /**
23
+ * Sign this computer into the teams the person chooses. Returns `{ result: { saved, current, failed }, page }`:
24
+ * the contexts made, the current context, the teams that could not be signed into, and the page the tab ends on.
25
+ */
26
+ export async function signInToTeams({ supercodeHome, origin, identityToken, ask, as = null, fetch = globalThis.fetch, input = process.stdin, prompt = process.stderr }) {
27
+ const store = new WorkspaceContexts(supercodeHome), before = store.read().current;
28
+ const login = (teamId, name) => loginContext({ supercodeHome, name, origin, teamId, token: null, invite: null, identityToken, as, fetch });
29
+ let teams = await teamsForIdentity({ origin, identityToken, fetch });
30
+ if (teams === null) {
31
+ // A server from before the listing signs a person on one team straight in and names several in its refusal,
32
+ // without saying which of them they are a member of.
33
+ try {
34
+ return finished(store, before, [await login(null, as)], []);
35
+ } catch (error) {
36
+ const several = /you belong to several teams \((.+)\); name one with --team/u.exec(error.message);
37
+ if (!several) throw error;
38
+ teams = several[1].split(', ').map((name) => ({ id: name, name, role: null, joined: null }));
39
+ }
40
+ }
41
+ if (!teams.length) throw new Error('you are not a member of a team on this server; ask a teammate for an invitation');
42
+ if (teams.length > 1 && as) throw new Error('--as names one team\'s context; add --team to choose which');
43
+ const chosen = teams.length === 1 ? [teams[0]] : await choose(teams, { ask, input, prompt, store, origin });
44
+ const nameFor = (team) => as ?? freeName(store, contextNameOf(team.name), origin, team.id);
45
+ // One sign-in, one credential: it is the person's on every team they are a member of, so the other teams' contexts
46
+ // share it. A team they join through their organization is signed into on its own, which joins it.
47
+ const first = await login(chosen[0].id, nameFor(chosen[0])), saved = [first], failed = [];
48
+ if (chosen.length > 1) {
49
+ let me;
50
+ try {
51
+ me = await new TeamsClient({ server: first.origin, credential: store.credential(first.credential_ref).token, fetch }).me();
52
+ } catch (error) {
53
+ for (const team of chosen.slice(1)) failed.push({ team: team.name, reason: error.message });
54
+ return finished(store, before, saved, failed);
55
+ }
56
+ for (const team of chosen.slice(1)) {
57
+ const membership = me.teams.find((item) => item.team.id === team.id || item.team.name === team.id);
58
+ try {
59
+ saved.push(membership
60
+ ? store.upsert(freeName(store, contextNameOf(membership.team.name), origin, membership.team.id), { origin: first.origin, server_id: first.server_id, team_id: membership.team.id, team_name: membership.team.name, principal_id: first.principal_id, credential_ref: first.credential_ref, label: null })
61
+ : await login(team.id, nameFor(team)));
62
+ } catch (error) {
63
+ failed.push({ team: team.name, reason: error.message });
64
+ }
65
+ }
66
+ }
67
+ return finished(store, before, saved, failed);
68
+ }
69
+
70
+ /**
71
+ * A context name for a team: its own, unless another server's or another team's context holds it, then the first
72
+ * free `-2`, `-3`… (two teams named "Acme Inc" and "acme-inc" are two contexts).
73
+ */
74
+ function freeName(store, base, origin, teamId) {
75
+ const contexts = store.read().contexts;
76
+ for (let index = 1; ; index += 1) {
77
+ const name = index === 1 ? base : `${base.slice(0, 60)}-${index}`;
78
+ const held = contexts[name];
79
+ // Against an older server a team is known by its name until it is signed into.
80
+ if (!held || (held.origin === origin && (held.team_id === teamId || held.team_name === teamId))) return name;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * What the sign-in did. The current context changes only when there was none, as a single login's does, or when it
86
+ * named a context that no longer exists (`local` is never a stored context, and stays).
87
+ */
88
+ function finished(store, before, saved, failed) {
89
+ const contexts = store.read().contexts;
90
+ const named = saved.map((context) => ({ ...context, name: Object.entries(contexts).find(([, value]) => value.credential_ref === context.credential_ref && value.team_id === context.team_id)[0] }));
91
+ if (!before || (before !== 'local' && !contexts[before])) store.use(named[0].name);
92
+ const current = store.read().current, ours = named.find((context) => context.name === current);
93
+ const teamName = (context) => context.team_name ?? context.team_id;
94
+ const lede = [
95
+ `supercode on ${hostname()} can use ${list(named.map(teamName))}.`,
96
+ ...failed.map(({ team, reason }) => `${team} was not signed in: ${reason}.`),
97
+ `Back in the terminal, run supercode teams connect to put this computer on ${ours ? teamName(ours) : 'a team'}.`,
98
+ ].join(' ');
99
+ return {
100
+ result: { saved: named, current, failed },
101
+ page: {
102
+ ok: true,
103
+ title: "You're signed in",
104
+ lede,
105
+ items: named.map((context) => ({ label: teamName(context), detail: context.name === current ? 'commands use this team' : `switch with: supercode teams context use ${context.name}` })),
106
+ },
107
+ };
108
+ }
109
+
110
+ /** What the terminal says after a login that did not name its team. */
111
+ export function loginSummary({ saved, current, failed }, origin) {
112
+ const teamName = (context) => context.team_name ?? context.team_id, ours = saved.find((context) => context.name === current);
113
+ const lines = [`Signed in to ${saved.length === 1 ? `team ${teamName(saved[0])}` : `${saved.length} teams: ${saved.map(teamName).join(', ')}`} (${new URL(origin).host}).`];
114
+ for (const { team, reason } of failed) lines.push(`Not signed in to ${team}: ${reason}`);
115
+ const other = saved.find((context) => context.name !== current);
116
+ if (!ours) lines.push(`Commands still use the context ${current}; switch with: supercode teams context use ${saved[0].name}`);
117
+ else if (other) lines.push(`Commands use ${teamName(ours)}; switch with: supercode teams context use ${other.name}`);
118
+ lines.push('Next, from the folder you work in: supercode teams connect');
119
+ return `${lines.join('\n')}\n`;
120
+ }
121
+
122
+ const list = (names) => names.length < 2 ? names.join('') : `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
123
+
124
+ function roleOf(team) {
125
+ if (team.joined === false) return 'you join through your organization';
126
+ return team.role ?? undefined;
127
+ }
128
+
129
+ /**
130
+ * Which teams to sign into: asked in the browser tab when there is one, and at the terminal as well when it is one
131
+ * (whichever answers first, so a closed tab does not leave the login waiting); with nobody to ask, every team the
132
+ * person is a member of, never joining one for them.
133
+ */
134
+ async function choose(teams, { ask, input, prompt, store, origin }) {
135
+ const already = new Set(Object.values(store.read().contexts).filter((context) => context.origin === origin).map((context) => context.team_id));
136
+ const preselected = (team) => team.joined !== false && (already.size === 0 || already.has(team.id));
137
+ const terminal = Boolean(input.isTTY && prompt.isTTY);
138
+ if (!ask && !terminal) {
139
+ const joined = teams.filter((team) => team.joined === true);
140
+ if (!joined.length) throw new Error(`you may sign in to several teams (${teams.map((team) => team.name).join(', ')}); name one with --team`);
141
+ return joined;
142
+ }
143
+ const stop = new AbortController(), answers = [];
144
+ if (ask) {
145
+ prompt.write(terminal ? 'Choose your teams in the browser tab, or here.\n' : 'Choose your teams in the browser tab.\n');
146
+ const asked = ask({
147
+ title: 'Choose your teams',
148
+ lede: `You belong to ${teams.length} teams. Sign in to the ones you work in on this computer; you can add the others later.`,
149
+ options: teams.map((team) => ({ id: team.id, label: team.name, detail: roleOf(team), checked: preselected(team) })),
150
+ submit: 'Sign in on this computer',
151
+ });
152
+ // Answered at the terminal first, the tab's question is abandoned when the sign-in ends.
153
+ asked.catch(() => {});
154
+ answers.push(asked.then((ids) => teams.filter((team) => ids.includes(team.id))));
155
+ }
156
+ if (terminal) answers.push(askTerminal(teams, { input, prompt, preselected, signal: stop.signal }));
157
+ try {
158
+ return await Promise.race(answers);
159
+ } finally {
160
+ stop.abort();
161
+ }
162
+ }
163
+
164
+ /** The terminal's question; once `signal` aborts (the tab answered), it never settles. */
165
+ async function askTerminal(teams, { input, prompt, preselected, signal }) {
166
+ prompt.write(`You belong to ${teams.length} teams:\n`);
167
+ teams.forEach((team, index) => prompt.write(` ${index + 1}. ${team.name}${roleOf(team) ? ` (${roleOf(team)})` : ''}\n`));
168
+ const defaults = teams.map((team, index) => preselected(team) ? index + 1 : null).filter(Boolean);
169
+ const shown = defaults.length ? defaults.join(',') : '1';
170
+ const reader = createInterface({ input, output: prompt });
171
+ try {
172
+ for (;;) {
173
+ const answer = (await reader.question(`Sign in to which? (numbers like 1,2, or "all") [${shown}] `, { signal })).trim().toLowerCase();
174
+ const picked = answer === 'all' ? teams.map((_, index) => index + 1) : (answer || shown).split(/[\s,]+/u).filter(Boolean).map(Number);
175
+ if (picked.length && picked.every((number) => Number.isInteger(number) && number >= 1 && number <= teams.length)) return [...new Set(picked)].map((number) => teams[number - 1]);
176
+ prompt.write(`Answer with numbers from 1 to ${teams.length}.\n`);
177
+ }
178
+ } catch (error) {
179
+ if (signal.aborted) return new Promise(() => {});
180
+ throw error;
181
+ } finally {
182
+ reader.close();
183
+ if (signal.aborted) prompt.write('\n');
184
+ }
185
+ }