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.
- package/LICENSE +21 -0
- package/README.md +101 -2
- package/bin/run-logged +16 -0
- package/bin/secret-list +7 -0
- package/bin/secret-psql +41 -0
- package/bin/secret-set +17 -0
- package/bin/secret-store +82 -0
- package/bin/secret-test +10 -0
- package/bin/with-secrets +25 -0
- package/dist/.built +1 -0
- package/dist/cli/agent.js +144 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/doctor.js +173 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/mcp.js +204 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/output.js +37 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/records.js +121 -0
- package/dist/cli/records.js.map +1 -0
- package/dist/cli/running.js +270 -0
- package/dist/cli/running.js.map +1 -0
- package/dist/cli/shell.js +43 -0
- package/dist/cli/shell.js.map +1 -0
- package/dist/cli/wait.js +59 -0
- package/dist/cli/wait.js.map +1 -0
- package/dist/core/addresses.js +55 -0
- package/dist/core/addresses.js.map +1 -0
- package/dist/core/arrange.js +329 -0
- package/dist/core/arrange.js.map +1 -0
- package/dist/core/cables.js +158 -0
- package/dist/core/cables.js.map +1 -0
- package/dist/core/expressions.js +72 -0
- package/dist/core/expressions.js.map +1 -0
- package/dist/core/frameworks.js +13 -0
- package/dist/core/frameworks.js.map +1 -0
- package/dist/core/kinds.js +244 -0
- package/dist/core/kinds.js.map +1 -0
- package/dist/core/launch.js +181 -0
- package/dist/core/launch.js.map +1 -0
- package/dist/core/logfmt.js +144 -0
- package/dist/core/logfmt.js.map +1 -0
- package/dist/core/mock.js +27 -0
- package/dist/core/mock.js.map +1 -0
- package/dist/core/plan.js +147 -0
- package/dist/core/plan.js.map +1 -0
- package/dist/core/ports.js +155 -0
- package/dist/core/ports.js.map +1 -0
- package/dist/core/presets.js +186 -0
- package/dist/core/presets.js.map +1 -0
- package/dist/core/secret-types.js +3 -0
- package/dist/core/secret-types.js.map +1 -0
- package/dist/core/sign-in.js +30 -0
- package/dist/core/sign-in.js.map +1 -0
- package/dist/core/stack-path.js +64 -0
- package/dist/core/stack-path.js.map +1 -0
- package/dist/core/stack.js +509 -0
- package/dist/core/stack.js.map +1 -0
- package/dist/core/terraform.js +393 -0
- package/dist/core/terraform.js.map +1 -0
- package/dist/data.js +37 -0
- package/dist/data.js.map +1 -0
- package/dist/engine/actions.js +99 -0
- package/dist/engine/actions.js.map +1 -0
- package/dist/engine/app-commands.js +106 -0
- package/dist/engine/app-commands.js.map +1 -0
- package/dist/engine/app-start.js +167 -0
- package/dist/engine/app-start.js.map +1 -0
- package/dist/engine/app-state.js +43 -0
- package/dist/engine/app-state.js.map +1 -0
- package/dist/engine/boards.js +219 -0
- package/dist/engine/boards.js.map +1 -0
- package/dist/engine/branch.js +194 -0
- package/dist/engine/branch.js.map +1 -0
- package/dist/engine/browser-console.js +45 -0
- package/dist/engine/browser-console.js.map +1 -0
- package/dist/engine/calls.js +97 -0
- package/dist/engine/calls.js.map +1 -0
- package/dist/engine/checks.js +270 -0
- package/dist/engine/checks.js.map +1 -0
- package/dist/engine/clones.js +67 -0
- package/dist/engine/clones.js.map +1 -0
- package/dist/engine/config.js +26 -0
- package/dist/engine/config.js.map +1 -0
- package/dist/engine/deployed.js +488 -0
- package/dist/engine/deployed.js.map +1 -0
- package/dist/engine/devices.js +81 -0
- package/dist/engine/devices.js.map +1 -0
- package/dist/engine/discover.js +236 -0
- package/dist/engine/discover.js.map +1 -0
- package/dist/engine/down.js +253 -0
- package/dist/engine/down.js.map +1 -0
- package/dist/engine/history.js +215 -0
- package/dist/engine/history.js.map +1 -0
- package/dist/engine/investigate.js +278 -0
- package/dist/engine/investigate.js.map +1 -0
- package/dist/engine/libraries.js +129 -0
- package/dist/engine/libraries.js.map +1 -0
- package/dist/engine/linked.js +146 -0
- package/dist/engine/linked.js.map +1 -0
- package/dist/engine/listen.js +223 -0
- package/dist/engine/listen.js.map +1 -0
- package/dist/engine/locks.js +38 -0
- package/dist/engine/locks.js.map +1 -0
- package/dist/engine/messages.js +177 -0
- package/dist/engine/messages.js.map +1 -0
- package/dist/engine/needs.js +363 -0
- package/dist/engine/needs.js.map +1 -0
- package/dist/engine/processes.js +350 -0
- package/dist/engine/processes.js.map +1 -0
- package/dist/engine/query.js +241 -0
- package/dist/engine/query.js.map +1 -0
- package/dist/engine/queues.js +100 -0
- package/dist/engine/queues.js.map +1 -0
- package/dist/engine/recipes.js +248 -0
- package/dist/engine/recipes.js.map +1 -0
- package/dist/engine/reload.js +32 -0
- package/dist/engine/reload.js.map +1 -0
- package/dist/engine/repos.js +214 -0
- package/dist/engine/repos.js.map +1 -0
- package/dist/engine/research.js +134 -0
- package/dist/engine/research.js.map +1 -0
- package/dist/engine/run-logs.js +79 -0
- package/dist/engine/run-logs.js.map +1 -0
- package/dist/engine/run.js +54 -0
- package/dist/engine/run.js.map +1 -0
- package/dist/engine/runs.js +223 -0
- package/dist/engine/runs.js.map +1 -0
- package/dist/engine/session.js +130 -0
- package/dist/engine/session.js.map +1 -0
- package/dist/engine/share.js +124 -0
- package/dist/engine/share.js.map +1 -0
- package/dist/engine/stack-file.js +206 -0
- package/dist/engine/stack-file.js.map +1 -0
- package/dist/engine/stack-share.js +86 -0
- package/dist/engine/stack-share.js.map +1 -0
- package/dist/engine/stacks.js +143 -0
- package/dist/engine/stacks.js.map +1 -0
- package/dist/engine/sync.js +214 -0
- package/dist/engine/sync.js.map +1 -0
- package/dist/engine/traces.js +65 -0
- package/dist/engine/traces.js.map +1 -0
- package/dist/engine/up.js +621 -0
- package/dist/engine/up.js.map +1 -0
- package/dist/engine/watch.js +71 -0
- package/dist/engine/watch.js.map +1 -0
- package/dist/engine/wire.js +120 -0
- package/dist/engine/wire.js.map +1 -0
- package/dist/failure.js +16 -0
- package/dist/failure.js.map +1 -0
- package/dist/github.js +95 -0
- package/dist/github.js.map +1 -0
- package/dist/ids.js +6 -0
- package/dist/ids.js.map +1 -0
- package/dist/json-files.js +36 -0
- package/dist/json-files.js.map +1 -0
- package/dist/koppler.mjs +840 -0
- package/dist/koppler.mjs.map +1 -0
- package/dist/lambda-local.mjs +250 -0
- package/dist/lambda-local.mjs.map +1 -0
- package/dist/lambda-secrets-proxy.go +31 -0
- package/dist/mock-server.mjs +23 -0
- package/dist/mock-server.mjs.map +1 -0
- package/dist/page/assets/JetBrainsMono-cyrillic-D2jMJ_bk.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-greek-D2nYnAec.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-latin-Db4Uuiha.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-latin-ext-Bnd2h4Br.woff2 +0 -0
- package/dist/page/assets/JetBrainsMono-vietnamese-CNlsqbWb.woff2 +0 -0
- package/dist/page/assets/SchibstedGrotesk-latin-DxefFeRw.woff2 +0 -0
- package/dist/page/assets/SchibstedGrotesk-latin-ext-CnpJE18X.woff2 +0 -0
- package/dist/page/assets/index-Bkj_pX35.js +42 -0
- package/dist/page/assets/index-Dq45jswH.css +1 -0
- package/dist/page/index.html +14 -0
- package/dist/page/theme.js +8 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/safe-env.js +73 -0
- package/dist/safe-env.js.map +1 -0
- package/dist/sam-local.mjs +135 -0
- package/dist/sam-local.mjs.map +1 -0
- package/dist/scrub.js +87 -0
- package/dist/scrub.js.map +1 -0
- package/dist/secret-check.mjs +262 -0
- package/dist/secret-check.mjs.map +1 -0
- package/dist/server/browser-console.js +117 -0
- package/dist/server/browser-console.js.map +1 -0
- package/dist/server/cable-test.js +365 -0
- package/dist/server/cable-test.js.map +1 -0
- package/dist/server/choices.js +51 -0
- package/dist/server/choices.js.map +1 -0
- package/dist/server/files.js +107 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/gate.js +128 -0
- package/dist/server/gate.js.map +1 -0
- package/dist/server/logs.js +76 -0
- package/dist/server/logs.js.map +1 -0
- package/dist/server/mobile.js +113 -0
- package/dist/server/mobile.js.map +1 -0
- package/dist/server/routes.js +326 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/runs.js +111 -0
- package/dist/server/runs.js.map +1 -0
- package/dist/server/sanitize.js +242 -0
- package/dist/server/sanitize.js.map +1 -0
- package/dist/server/secrets.js +111 -0
- package/dist/server/secrets.js.map +1 -0
- package/dist/server/status.js +321 -0
- package/dist/server/status.js.map +1 -0
- package/dist/server/token.js +38 -0
- package/dist/server/token.js.map +1 -0
- package/dist/server/waiting.js +79 -0
- package/dist/server/waiting.js.map +1 -0
- package/dist/status-watch.mjs +63 -0
- package/dist/status-watch.mjs.map +1 -0
- package/package.json +74 -4
- package/skills/koppler-run/SKILL.md +169 -0
- package/skills/koppler-run/VERSION +1 -0
- package/skills/koppler-run/references/action-log.md +50 -0
- package/skills/koppler-run/references/permissions.md +25 -0
- package/skills/koppler-run/references/preparing-nodes.md +62 -0
- package/skills/koppler-run/references/reporting-bugs.md +76 -0
- package/skills/koppler-run/references/research.md +40 -0
- package/skills/koppler-run/references/secrets.md +56 -0
- package/skills/koppler-run/references/stack.md +197 -0
- package/skills/koppler-run/references/status.md +92 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// Switches an app's clone to the branch the board asks for, the way the user chose on the card for uncommitted changes:
|
|
2
|
+
// set aside (stashed under a Koppler label, back on Stop) or taken along. Every switch is logged, so it can be undone.
|
|
3
|
+
// Usage: koppler switch <app> <branch> [--set-aside | --take-along]
|
|
4
|
+
// koppler switch <app> --back
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { fail } from "../failure.js";
|
|
9
|
+
import { readData } from "../data.js";
|
|
10
|
+
import { updateActions } from "./actions.js";
|
|
11
|
+
const said = (failure) => (failure.stderr || failure.message || '').trim();
|
|
12
|
+
const git = (repo, ...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
13
|
+
function tryGit(repo, ...args) {
|
|
14
|
+
try {
|
|
15
|
+
return { ok: true, out: git(repo, ...args) };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
return { ok: false, out: said(error) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const firstLine = (text) => text.split('\n')[0] ?? '';
|
|
22
|
+
/** How many files the clone has changed and not committed, new ones included. */
|
|
23
|
+
const changed = (repo) => git(repo, 'status', '--porcelain').split('\n').filter(Boolean).length;
|
|
24
|
+
const changes = (count) => `${String(count)} uncommitted ${count === 1 ? 'change' : 'changes'}`;
|
|
25
|
+
const head = (repo) => git(repo, 'rev-parse', '--abbrev-ref', 'HEAD');
|
|
26
|
+
const named = (from, detached) => (detached ? `commit ${from.slice(0, 7)}` : from);
|
|
27
|
+
/** The stash entry Koppler made under this label, as git names it (stash@{n}). */
|
|
28
|
+
function stashOf(repo, label) {
|
|
29
|
+
const entries = git(repo, 'stash', 'list', '--format=%gd%x09%s').split('\n');
|
|
30
|
+
return entries.map(line => line.split('\t')).find(([, subject]) => subject?.endsWith(label))?.[0];
|
|
31
|
+
}
|
|
32
|
+
/** A git failure nobody foresaw (the clone is gone, not a repository) is an answer, not a crash in the middle of a Stop. */
|
|
33
|
+
function safely(app, work) {
|
|
34
|
+
try {
|
|
35
|
+
return work();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const failure = error;
|
|
39
|
+
if (failure.status === undefined && failure.spawnargs === undefined)
|
|
40
|
+
throw error;
|
|
41
|
+
return { error: `${app}: ${said(failure).split('\n').pop() ?? ''}` };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const isSwitch = (app, to) => (action) => action.kind === 'checkout' && action.app === app && (to === undefined || action.to === to);
|
|
45
|
+
const forget = (logFile, app, to) => {
|
|
46
|
+
updateActions(logFile, log => {
|
|
47
|
+
const at = log.findLastIndex(isSwitch(app, to));
|
|
48
|
+
if (at >= 0)
|
|
49
|
+
log.splice(at, 1);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export const switchTo = ({ app, repo, to, how, logFile }) => safely(app, () => {
|
|
53
|
+
// A clone on a commit instead of a branch goes back to that commit.
|
|
54
|
+
const detached = head(repo) === 'HEAD';
|
|
55
|
+
const from = detached ? git(repo, 'rev-parse', 'HEAD') : head(repo);
|
|
56
|
+
const was = named(from, detached);
|
|
57
|
+
if (from === to)
|
|
58
|
+
return { message: `${app} is already on ${to}` };
|
|
59
|
+
// Given to git as an argument: never something git would read as an option.
|
|
60
|
+
if (to.startsWith('-'))
|
|
61
|
+
return { error: `${to} is not a branch name` };
|
|
62
|
+
const dirty = changed(repo);
|
|
63
|
+
if (dirty && !how)
|
|
64
|
+
return {
|
|
65
|
+
error: `${app} has ${changes(dirty)} on ${was}: choose --set-aside or --take-along (the canvas asks this on the card)`,
|
|
66
|
+
};
|
|
67
|
+
const label = dirty && how === 'set-aside' ? `koppler: ${app} on ${was} while it runs ${to}` : null;
|
|
68
|
+
// Logged before anything changes, so a crash part-way still leaves Stop an entry to undo.
|
|
69
|
+
const entry = {
|
|
70
|
+
kind: 'checkout',
|
|
71
|
+
app,
|
|
72
|
+
repo,
|
|
73
|
+
from,
|
|
74
|
+
to,
|
|
75
|
+
...(detached && { detached: true }),
|
|
76
|
+
...(label && { stash: label }),
|
|
77
|
+
...(dirty > 0 && how === 'take-along' && { carried: true }),
|
|
78
|
+
};
|
|
79
|
+
updateActions(logFile, log => void log.push(entry));
|
|
80
|
+
if (label) {
|
|
81
|
+
const aside = tryGit(repo, 'stash', 'push', '--include-untracked', '-m', label);
|
|
82
|
+
if (!aside.ok) {
|
|
83
|
+
forget(logFile, app, to);
|
|
84
|
+
return { error: `${app}'s changes could not be set aside: ${firstLine(aside.out)}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const switched = tryGit(repo, 'switch', to);
|
|
88
|
+
if (!switched.ok) {
|
|
89
|
+
// What was set aside comes straight back; the entry goes once it has.
|
|
90
|
+
const ref = label ? stashOf(repo, label) : undefined;
|
|
91
|
+
const back = ref ? tryGit(repo, 'stash', 'pop', ref).ok : true;
|
|
92
|
+
if (back)
|
|
93
|
+
forget(logFile, app, to);
|
|
94
|
+
const clash = how === 'take-along' && /overwritten|conflict/i.test(switched.out);
|
|
95
|
+
const why = clash
|
|
96
|
+
? `${app}'s changes clash with ${to}; set them aside instead (--set-aside)`
|
|
97
|
+
: `${app} could not switch to ${to}: ${firstLine(switched.out)}`;
|
|
98
|
+
return { error: why + (back ? '' : `; your set-aside changes stay in the stash as "${label ?? ''}"`) };
|
|
99
|
+
}
|
|
100
|
+
const carried = label
|
|
101
|
+
? `, your ${changes(dirty)} ${dirty === 1 ? 'is' : 'are'} set aside until Stop`
|
|
102
|
+
: dirty
|
|
103
|
+
? `, your ${changes(dirty)} came along`
|
|
104
|
+
: '';
|
|
105
|
+
return { message: `${app}: ${was} -> ${to}${carried}` };
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* Brings back what was set aside under `stash`, onto a clean clone only, so a clash can be undone without touching
|
|
109
|
+
* anything else. Gives the words to add to the message, or the reason it could not.
|
|
110
|
+
*/
|
|
111
|
+
function restoreStash(app, repo, stash, was) {
|
|
112
|
+
const ref = stashOf(repo, stash);
|
|
113
|
+
if (!ref)
|
|
114
|
+
return { note: ', your set-aside changes were taken back already' };
|
|
115
|
+
if (changed(repo))
|
|
116
|
+
return {
|
|
117
|
+
error: `${app} is back on ${was} with new changes; the ones set aside wait in the stash as "${stash}" (git stash pop, once these are committed)`,
|
|
118
|
+
};
|
|
119
|
+
const untracked = tryGit(repo, 'ls-tree', '-r', '--name-only', '-z', `${ref}^3`);
|
|
120
|
+
if (tryGit(repo, 'stash', 'pop', ref).ok)
|
|
121
|
+
return { note: ', your set-aside changes are back' };
|
|
122
|
+
// Git leaves a clashing change half applied and keeps the stash. The clone goes back to clean; the changes stay in
|
|
123
|
+
// the stash.
|
|
124
|
+
git(repo, 'reset', '--hard', '-q');
|
|
125
|
+
for (const file of untracked.ok ? untracked.out.split('\0').filter(Boolean) : [])
|
|
126
|
+
fs.rmSync(path.join(repo, file), { force: true });
|
|
127
|
+
return {
|
|
128
|
+
error: `${app} is back on ${was}, but your set-aside changes clash with what it holds now. They are safe in the stash as "${stash}": take them back with git stash pop and settle the clash there`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Undoes the app's latest switch. The entry leaves the log only once the clone is where it was and the set-aside
|
|
133
|
+
* changes are back, so Stop can be run again.
|
|
134
|
+
*/
|
|
135
|
+
export const switchBack = ({ app, logFile }) => safely(app, () => {
|
|
136
|
+
const entry = updateActions(logFile, log => log.findLast(isSwitch(app)));
|
|
137
|
+
if (!entry)
|
|
138
|
+
return { message: `${app}: no branch switch to undo` };
|
|
139
|
+
const { repo, from, to, stash, carried, detached } = entry;
|
|
140
|
+
const was = named(from, detached);
|
|
141
|
+
if (!fs.existsSync(repo))
|
|
142
|
+
return { error: `${app}'s clone is no longer at ${repo}, so it could not be switched back to ${was}` };
|
|
143
|
+
const there = detached ? head(repo) === 'HEAD' && git(repo, 'rev-parse', 'HEAD') === from : head(repo) === from;
|
|
144
|
+
const now = head(repo);
|
|
145
|
+
if (!there && now !== to) {
|
|
146
|
+
// The user moved the clone on since: it stays where they put it, and the switch is no longer Koppler's to undo.
|
|
147
|
+
forget(logFile, app, to);
|
|
148
|
+
const where = now === 'HEAD' ? 'the commit you checked out' : now;
|
|
149
|
+
const aside = stash ? `; your set-aside changes wait in the stash as "${stash}"` : '';
|
|
150
|
+
return { message: `${app}: left on ${where}, where you moved it; not switched back to ${was}${aside}` };
|
|
151
|
+
}
|
|
152
|
+
if (!there) {
|
|
153
|
+
const dirty = changed(repo);
|
|
154
|
+
// Changes made while it ran stay the user's: switching over them is their call, unless they came along in the
|
|
155
|
+
// first place.
|
|
156
|
+
if (dirty && !carried)
|
|
157
|
+
return {
|
|
158
|
+
error: `${app} has ${String(dirty)} new uncommitted ${dirty === 1 ? 'change' : 'changes'} on ${to}; it stays there until they are committed or set aside`,
|
|
159
|
+
};
|
|
160
|
+
const switched = tryGit(repo, 'switch', ...(detached ? ['--detach'] : []), from);
|
|
161
|
+
if (!switched.ok)
|
|
162
|
+
return { error: `${app} could not switch back to ${was}: ${firstLine(switched.out)}` };
|
|
163
|
+
}
|
|
164
|
+
const restored = stash ? restoreStash(app, repo, stash, was) : { note: '' };
|
|
165
|
+
if ('error' in restored)
|
|
166
|
+
return restored;
|
|
167
|
+
forget(logFile, app, to);
|
|
168
|
+
return { message: `${app}: back on ${was}${restored.note}` };
|
|
169
|
+
});
|
|
170
|
+
/** The clone of an app on this machine, as the last sync found it. */
|
|
171
|
+
function cloneOf(app) {
|
|
172
|
+
const apps = readData('local')?.['apps'];
|
|
173
|
+
return apps?.[app]?.path;
|
|
174
|
+
}
|
|
175
|
+
export function switchCommand(args, { temp }) {
|
|
176
|
+
const [app, to] = args.filter(arg => !arg.startsWith('--'));
|
|
177
|
+
if (!app)
|
|
178
|
+
fail('usage: koppler switch <app> <branch> [--set-aside | --take-along] | koppler switch <app> --back');
|
|
179
|
+
const logFile = path.join(temp, 'actions.json');
|
|
180
|
+
let outcome;
|
|
181
|
+
if (args.includes('--back'))
|
|
182
|
+
outcome = switchBack({ app, logFile });
|
|
183
|
+
else {
|
|
184
|
+
const repo = cloneOf(app) ?? fail(`${app} has no clone on this Mac (koppler sync)`);
|
|
185
|
+
if (!to)
|
|
186
|
+
fail('usage: koppler switch <app> <branch> [--set-aside | --take-along]');
|
|
187
|
+
const how = args.includes('--set-aside') ? 'set-aside' : args.includes('--take-along') ? 'take-along' : null;
|
|
188
|
+
outcome = switchTo({ app, repo, to, how, logFile });
|
|
189
|
+
}
|
|
190
|
+
if (outcome.error !== undefined)
|
|
191
|
+
fail(outcome.error);
|
|
192
|
+
console.log(outcome.message);
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=branch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"branch.js","sourceRoot":"","sources":["../../src/engine/branch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// A web app set to show its browser console on its card: when it starts, its page loads the canvas server's console
|
|
2
|
+
// script, through one line added to its HTML entry file. The line is a temporary change: Stop takes it out.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { consoleKey } from "../server/browser-console.js";
|
|
6
|
+
import { tempDir } from "../paths.js";
|
|
7
|
+
import { readActions, updateActions } from "./actions.js";
|
|
8
|
+
import { insideClone } from "./recipes.js";
|
|
9
|
+
/** Adds the script line to the app's HTML file, logged so Stop undoes it; says what it did, or why not. */
|
|
10
|
+
export function showBrowserConsole(app, repo, file) {
|
|
11
|
+
const target = insideClone(repo, file);
|
|
12
|
+
if (!target)
|
|
13
|
+
return `${file} is not a file inside ${app}'s clone`;
|
|
14
|
+
let canvas;
|
|
15
|
+
try {
|
|
16
|
+
canvas = new URL(fs.readFileSync(path.join(tempDir(), 'canvas-url'), 'utf8').trim());
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return 'the canvas is not running, so the browser console has nowhere to go';
|
|
20
|
+
}
|
|
21
|
+
let body;
|
|
22
|
+
try {
|
|
23
|
+
body = fs.readFileSync(target, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return `${file} is not in the clone`;
|
|
27
|
+
}
|
|
28
|
+
const line = `<script src="http://${canvas.host}/console.js?app=${app}&key=${consoleKey(app)}"></script>`;
|
|
29
|
+
if (body.includes(line))
|
|
30
|
+
return 'already shows its browser console';
|
|
31
|
+
const log = path.join(tempDir(), 'actions.json');
|
|
32
|
+
if (readActions(log).some(action => action.kind === 'edit' && action.file === target && action.after.includes('/console.js?')))
|
|
33
|
+
return 'already shows its browser console';
|
|
34
|
+
const at = body.match(/<\/head>/i)?.[0];
|
|
35
|
+
if (!at)
|
|
36
|
+
return `${file} has no </head> to put the console script before`;
|
|
37
|
+
// Logged before the file changes, so a crash part-way still leaves Stop an entry to undo.
|
|
38
|
+
const entry = { kind: 'edit', app, file: target, before: at, after: `${line}${at}` };
|
|
39
|
+
updateActions(log, list => {
|
|
40
|
+
list.push(entry);
|
|
41
|
+
});
|
|
42
|
+
fs.writeFileSync(target, body.replace(at, () => entry.after));
|
|
43
|
+
return 'shows its browser console on its card';
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=browser-console.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-console.js","sourceRoot":"","sources":["../../src/engine/browser-console.ts"],"names":[],"mappings":"AAAA,oHAAoH;AACpH,4GAA4G;AAC5G,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,aAAa,EAAmB,MAAM,cAAc,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,2GAA2G;AAC3G,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,IAAY,EAAE,IAAY;IACxE,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM;QAAE,OAAO,GAAG,IAAI,yBAAyB,GAAG,UAAU,CAAC;IAClE,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,qEAAqE,CAAC;IAC/E,CAAC;IACD,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,IAAI,sBAAsB,CAAC;IACvC,CAAC;IACD,MAAM,IAAI,GAAG,uBAAuB,MAAM,CAAC,IAAI,mBAAmB,GAAG,QAAQ,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC;IAC1G,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,mCAAmC,CAAC;IACpE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,CAAC,CAAC;IACjD,IACE,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CACnB,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,CACpG;QAED,OAAO,mCAAmC,CAAC;IAC7C,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,EAAE;QAAE,OAAO,GAAG,IAAI,kDAAkD,CAAC;IAC1E,0FAA0F;IAC1F,MAAM,KAAK,GAAe,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,IAAI,GAAG,EAAE,EAAE,EAAE,CAAC;IACjG,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE;QACxB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,aAAa,CACd,MAAM,EACN,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CACpC,CAAC;IACF,OAAO,uCAAuC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Which app calls which, found from the deployed addresses the stack knows and the text of each clone.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import {} from "./config.js";
|
|
4
|
+
/** Every deployed host each app is known by, from its own deployedUrl and from how other apps call it. */
|
|
5
|
+
export function hostsByApp(stack) {
|
|
6
|
+
const hosts = {};
|
|
7
|
+
const add = (id, url) => {
|
|
8
|
+
try {
|
|
9
|
+
(hosts[id] ??= new Set()).add(new URL(url ?? '').hostname.toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// No address, or not one.
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
for (const [id, app] of Object.entries(stack.apps)) {
|
|
16
|
+
const envs = Object.values(app.envs ?? {});
|
|
17
|
+
add(id, app.deployedUrl);
|
|
18
|
+
for (const env of envs)
|
|
19
|
+
add(id, env['deployedUrl']);
|
|
20
|
+
for (const [callee, call] of Object.entries(app.consumes ?? {})) {
|
|
21
|
+
add(callee, call.deployed);
|
|
22
|
+
for (const env of envs)
|
|
23
|
+
add(callee, env['consumes']?.[callee]?.deployed);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return hosts;
|
|
27
|
+
}
|
|
28
|
+
/** A host that two apps share (one app's QA address pointing at another) cannot say which app is called, so it is left out. */
|
|
29
|
+
export function hostOwners(stack) {
|
|
30
|
+
const owners = new Map();
|
|
31
|
+
for (const [id, hosts] of Object.entries(hostsByApp(stack)))
|
|
32
|
+
for (const host of hosts)
|
|
33
|
+
owners.set(host, [...(owners.get(host) ?? []), id]);
|
|
34
|
+
const unique = new Map();
|
|
35
|
+
for (const [host, ids] of owners)
|
|
36
|
+
if (ids.length === 1 && ids[0])
|
|
37
|
+
unique.set(host, ids[0]);
|
|
38
|
+
return { unique, shared: [...owners].filter(([, ids]) => ids.length > 1) };
|
|
39
|
+
}
|
|
40
|
+
/** Calls found in the caller's repo (a line naming another app's deployed host) that the stack does not list yet. */
|
|
41
|
+
export function missingCalls(stack, grepRepo) {
|
|
42
|
+
const { unique } = hostOwners(stack);
|
|
43
|
+
const found = {};
|
|
44
|
+
for (const [caller, app] of Object.entries(stack.apps)) {
|
|
45
|
+
const hosts = [...unique].filter(([, owner]) => owner !== caller).map(([host]) => host);
|
|
46
|
+
if (!hosts.length)
|
|
47
|
+
continue;
|
|
48
|
+
for (const line of grepRepo(caller, hosts)) {
|
|
49
|
+
const host = hosts.find(one => line.toLowerCase().includes(one));
|
|
50
|
+
const callee = host && unique.get(host);
|
|
51
|
+
if (!callee || app.consumes?.[callee] || found[caller]?.[callee])
|
|
52
|
+
continue;
|
|
53
|
+
(found[caller] ??= {})[callee] = line.slice(0, 200);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
// Tests, docs, lock files and one-off migration tools mention hosts without being the app's own calls.
|
|
59
|
+
const NOT_CODE = [
|
|
60
|
+
'*.lock',
|
|
61
|
+
'package-lock.json',
|
|
62
|
+
'*.md',
|
|
63
|
+
'**/test/**',
|
|
64
|
+
'**/tests/**',
|
|
65
|
+
'**/*Test*/**',
|
|
66
|
+
'**/docs/**',
|
|
67
|
+
'**/*Migration*/**',
|
|
68
|
+
].map(glob => `:(exclude,glob)${glob.includes('/') ? glob : '**/' + glob}`);
|
|
69
|
+
const MAX_GREP = 16 * 1024 * 1024;
|
|
70
|
+
export const lines = (text) => text.split('\n').filter(Boolean);
|
|
71
|
+
export function grepClone(repo, hosts) {
|
|
72
|
+
const patterns = hosts.flatMap(host => ['-e', host]);
|
|
73
|
+
try {
|
|
74
|
+
return lines(execFileSync('git', ['-C', repo, 'grep', '-n', '-I', '-F', '-i', ...patterns, '--', ...NOT_CODE], {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
maxBuffer: MAX_GREP,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Every http(s) address written in a clone's code, as "file:line:url". */
|
|
84
|
+
export function urlsInClone(repo) {
|
|
85
|
+
const find = ['grep', '-n', '-I', '-o', '-i', '-E', 'https?://[a-z0-9.-]+', '--', ...NOT_CODE];
|
|
86
|
+
try {
|
|
87
|
+
return lines(execFileSync('git', ['-C', repo, ...find], {
|
|
88
|
+
encoding: 'utf8',
|
|
89
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
90
|
+
maxBuffer: MAX_GREP,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=calls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calls.js","sourceRoot":"","sources":["../../src/engine/calls.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// What a stack needs on this Mac, checked when the stack loads. Koppler itself needs only git and Node.
|
|
2
|
+
// The rest comes from the stack: the programs its kinds of app are started with, the programs its own commands call
|
|
3
|
+
// (a tunnel's command, an AWS sign-in), and its `checks`, which are shell commands and so run only once the user approved them.
|
|
4
|
+
// Usage: koppler doctor [--json] run them and say what is missing
|
|
5
|
+
// koppler doctor --approve show the stack's own check commands and allow them to run (again after they change)
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { resolveExpressions } from "../core/expressions.js";
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { writeJson } from "../json-files.js";
|
|
12
|
+
import { of as kindOf } from "../core/kinds.js";
|
|
13
|
+
import { isSignIn, signInOf } from "../core/sign-in.js";
|
|
14
|
+
import { readData } from "../data.js";
|
|
15
|
+
import { configDir, tempDir } from "../paths.js";
|
|
16
|
+
const approvalsFile = () => path.join(configDir(), 'approved-checks.json');
|
|
17
|
+
const TOOL = /^[A-Za-z0-9._+-]{1,64}$/;
|
|
18
|
+
const text = (value, max = 300) => (typeof value === 'string' ? value.slice(0, max) : '');
|
|
19
|
+
/** The checks a stack calls for. `safe` ones only ask whether a program is installed; the stack's own `checks` are shell. */
|
|
20
|
+
export function checksFor(stack) {
|
|
21
|
+
const apps = Object.entries(stack?.apps ?? {});
|
|
22
|
+
const list = new Map();
|
|
23
|
+
const tool = (name, who, fix) => {
|
|
24
|
+
if (name === undefined || !TOOL.test(name))
|
|
25
|
+
return;
|
|
26
|
+
const check = list.get(name) ?? { name, tool: name, safe: true, for: [], fix: '' };
|
|
27
|
+
if (who && !check.for.includes(who))
|
|
28
|
+
check.for.push(who);
|
|
29
|
+
check.fix ||= typeof fix === 'string' ? fix : '';
|
|
30
|
+
list.set(name, check);
|
|
31
|
+
};
|
|
32
|
+
tool('git', 'Koppler', 'xcode-select --install');
|
|
33
|
+
tool('node', 'Koppler', 'Install Node 22 or newer');
|
|
34
|
+
for (const [, app] of apps.filter(([, one]) => !one.outside)) {
|
|
35
|
+
const kind = kindOf(app, stack);
|
|
36
|
+
for (const need of kind.needs ?? [])
|
|
37
|
+
tool(need.tool, kind.label, need.fix);
|
|
38
|
+
// Every environment's prerequisites: an AWS sign-in uses the aws program, a command is started by its first word.
|
|
39
|
+
const prereqs = [...(app.prereqs ?? []), ...Object.values(app.envs ?? {}).flatMap(env => env?.prereqs ?? [])];
|
|
40
|
+
for (const prereq of prereqs) {
|
|
41
|
+
const signIn = signInOf(stack, prereq);
|
|
42
|
+
if (signIn?.tool)
|
|
43
|
+
tool(signIn.tool, app.name ?? 'an app', stack?.install?.[signIn.tool]);
|
|
44
|
+
const first = text(prereq?.cmd).trim().split(/\s+/)[0];
|
|
45
|
+
if (first)
|
|
46
|
+
tool(first, prereq?.what ?? app.name ?? 'an app', stack?.install?.[first]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const check of list.values())
|
|
50
|
+
check.fix ||= text(stack?.install?.[check.tool]) || `Install ${check.tool} and make sure it is on your PATH`;
|
|
51
|
+
const written = (Array.isArray(stack?.checks) ? stack.checks : []);
|
|
52
|
+
const own = written
|
|
53
|
+
.filter((check) => {
|
|
54
|
+
const { name, run } = (check ?? {});
|
|
55
|
+
return typeof name === 'string' && typeof run === 'string';
|
|
56
|
+
})
|
|
57
|
+
.slice(0, 50)
|
|
58
|
+
.map((check) => {
|
|
59
|
+
const kinds = check['kinds'];
|
|
60
|
+
return {
|
|
61
|
+
name: text(check.name, 80),
|
|
62
|
+
run: text(check.run, 2000),
|
|
63
|
+
ok: text(check['ok']),
|
|
64
|
+
fix: text(check['fix']) || 'This check of the stack fails',
|
|
65
|
+
safe: false,
|
|
66
|
+
...(Array.isArray(kinds) && { kinds: kinds.filter(kind => typeof kind === 'string') }),
|
|
67
|
+
};
|
|
68
|
+
})
|
|
69
|
+
.filter(check => !check.kinds || apps.some(([, app]) => app.kind !== undefined && check.kinds?.includes(app.kind)));
|
|
70
|
+
return [...list.values(), ...own];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What the stack does not say for an environment other than the first: a tunnel the first one has, or how to get a
|
|
74
|
+
* secret there. Never filled in from the first environment: its hosts and secrets are another environment's.
|
|
75
|
+
*/
|
|
76
|
+
export function envGaps(stack, only) {
|
|
77
|
+
const [first, ...rest] = stack?.envs ?? [];
|
|
78
|
+
const gaps = [];
|
|
79
|
+
for (const env of rest.filter(one => !only || one === only))
|
|
80
|
+
for (const [id, app] of Object.entries(stack?.apps ?? {})) {
|
|
81
|
+
if (app.outside)
|
|
82
|
+
continue;
|
|
83
|
+
const own = app.envs?.[env] ?? {};
|
|
84
|
+
const label = env.toUpperCase();
|
|
85
|
+
const from = String(first).toUpperCase();
|
|
86
|
+
const name = app.name ?? id;
|
|
87
|
+
// Tunnels, sign-ins and hosts entries belong to one environment: each kind the first one has must be in this
|
|
88
|
+
// one's own list too.
|
|
89
|
+
const has = (kind) => (own.prereqs ?? []).some(prereq => prereq?.[kind]);
|
|
90
|
+
const signsIn = (own.prereqs ?? []).some(isSignIn);
|
|
91
|
+
const gap = (kind, what, detail) => gaps.push({ app: id, env, kind, name: what, detail });
|
|
92
|
+
for (const prereq of app.prereqs ?? []) {
|
|
93
|
+
if (prereq?.tunnel && !has('tunnel'))
|
|
94
|
+
gap('tunnel', prereq.what ?? prereq.tunnel, `No ${label} tunnel for ${name}'s ${prereq.what ?? 'database'} (${from} uses ${prereq.tunnel})`);
|
|
95
|
+
const signIn = signInOf(stack, prereq);
|
|
96
|
+
if (signIn && !signsIn)
|
|
97
|
+
gap('sign-in', prereq?.sso ?? prereq?.signIn, `No ${label} sign-in for ${name} (${from} uses ${prereq?.sso ? `the AWS profile ${prereq.sso}` : signIn.what})`);
|
|
98
|
+
if (prereq?.hosts && !has('hosts'))
|
|
99
|
+
gap('hosts', prereq.hosts, `No ${label} hosts entry for ${name} (${from} uses ${prereq.hosts})`);
|
|
100
|
+
}
|
|
101
|
+
if (!own.secrets)
|
|
102
|
+
for (const secret of app.secrets ?? [])
|
|
103
|
+
if (secret?.fetch)
|
|
104
|
+
gap('secret', secret.env, `No steps for getting ${String(secret.env)} for ${name} on ${label}`);
|
|
105
|
+
}
|
|
106
|
+
return gaps;
|
|
107
|
+
}
|
|
108
|
+
/** Apps whose tunnels for `env` are missing: they must not borrow the first environment's. */
|
|
109
|
+
export const missingTunnels = (stack, env) => [
|
|
110
|
+
...new Set(envGaps(stack, env)
|
|
111
|
+
.filter(gap => gap.kind === 'tunnel')
|
|
112
|
+
.map(gap => gap.app)),
|
|
113
|
+
];
|
|
114
|
+
/**
|
|
115
|
+
* Everything in the stack that Koppler would run or write on this Mac, beyond the stack's facts: its own checks, the
|
|
116
|
+
* start commands it gives (per app, per environment, and per kind it defines), and the code changes it remembers.
|
|
117
|
+
* A shared stack can be edited by anyone on the team, so none of it happens until the user has seen exactly this list.
|
|
118
|
+
*/
|
|
119
|
+
export function stackCommands(stack) {
|
|
120
|
+
const out = [];
|
|
121
|
+
const add = (what, name, run) => {
|
|
122
|
+
if (typeof run === 'string' && run)
|
|
123
|
+
out.push({ what, name, run });
|
|
124
|
+
};
|
|
125
|
+
for (const check of checksFor(stack))
|
|
126
|
+
if (!check.safe)
|
|
127
|
+
add('check', check.name, check.run);
|
|
128
|
+
// Only the check: Koppler runs it. The login is shown for the user to run themselves.
|
|
129
|
+
for (const [name, signIn] of Object.entries(stack?.signIns ?? {})) {
|
|
130
|
+
add('sign-in check', name, signIn?.check);
|
|
131
|
+
// What it gives the apps it is for reaches them at start, so it is allowed with the rest.
|
|
132
|
+
for (const [setting, value] of Object.entries(signIn?.env ?? {}))
|
|
133
|
+
add('sign-in setting', `${name}: ${setting}`, value);
|
|
134
|
+
}
|
|
135
|
+
for (const [id, kind] of Object.entries(stack?.kinds ?? {})) {
|
|
136
|
+
add('start of every', `${id} app`, kind?.start);
|
|
137
|
+
add('stop of every', `${id} app`, kind?.stop);
|
|
138
|
+
}
|
|
139
|
+
for (const [id, app] of Object.entries(stack?.apps ?? {})) {
|
|
140
|
+
const name = app?.name ?? id;
|
|
141
|
+
add('start', name, app?.start);
|
|
142
|
+
for (const [env, own] of Object.entries(app?.envs ?? {}))
|
|
143
|
+
add(`start on ${env}`, name, own?.start);
|
|
144
|
+
for (const [key, command] of Object.entries(app?.commands ?? {}))
|
|
145
|
+
add(`command ${key} of`, name, command?.run);
|
|
146
|
+
const changes = (Array.isArray(app?.localChanges) ? app.localChanges : []);
|
|
147
|
+
for (const change of changes)
|
|
148
|
+
add('code change', `${name}, ${String(change?.['file'])}`, `${String(change?.['find'])}\n becomes\n${String(change?.['replace'])}`);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** Whether this app's start runs a command the stack wrote, on that environment. */
|
|
153
|
+
export const startsFromStack = (stack, id, env) => {
|
|
154
|
+
const app = stack?.apps?.[id];
|
|
155
|
+
return !!(app && (app.start || app.envs?.[env]?.start || stack.kinds?.[app.kind ?? '']?.start));
|
|
156
|
+
};
|
|
157
|
+
/** Changes whenever a command of the stack's own changes; names and order of safe checks do not count. */
|
|
158
|
+
export const fingerprint = (commands) => crypto
|
|
159
|
+
.createHash('sha256')
|
|
160
|
+
.update(JSON.stringify(commands.map(command => [command.what, command.name, command.run])))
|
|
161
|
+
.digest('hex');
|
|
162
|
+
const approvals = () => {
|
|
163
|
+
try {
|
|
164
|
+
return JSON.parse(fs.readFileSync(approvalsFile(), 'utf8'));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return {};
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
/** Approved when the stack runs nothing of its own, or the user approved exactly what it runs now. */
|
|
171
|
+
export const approved = (source, stack) => {
|
|
172
|
+
const commands = stackCommands(stack);
|
|
173
|
+
return !commands.length || approvals()[source] === fingerprint(commands);
|
|
174
|
+
};
|
|
175
|
+
export function approve(source, stack) {
|
|
176
|
+
writeJson(approvalsFile(), { ...approvals(), [source]: fingerprint(stackCommands(stack)) });
|
|
177
|
+
}
|
|
178
|
+
export const APPROVE = 'koppler doctor --approve';
|
|
179
|
+
const ran = (run, timeout) => {
|
|
180
|
+
try {
|
|
181
|
+
execFileSync('/bin/sh', ['-c', run], { stdio: 'ignore', ...(timeout && { timeout }) });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const has = (tool) => ran(`command -v ${tool}`);
|
|
189
|
+
const passes = (run) => ran(run, 20000);
|
|
190
|
+
/** Runs what may run. A stack check that was not approved stays `pending`: listed, not run. */
|
|
191
|
+
export function runChecks(stack, { source = stack?.source ?? '', exists = has, shell = passes } = {}) {
|
|
192
|
+
const ok = approved(source, stack);
|
|
193
|
+
const results = checksFor(stack).map((check) => {
|
|
194
|
+
if (check.safe) {
|
|
195
|
+
const base = { name: check.name, from: 'tool', ...(check.for.length && { for: check.for }) };
|
|
196
|
+
return exists(check.tool)
|
|
197
|
+
? { ...base, state: 'ok', detail: 'installed' }
|
|
198
|
+
: { ...base, state: 'missing', detail: check.fix };
|
|
199
|
+
}
|
|
200
|
+
const base = { name: check.name, from: 'stack' };
|
|
201
|
+
if (!ok)
|
|
202
|
+
return { ...base, state: 'pending', detail: 'Waits for your approval: koppler doctor --approve' };
|
|
203
|
+
return shell(check.run)
|
|
204
|
+
? { ...base, state: 'ok', detail: check.ok || 'ok' }
|
|
205
|
+
: { ...base, state: 'missing', detail: check.fix };
|
|
206
|
+
});
|
|
207
|
+
const gaps = envGaps(stack).map((gap) => ({
|
|
208
|
+
name: `${gap.app} ${gap.env}`,
|
|
209
|
+
from: 'stack',
|
|
210
|
+
state: 'gap',
|
|
211
|
+
detail: gap.detail,
|
|
212
|
+
app: gap.app,
|
|
213
|
+
env: gap.env,
|
|
214
|
+
kind: gap.kind,
|
|
215
|
+
}));
|
|
216
|
+
return [...results, ...gaps];
|
|
217
|
+
}
|
|
218
|
+
const WORD = { ok: 'ok', missing: 'MISSING', pending: 'PENDING', gap: 'GAP' };
|
|
219
|
+
export function report(results) {
|
|
220
|
+
for (const result of results) {
|
|
221
|
+
const who = result.for?.length && result.state !== 'ok' ? ` (needed by ${result.for.join(', ')})` : '';
|
|
222
|
+
console.log(`${WORD[result.state].padEnd(8)} ${result.name.padEnd(14)} ${result.detail}${who}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const save = (stack, results) => {
|
|
226
|
+
const saved = { at: new Date().toISOString(), stack: stack.name ?? '', results };
|
|
227
|
+
writeJson(path.join(tempDir(), 'checks.json'), saved);
|
|
228
|
+
};
|
|
229
|
+
/** Runs the loaded stack's checks, prints them, and leaves the result for the canvas. False when something is missing. */
|
|
230
|
+
export function checkStack({ quiet = false } = {}) {
|
|
231
|
+
const stack = resolveExpressions(readData('registry'));
|
|
232
|
+
if (!stack)
|
|
233
|
+
return true;
|
|
234
|
+
const results = runChecks(stack);
|
|
235
|
+
fs.mkdirSync(tempDir(), { recursive: true });
|
|
236
|
+
save(stack, results);
|
|
237
|
+
if (!quiet)
|
|
238
|
+
report(results);
|
|
239
|
+
return !results.some(result => result.state === 'missing');
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* The tools and the stack's own checks on this Mac, saved for the canvas. With `approve`, the stack's check commands
|
|
243
|
+
* are shown first and run only once `confirm` says yes: they come from a shared file anyone on the team can edit.
|
|
244
|
+
*/
|
|
245
|
+
export async function checkTools({ approve: wantApprove = false, confirm = () => Promise.resolve(false), } = {}) {
|
|
246
|
+
const stack = resolveExpressions(readData('registry'));
|
|
247
|
+
if (!stack)
|
|
248
|
+
return { error: 'no stack loaded (koppler init)' };
|
|
249
|
+
const commands = stackCommands(stack);
|
|
250
|
+
const source = stack.source ?? '';
|
|
251
|
+
if (wantApprove) {
|
|
252
|
+
if (!commands.length)
|
|
253
|
+
console.log('This stack runs nothing of its own, so there is nothing to approve.');
|
|
254
|
+
else {
|
|
255
|
+
console.log(`The stack at ${source} would run or write these on this Mac. Anyone who can edit the stack wrote them:\n`);
|
|
256
|
+
for (const command of commands) {
|
|
257
|
+
const lines = command.run.split('\n').map(line => ` ${line}`);
|
|
258
|
+
console.log(` ${command.what}: ${command.name}\n${lines.join('\n')}\n`);
|
|
259
|
+
}
|
|
260
|
+
if (!(await confirm('Allow all of these?')))
|
|
261
|
+
return { error: "not approved; the stack's own commands and code changes stay off" };
|
|
262
|
+
approve(source, stack);
|
|
263
|
+
console.log('Approved. They are allowed from now on, until any of them changes.\n');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const results = runChecks(stack);
|
|
267
|
+
save(stack, results);
|
|
268
|
+
return { results };
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=checks.js.map
|