conductor-remote 1.16.0 → 1.17.0
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/README.md +1 -6
- package/dist/assets/index-CFZawBc8.css +1 -0
- package/dist/assets/index-CFyDd-9V.js +41 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/src/merge.js +115 -0
- package/dist-node/src/server.js +13 -0
- package/package.json +1 -1
- package/dist/assets/index-CbgaQzcX.js +0 -40
- package/dist/assets/index-DLTj56Tj.css +0 -1
package/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
12
12
|
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
|
13
13
|
<title>Conductor Remote</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-CFyDd-9V.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CFZawBc8.css">
|
|
16
16
|
<link rel="manifest" href="/manifest.webmanifest"></head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/dist/sw.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(i[r])return;let
|
|
1
|
+
if(!self.define){let e,i={};const n=(n,s)=>(n=new URL(n+".js",s).href,i[n]||new Promise(i=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=i,document.head.appendChild(e)}else e=n,importScripts(n),i()}).then(()=>{let e=i[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(s,o)=>{const r=e||("document"in self?document.currentScript.src:"")||location.href;if(i[r])return;let c={};const t=e=>n(e,r),l={module:{uri:r},exports:c,require:t};i[r]=Promise.all(s.map(e=>l[e]||t(e))).then(e=>(o(...e),c))}}define(["./workbox-9c191d2f"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"index.html",revision:"8cad4855e591d94059cbf97bab27d3f0"},{url:"assets/workbox-window.prod.es5-BBnX5xw4.js",revision:null},{url:"assets/index-CFyDd-9V.js",revision:null},{url:"assets/index-CFZawBc8.css",revision:null},{url:"apple-touch-icon.png",revision:"2b9301416b880d45d4bb655f2600d1f2"},{url:"icon-192.png",revision:"c5e01ac58768627e18ee7b8b6a9239ef"},{url:"icon-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon-maskable-512.png",revision:"a40638c55e310312457a621c9a0002c8"},{url:"icon.svg",revision:"c1aee186821798733dd477e69a0ef243"},{url:"manifest.webmanifest",revision:"cf88fbc5755108a7fe0616fa160a8a15"}],{}),e.cleanupOutdatedCaches(),e.registerRoute(new e.NavigationRoute(e.createHandlerBoundToURL("/index.html"),{denylist:[/^\/api\//]}))});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
async function git(cwd, args) {
|
|
5
|
+
const { stdout } = await exec('git', ['-C', cwd, ...args], {
|
|
6
|
+
encoding: 'utf8',
|
|
7
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
8
|
+
timeout: 20_000
|
|
9
|
+
});
|
|
10
|
+
return stdout;
|
|
11
|
+
}
|
|
12
|
+
/** Count of uncommitted (tracked + untracked) entries in a checkout. */
|
|
13
|
+
async function countDirty(cwd) {
|
|
14
|
+
const out = await git(cwd, ['status', '--porcelain']).catch(() => '');
|
|
15
|
+
return out.split('\n').filter(Boolean).length;
|
|
16
|
+
}
|
|
17
|
+
/** Commits on `branch` not reachable from `base`. */
|
|
18
|
+
async function countAhead(root, base, branch) {
|
|
19
|
+
const out = await git(root, ['rev-list', '--count', `${base}..${branch}`]).catch(() => '0');
|
|
20
|
+
const n = Number(out.trim());
|
|
21
|
+
return Number.isFinite(n) ? n : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Would `git merge <branch>` into `base` conflict? `merge-tree --write-tree`
|
|
25
|
+
* merges in memory (touches nothing) and exits 1 on conflict, 0 when clean. Any
|
|
26
|
+
* other failure → treat as non-conflicting (the real merge will surface it).
|
|
27
|
+
*/
|
|
28
|
+
async function wouldConflict(root, base, branch) {
|
|
29
|
+
try {
|
|
30
|
+
await git(root, ['merge-tree', '--write-tree', base, branch]);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return err.code === 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Everything the POST needs to know before merging — also served to the PWA so
|
|
39
|
+
* the confirm sheet can show the target, commit count, and any blocker up front.
|
|
40
|
+
*/
|
|
41
|
+
export async function mergePrecheck(ws) {
|
|
42
|
+
const branch = ws.branch ?? '';
|
|
43
|
+
const base = ws.baseBranch;
|
|
44
|
+
const root = ws.repo_root;
|
|
45
|
+
const block = (reason, detail, extra = {}) => ({
|
|
46
|
+
base,
|
|
47
|
+
branch,
|
|
48
|
+
canMerge: false,
|
|
49
|
+
reason,
|
|
50
|
+
detail,
|
|
51
|
+
ahead: 0,
|
|
52
|
+
uncommitted: 0,
|
|
53
|
+
...extra
|
|
54
|
+
});
|
|
55
|
+
if (!branch)
|
|
56
|
+
return block('no-branch', 'workspace has no branch');
|
|
57
|
+
if (!root)
|
|
58
|
+
return block('no-repo', 'repo root unresolved');
|
|
59
|
+
const uncommitted = ws.worktree ? await countDirty(ws.worktree) : 0;
|
|
60
|
+
// The primary checkout must be on the base branch — we never switch Conductor's
|
|
61
|
+
// checkout out from under it; refuse and tell the user to check it out instead.
|
|
62
|
+
const head = (await git(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => '')).trim();
|
|
63
|
+
if (head && head !== base)
|
|
64
|
+
return block('not-on-base', `the ${ws.repo_name ?? 'repo'} checkout is on '${head}', not '${base}'`, {
|
|
65
|
+
uncommitted,
|
|
66
|
+
headBranch: head
|
|
67
|
+
});
|
|
68
|
+
if (await countDirty(root))
|
|
69
|
+
return block('dirty-base', `the '${base}' checkout has uncommitted changes`, { uncommitted });
|
|
70
|
+
const ahead = await countAhead(root, base, branch);
|
|
71
|
+
if (ahead === 0)
|
|
72
|
+
return block('nothing-to-merge', `nothing new to merge into '${base}'`, { uncommitted });
|
|
73
|
+
if (await wouldConflict(root, base, branch))
|
|
74
|
+
return {
|
|
75
|
+
base,
|
|
76
|
+
branch,
|
|
77
|
+
canMerge: false,
|
|
78
|
+
reason: 'conflicts',
|
|
79
|
+
detail: `merging into '${base}' would conflict`,
|
|
80
|
+
ahead,
|
|
81
|
+
uncommitted
|
|
82
|
+
};
|
|
83
|
+
return { base, branch, canMerge: true, reason: 'ok', ahead, uncommitted };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Perform the merge, faithful to Conductor: a local `git merge --no-edit <branch>`
|
|
87
|
+
* into the base branch in the primary checkout. Re-runs the precheck as the
|
|
88
|
+
* authoritative gate (the PWA's copy is advisory and may be stale), and aborts a
|
|
89
|
+
* conflicting merge so the checkout is never left half-merged.
|
|
90
|
+
*/
|
|
91
|
+
export async function mergeWorkspace(ws) {
|
|
92
|
+
const pre = await mergePrecheck(ws);
|
|
93
|
+
if (!pre.canMerge)
|
|
94
|
+
return { ok: false, base: pre.base, branch: pre.branch, error: pre.detail, reason: pre.reason };
|
|
95
|
+
const root = ws.repo_root;
|
|
96
|
+
try {
|
|
97
|
+
const summary = await git(root, ['merge', '--no-edit', pre.branch]);
|
|
98
|
+
return { ok: true, base: pre.base, branch: pre.branch, summary: summary.trim() };
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
const stderr = err.stderr;
|
|
102
|
+
const message = (stderr || (err instanceof Error ? err.message : String(err))).trim();
|
|
103
|
+
const conflicted = /conflict/i.test(message);
|
|
104
|
+
// Leave the primary checkout clean rather than stuck mid-merge.
|
|
105
|
+
if (conflicted)
|
|
106
|
+
await git(root, ['merge', '--abort']).catch(() => undefined);
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
base: pre.base,
|
|
110
|
+
branch: pre.branch,
|
|
111
|
+
error: message,
|
|
112
|
+
reason: conflicted ? 'conflicts' : 'error'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist-node/src/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { startAutoUpdate, updateStatus } from "./autoupdate.js";
|
|
|
7
7
|
import { loadConfig } from "./config.js";
|
|
8
8
|
import { ConductorDb } from "./db.js";
|
|
9
9
|
import { workspaceDiff } from "./git.js";
|
|
10
|
+
import { mergePrecheck, mergeWorkspace } from "./merge.js";
|
|
10
11
|
import { attachPrStatus } from "./pr.js";
|
|
11
12
|
import { Reads } from "./reads.js";
|
|
12
13
|
import { describeActuator, newChat, pickActuator } from "./writes.js";
|
|
@@ -172,6 +173,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
172
173
|
const diff = await workspaceDiff(ws.worktree, ws.baseBranch);
|
|
173
174
|
return json(req, res, 200, diff);
|
|
174
175
|
}
|
|
176
|
+
// GET /api/workspaces/:id/merge — precheck: can this branch merge into its base?
|
|
177
|
+
// POST /api/workspaces/:id/merge — do it (local `git merge`, mirroring Conductor’s merge button)
|
|
178
|
+
m = pathname.match(/^\/api\/workspaces\/([^/]+)\/merge$/);
|
|
179
|
+
if ((req.method === 'GET' || req.method === 'POST') && m) {
|
|
180
|
+
const ws = reads.getWorkspace(decodeURIComponent(m[1]));
|
|
181
|
+
if (!ws)
|
|
182
|
+
return json(req, res, 404, { error: 'workspace not found' });
|
|
183
|
+
if (req.method === 'GET')
|
|
184
|
+
return json(req, res, 200, await mergePrecheck(ws));
|
|
185
|
+
const result = await mergeWorkspace(ws);
|
|
186
|
+
return json(req, res, result.ok ? 200 : 409, result);
|
|
187
|
+
}
|
|
175
188
|
// GET /api/sessions/:id/messages?after=<rowid>
|
|
176
189
|
m = pathname.match(/^\/api\/sessions\/([^/]+)\/messages$/);
|
|
177
190
|
if (req.method === 'GET' && m) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|