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,100 @@
|
|
|
1
|
+
// Queues switched to local on the board.
|
|
2
|
+
// SQS: one AWS emulator in Docker (the image the Lambda kind uses) holds them all; every app on one starts with the AWS
|
|
3
|
+
// SDK's SQS endpoint pointed at it and its queue settings set to the local queue.
|
|
4
|
+
// Kafka: one local broker in Docker (apache/kafka); the topic is created on it and every app on one starts with its
|
|
5
|
+
// broker settings pointed at it.
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { of as kindOf } from "../core/kinds.js";
|
|
8
|
+
import { LOCAL_QUEUE_TYPES } from "../core/plan.js";
|
|
9
|
+
export const PORT = 4566;
|
|
10
|
+
export const ENDPOINT = `http://127.0.0.1:${String(PORT)}`;
|
|
11
|
+
// Stopping the Docker client does not stop its container: it is removed on stop, and a leftover one before starting.
|
|
12
|
+
const RM = 'docker rm -f koppler-queues >/dev/null 2>&1';
|
|
13
|
+
// Run by Stop as well: the shell's own cleanup never happens when it has to be killed.
|
|
14
|
+
export const STOP_EMULATOR = ['docker', 'rm', '-f', 'koppler-queues'];
|
|
15
|
+
export const STOP_BROKER = ['docker', 'rm', '-f', 'koppler-kafka'];
|
|
16
|
+
export const EMULATOR = [
|
|
17
|
+
'sh',
|
|
18
|
+
'-c',
|
|
19
|
+
`${RM}; trap '${RM}; exit 0' TERM INT HUP; docker run --rm --name koppler-queues -p 127.0.0.1:${String(PORT)}:4566 floci/floci:latest & wait`,
|
|
20
|
+
];
|
|
21
|
+
export const KAFKA_PORT = 9092;
|
|
22
|
+
export const KAFKA = `localhost:${String(KAFKA_PORT)}`;
|
|
23
|
+
const RM_KAFKA = 'docker rm -f koppler-kafka >/dev/null 2>&1';
|
|
24
|
+
export const BROKER = [
|
|
25
|
+
'sh',
|
|
26
|
+
'-c',
|
|
27
|
+
`${RM_KAFKA}; trap '${RM_KAFKA}; exit 0' TERM INT HUP; docker run --rm --name koppler-kafka -p 127.0.0.1:${String(KAFKA_PORT)}:9092 apache/kafka:latest & wait`,
|
|
28
|
+
];
|
|
29
|
+
/** How an app uses a queue, as its `publishes` or `reads` records it. */
|
|
30
|
+
const useOf = (stack, app, queue) => ['publishes', 'reads']
|
|
31
|
+
.map(key => stack.apps[app]?.[key]?.[queue])
|
|
32
|
+
.find(Boolean);
|
|
33
|
+
/** The local queues of a board and the apps on each; what cannot be local is in `left`. */
|
|
34
|
+
export function localQueues(stack, queues, ids, env) {
|
|
35
|
+
const out = [];
|
|
36
|
+
const left = [];
|
|
37
|
+
for (const [id, where] of Object.entries(queues ?? {})) {
|
|
38
|
+
const queue = stack.queues?.[id];
|
|
39
|
+
if (where !== 'local' || !queue)
|
|
40
|
+
continue;
|
|
41
|
+
if (!LOCAL_QUEUE_TYPES.includes(queue.type)) {
|
|
42
|
+
left.push(`${queue.name}: Koppler cannot run a ${queue.type} queue locally yet; it stays the deployed one`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const apps = ids.flatMap(app => {
|
|
46
|
+
const use = useOf(stack, app, id);
|
|
47
|
+
return use ? [{ id: app, env: [use.env ?? []].flat() }] : [];
|
|
48
|
+
});
|
|
49
|
+
out.push({ id, name: queue.name, type: queue.type, queue: queue.envs?.[env]?.queue ?? queue.queue ?? id, apps });
|
|
50
|
+
}
|
|
51
|
+
return { queues: out, left };
|
|
52
|
+
}
|
|
53
|
+
/** Settings per app for the created queues (`urls` by queue id); apps that take none at start are left for the agent. */
|
|
54
|
+
export function queueEnv(stack, list, urls) {
|
|
55
|
+
const set = {};
|
|
56
|
+
const left = [];
|
|
57
|
+
for (const queue of list)
|
|
58
|
+
for (const { id, env } of queue.apps) {
|
|
59
|
+
const app = stack.apps[id];
|
|
60
|
+
const url = urls[queue.id];
|
|
61
|
+
if (!app || url === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
if (kindOf(app).platforms || app.kind === 'lambda') {
|
|
64
|
+
left.push(`${app.name}: point it at the local ${queue.name} queue (${url}); Koppler cannot pass it at start`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// A Kafka app's settings name its brokers; an SQS app's name the queue, with the SDK's endpoint beside them.
|
|
68
|
+
const kafka = queue.type === 'kafka';
|
|
69
|
+
Object.assign((set[id] ??= {}), kafka ? {} : { AWS_ENDPOINT_URL_SQS: ENDPOINT }, Object.fromEntries(env.map(name => [name, kafka ? KAFKA : url])));
|
|
70
|
+
}
|
|
71
|
+
return { set, left };
|
|
72
|
+
}
|
|
73
|
+
/** Creates the topic on the local broker; returns its name. */
|
|
74
|
+
export function createTopic(name) {
|
|
75
|
+
const create = ['--bootstrap-server', 'localhost:9092', '--create', '--if-not-exists', '--topic', name];
|
|
76
|
+
execFileSync('docker', ['exec', 'koppler-kafka', '/opt/kafka/bin/kafka-topics.sh', ...create], {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
79
|
+
});
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
/** Creates the queue in the running emulator and returns its local URL. */
|
|
83
|
+
export function createQueue(name) {
|
|
84
|
+
const attrs = name.endsWith('.fifo') ? ['--attributes', 'FifoQueue=true,ContentBasedDeduplication=true'] : [];
|
|
85
|
+
const create = ['sqs', 'create-queue', '--queue-name', name, ...attrs, '--query', 'QueueUrl'];
|
|
86
|
+
return execFileSync('aws', ['--endpoint-url', ENDPOINT, '--region', 'eu-west-1', '--output', 'text', ...create], {
|
|
87
|
+
encoding: 'utf8',
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
// The emulator takes any key; the user's own profile and session must not reach it.
|
|
90
|
+
env: {
|
|
91
|
+
...process.env,
|
|
92
|
+
AWS_PROFILE: undefined,
|
|
93
|
+
AWS_SESSION_TOKEN: undefined,
|
|
94
|
+
AWS_ACCESS_KEY_ID: 'test',
|
|
95
|
+
AWS_SECRET_ACCESS_KEY: 'test',
|
|
96
|
+
AWS_PAGER: '',
|
|
97
|
+
},
|
|
98
|
+
}).trim();
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=queues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"queues.js","sourceRoot":"","sources":["../../src/engine/queues.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Remembered code changes ("recipes"): the temporary edit an app needs for a run, kept so it is applied the same way
|
|
2
|
+
// next time instead of worked out again. Shared in stack.json (apps.<id>.localChanges) unless it names something only this
|
|
3
|
+
// Mac has (a home path, a loopback alias, the user name); then it stays in the private config. How each one did here
|
|
4
|
+
// (worked or not, last run) is always private.
|
|
5
|
+
// Usage: koppler recipes [app]
|
|
6
|
+
// koppler recipes --remember <app> --why "<reason>" [--when always|calls:<app>|env:<env>] [--private] [--any-os] from the app's last logged edit
|
|
7
|
+
// koppler recipes --apply <app...> [--env <env>] apply what matches the latest Send (its environment and cables), logged for Stop
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { flagValue } from "../cli/output.js";
|
|
13
|
+
import { fail } from "../failure.js";
|
|
14
|
+
import { readClones, readStack } from "../data.js";
|
|
15
|
+
import { configDir, tempDir } from "../paths.js";
|
|
16
|
+
import { readActions, updateActions } from "./actions.js";
|
|
17
|
+
import { APPROVE, approved } from "./checks.js";
|
|
18
|
+
import { runs } from "./history.js";
|
|
19
|
+
import { saveStack } from "./stack-file.js";
|
|
20
|
+
import { readJson, writeJson } from "../json-files.js";
|
|
21
|
+
const privateFile = () => path.join(configDir(), 'recipes.json');
|
|
22
|
+
const writePrivate = (mine) => {
|
|
23
|
+
writeJson(privateFile(), mine);
|
|
24
|
+
};
|
|
25
|
+
export const recipeId = (recipe) => crypto
|
|
26
|
+
.createHash('sha256')
|
|
27
|
+
.update(JSON.stringify([recipe.app, recipe.file, recipe.find]))
|
|
28
|
+
.digest('hex')
|
|
29
|
+
.slice(0, 12);
|
|
30
|
+
/** Something only this Mac has: a home folder, a drive path, a loopback alias other than 127.0.0.1, or the user name. */
|
|
31
|
+
export const onlyHere = (text, user = os.userInfo().username, home = os.homedir()) => [home, `/Users/${user}`, `/home/${user}`].some(place => text.includes(place)) ||
|
|
32
|
+
/(^|[\s"'=(])~\//.test(text) ||
|
|
33
|
+
/\b[A-Z]:\\\\/.test(text) ||
|
|
34
|
+
/\b127\.0\.0\.(?!1\b)\d+\b/.test(text);
|
|
35
|
+
const whenOf = (given) => {
|
|
36
|
+
if (given?.startsWith('calls:'))
|
|
37
|
+
return { calls: given.slice(6) };
|
|
38
|
+
if (given?.startsWith('env:'))
|
|
39
|
+
return { env: given.slice(4) };
|
|
40
|
+
return { always: true };
|
|
41
|
+
};
|
|
42
|
+
/** A recipe is for the operating system it was made on (darwin, linux, win32) unless it was widened to any. */
|
|
43
|
+
export const matches = (recipe, { env, calls = [], platform = process.platform }) => {
|
|
44
|
+
const { when } = recipe;
|
|
45
|
+
const here = !recipe.os || recipe.os === 'any' || recipe.os === platform;
|
|
46
|
+
return here && !!(when?.always || (when?.env && when.env === env) || (when?.calls && calls.includes(when.calls)));
|
|
47
|
+
};
|
|
48
|
+
/** Every recipe for an app: the stack's, then this Mac's, each with how it did here. */
|
|
49
|
+
export function recipesFor(stack, app) {
|
|
50
|
+
const mine = readJson(privateFile(), {});
|
|
51
|
+
const shared = (stack?.apps?.[app]?.localChanges ?? []);
|
|
52
|
+
const own = mine.stacks?.[stack?.source ?? '']?.[app] ?? [];
|
|
53
|
+
const scoped = [
|
|
54
|
+
...shared.map(recipe => ({ ...recipe, app, scope: 'shared' })),
|
|
55
|
+
...own.map(recipe => ({ ...recipe, app, scope: 'private' })),
|
|
56
|
+
];
|
|
57
|
+
return scoped.map(recipe => {
|
|
58
|
+
const id = recipeId(recipe);
|
|
59
|
+
return { ...recipe, id, here: mine.runs?.[id] ?? null };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function remember({ registry: stack, app, edit, why, whenText, forcePrivate, anyOs, }) {
|
|
63
|
+
if (!edit)
|
|
64
|
+
return { error: `no logged edit for ${app} to remember (the agent logs each edit in temp/actions.json first)` };
|
|
65
|
+
const inRepo = path.isAbsolute(edit.file) && edit.repo ? path.relative(edit.repo, edit.file) : edit.file;
|
|
66
|
+
const unix = (text) => text.replace(/\r\n/g, '\n');
|
|
67
|
+
const recipe = {
|
|
68
|
+
why: (why ?? '').slice(0, 200) || 'needed to run locally',
|
|
69
|
+
when: whenOf(whenText),
|
|
70
|
+
os: anyOs ? 'any' : process.platform,
|
|
71
|
+
file: inRepo.split(path.sep).join('/'),
|
|
72
|
+
find: unix(edit.before),
|
|
73
|
+
replace: unix(edit.after),
|
|
74
|
+
};
|
|
75
|
+
const id = recipeId({ ...recipe, app });
|
|
76
|
+
const add = (list) => {
|
|
77
|
+
if (!list.some(known => recipeId({ ...known, app }) === id))
|
|
78
|
+
list.push(recipe);
|
|
79
|
+
};
|
|
80
|
+
const source = stack?.source;
|
|
81
|
+
if (forcePrivate || onlyHere(JSON.stringify(recipe)) || !source) {
|
|
82
|
+
const mine = readJson(privateFile(), {});
|
|
83
|
+
add((((mine.stacks ??= {})[source ?? 'none'] ??= {})[app] ??= []));
|
|
84
|
+
writePrivate(mine);
|
|
85
|
+
return { id, scope: 'private' };
|
|
86
|
+
}
|
|
87
|
+
const file = readJson(source, null);
|
|
88
|
+
const shared = file?.apps?.[app];
|
|
89
|
+
if (!shared)
|
|
90
|
+
return { error: `${app} is not in ${source}` };
|
|
91
|
+
const before = readJson(source, null);
|
|
92
|
+
add((shared.localChanges ??= []));
|
|
93
|
+
const refused = saveStack(source, before, file);
|
|
94
|
+
if (refused)
|
|
95
|
+
return refused;
|
|
96
|
+
return { id, scope: 'shared' };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A remembered change only ever edits a file inside the app's clone, whatever the shared stack says: no absolute path,
|
|
100
|
+
* no .., and no symlink that leads out. Returns the full path, or null.
|
|
101
|
+
*/
|
|
102
|
+
export function insideClone(repo, rel) {
|
|
103
|
+
if (typeof rel !== 'string' || !rel || path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..'))
|
|
104
|
+
return null;
|
|
105
|
+
let root;
|
|
106
|
+
try {
|
|
107
|
+
root = fs.realpathSync(repo);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
let full = path.join(root, rel);
|
|
113
|
+
try {
|
|
114
|
+
full = fs.realpathSync(full);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Not there yet: its own path is as far as it resolves.
|
|
118
|
+
}
|
|
119
|
+
return full.startsWith(root + path.sep) ? full : null;
|
|
120
|
+
}
|
|
121
|
+
/** Applies each matching recipe once: the original text must be in the file exactly once, else it is reported, never guessed. */
|
|
122
|
+
export function prepare({ registry: stack, apps, env, calls, repoOf, logFile, }) {
|
|
123
|
+
const log = readActions(logFile);
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const app of apps)
|
|
126
|
+
for (const recipe of recipesFor(stack, app).filter(one => matches(one, { env, calls: calls[app] ?? [] }))) {
|
|
127
|
+
const { id, why } = recipe;
|
|
128
|
+
const say = (state, detail) => out.push({ app, id, state, why, ...(detail && { detail }) });
|
|
129
|
+
const repo = repoOf(app);
|
|
130
|
+
const file = repo ? insideClone(repo, recipe.file) : undefined;
|
|
131
|
+
if (log.some(action => action.kind === 'edit' && action.recipe === id)) {
|
|
132
|
+
say('already');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (repo && !file) {
|
|
136
|
+
say('refused', `${recipe.file} is not a file inside ${app}'s clone`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let body;
|
|
140
|
+
try {
|
|
141
|
+
body = fs.readFileSync(file, 'utf8');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
say('missing', `${recipe.file} is not in the clone`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Kept with \n line endings; a file with Windows endings is matched in its own.
|
|
148
|
+
const eol = body.includes('\r\n') ? '\r\n' : '\n';
|
|
149
|
+
const find = recipe.find.replace(/\n/g, eol);
|
|
150
|
+
const replace = recipe.replace.replace(/\n/g, eol);
|
|
151
|
+
const count = body.split(find).length - 1;
|
|
152
|
+
if (body.includes(replace)) {
|
|
153
|
+
say('already');
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (count !== 1) {
|
|
157
|
+
say('moved', `${recipe.file}: the original text is ${count ? 'there more than once' : 'no longer there'}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Logged before the file changes, so a crash part-way still leaves Stop an entry to undo.
|
|
161
|
+
const entry = { kind: 'edit', app, file: file, before: find, after: replace, recipe: id };
|
|
162
|
+
updateActions(logFile, list => {
|
|
163
|
+
list.push(entry);
|
|
164
|
+
});
|
|
165
|
+
log.push(entry);
|
|
166
|
+
fs.writeFileSync(file, body.replace(find, () => replace));
|
|
167
|
+
out.push({ app, id, state: 'applied', why, scope: recipe.scope });
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/** After a run: each recipe it applied worked or not on this Mac, and when. */
|
|
172
|
+
export function markRuns(logFile, ok) {
|
|
173
|
+
const ids = readJson(logFile, []).flatMap(action => (action.recipe ? [action.recipe] : []));
|
|
174
|
+
if (!ids.length)
|
|
175
|
+
return;
|
|
176
|
+
const mine = readJson(privateFile(), {});
|
|
177
|
+
const at = new Date().toISOString();
|
|
178
|
+
for (const id of ids)
|
|
179
|
+
(mine.runs ??= {})[id] = { at, ok };
|
|
180
|
+
writePrivate(mine);
|
|
181
|
+
}
|
|
182
|
+
const OUTCOME = {
|
|
183
|
+
applied: 'applied',
|
|
184
|
+
already: 'already in place',
|
|
185
|
+
moved: 'NOT applied, the code moved',
|
|
186
|
+
missing: 'NOT applied, file missing',
|
|
187
|
+
refused: 'NOT applied, the file is outside the clone',
|
|
188
|
+
};
|
|
189
|
+
const REMEMBER = 'usage: koppler recipes --remember <app> --why "<reason>" [--when always|calls:<app>|env:<env>] [--private]';
|
|
190
|
+
export function recipesCommand(command, args) {
|
|
191
|
+
const stack = readStack() ?? fail('no stack loaded (koppler init)');
|
|
192
|
+
const logFile = path.join(tempDir(), 'actions.json');
|
|
193
|
+
const ids = args.filter((arg, at) => !arg.startsWith('--') && !args[at - 1]?.startsWith('--'));
|
|
194
|
+
if (command === 'recipes') {
|
|
195
|
+
for (const app of ids.length ? ids : Object.keys(stack.apps))
|
|
196
|
+
for (const recipe of recipesFor(stack, app)) {
|
|
197
|
+
const { here, when } = recipe;
|
|
198
|
+
const did = here
|
|
199
|
+
? `${here.ok ? 'works on my machine' : 'failed here'}, last run ${here.at.slice(0, 10)}`
|
|
200
|
+
: 'not run here yet';
|
|
201
|
+
const applies = when?.always
|
|
202
|
+
? 'always'
|
|
203
|
+
: when?.calls
|
|
204
|
+
? `when it calls local ${when.calls}`
|
|
205
|
+
: `on ${String(when?.env)}`;
|
|
206
|
+
console.log(`${app} ${recipe.file} (${recipe.scope}, ${did})\n ${String(recipe.why)}; ${applies}`);
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (command === 'remember') {
|
|
211
|
+
const app = ids[0] ?? fail(REMEMBER);
|
|
212
|
+
const logged = readJson(logFile, []);
|
|
213
|
+
const edit = logged.findLast((action) => action.kind === 'edit' && action.app === app && !action.recipe);
|
|
214
|
+
const remembered = remember({
|
|
215
|
+
registry: stack,
|
|
216
|
+
app,
|
|
217
|
+
edit: edit && { ...edit, repo: readClones()[app]?.path },
|
|
218
|
+
why: flagValue(args, '--why'),
|
|
219
|
+
whenText: flagValue(args, '--when'),
|
|
220
|
+
forcePrivate: args.includes('--private'),
|
|
221
|
+
anyOs: args.includes('--any-os'),
|
|
222
|
+
});
|
|
223
|
+
if (remembered.error !== undefined)
|
|
224
|
+
fail(remembered.error);
|
|
225
|
+
const where = remembered.scope === 'shared' ? 'in the stack, for the team: commit stack.json' : 'on this Mac only';
|
|
226
|
+
console.log(`Remembered for ${app} (${where}); id ${remembered.id}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
// The board the user sent: its environment and which local apps each app calls.
|
|
230
|
+
const run = runs().findLast(one => one.kind === 'run');
|
|
231
|
+
const env = flagValue(args, '--env') ?? run?.env ?? stack.envs[0] ?? '';
|
|
232
|
+
const edges = run?.edges ?? [];
|
|
233
|
+
const calls = Object.fromEntries(ids.map(app => [app, edges.filter(edge => edge.from === app).map(edge => edge.to)]));
|
|
234
|
+
const clones = readClones();
|
|
235
|
+
// A code change the stack remembers is the stack's own doing: applied only once the user allowed it.
|
|
236
|
+
const allowed = approved(stack.source ?? '', stack);
|
|
237
|
+
const held = allowed ? [] : ids.filter(id => stack.apps[id]?.localChanges?.length);
|
|
238
|
+
if (held.length)
|
|
239
|
+
console.log(`${held.join(', ')}: the stack's code changes were not applied; the user has to allow them first, in their own terminal: ${APPROVE}`);
|
|
240
|
+
const own = Object.fromEntries(Object.entries(stack.apps).map(([id, app]) => [id, { ...app, localChanges: [] }]));
|
|
241
|
+
const registry = allowed ? stack : { ...stack, apps: own };
|
|
242
|
+
const results = prepare({ registry, apps: ids, env, calls, repoOf: app => clones[app]?.path, logFile });
|
|
243
|
+
if (!results.length)
|
|
244
|
+
console.log('No remembered changes for these apps.');
|
|
245
|
+
for (const result of results)
|
|
246
|
+
console.log(`${result.app}: ${OUTCOME[result.state]}: ${String(result.why)}${result.detail ? ` (${result.detail})` : ''}`);
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=recipes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipes.js","sourceRoot":"","sources":["../../src/engine/recipes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `koppler reload <app> [--restart]`: hot reload a Flutter app Koppler runs, through the signals `flutter run` takes.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { positional } from "../cli/output.js";
|
|
4
|
+
import { fail, Failure } from "../failure.js";
|
|
5
|
+
import { flutterPid } from "./app-start.js";
|
|
6
|
+
import { NAME } from "./processes.js";
|
|
7
|
+
/** Sends hot reload (or with `restart`, hot restart) to the app's `flutter run`; says what it did. */
|
|
8
|
+
export function hotReload(app, restart) {
|
|
9
|
+
if (!NAME.test(app))
|
|
10
|
+
throw new Failure(`${app} is not an app`);
|
|
11
|
+
let pid;
|
|
12
|
+
try {
|
|
13
|
+
pid = Number(fs.readFileSync(flutterPid(app), 'utf8').trim());
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Failure(`${app} is not a Flutter app Koppler runs: start it with Koppler (Started by Koppler) to reload it`);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, restart ? 'SIGUSR2' : 'SIGUSR1');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Failure(`${app} no longer runs`);
|
|
23
|
+
}
|
|
24
|
+
return `${restart ? 'Hot restarted' : 'Hot reloaded'} ${app}`;
|
|
25
|
+
}
|
|
26
|
+
export function reloadCommand(args) {
|
|
27
|
+
const [app] = positional(args);
|
|
28
|
+
if (!app)
|
|
29
|
+
fail('usage: koppler reload <app> [--restart]');
|
|
30
|
+
console.log(hotReload(app, args.includes('--restart')));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=reload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reload.js","sourceRoot":"","sources":["../../src/engine/reload.ts"],"names":[],"mappings":"AAAA,sHAAsH;AACtH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEtC,sGAAsG;AACtG,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,OAAgB;IACrD,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,MAAM,IAAI,OAAO,CAAC,GAAG,GAAG,gBAAgB,CAAC,CAAC;IAC/D,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,OAAO,CACf,GAAG,GAAG,6FAA6F,CACpG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,OAAO,CAAC,GAAG,GAAG,iBAAiB,CAAC,CAAC;IAC7C,CAAC;IACD,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAc;IAC1C,MAAM,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,CAAC,GAAG;QAAE,IAAI,CAAC,yCAAyC,CAAC,CAAC;IAC1D,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Finds the git clones on this Mac (any host) and the stacks among them, and which clones could join a stack.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { writeJson } from "../json-files.js";
|
|
6
|
+
import { detectKind } from "./investigate.js";
|
|
7
|
+
import { INDEX, tilde } from "./config.js";
|
|
8
|
+
const SKIP = new Set([
|
|
9
|
+
'node_modules',
|
|
10
|
+
'Library',
|
|
11
|
+
'Applications',
|
|
12
|
+
'Pictures',
|
|
13
|
+
'Music',
|
|
14
|
+
'Movies',
|
|
15
|
+
'.worktrees',
|
|
16
|
+
'bin',
|
|
17
|
+
'obj',
|
|
18
|
+
'build',
|
|
19
|
+
'dist',
|
|
20
|
+
'.dart_tool',
|
|
21
|
+
'Pods',
|
|
22
|
+
'vendor',
|
|
23
|
+
'target',
|
|
24
|
+
]);
|
|
25
|
+
export const DEFAULT_ROOTS = [
|
|
26
|
+
'~/development',
|
|
27
|
+
'~/dev',
|
|
28
|
+
'~/src',
|
|
29
|
+
'~/code',
|
|
30
|
+
'~/projects',
|
|
31
|
+
'~/workspace',
|
|
32
|
+
'~/repos',
|
|
33
|
+
'~/git',
|
|
34
|
+
'~/github',
|
|
35
|
+
];
|
|
36
|
+
export const DEFAULT_DEPTH = 4;
|
|
37
|
+
/** One key per remote, `host/path`, for any git host: git@github.com:a/b.git and https://github.com/a/b give the same key. */
|
|
38
|
+
export function remoteKey(given) {
|
|
39
|
+
const url = given?.trim();
|
|
40
|
+
if (!url || /^file:/i.test(url))
|
|
41
|
+
return null;
|
|
42
|
+
let host;
|
|
43
|
+
let repoPath;
|
|
44
|
+
const scp = /^(?:[^@/]+@)?([^:/]+):(?!\/)(.+)$/.exec(url);
|
|
45
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) {
|
|
46
|
+
let parsed;
|
|
47
|
+
try {
|
|
48
|
+
parsed = new URL(url);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
host = parsed.hostname;
|
|
54
|
+
// A stray % (not an escape) is kept as written.
|
|
55
|
+
try {
|
|
56
|
+
repoPath = decodeURIComponent(parsed.pathname);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
repoPath = parsed.pathname;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (scp)
|
|
63
|
+
[, host = '', repoPath = ''] = scp;
|
|
64
|
+
else
|
|
65
|
+
return null;
|
|
66
|
+
host = host.toLowerCase();
|
|
67
|
+
repoPath = repoPath
|
|
68
|
+
.replace(/^\/+|\/+$/g, '')
|
|
69
|
+
.replace(/\.git$/i, '')
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
// Azure DevOps spells the same repo three ways.
|
|
72
|
+
if (host === 'ssh.dev.azure.com') {
|
|
73
|
+
host = 'dev.azure.com';
|
|
74
|
+
repoPath = repoPath.replace(/^v3\//, '');
|
|
75
|
+
}
|
|
76
|
+
const account = /^([^.]+)\.visualstudio\.com$/.exec(host)?.[1];
|
|
77
|
+
if (account) {
|
|
78
|
+
host = 'dev.azure.com';
|
|
79
|
+
repoPath = `${account}/${repoPath}`;
|
|
80
|
+
}
|
|
81
|
+
if (host === 'dev.azure.com')
|
|
82
|
+
repoPath = repoPath.replace('/_git/', '/');
|
|
83
|
+
return repoPath.includes('/') ? `${host}/${repoPath}` : null;
|
|
84
|
+
}
|
|
85
|
+
/** A stack slug without a host (`owner/repo`) matches that path on any host; with a host it must match exactly. */
|
|
86
|
+
export function slugMatches(slug, key) {
|
|
87
|
+
if (!slug)
|
|
88
|
+
return false;
|
|
89
|
+
const wanted = slug.toLowerCase().replace(/\.git$/, '');
|
|
90
|
+
const parts = wanted.split('/');
|
|
91
|
+
return parts[0]?.includes('.')
|
|
92
|
+
? key === wanted
|
|
93
|
+
: key.endsWith('/' + wanted) && key.split('/').length === parts.length + 1;
|
|
94
|
+
}
|
|
95
|
+
function remotesOf(gitDir) {
|
|
96
|
+
let config;
|
|
97
|
+
try {
|
|
98
|
+
config = fs.readFileSync(path.join(gitDir, 'config'), 'utf8');
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
return [...config.matchAll(/^\s*url\s*=\s*(\S+)/gm)].flatMap(match => remoteKey(match[1]) ?? []);
|
|
104
|
+
}
|
|
105
|
+
export function findRepos(dir, maxDepth = DEFAULT_DEPTH, depth = 0, found = []) {
|
|
106
|
+
let entries;
|
|
107
|
+
try {
|
|
108
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
// Only real clones (.git directory); worktrees have a .git file and are skipped on purpose.
|
|
114
|
+
if (entries.some(entry => entry.name === '.git' && entry.isDirectory())) {
|
|
115
|
+
const keys = [...new Set(remotesOf(path.join(dir, '.git')))];
|
|
116
|
+
if (keys.length)
|
|
117
|
+
found.push({ path: dir, keys });
|
|
118
|
+
}
|
|
119
|
+
if (depth < maxDepth)
|
|
120
|
+
for (const entry of entries)
|
|
121
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && !SKIP.has(entry.name))
|
|
122
|
+
findRepos(path.join(dir, entry.name), maxDepth, depth + 1, found);
|
|
123
|
+
return found;
|
|
124
|
+
}
|
|
125
|
+
export function findStacks(repos) {
|
|
126
|
+
const stacks = [];
|
|
127
|
+
for (const { path: repo } of repos) {
|
|
128
|
+
if (fs.existsSync(path.join(repo, '.koppler', 'stack.json')))
|
|
129
|
+
stacks.push(path.join(repo, '.koppler'));
|
|
130
|
+
let dirs = [];
|
|
131
|
+
try {
|
|
132
|
+
dirs = fs.readdirSync(path.join(repo, 'stacks'), { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// No stacks folder in this repo.
|
|
136
|
+
}
|
|
137
|
+
for (const dir of dirs)
|
|
138
|
+
if (dir.isDirectory() && fs.existsSync(path.join(repo, 'stacks', dir.name, 'stack.json')))
|
|
139
|
+
stacks.push(path.join(repo, 'stacks', dir.name));
|
|
140
|
+
}
|
|
141
|
+
return stacks;
|
|
142
|
+
}
|
|
143
|
+
export const git = (cwd, ...args) => execFileSync('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
144
|
+
const MAX_BRANCHES = 300;
|
|
145
|
+
/** Local and remote-tracking branches, most recently committed first, without remote prefixes or duplicates. */
|
|
146
|
+
export function branchList(refs) {
|
|
147
|
+
const seen = new Set();
|
|
148
|
+
for (const ref of refs.split('\n')) {
|
|
149
|
+
const name = ref
|
|
150
|
+
.trim()
|
|
151
|
+
.replace(/^refs\/heads\//, '')
|
|
152
|
+
.replace(/^refs\/remotes\/[^/]+\//, '');
|
|
153
|
+
if (name && name !== 'HEAD')
|
|
154
|
+
seen.add(name);
|
|
155
|
+
}
|
|
156
|
+
return [...seen].slice(0, MAX_BRANCHES);
|
|
157
|
+
}
|
|
158
|
+
export const branchesOf = (repo) => {
|
|
159
|
+
try {
|
|
160
|
+
return branchList(git(repo, 'for-each-ref', '--sort=-committerdate', '--format=%(refname)', 'refs/heads', 'refs/remotes'));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
export function scan(roots, depth) {
|
|
167
|
+
const started = Date.now();
|
|
168
|
+
const repos = roots.flatMap(root => findRepos(root, depth));
|
|
169
|
+
const index = { at: new Date().toISOString(), roots, depth, repos };
|
|
170
|
+
writeJson(INDEX, index);
|
|
171
|
+
const hosts = {};
|
|
172
|
+
for (const repo of repos)
|
|
173
|
+
for (const host of new Set(repo.keys.map(key => key.split('/')[0] ?? '')))
|
|
174
|
+
hosts[host] = (hosts[host] ?? 0) + 1;
|
|
175
|
+
const byHost = Object.entries(hosts)
|
|
176
|
+
.sort((a, b) => b[1] - a[1])
|
|
177
|
+
.map(([host, count]) => `${host} ${String(count)}`);
|
|
178
|
+
const took = ((Date.now() - started) / 1000).toFixed(1);
|
|
179
|
+
console.log(`REPOS ${String(repos.length)} in ${roots.map(tilde).join(', ')} (${byHost.join(', ') || 'no remotes'}) in ${took}s`);
|
|
180
|
+
return index;
|
|
181
|
+
}
|
|
182
|
+
const slugOfKey = (key) => (key.startsWith('github.com/') ? key.slice('github.com/'.length) : key);
|
|
183
|
+
const WORDS = { api: 'API', ui: 'UI', sdk: 'SDK', db: 'DB', cdr: 'CDR', id: 'ID' };
|
|
184
|
+
export const nameFromRepo = (repo) => repo
|
|
185
|
+
.split(/[-_.]+/)
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.map(word => WORDS[word.toLowerCase()] ?? word.charAt(0).toUpperCase() + word.slice(1))
|
|
188
|
+
.join(' ');
|
|
189
|
+
/** Clones that could join the stack, for the picker on the canvas: where each is, what kind it looks like, and whether it is in already. */
|
|
190
|
+
export function candidateRepos(index, stack, stackDir) {
|
|
191
|
+
const seen = new Set();
|
|
192
|
+
const stackPath = path.resolve(stackDir ?? '/nowhere');
|
|
193
|
+
return index.repos
|
|
194
|
+
.filter(repo => fs.existsSync(repo.path) && !stackPath.startsWith(path.resolve(repo.path) + path.sep))
|
|
195
|
+
.flatMap((repo) => {
|
|
196
|
+
const slug = slugOfKey(repo.keys[0] ?? '');
|
|
197
|
+
if (seen.has(slug))
|
|
198
|
+
return [];
|
|
199
|
+
seen.add(slug);
|
|
200
|
+
const inStack = Object.entries(stack.apps ?? {}).find(([, app]) => repo.keys.some(key => slugMatches(app.slug, key)))?.[0];
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
slug,
|
|
204
|
+
name: nameFromRepo(slug.split('/').pop() ?? ''),
|
|
205
|
+
kind: detectKind(repo.path, stack),
|
|
206
|
+
path: tilde(repo.path),
|
|
207
|
+
folder: tilde(path.dirname(repo.path)),
|
|
208
|
+
...(inStack && { inStack }),
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
})
|
|
212
|
+
.sort((a, b) => a.folder.localeCompare(b.folder) || a.name.localeCompare(b.name));
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=repos.js.map
|