koppler 0.0.0-stage → 0.0.2

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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +101 -2
  3. package/bin/run-logged +16 -0
  4. package/bin/secret-list +7 -0
  5. package/bin/secret-psql +41 -0
  6. package/bin/secret-set +17 -0
  7. package/bin/secret-store +82 -0
  8. package/bin/secret-test +10 -0
  9. package/bin/with-secrets +25 -0
  10. package/dist/.built +1 -0
  11. package/dist/cli/agent.js +144 -0
  12. package/dist/cli/agent.js.map +1 -0
  13. package/dist/cli/doctor.js +173 -0
  14. package/dist/cli/doctor.js.map +1 -0
  15. package/dist/cli/mcp.js +204 -0
  16. package/dist/cli/mcp.js.map +1 -0
  17. package/dist/cli/output.js +37 -0
  18. package/dist/cli/output.js.map +1 -0
  19. package/dist/cli/records.js +121 -0
  20. package/dist/cli/records.js.map +1 -0
  21. package/dist/cli/running.js +270 -0
  22. package/dist/cli/running.js.map +1 -0
  23. package/dist/cli/shell.js +43 -0
  24. package/dist/cli/shell.js.map +1 -0
  25. package/dist/cli/wait.js +59 -0
  26. package/dist/cli/wait.js.map +1 -0
  27. package/dist/core/addresses.js +55 -0
  28. package/dist/core/addresses.js.map +1 -0
  29. package/dist/core/arrange.js +329 -0
  30. package/dist/core/arrange.js.map +1 -0
  31. package/dist/core/cables.js +158 -0
  32. package/dist/core/cables.js.map +1 -0
  33. package/dist/core/expressions.js +72 -0
  34. package/dist/core/expressions.js.map +1 -0
  35. package/dist/core/frameworks.js +13 -0
  36. package/dist/core/frameworks.js.map +1 -0
  37. package/dist/core/kinds.js +244 -0
  38. package/dist/core/kinds.js.map +1 -0
  39. package/dist/core/launch.js +181 -0
  40. package/dist/core/launch.js.map +1 -0
  41. package/dist/core/logfmt.js +144 -0
  42. package/dist/core/logfmt.js.map +1 -0
  43. package/dist/core/mock.js +27 -0
  44. package/dist/core/mock.js.map +1 -0
  45. package/dist/core/plan.js +147 -0
  46. package/dist/core/plan.js.map +1 -0
  47. package/dist/core/ports.js +155 -0
  48. package/dist/core/ports.js.map +1 -0
  49. package/dist/core/presets.js +186 -0
  50. package/dist/core/presets.js.map +1 -0
  51. package/dist/core/secret-types.js +3 -0
  52. package/dist/core/secret-types.js.map +1 -0
  53. package/dist/core/sign-in.js +30 -0
  54. package/dist/core/sign-in.js.map +1 -0
  55. package/dist/core/stack-path.js +64 -0
  56. package/dist/core/stack-path.js.map +1 -0
  57. package/dist/core/stack.js +509 -0
  58. package/dist/core/stack.js.map +1 -0
  59. package/dist/core/terraform.js +393 -0
  60. package/dist/core/terraform.js.map +1 -0
  61. package/dist/data.js +37 -0
  62. package/dist/data.js.map +1 -0
  63. package/dist/engine/actions.js +99 -0
  64. package/dist/engine/actions.js.map +1 -0
  65. package/dist/engine/app-commands.js +106 -0
  66. package/dist/engine/app-commands.js.map +1 -0
  67. package/dist/engine/app-start.js +167 -0
  68. package/dist/engine/app-start.js.map +1 -0
  69. package/dist/engine/app-state.js +43 -0
  70. package/dist/engine/app-state.js.map +1 -0
  71. package/dist/engine/boards.js +219 -0
  72. package/dist/engine/boards.js.map +1 -0
  73. package/dist/engine/branch.js +194 -0
  74. package/dist/engine/branch.js.map +1 -0
  75. package/dist/engine/browser-console.js +45 -0
  76. package/dist/engine/browser-console.js.map +1 -0
  77. package/dist/engine/calls.js +97 -0
  78. package/dist/engine/calls.js.map +1 -0
  79. package/dist/engine/checks.js +270 -0
  80. package/dist/engine/checks.js.map +1 -0
  81. package/dist/engine/clones.js +67 -0
  82. package/dist/engine/clones.js.map +1 -0
  83. package/dist/engine/config.js +26 -0
  84. package/dist/engine/config.js.map +1 -0
  85. package/dist/engine/deployed.js +488 -0
  86. package/dist/engine/deployed.js.map +1 -0
  87. package/dist/engine/devices.js +81 -0
  88. package/dist/engine/devices.js.map +1 -0
  89. package/dist/engine/discover.js +236 -0
  90. package/dist/engine/discover.js.map +1 -0
  91. package/dist/engine/down.js +253 -0
  92. package/dist/engine/down.js.map +1 -0
  93. package/dist/engine/history.js +215 -0
  94. package/dist/engine/history.js.map +1 -0
  95. package/dist/engine/investigate.js +278 -0
  96. package/dist/engine/investigate.js.map +1 -0
  97. package/dist/engine/libraries.js +129 -0
  98. package/dist/engine/libraries.js.map +1 -0
  99. package/dist/engine/linked.js +146 -0
  100. package/dist/engine/linked.js.map +1 -0
  101. package/dist/engine/listen.js +223 -0
  102. package/dist/engine/listen.js.map +1 -0
  103. package/dist/engine/locks.js +38 -0
  104. package/dist/engine/locks.js.map +1 -0
  105. package/dist/engine/messages.js +177 -0
  106. package/dist/engine/messages.js.map +1 -0
  107. package/dist/engine/needs.js +363 -0
  108. package/dist/engine/needs.js.map +1 -0
  109. package/dist/engine/processes.js +350 -0
  110. package/dist/engine/processes.js.map +1 -0
  111. package/dist/engine/query.js +241 -0
  112. package/dist/engine/query.js.map +1 -0
  113. package/dist/engine/queues.js +100 -0
  114. package/dist/engine/queues.js.map +1 -0
  115. package/dist/engine/recipes.js +248 -0
  116. package/dist/engine/recipes.js.map +1 -0
  117. package/dist/engine/reload.js +32 -0
  118. package/dist/engine/reload.js.map +1 -0
  119. package/dist/engine/repos.js +214 -0
  120. package/dist/engine/repos.js.map +1 -0
  121. package/dist/engine/research.js +134 -0
  122. package/dist/engine/research.js.map +1 -0
  123. package/dist/engine/run-logs.js +79 -0
  124. package/dist/engine/run-logs.js.map +1 -0
  125. package/dist/engine/run.js +54 -0
  126. package/dist/engine/run.js.map +1 -0
  127. package/dist/engine/runs.js +223 -0
  128. package/dist/engine/runs.js.map +1 -0
  129. package/dist/engine/session.js +130 -0
  130. package/dist/engine/session.js.map +1 -0
  131. package/dist/engine/share.js +124 -0
  132. package/dist/engine/share.js.map +1 -0
  133. package/dist/engine/stack-file.js +206 -0
  134. package/dist/engine/stack-file.js.map +1 -0
  135. package/dist/engine/stack-share.js +86 -0
  136. package/dist/engine/stack-share.js.map +1 -0
  137. package/dist/engine/stacks.js +143 -0
  138. package/dist/engine/stacks.js.map +1 -0
  139. package/dist/engine/sync.js +214 -0
  140. package/dist/engine/sync.js.map +1 -0
  141. package/dist/engine/traces.js +65 -0
  142. package/dist/engine/traces.js.map +1 -0
  143. package/dist/engine/up.js +621 -0
  144. package/dist/engine/up.js.map +1 -0
  145. package/dist/engine/watch.js +71 -0
  146. package/dist/engine/watch.js.map +1 -0
  147. package/dist/engine/wire.js +120 -0
  148. package/dist/engine/wire.js.map +1 -0
  149. package/dist/failure.js +16 -0
  150. package/dist/failure.js.map +1 -0
  151. package/dist/github.js +95 -0
  152. package/dist/github.js.map +1 -0
  153. package/dist/ids.js +6 -0
  154. package/dist/ids.js.map +1 -0
  155. package/dist/json-files.js +36 -0
  156. package/dist/json-files.js.map +1 -0
  157. package/dist/koppler.mjs +840 -0
  158. package/dist/koppler.mjs.map +1 -0
  159. package/dist/lambda-local.mjs +250 -0
  160. package/dist/lambda-local.mjs.map +1 -0
  161. package/dist/lambda-secrets-proxy.go +31 -0
  162. package/dist/mock-server.mjs +23 -0
  163. package/dist/mock-server.mjs.map +1 -0
  164. package/dist/page/assets/JetBrainsMono-cyrillic-D2jMJ_bk.woff2 +0 -0
  165. package/dist/page/assets/JetBrainsMono-greek-D2nYnAec.woff2 +0 -0
  166. package/dist/page/assets/JetBrainsMono-latin-Db4Uuiha.woff2 +0 -0
  167. package/dist/page/assets/JetBrainsMono-latin-ext-Bnd2h4Br.woff2 +0 -0
  168. package/dist/page/assets/JetBrainsMono-vietnamese-CNlsqbWb.woff2 +0 -0
  169. package/dist/page/assets/SchibstedGrotesk-latin-DxefFeRw.woff2 +0 -0
  170. package/dist/page/assets/SchibstedGrotesk-latin-ext-CnpJE18X.woff2 +0 -0
  171. package/dist/page/assets/index-Bkj_pX35.js +42 -0
  172. package/dist/page/assets/index-Dq45jswH.css +1 -0
  173. package/dist/page/index.html +14 -0
  174. package/dist/page/theme.js +8 -0
  175. package/dist/paths.js +19 -0
  176. package/dist/paths.js.map +1 -0
  177. package/dist/safe-env.js +73 -0
  178. package/dist/safe-env.js.map +1 -0
  179. package/dist/sam-local.mjs +135 -0
  180. package/dist/sam-local.mjs.map +1 -0
  181. package/dist/scrub.js +87 -0
  182. package/dist/scrub.js.map +1 -0
  183. package/dist/secret-check.mjs +262 -0
  184. package/dist/secret-check.mjs.map +1 -0
  185. package/dist/server/browser-console.js +117 -0
  186. package/dist/server/browser-console.js.map +1 -0
  187. package/dist/server/cable-test.js +365 -0
  188. package/dist/server/cable-test.js.map +1 -0
  189. package/dist/server/choices.js +51 -0
  190. package/dist/server/choices.js.map +1 -0
  191. package/dist/server/files.js +107 -0
  192. package/dist/server/files.js.map +1 -0
  193. package/dist/server/gate.js +128 -0
  194. package/dist/server/gate.js.map +1 -0
  195. package/dist/server/logs.js +76 -0
  196. package/dist/server/logs.js.map +1 -0
  197. package/dist/server/mobile.js +113 -0
  198. package/dist/server/mobile.js.map +1 -0
  199. package/dist/server/routes.js +326 -0
  200. package/dist/server/routes.js.map +1 -0
  201. package/dist/server/runs.js +111 -0
  202. package/dist/server/runs.js.map +1 -0
  203. package/dist/server/sanitize.js +242 -0
  204. package/dist/server/sanitize.js.map +1 -0
  205. package/dist/server/secrets.js +111 -0
  206. package/dist/server/secrets.js.map +1 -0
  207. package/dist/server/status.js +321 -0
  208. package/dist/server/status.js.map +1 -0
  209. package/dist/server/token.js +38 -0
  210. package/dist/server/token.js.map +1 -0
  211. package/dist/server/waiting.js +79 -0
  212. package/dist/server/waiting.js.map +1 -0
  213. package/dist/status-watch.mjs +63 -0
  214. package/dist/status-watch.mjs.map +1 -0
  215. package/package.json +74 -4
  216. package/skills/koppler-run/SKILL.md +169 -0
  217. package/skills/koppler-run/VERSION +1 -0
  218. package/skills/koppler-run/references/action-log.md +50 -0
  219. package/skills/koppler-run/references/permissions.md +25 -0
  220. package/skills/koppler-run/references/preparing-nodes.md +62 -0
  221. package/skills/koppler-run/references/reporting-bugs.md +76 -0
  222. package/skills/koppler-run/references/research.md +40 -0
  223. package/skills/koppler-run/references/secrets.md +56 -0
  224. package/skills/koppler-run/references/stack.md +197 -0
  225. package/skills/koppler-run/references/status.md +92 -0
