breakaway 1.4.0-main.50 → 1.4.0-main.52
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/package.json +1 -1
- package/scripts/tasks/github-connect.js +50 -0
- package/scripts/tasks.mjs +23 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.4.0-main.
|
|
3
|
+
"version": "1.4.0-main.52",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Before github-connect trades its code: whether the board already has a GitHub App (CLI-2). Each code comes
|
|
3
|
+
* from a new App on GitHub, so a second run on a board that has one would overwrite the keys of the App that works
|
|
4
|
+
* with a second App's. Read from the board's Connections report (`GET /api/connections`, its `github.app`
|
|
5
|
+
* row). Pure, so it's tested without a board.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `ok` false stops the command before the code is traded (it still works, within its hour); `note` is what
|
|
10
|
+
* a run that goes ahead says first.
|
|
11
|
+
*
|
|
12
|
+
* @param {{ connections?: { id: string, state: string, detail?: string, at?: string | null, link?: string | null }[] } | null | undefined} report
|
|
13
|
+
* @param {{ replace: boolean }} options
|
|
14
|
+
* @returns {{ ok: boolean, message?: string, note?: string }}
|
|
15
|
+
*/
|
|
16
|
+
export function appInPlace(report, { replace }) {
|
|
17
|
+
const row = Array.isArray(report?.connections) ? report.connections.find((c) => c.id === 'github.app') : undefined;
|
|
18
|
+
if (!row)
|
|
19
|
+
return replace
|
|
20
|
+
? {
|
|
21
|
+
ok: true,
|
|
22
|
+
note: "Couldn't ask the board whether it already has a GitHub App; connecting a new one (--replace).",
|
|
23
|
+
}
|
|
24
|
+
: {
|
|
25
|
+
ok: false,
|
|
26
|
+
message:
|
|
27
|
+
"couldn't ask the board whether it already has a GitHub App, so nothing was changed; the code still works within its hour. Check the board answers (npx breakaway connections), then run this again; or add --replace to connect a new App whatever is there.",
|
|
28
|
+
};
|
|
29
|
+
if (row.state === 'off') return { ok: true };
|
|
30
|
+
const what = `${row.detail}${row.link ? `, ${row.link}` : ''}`;
|
|
31
|
+
const deleteOld = `Once the new one works, delete the old one on GitHub (Settings → Developer settings → GitHub Apps)${row.link ? `: ${row.link}` : ''}.`;
|
|
32
|
+
if (replace) return { ok: true, note: `Replacing the board's GitHub App (${what}). ${deleteOld}` };
|
|
33
|
+
// Checked, and GitHub refused it: it doesn't work, so a new one takes its place.
|
|
34
|
+
if (row.state === 'attention' && row.at)
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
note: `The board's GitHub App doesn't work (${row.detail}); connecting the new one in its place. ${deleteOld}`,
|
|
38
|
+
};
|
|
39
|
+
const kept =
|
|
40
|
+
"Nothing was changed: this code's App would replace that one. If you made the new App by mistake, delete it on GitHub (Settings → Developer settings → GitHub Apps). To switch the board to it, run npx breakaway github-connect <code> --replace within the code's hour, then delete the old one.";
|
|
41
|
+
if (row.state === 'working')
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
message: `the board already has a working GitHub App: ${what}. ${kept}`,
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
message: `the board already has a GitHub App's keys (${what}); press Check now on its Connections view to see whether it works. ${kept}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
package/scripts/tasks.mjs
CHANGED
|
@@ -42,6 +42,7 @@ import { looksLikeSecret } from '../src/ping.js';
|
|
|
42
42
|
import { promptPathOf } from '../src/repos.js';
|
|
43
43
|
import { hookFailure, sessionProxy, routeThroughSessionProxy } from './tasks/proxy.js';
|
|
44
44
|
import { githubFromRemote, inRepo, pickRepo } from './tasks/repo.js';
|
|
45
|
+
import { appInPlace } from './tasks/github-connect.js';
|
|
45
46
|
import { checkInstall } from './tasks/install-check.js';
|
|
46
47
|
import { NO_TERMINAL, ask as askIn } from './tasks/ask.js';
|
|
47
48
|
import {
|
|
@@ -296,10 +297,11 @@ Setup (owner)
|
|
|
296
297
|
setup connect this machine's Taskwarrior (writes taskrc in this machine's folder for the board, with every repository's report and context)
|
|
297
298
|
rotate-sync new client ID and sync secret; the server re-encrypts its history
|
|
298
299
|
rotate-token new API token; every browser is signed out
|
|
299
|
-
github-connect <code> store the GitHub App's keys (the board's GitHub view gives the code)
|
|
300
|
+
github-connect <code> store the GitHub App's keys (the board's GitHub view gives the code); refuses when the board
|
|
301
|
+
already has an App, unless --replace
|
|
300
302
|
agents-connect store the agent routine's URL and token (docs/tasks.md#cloud-agents-from-the-board)
|
|
301
303
|
--repo <slug> connects another repository's routine; --replace drops ones this machine doesn't hold
|
|
302
|
-
init-secrets once, for a brand-new board
|
|
304
|
+
init-secrets once, for a brand-new board; --force starts again, keeping the old file as a .bak
|
|
303
305
|
|
|
304
306
|
Install repository (no board needed: the files and steps that deploy a board from its own repository)
|
|
305
307
|
install init [dir] write an install repository into <dir> (default: here): its config, breakaway.json, the Deploy and
|
|
@@ -1727,6 +1729,12 @@ const commands = {
|
|
|
1727
1729
|
const code = need(args[0], 'code');
|
|
1728
1730
|
// Before the code is traded: it works once, and the keys it gives have to go into the board's own secrets.
|
|
1729
1731
|
await ensureBoardInstall('github-connect');
|
|
1732
|
+
// Each code comes from a new App: on a board that has one, it would replace the working App's keys (CLI-2).
|
|
1733
|
+
const inPlace = appInPlace(await call('GET', 'connections', undefined, { soft: true }), {
|
|
1734
|
+
replace: Boolean(opts.replace),
|
|
1735
|
+
});
|
|
1736
|
+
if (!inPlace.ok) fail(inPlace.message);
|
|
1737
|
+
if (inPlace.note) console.log(inPlace.note);
|
|
1730
1738
|
const res = await fetch(`https://api.github.com/app-manifests/${enc(code)}/conversions`, {
|
|
1731
1739
|
method: 'POST',
|
|
1732
1740
|
headers: {
|
|
@@ -1775,6 +1783,9 @@ const commands = {
|
|
|
1775
1783
|
const install = installConfig();
|
|
1776
1784
|
// A new install has no address until it's deployed (CLD-139).
|
|
1777
1785
|
const known = BOARD.from !== 'default' || install.url !== null;
|
|
1786
|
+
// --force never loses the only copy of the sync secret: the old file is kept the way rotations keep it (CLI-2).
|
|
1787
|
+
const kept = backupEnvFile();
|
|
1788
|
+
if (kept) console.log(`Kept the old one as ${kept} (0600).`);
|
|
1778
1789
|
writePrivate(
|
|
1779
1790
|
ENV_FILE,
|
|
1780
1791
|
envFile({
|
|
@@ -2226,9 +2237,18 @@ function writePrivate(path, content) {
|
|
|
2226
2237
|
chmodSync(path, 0o600);
|
|
2227
2238
|
}
|
|
2228
2239
|
|
|
2240
|
+
/** Moves tasks.env aside as tasks.env.<date>.bak (0600), and says where; null when there's none. */
|
|
2241
|
+
function backupEnvFile() {
|
|
2242
|
+
if (!existsSync(ENV_FILE)) return null;
|
|
2243
|
+
const backup = `${ENV_FILE}.${new Date().toISOString().replace(/[:.]/gu, '-')}.bak`;
|
|
2244
|
+
renameSync(ENV_FILE, backup);
|
|
2245
|
+
chmodSync(backup, 0o600);
|
|
2246
|
+
return backup;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2229
2249
|
/** tasks.env.next becomes tasks.env; the old one is kept as tasks.env.<date>.bak. */
|
|
2230
2250
|
function promoteEnvFile() {
|
|
2231
|
-
|
|
2251
|
+
backupEnvFile();
|
|
2232
2252
|
renameSync(`${ENV_FILE}.next`, ENV_FILE);
|
|
2233
2253
|
}
|
|
2234
2254
|
|