@@ -0,0 +1,621 @@
1
+ // `koppler up`: everything a run needs, started in one go: its tunnels, then its apps with the ones called first, each
2
+ // waited for. Every run is recorded like a Run from the canvas, so the canvas, the history and Stop all know it.
3
+ // With --run <id> it carries out that confirmed Send: stops and restarts, branches as chosen on the cards, remembered
4
+ // changes, each cable's local address passed to its caller, then starts. What it cannot do alone is listed for the
5
+ // agent (exit 3).
6
+ import { keepRunLogs } from "./run-logs.js";
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { envOf, flagValue, positional } from "../cli/output.js";
10
+ import { fail, Failure } from "../failure.js";
11
+ import { of as kindOf } from "../core/kinds.js";
12
+ import { aliasReady, planSteps, planTunnels } from "../core/ports.js";
13
+ import { signInOf } from "../core/sign-in.js";
14
+ import { readClones, readData, readStack } from "../data.js";
15
+ import { CODE, tempDir } from "../paths.js";
16
+ import { readActions, writeLogged } from "./actions.js";
17
+ import { lambdaOverrides } from "./app-start.js";
18
+ import { switchTo } from "./branch.js";
19
+ import { APPROVE, approved, missingTunnels, startsFromStack } from "./checks.js";
20
+ import { link } from "./libraries.js";
21
+ import { isDone } from "./needs.js";
22
+ import { alive, listener, readRecord, records, sh, sleep, startProcess, stopOne } from "./processes.js";
23
+ import { watcherOf } from "./watch.js";
24
+ import * as localQueue from "./queues.js";
25
+ import { matches, prepare, recipesFor } from "./recipes.js";
26
+ import { run } from "./run.js";
27
+ import { appliedCommand, planFor, runningPart } from "./runs.js";
28
+ import { appendRun, live, runRecord, runs } from "./history.js";
29
+ import { wireInfo } from "./wire.js";
30
+ import { writeJson } from "../json-files.js";
31
+ import { appOn } from "../core/stack.js";
32
+ const USAGE = 'usage: koppler up <app...> [--cable <caller>:<callee>]... [--env <env>] | --board "<name>" | --run <id>';
33
+ const running = (name) => {
34
+ const record = readRecord(name);
35
+ return !!record && alive(record);
36
+ };
37
+ const nameOf = (stack, id) => stack.apps[id]?.name ?? id;
38
+ const prereqsOf = (stack, id, env) => {
39
+ return appOn(stack, id, env)?.prereqs ?? [];
40
+ };
41
+ /** The tunnels an app needs on an environment, as the stack states them. */
42
+ export const tunnelsOf = (stack, id, env) => prereqsOf(stack, id, env).flatMap(({ tunnel, port, env: setting, what, connectsAs }) => tunnel && port ? [{ app: id, tunnel, port, env: setting, what, connectsAs }] : []);
43
+ const settingsFor = (up, id) => (up.setEnv[id] ??= {});
44
+ /** Records what was asked for as a run, the way a Run from the canvas is, and returns its id. */
45
+ function recordRun(stack, asked, label) {
46
+ const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
47
+ const nodes = Object.entries(asked.nodes).map(([node, chosen]) => ({ id: node, ...chosen }));
48
+ const data = { id, env: asked.env, mode: 'auto', nodes, edges: asked.edges, queues: asked.queues };
49
+ const plan = planFor({ ...data, nodes: asked.nodes });
50
+ appendRun(runRecord({ tag: 'koppler', data: { ...data, plan } }) ?? fail(`could not start ${label}`));
51
+ console.log(`Running ${label} on ${asked.env.toUpperCase()} (run ${id}); the canvas shows it.`);
52
+ const said = ['starts', 'restarts', 'stops'].flatMap(word => plan[word].length ? [`${word} ${plan[word].map(step => nameOf(stack, step.id)).join(', ')}`] : []);
53
+ if (said.length)
54
+ console.log(`It ${said.join(', ')}.`);
55
+ return id;
56
+ }
57
+ /** `--board "<name>"`: a saved board or a preset of the stack. */
58
+ function boardNamed(stack, args) {
59
+ const want = flagValue(args, '--board') ?? fail('usage: koppler up --board "<board or preset name>" [--env <env>]');
60
+ const own = (readData('state')?.['presets'] ?? []);
61
+ const boards = [...own, ...(stack.presets ?? [])];
62
+ const board = boards.find(one => one.name === want) ??
63
+ boards.find(one => one.name?.toLowerCase() === want.toLowerCase()) ??
64
+ fail(`no board called ${want}; koppler use --boards lists them`);
65
+ const asked = {
66
+ env: envOf(stack, args),
67
+ nodes: board.nodes ?? {},
68
+ edges: board.edges ?? [],
69
+ queues: board.queues ?? {},
70
+ };
71
+ return recordRun(stack, asked, board.name ?? want);
72
+ }
73
+ /** Apps from the terminal or chat, added to what already runs on that environment. */
74
+ function appsNamed(stack, args) {
75
+ const ids = positional(args, ['--env', '--cable']);
76
+ if (!ids.length)
77
+ fail(USAGE);
78
+ for (const id of ids)
79
+ if (!stack.apps[id])
80
+ fail(`${id} is not an app in the stack`);
81
+ for (const id of ids) {
82
+ const outside = stack.apps[id]?.outside;
83
+ if (outside)
84
+ fail(`${nameOf(stack, id)} does not run on this Mac: ${String(outside)}`);
85
+ }
86
+ const env = envOf(stack, args);
87
+ const now = live();
88
+ const kept = now?.env === env ? now : {};
89
+ const known = kept.nodes ?? {};
90
+ const cables = args.flatMap((arg, at) => {
91
+ if (args[at - 1] !== '--cable')
92
+ return [];
93
+ const [from = '', to = ''] = arg.split(':');
94
+ if (!stack.apps[from] || !stack.apps[to])
95
+ fail(`usage: --cable <caller>:<callee>, both apps in the stack (got ${arg})`);
96
+ const runs = (id) => ids.includes(id) || !!known[id];
97
+ if (!runs(from) || !runs(to))
98
+ fail(`${arg}: both ends must run; add them to the apps`);
99
+ return [{ from, to }];
100
+ });
101
+ const nodes = { ...known, ...Object.fromEntries(ids.map(id => [id, known[id] ?? { run: 'cli' }])) };
102
+ const edges = [...(kept.edges ?? []), ...cables].filter((edge, at, all) => all.findIndex(other => other.from === edge.from && other.to === edge.to) === at);
103
+ const label = ids.map(id => nameOf(stack, id)).join(', ');
104
+ return recordRun(stack, { env, nodes, edges, queues: kept.queues ?? {} }, label);
105
+ }
106
+ /** Each app's steps as the run goes, shown on its card; if the run stops on an error, the step it was on fails. */
107
+ function progressOf(board, env) {
108
+ const file = path.join(tempDir(), 'progress.json');
109
+ const progress = {
110
+ run: board?.id ?? null,
111
+ env,
112
+ at: new Date().toISOString(),
113
+ apps: {},
114
+ };
115
+ const save = () => {
116
+ writeJson(file, progress);
117
+ };
118
+ process.on('exit', code => {
119
+ if (!code || code === 3)
120
+ return;
121
+ for (const list of Object.values(progress.apps)) {
122
+ const open = list.find(step => step.state === 'todo');
123
+ if (!open)
124
+ continue;
125
+ open.state = 'failed';
126
+ open.detail = 'The run stopped here; the error is in the run log, and the agent takes over.';
127
+ }
128
+ if (board)
129
+ save();
130
+ });
131
+ return (id, label, state, detail) => {
132
+ if (!board)
133
+ return;
134
+ const list = (progress.apps[id] ??= []);
135
+ const next = { label, state, ...(detail && { detail: detail.slice(0, 300) }) };
136
+ const known = list.find(step => step.label === label);
137
+ if (known)
138
+ Object.assign(known, next);
139
+ else
140
+ list.push(next);
141
+ save();
142
+ };
143
+ }
144
+ function announce(up) {
145
+ const { stack, env, board, node, step } = up;
146
+ for (const id of up.ids) {
147
+ const app = stack.apps[id];
148
+ const branch = node(id).branch;
149
+ if (branch)
150
+ step(id, 'Branch', 'todo', `Switch to ${branch}`);
151
+ const mock = node(id).run === 'mock';
152
+ if (!mock && node(id).tunnels !== false && tunnelsOf(stack, id, env).length)
153
+ step(id, 'Tunnels', 'todo');
154
+ if (!mock && Object.keys(board?.queues ?? {}).some(queue => app?.publishes?.[queue] || app?.reads?.[queue]))
155
+ step(id, 'Local queue', 'todo');
156
+ const ide = node(id).run === 'ide';
157
+ step(id, 'Start', ide ? 'user' : 'todo', ide ? 'Start it in your IDE.' : null);
158
+ }
159
+ }
160
+ /** What the plan stops or restarts goes first; its own code changes stay for the agent to revert. */
161
+ async function stopFirst(up, board) {
162
+ let actions;
163
+ try {
164
+ actions = readActions(up.logFile);
165
+ }
166
+ catch (error) {
167
+ fail(error.message);
168
+ }
169
+ const { stops = [], restarts = [] } = board.plan ?? {};
170
+ for (const { id } of [...stops, ...restarts]) {
171
+ const record = readRecord(id);
172
+ if (record && alive(record))
173
+ console.log(await stopOne(record));
174
+ const leaves = stops.some(step => step.id === id);
175
+ if (leaves && actions.some(action => action.app === id && action.kind !== 'process'))
176
+ up.left.push(`revert ${nameOf(up.stack, id)}'s changes (it left the board)`);
177
+ }
178
+ }
179
+ /** Branches as the board asks, uncommitted changes as chosen on the card. */
180
+ function switchBranches(up) {
181
+ for (const id of up.ids) {
182
+ const { branch, changes } = up.node(id);
183
+ const repo = up.clones[id]?.path;
184
+ if (!branch || !repo)
185
+ continue;
186
+ const switched = switchTo({ app: id, repo, to: branch, how: (changes ?? null), logFile: up.logFile });
187
+ if (switched.error === undefined) {
188
+ console.log(switched.message);
189
+ up.step(id, 'Branch', 'done', switched.message);
190
+ continue;
191
+ }
192
+ up.left.push(switched.error);
193
+ up.skip.add(id);
194
+ up.step(id, 'Branch', 'failed', switched.error);
195
+ up.step(id, 'Start', 'failed', 'Not started: its branch could not be switched.');
196
+ }
197
+ }
198
+ /** Local libraries the board asks for, linked before the app starts. */
199
+ function linkLibraries(up) {
200
+ for (const id of up.ids) {
201
+ const asked = up.node(id).libraries ?? [];
202
+ if (up.skip.has(id) || !asked.length)
203
+ continue;
204
+ const clone = up.clones[id];
205
+ const wanted = (clone?.libraries ?? []).filter(library => asked.includes(library.name));
206
+ const names = wanted.map(library => library.name).join(', ');
207
+ try {
208
+ if (!clone)
209
+ throw new Error('it is not cloned');
210
+ link(id, clone.path, wanted, up.logFile);
211
+ console.log(`${id}: local ${names}`);
212
+ up.step(id, 'Libraries', 'done', `Local ${names}`);
213
+ }
214
+ catch (error) {
215
+ const { message } = error;
216
+ up.left.push(`${nameOf(up.stack, id)}: ${message}`);
217
+ up.step(id, 'Libraries', 'failed', message);
218
+ }
219
+ }
220
+ }
221
+ /** Remembered changes that match this board. */
222
+ function applyRemembered(up) {
223
+ const { stack, ids, env, edges, left } = up;
224
+ // A code change the stack remembers is the stack's own doing: applied only once the user allowed it.
225
+ const allowed = approved(stack.source ?? '', stack);
226
+ const shared = ids.filter(id => stack.apps[id]?.localChanges?.length);
227
+ if (!allowed && shared.length)
228
+ left.push(`${shared.map(id => nameOf(stack, id)).join(', ')}: the stack's code changes were not applied; the user has to allow them first, in their own terminal: ${APPROVE}`);
229
+ const withoutShared = Object.fromEntries(Object.entries(stack.apps).map(([id, app]) => [id, { ...app, localChanges: [] }]));
230
+ const applied = prepare({
231
+ registry: allowed ? stack : { ...stack, apps: withoutShared },
232
+ apps: ids.filter(id => !up.skip.has(id)),
233
+ env,
234
+ calls: Object.fromEntries(ids.map(id => [id, edges.filter(edge => edge.from === id).map(edge => edge.to)])),
235
+ repoOf: id => up.clones[id]?.path,
236
+ logFile: up.logFile,
237
+ });
238
+ for (const { app, state, why, detail } of applied) {
239
+ const change = `${nameOf(stack, app)}: remembered change "${String(why)}"`;
240
+ if (state === 'applied') {
241
+ console.log(`${app}: remembered change applied (${String(why)})`);
242
+ up.step(app, 'Remembered change', 'done', why);
243
+ }
244
+ if (state === 'moved' || state === 'missing')
245
+ left.push(`${change} no longer fits (${String(detail)})`);
246
+ if (state === 'refused')
247
+ left.push(`${change} was refused: ${String(detail)}. Check it in stack.json`);
248
+ }
249
+ }
250
+ /**
251
+ * Whether the caller's code already points at that address: a change the agent logged for it holds the address, or the
252
+ * file the stack names does.
253
+ */
254
+ // ponytail: the address is looked for as text, so port 5001 also matches 50010; compare whole addresses if that bites
255
+ function pointsAt(up, app, address, file) {
256
+ const has = (text) => [text ?? []].flat().some(part => part.includes(address));
257
+ const logged = readActions(up.logFile).some(action => action.app === app &&
258
+ ((action.kind === 'edit' && has(action.after)) || (action.kind === 'env-file' && has(action.lines))));
259
+ const repo = up.clones[app]?.path;
260
+ if (logged || !repo || !file)
261
+ return logged;
262
+ try {
263
+ return has(fs.readFileSync(path.join(repo, file), 'utf8'));
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
269
+ /** Each cable: the caller gets its local callee's address at start, or a remembered change already points it there. */
270
+ function wireCables(up) {
271
+ const { stack, env, left } = up;
272
+ // A Lambda that finds a callee's address in its secret: those keys are set over the secret when it starts.
273
+ const secretKeys = {};
274
+ // A mock answers the same whoever it would call.
275
+ for (const { from, to } of up.edges.filter(edge => up.node(edge.from).run !== 'mock')) {
276
+ const wired = wireInfo(stack, up.clones, from, to, env, up.node(from).platform);
277
+ const viaRecipe = recipesFor(stack, from).some(recipe => recipe.when?.calls === to && matches(recipe, { env, calls: [to] }));
278
+ if (wired.error !== undefined) {
279
+ if (!viaRecipe)
280
+ left.push(wired.error);
281
+ continue;
282
+ }
283
+ if (wired.how === 'env' && !kindOf(stack.apps[from]).platforms)
284
+ for (const name of [wired.call.env ?? []].flat())
285
+ settingsFor(up, from)[name] = wired.value;
286
+ else if (wired.how === 'secret' && wired.call.key)
287
+ (secretKeys[from] ??= {})[wired.call.key] = wired.value;
288
+ else if (!viaRecipe && !pointsAt(up, from, wired.value, wired.file ?? wired.call.file)) {
289
+ const where = wired.call.file ? ` in ${wired.call.file}` : '';
290
+ const how = wired.how === 'unknown' ? 'how it picks the address is not recorded yet' : `${String(wired.how)}${where}`;
291
+ const caller = nameOf(stack, from);
292
+ // Started now, the caller would call the deployed app and need a restart once the change is made.
293
+ const then = running(from)
294
+ ? `${caller} runs without it: restart it after the change`
295
+ : up.node(from).run === 'ide'
296
+ ? `start ${caller} in your IDE after the change`
297
+ : `${caller} is not started until then: log the change, then run this again`;
298
+ left.push(`${caller} -> local ${nameOf(stack, to)}: point it at ${wired.value} (${how}); ${then}`);
299
+ up.waits.add(from);
300
+ }
301
+ }
302
+ for (const [app, set] of Object.entries(secretKeys))
303
+ writeLogged(up.logFile, app, lambdaOverrides(app), `${JSON.stringify({ set }, null, 2)}\n`);
304
+ }
305
+ /** Tunnels first; the board's "no tunnels" choice is kept. */
306
+ async function openTunnels(up) {
307
+ const { stack, ids, env, node } = up;
308
+ const needs = ids
309
+ .filter(id => node(id).tunnels !== false && node(id).run !== 'mock' && !up.skip.has(id))
310
+ .flatMap(id => tunnelsOf(stack, id, env));
311
+ const appPorts = ids.map(id => Number(new URL(stack.apps[id]?.url ?? 'http://x').port)).filter(Boolean);
312
+ for (const tunnel of planTunnels(needs, appPorts)) {
313
+ const steps = planSteps(tunnel);
314
+ const port = String(tunnel.port);
315
+ // A tunnel that moved takes its apps with it: each is started with the new address, so none dials whatever holds
316
+ // the old port.
317
+ for (const { app, env: setting, value } of steps.settings) {
318
+ if (node(app).run === 'ide')
319
+ up.left.push(`${nameOf(stack, app)}: its ${tunnel.what} tunnel moved to port ${port}; set ${setting}=${value} where you start it`);
320
+ else
321
+ settingsFor(up, app)[setting] = value;
322
+ }
323
+ if (running(steps.name)) {
324
+ console.log(`${steps.name}: already open on ${port}`);
325
+ continue;
326
+ }
327
+ // Something else on the port may lead to another environment's database: it is never taken for this tunnel.
328
+ const holder = listener(tunnel.port, tunnel.bind);
329
+ if (holder) {
330
+ const ours = records().find(record => record.port === tunnel.port && alive(record))?.name;
331
+ fail(`port ${port} is held by ${ours ?? `${holder.command ?? 'another program'} (pid ${String(holder.pid)})`}, not by ${steps.name}: it may reach another environment's ${tunnel.what.toLowerCase()}. ${ours ? `Stop it (koppler stop ${ours})` : 'Close it'}, then run again.`);
332
+ }
333
+ if (steps.sudo.length &&
334
+ !aliasReady(tunnel, hostsFile(), process.platform === 'darwin' ? sh('ifconfig', ['lo0']) : null))
335
+ fail(`${steps.name} needs its own local address first; in your terminal:\n ${steps.sudo.join('\n ')}\nthen run koppler up again`);
336
+ await run([steps.name, '--port', port, '--bind', tunnel.bind, '--restart', '--', ...steps.tunnelArgs]);
337
+ }
338
+ for (const id of new Set(needs.map(need => need.app))) {
339
+ const mine = needs.filter(need => need.app === id).map(need => need.what ?? need.tunnel);
340
+ up.step(id, 'Tunnels', 'done', `${mine.join(', ')} open`);
341
+ }
342
+ }
343
+ const hostsFile = () => {
344
+ try {
345
+ return fs.readFileSync('/etc/hosts', 'utf8');
346
+ }
347
+ catch {
348
+ return '';
349
+ }
350
+ };
351
+ /** Why a queue could not be made, as the program that makes it said it. */
352
+ const lastLine = (error) => {
353
+ const { stderr, message } = error;
354
+ return (stderr || message).trim().split('\n').pop() ?? '';
355
+ };
356
+ /** The port answers before the emulator or broker is ready; it takes a few seconds. */
357
+ async function created(queue, left) {
358
+ for (let attempt = 1; attempt <= 30; attempt++) {
359
+ try {
360
+ return queue.type === 'kafka'
361
+ ? `${localQueue.KAFKA}/${localQueue.createTopic(queue.queue)}`
362
+ : localQueue.createQueue(queue.queue);
363
+ }
364
+ catch (error) {
365
+ if (attempt === 30)
366
+ left.push(`${queue.name}: the local queue could not be created (${lastLine(error)})`);
367
+ else
368
+ await sleep(1000);
369
+ }
370
+ }
371
+ return undefined;
372
+ }
373
+ /** Queues switched to local: the emulator, each queue in it, and every app on one started with its local address. */
374
+ async function startQueues(up, chosen) {
375
+ const { stack, left } = up;
376
+ const local = localQueue.localQueues(stack, chosen,
377
+ // Only an app switched onto its queues is given a local one.
378
+ up.ids.filter(id => !up.skip.has(id) && up.node(id).queues === true), up.env);
379
+ left.push(...local.left);
380
+ const kafka = (queue) => queue.type === 'kafka';
381
+ if (local.queues.some(queue => !kafka(queue)) && !running('queues'))
382
+ await run(['queues', '--port', String(localQueue.PORT), '--', ...localQueue.EMULATOR], {
383
+ onStop: [localQueue.STOP_EMULATOR],
384
+ });
385
+ if (local.queues.some(kafka) && !running('kafka'))
386
+ await run(['kafka', '--port', String(localQueue.KAFKA_PORT), '--', ...localQueue.BROKER], {
387
+ onStop: [localQueue.STOP_BROKER],
388
+ });
389
+ const urls = {};
390
+ for (const queue of local.queues) {
391
+ const url = await created(queue, left);
392
+ if (!url)
393
+ continue;
394
+ urls[queue.id] = url;
395
+ console.log(`queues: ${queue.name} is local at ${url}`);
396
+ }
397
+ const settings = localQueue.queueEnv(stack, local.queues.filter(queue => urls[queue.id]), urls);
398
+ left.push(...settings.left);
399
+ for (const [id, set] of Object.entries(settings.set))
400
+ Object.assign(settingsFor(up, id), set);
401
+ for (const queue of local.queues)
402
+ for (const { id } of queue.apps) {
403
+ const pointed = !!settings.set[id];
404
+ if (urls[queue.id])
405
+ up.step(id, 'Local queue', pointed ? 'done' : 'failed', `${queue.name}: ${pointed ? 'local' : 'point it there yourself'}`);
406
+ else
407
+ up.step(id, 'Local queue', 'failed', `${queue.name} could not be created`);
408
+ }
409
+ }
410
+ /**
411
+ * Apps the others call start first, so a caller finds its callee answering, and an app's `after` (a migration that
412
+ * runs to completion) before it; a loop just starts in board order.
413
+ */
414
+ async function startApps(up) {
415
+ const { ids, env, edges, node, step } = up;
416
+ const order = [];
417
+ const seen = new Set();
418
+ const visit = (id) => {
419
+ if (seen.has(id))
420
+ return;
421
+ seen.add(id);
422
+ for (const edge of edges)
423
+ if (edge.from === id)
424
+ visit(edge.to);
425
+ for (const first of up.stack.apps[id]?.after ?? [])
426
+ if (ids.includes(first))
427
+ visit(first);
428
+ order.push(id);
429
+ };
430
+ ids.forEach(visit);
431
+ const yours = [];
432
+ // An app whose start failed, and why; the apps that need it are not started, unless a call says to start anyway.
433
+ const down = new Map();
434
+ const needs = (id) => [
435
+ ...edges
436
+ .filter(edge => edge.from === id && up.stack.apps[id]?.consumes?.[edge.to]?.ifDown !== 'start')
437
+ .map(edge => edge.to),
438
+ ...(up.stack.apps[id]?.after ?? []),
439
+ ];
440
+ // Every app still to start shows what it waits for, until its turn comes.
441
+ for (const id of order) {
442
+ if (up.skip.has(id) || node(id).run === 'ide' || running(id) || up.waits.has(id))
443
+ continue;
444
+ const first = needs(id).filter(dep => ids.includes(dep) && !running(dep));
445
+ step(id, 'Start', 'todo', first.length ? `Waiting for ${first.map(dep => nameOf(up.stack, dep)).join(' and ')}.` : 'Waiting its turn.');
446
+ }
447
+ for (const id of order) {
448
+ if (up.skip.has(id))
449
+ continue;
450
+ const missing = needs(id).find(dep => down.has(dep) || up.skip.has(dep));
451
+ if (missing) {
452
+ const why = `Not started: ${nameOf(up.stack, missing)} did not start.`;
453
+ down.set(id, why);
454
+ step(id, 'Start', 'failed', why);
455
+ continue;
456
+ }
457
+ const { run: runIn, platform } = node(id);
458
+ if (runIn === 'ide')
459
+ yours.push(nameOf(up.stack, id));
460
+ else if (running(id)) {
461
+ console.log(`${id}: already running`);
462
+ step(id, 'Start', 'done', 'Already running.');
463
+ }
464
+ else if (up.waits.has(id))
465
+ step(id, 'Start', 'todo', 'Waits for a change in its code that points it at a local app.');
466
+ else {
467
+ const setEnv = up.setEnv[id];
468
+ try {
469
+ const how = runIn === 'mock' ? ['--mock'] : platform ? ['--device', platform] : [];
470
+ await run([id, '--env', env, ...how], setEnv ? { setEnv } : {});
471
+ }
472
+ catch (error) {
473
+ if (!(error instanceof Failure))
474
+ throw error;
475
+ console.error(error.message);
476
+ down.set(id, error.message);
477
+ step(id, 'Start', 'failed', error.message.split('\n')[0] ?? '');
478
+ continue;
479
+ }
480
+ step(id, 'Start', 'done', up.stack.apps[id]?.once ? 'Finished.' : 'Running.');
481
+ }
482
+ }
483
+ if (yours.length)
484
+ console.log(`Start ${yours.join(', ')} in your IDE.`);
485
+ // A card set to restart on change has a watcher beside its app; one set back has none.
486
+ for (const id of order) {
487
+ const watcher = readRecord(watcherOf(id));
488
+ const wants = node(id).watch === true && node(id).run !== 'ide' && running(id) && !up.stack.apps[id]?.once;
489
+ if (wants && !(watcher && alive(watcher)))
490
+ await startProcess({
491
+ name: watcherOf(id),
492
+ command: [process.execPath, path.join(CODE, 'koppler.mjs'), 'watch', id],
493
+ cwd: up.clones[id]?.path ?? process.cwd(),
494
+ restart: false,
495
+ });
496
+ else if (!wants && watcher)
497
+ console.log(await stopOne(watcher));
498
+ }
499
+ if (down.size)
500
+ throw new Failure(`Not everything started:\n${[...down].map(([id, why]) => ` ${nameOf(up.stack, id)}: ${why.split('\n')[0] ?? ''}`).join('\n')}`);
501
+ }
502
+ /** Carries a run out. Without a confirmed Send (`board`), only tunnels and apps start, and nothing is recorded. */
503
+ async function carryOut(stack, args) {
504
+ const runId = flagValue(args, '--run');
505
+ const all = runs();
506
+ const board = runId ? (all.find(one => one.id === runId) ?? fail(`no run ${runId} in the history`)) : null;
507
+ keepRunLogs(board?.id);
508
+ const env = board?.env ?? flagValue(args, '--env') ?? stack.envs[0] ?? '';
509
+ const latest = board ?? all.findLast(one => one.kind === 'run' && one.env === env);
510
+ const ids = board
511
+ ? Object.keys(board.nodes).filter(id => stack.apps[id])
512
+ : args.filter((arg, at) => !arg.startsWith('--') && !['--env', '--run'].includes(args[at - 1] ?? ''));
513
+ if (!ids.length && !board)
514
+ fail('usage: koppler up <app...> [--env <env>] | koppler up --run <id>');
515
+ for (const id of ids)
516
+ if (!stack.apps[id])
517
+ fail(`${id} is not an app in the stack`);
518
+ // An app with no tunnels of its own on this environment never gets the first environment's: that would reach the
519
+ // wrong database. Checked before anything starts, like every other refusal.
520
+ const wantsTunnels = (id) => board?.nodes[id]?.tunnels !== false;
521
+ const noTunnel = missingTunnels(stack, env).filter(id => ids.includes(id) && wantsTunnels(id));
522
+ if (noTunnel.length)
523
+ fail(`${noTunnel.map(id => nameOf(stack, id)).join(', ')}: the stack has no ${env.toUpperCase()} tunnel, and another environment's is never used. Set it with koppler stack set apps.<id>.envs.${env}.prereqs, or turn Tunnels off on the card.`);
524
+ // Checked before anything starts: a run never gets half-way and then stops at a command that needs the user's yes.
525
+ const waiting = ids.filter(id => startsFromStack(stack, id, env));
526
+ if (waiting.length && !approved(stack.source ?? '', stack))
527
+ fail(`${waiting.map(id => nameOf(stack, id)).join(', ')}: started by a command from the stack that the user has not allowed yet, or that changed since. They review it in their own terminal: ${APPROVE}`);
528
+ // An app started on a sign-in that has expired crashes with an error of its own: said here instead, before
529
+ // anything starts. A sign-in of the stack's own is checked by its command, which the user must have allowed.
530
+ const signIns = new Map();
531
+ for (const id of ids)
532
+ for (const need of prereqsOf(stack, id, env)) {
533
+ const signIn = signInOf(stack, need);
534
+ if (signIn)
535
+ signIns.set(signIn.id, { signIn, apps: [...(signIns.get(signIn.id)?.apps ?? []), nameOf(stack, id)] });
536
+ }
537
+ const mayRun = approved(stack.source ?? '', stack);
538
+ const own = [...signIns.values()].filter(({ signIn }) => 'shell' in signIn.check);
539
+ if (own.length && !mayRun)
540
+ fail(`${own.flatMap(one => one.apps).join(', ')}: the sign-in is checked by a command from the stack that the user has not allowed yet, or that changed since. They review it in their own terminal: ${APPROVE}`);
541
+ const holds = ({ check, account }) => isDone('shell' in check ? { shell: check.shell } : { sso: check.argv.at(-1) ?? '', ...(account && { account }) }, {
542
+ approved: mayRun,
543
+ });
544
+ const expired = [...signIns.values()].filter(({ signIn }) => holds(signIn) === false);
545
+ if (expired.length) {
546
+ const apps = [...new Set(expired.flatMap(one => one.apps))];
547
+ fail(`Sign in first: ${apps.join(', ')} ${apps.length === 1 ? 'needs' : 'need'} ${expired.map(one => one.signIn.what).join(' and ')} at start, and the sign-in has expired${expired.some(one => one.signIn.account) ? ' or is on another account' : ''}. In your own terminal:\n${expired.map(one => ` ${one.signIn.login}`).join('\n')}\nthen run again`);
548
+ }
549
+ // What a sign-in of the stack's gives its apps is passed to them at start.
550
+ const given = Object.fromEntries(ids.map(id => [
551
+ id,
552
+ Object.assign({}, ...prereqsOf(stack, id, env).map(need => (need.signIn ? signInOf(stack, need)?.env : {}))),
553
+ ]));
554
+ const up = {
555
+ stack,
556
+ clones: readClones(),
557
+ env,
558
+ ids,
559
+ board,
560
+ node: id => latest?.nodes[id] ?? {},
561
+ edges: (latest?.edges ?? []).filter(edge => ids.includes(edge.from) && ids.includes(edge.to)),
562
+ left: [],
563
+ skip: new Set(),
564
+ waits: new Set(),
565
+ setEnv: Object.fromEntries(Object.entries(given).filter(([, env]) => Object.keys(env).length)),
566
+ logFile: path.join(tempDir(), 'actions.json'),
567
+ step: progressOf(board, env),
568
+ };
569
+ announce(up);
570
+ // What runs now is this board's, as far as it got: written when the run ends, and also when it stops on an error
571
+ // half-way, so the next plan and Stop compare with what really runs, not with the board before.
572
+ const live = () => {
573
+ if (!board)
574
+ return;
575
+ const { nodes, edges } = runningPart(board);
576
+ writeJson(path.join(tempDir(), 'live.json'), {
577
+ id: board.id,
578
+ at: new Date().toISOString(),
579
+ env,
580
+ nodes,
581
+ edges,
582
+ ...(board.queues && { queues: board.queues }),
583
+ });
584
+ };
585
+ try {
586
+ if (board) {
587
+ await stopFirst(up, board);
588
+ switchBranches(up);
589
+ linkLibraries(up);
590
+ applyRemembered(up);
591
+ wireCables(up);
592
+ }
593
+ await openTunnels(up);
594
+ if (board?.queues)
595
+ await startQueues(up, board.queues);
596
+ await startApps(up);
597
+ }
598
+ catch (error) {
599
+ live();
600
+ throw error;
601
+ }
602
+ // Even while something is left for the agent: the canvas shows it running straight away.
603
+ live();
604
+ if (up.left.length) {
605
+ console.log(`\nLeft for the agent:\n${up.left.map(line => ` - ${line}`).join('\n')}`);
606
+ throw new Failure('', 3);
607
+ }
608
+ console.log(`Up on ${env.toUpperCase()}.`);
609
+ if (board)
610
+ await appliedCommand([board.id]);
611
+ }
612
+ export async function up(args) {
613
+ const stack = readStack() ?? fail('no stack loaded (koppler init)');
614
+ if (args.includes('--board'))
615
+ await carryOut(stack, ['--run', boardNamed(stack, args)]);
616
+ else if (args.includes('--run'))
617
+ await carryOut(stack, args);
618
+ else
619
+ await carryOut(stack, ['--run', appsNamed(stack, args)]);
620
+ }
621
+ //# sourceMappingURL=up.js.map