convene-cli 1.8.0 → 1.10.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/dist/api.js +23 -0
- package/dist/binding-local.js +38 -0
- package/dist/binding.js +90 -0
- package/dist/cache.js +85 -0
- package/dist/catalog/catalog.generated.js +27 -2
- package/dist/commands/announce.js +89 -0
- package/dist/commands/auth.js +145 -7
- package/dist/commands/catchup.js +1 -1
- package/dist/commands/feedback.js +88 -0
- package/dist/commands/fetch.js +36 -1
- package/dist/commands/gate-push.js +1 -1
- package/dist/commands/guard.js +1 -1
- package/dist/commands/init.js +27 -4
- package/dist/commands/join.js +1 -1
- package/dist/commands/notify.js +12 -4
- package/dist/commands/offboard.js +35 -1
- package/dist/commands/override.js +1 -1
- package/dist/commands/reclaim.js +88 -0
- package/dist/commands/rotate.js +10 -3
- package/dist/commands/session-start.js +9 -1
- package/dist/commands/update.js +191 -2
- package/dist/commands/watch.js +1 -1
- package/dist/commands/wrap.js +95 -0
- package/dist/config.js +53 -3
- package/dist/ctx.js +8 -3
- package/dist/hook.js +37 -0
- package/dist/index.js +52 -6
- package/dist/protocol.js +3 -2
- package/dist/render.js +63 -0
- package/dist/version.js +24 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.filterFeedback = filterFeedback;
|
|
4
|
+
exports.feedback = feedback;
|
|
5
|
+
/**
|
|
6
|
+
* `convene feedback [id]` — the READ counterpart to `convene suggest`. Lists the
|
|
7
|
+
* feature_feedback (feature requests / bugs / notes) filed for this project, or
|
|
8
|
+
* shows one item by short_id/id.
|
|
9
|
+
*
|
|
10
|
+
* FAIL-OPEN, like `convene lanes`: reading your own backlog must NEVER break a
|
|
11
|
+
* workflow. Any read failure prints a short stderr note and returns (exit 0) —
|
|
12
|
+
* it never dies/throws. (Contrast `convene inbox`, which is die-loud.)
|
|
13
|
+
*
|
|
14
|
+
* The server endpoint already exists (GET /projects/:slug/feedback, member-gated,
|
|
15
|
+
* returns `{ items, role }`); this command is CLI-only. Filters (--status, --mine,
|
|
16
|
+
* by-id) are applied CLIENT-SIDE over the returned items. The item `body` is
|
|
17
|
+
* UNTRUSTED member free-text and only ever reaches the terminal through the
|
|
18
|
+
* inertToken-sanitizing renderers in render.ts — never inlined here.
|
|
19
|
+
*/
|
|
20
|
+
const ctx_1 = require("../ctx");
|
|
21
|
+
const render_1 = require("../render");
|
|
22
|
+
const FEEDBACK_TIMEOUT_MS = 2500; // explicit short timeout — NEVER the 10s default
|
|
23
|
+
/** Apply the client-side filters in a pure, testable way. */
|
|
24
|
+
function filterFeedback(items, opts) {
|
|
25
|
+
let out = items;
|
|
26
|
+
if (opts.id) {
|
|
27
|
+
out = out.filter((f) => f.short_id === opts.id || f.id === opts.id);
|
|
28
|
+
}
|
|
29
|
+
if (opts.status) {
|
|
30
|
+
const want = opts.status.toLowerCase();
|
|
31
|
+
out = out.filter((f) => (f.lifecycle || '').toLowerCase() === want);
|
|
32
|
+
}
|
|
33
|
+
if (opts.mine && opts.member) {
|
|
34
|
+
out = out.filter((f) => f.source_member === opts.member);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
async function feedback(id, opts) {
|
|
39
|
+
try {
|
|
40
|
+
const ctx = (0, ctx_1.getContext)({ project: opts.project });
|
|
41
|
+
const slug = (0, ctx_1.requireSlug)(ctx);
|
|
42
|
+
const res = await ctx.api.listFeedback(slug, {}, FEEDBACK_TIMEOUT_MS);
|
|
43
|
+
if (!res.ok || !res.json) {
|
|
44
|
+
// Fail-open: a backlog read failure is informational, never blocking.
|
|
45
|
+
process.stderr.write(`convene: feedback UNVERIFIED — could not reach the bus (${res.error ?? res.status})\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const all = Array.isArray(res.json.items) ? res.json.items : [];
|
|
49
|
+
const items = filterFeedback(all, {
|
|
50
|
+
id,
|
|
51
|
+
status: opts.status,
|
|
52
|
+
mine: opts.mine,
|
|
53
|
+
member: ctx.member,
|
|
54
|
+
});
|
|
55
|
+
if (opts.json) {
|
|
56
|
+
process.stdout.write(JSON.stringify(items, null, 2) + '\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Single-item show.
|
|
60
|
+
if (id) {
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
process.stdout.write(`no feedback matching "${id}" in ${slug}.\n`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const f of items)
|
|
66
|
+
process.stdout.write((0, render_1.feedbackDetail)(f) + '\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// List view.
|
|
70
|
+
if (items.length === 0) {
|
|
71
|
+
if (opts.status || opts.mine) {
|
|
72
|
+
process.stdout.write(`no feedback matches that filter in ${slug}.\n`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
process.stdout.write(`no feedback filed yet — file one with \`convene suggest "<text>"\`.\n`);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`${items.length} feedback item(s) for ${slug}:\n`);
|
|
80
|
+
for (const f of items)
|
|
81
|
+
process.stdout.write((0, render_1.feedbackLine)(f) + '\n');
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// Defensive: the command must never throw out of fail-open.
|
|
85
|
+
process.stderr.write(`convene: feedback UNVERIFIED — ${err?.message ?? 'unknown error'}\n`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -20,10 +20,12 @@ exports.runFetch = runFetch;
|
|
|
20
20
|
* stale cache and renders DEGRADED (loud-but-non-blocking).
|
|
21
21
|
*/
|
|
22
22
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
23
|
+
const node_child_process_1 = require("node:child_process");
|
|
23
24
|
const git_1 = require("../git");
|
|
24
25
|
const config_1 = require("../config");
|
|
25
26
|
const cache_1 = require("../cache");
|
|
26
27
|
const manifest_1 = require("../catalog/manifest");
|
|
28
|
+
const binding_local_1 = require("../binding-local");
|
|
27
29
|
const api_1 = require("../api");
|
|
28
30
|
const render_1 = require("../render");
|
|
29
31
|
const catchup_1 = require("./catchup");
|
|
@@ -145,6 +147,33 @@ function codexCwdFromStdin() {
|
|
|
145
147
|
function toRenderMessages(arr) {
|
|
146
148
|
return Array.isArray(arr) ? arr : [];
|
|
147
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Front-of-session auto-announce: the FIRST authenticated `fetch` of a session
|
|
152
|
+
* spawns a DETACHED, fire-and-forget `convene announce` so the bus learns who is
|
|
153
|
+
* working on what before any push — the cross-tool keystone (this path runs for
|
|
154
|
+
* Claude Code AND Codex's `fetch --codex-hook`). Detached + unref'd (mirrors
|
|
155
|
+
* session-start's launchWatch) so the prompt hot path is NEVER slowed by a network
|
|
156
|
+
* post. Gated by the cheap local (instance, branch) sentinel so we don't spawn a
|
|
157
|
+
* no-op process every prompt; the announce command re-checks it and carries the
|
|
158
|
+
* authoritative server idempotency-key, so a same-instant double-spawn is harmless.
|
|
159
|
+
* Best-effort: any failure only narrows cross-session visibility, never blocks.
|
|
160
|
+
*/
|
|
161
|
+
function maybeAnnounce(slug, top) {
|
|
162
|
+
try {
|
|
163
|
+
const instance = (0, cache_1.ensureSessionInstance)(slug);
|
|
164
|
+
const branch = (0, git_1.currentBranch)(top) ?? 'detached';
|
|
165
|
+
if ((0, cache_1.announceAlreadyPosted)(slug, instance, branch))
|
|
166
|
+
return; // already announced this (instance, branch)
|
|
167
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [process.argv[1], 'announce'], {
|
|
168
|
+
detached: true,
|
|
169
|
+
stdio: 'ignore',
|
|
170
|
+
});
|
|
171
|
+
child.unref();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* fail-open */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
148
177
|
async function runFetch(opts = {}) {
|
|
149
178
|
// Absolute watchdog: under any circumstance, do not hold the prompt past 6s.
|
|
150
179
|
// unref'd so the timer itself is never a live libuv handle at teardown; it still
|
|
@@ -173,7 +202,7 @@ async function runFetch(opts = {}) {
|
|
|
173
202
|
if (!proj?.slug)
|
|
174
203
|
return done(0); // repo not on the bus → silent no-op
|
|
175
204
|
const slug = proj.slug;
|
|
176
|
-
const cfg = (0, config_1.
|
|
205
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
177
206
|
const lookback = opts.lookback ?? 60;
|
|
178
207
|
const max = opts.max ?? 20;
|
|
179
208
|
const member = cfg.member;
|
|
@@ -191,6 +220,9 @@ async function runFetch(opts = {}) {
|
|
|
191
220
|
}), opts.codexHook);
|
|
192
221
|
return done(0);
|
|
193
222
|
}
|
|
223
|
+
// Authenticated + on the bus: spawn the front-of-session announce (detached,
|
|
224
|
+
// once per session/branch). Fires regardless of which render path runs below.
|
|
225
|
+
maybeAnnounce(slug, top);
|
|
194
226
|
// `--since-last`: render the catch-up digest since the read cursor instead
|
|
195
227
|
// of the time-windowed feed. Read-only (no advance), fail-open. Suppressed if
|
|
196
228
|
// SessionStart already surfaced a catch-up this boot (per-instance sentinel).
|
|
@@ -218,6 +250,9 @@ async function runFetch(opts = {}) {
|
|
|
218
250
|
const nudge = catalogBehindNudge(top);
|
|
219
251
|
if (nudge)
|
|
220
252
|
process.stdout.write(nudge + '\n');
|
|
253
|
+
const drift = (0, binding_local_1.localBindingDrift)(top, proj);
|
|
254
|
+
if (drift)
|
|
255
|
+
process.stdout.write(drift + '\n');
|
|
221
256
|
};
|
|
222
257
|
// Cache short-circuit for rapid successive prompts.
|
|
223
258
|
const cache = (0, cache_1.readCache)(slug);
|
|
@@ -137,7 +137,7 @@ async function run(opts) {
|
|
|
137
137
|
const slug = opts.project || proj?.slug || null;
|
|
138
138
|
if (!slug)
|
|
139
139
|
return 0; // repo not on the bus → no-op (zero network)
|
|
140
|
-
const cfg = (0, config_1.
|
|
140
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
141
141
|
const member = cfg.member;
|
|
142
142
|
const session = member ? (0, git_1.sessionId)(member, top) : null;
|
|
143
143
|
// Determine the ref(s) being pushed.
|
package/dist/commands/guard.js
CHANGED
|
@@ -205,7 +205,7 @@ async function run(opts) {
|
|
|
205
205
|
const slug = opts.project || proj?.slug || null;
|
|
206
206
|
if (!slug)
|
|
207
207
|
return 0; // not on the bus → no-op (covers BOTH match + non-match)
|
|
208
|
-
const cfg = (0, config_1.
|
|
208
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
209
209
|
const member = cfg.member;
|
|
210
210
|
const session = member ? (0, git_1.sessionId)(member, top) : null;
|
|
211
211
|
if (!cfg.apiKey || !session) {
|
package/dist/commands/init.js
CHANGED
|
@@ -7,7 +7,11 @@ exports.AIDER_CONF = exports.CONVENE_PATHS = void 0;
|
|
|
7
7
|
exports.upsertMarkerBlock = upsertMarkerBlock;
|
|
8
8
|
exports.removeMarkerBlock = removeMarkerBlock;
|
|
9
9
|
exports.removeGitignoreGuard = removeGitignoreGuard;
|
|
10
|
+
exports.writeAgentRules = writeAgentRules;
|
|
10
11
|
exports.removeTomlBlock = removeTomlBlock;
|
|
12
|
+
exports.writeMcpConfigs = writeMcpConfigs;
|
|
13
|
+
exports.writeCoordinationBlocks = writeCoordinationBlocks;
|
|
14
|
+
exports.commitConveneFiles = commitConveneFiles;
|
|
11
15
|
exports.refreshDocs = refreshDocs;
|
|
12
16
|
exports.init = init;
|
|
13
17
|
/**
|
|
@@ -275,6 +279,15 @@ const COORD_HOOKS = [
|
|
|
275
279
|
verb: 'beat',
|
|
276
280
|
note: 'debounced session activity-beat so a heads-down session still pulses on the bus',
|
|
277
281
|
},
|
|
282
|
+
// Stop fires at every turn-end (no tool matcher); `convene wrap` is idempotent +
|
|
283
|
+
// debounced via the last-broadcast-sha cursor, so it posts at most one wrap per
|
|
284
|
+
// stretch of new committed work and stays silent on idle turns. Never blocks.
|
|
285
|
+
{
|
|
286
|
+
event: 'Stop',
|
|
287
|
+
command: 'convene wrap',
|
|
288
|
+
verb: 'wrap',
|
|
289
|
+
note: 'session-end wrap status when new committed work landed (idempotent, fail-open)',
|
|
290
|
+
},
|
|
278
291
|
];
|
|
279
292
|
/**
|
|
280
293
|
* Wire the WP13 coordination hooks into a settings file (global or committed
|
|
@@ -609,7 +622,10 @@ async function refreshDocs(opts) {
|
|
|
609
622
|
if (!existing?.slug) {
|
|
610
623
|
(0, ctx_1.die)('this repo is not on Convene yet — run `convene setup` first; `--refresh-docs` only re-renders an already-onboarded repo.');
|
|
611
624
|
}
|
|
612
|
-
|
|
625
|
+
// Honor a committed host pin so a pinned repo re-renders its carriers at the
|
|
626
|
+
// bound host, not the ambient one (resolveConfigForRepo); unpinned repos are
|
|
627
|
+
// unchanged (it falls back to resolveConfig).
|
|
628
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, existing);
|
|
613
629
|
const slug = existing.slug;
|
|
614
630
|
const member = cfg.member;
|
|
615
631
|
const baseUrl = cfg.baseUrl;
|
|
@@ -647,10 +663,12 @@ async function init(opts) {
|
|
|
647
663
|
(0, ctx_1.die)('refusing to onboard non-interactively without confirmation — connecting THIS repo to Convene ' +
|
|
648
664
|
'is a deliberate choice, not a side-effect. If you intend it, re-run with `--yes`.');
|
|
649
665
|
}
|
|
650
|
-
const
|
|
666
|
+
const existing = (0, config_1.loadProjectConfig)(top);
|
|
667
|
+
// Honor a committed host pin on a RE-RUN (a freshly-onboarding repo has none, so
|
|
668
|
+
// this equals resolveConfig); keeps a re-run targeting the bound bus.
|
|
669
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, existing);
|
|
651
670
|
const baseUrl = cfg.baseUrl;
|
|
652
671
|
let member = cfg.member;
|
|
653
|
-
const existing = (0, config_1.loadProjectConfig)(top);
|
|
654
672
|
const skipHook = opts.noHook === true || opts.hook === false;
|
|
655
673
|
const skipGithook = opts.noGithook === true || opts.githook === false;
|
|
656
674
|
const skipJoinToken = opts.noJoinToken === true || opts.joinToken === false;
|
|
@@ -760,7 +778,12 @@ async function init(opts) {
|
|
|
760
778
|
displayName = displayName || slug;
|
|
761
779
|
// 3. .convene/project.json (committed). The joinToken is a PROJECT-SCOPED
|
|
762
780
|
// enrollment secret — safe to commit in a PRIVATE repo (repo-read = team).
|
|
763
|
-
|
|
781
|
+
// Preserve any sibling committed fields (bestPractices manifest, host-pin
|
|
782
|
+
// binding) on a re-run — strip the stale schema so writeProjectConfig
|
|
783
|
+
// re-derives it. For a FRESH repo `existing` is null, so this stays
|
|
784
|
+
// byte-identical to a plain `{ slug, displayName, joinToken }` schema-1 write.
|
|
785
|
+
const { schema: _schema, ...preserved } = existing ?? {};
|
|
786
|
+
const projFile = (0, config_1.writeProjectConfig)(top, { ...preserved, slug, displayName, ...(joinToken ? { joinToken } : {}) });
|
|
764
787
|
log(`✓ ${node_path_1.default.relative(top, projFile)}${joinToken ? ' (incl. join token — private repos only)' : ''}`);
|
|
765
788
|
// 4. .gitignore guard
|
|
766
789
|
ensureGitignoreGuard(top);
|
package/dist/commands/join.js
CHANGED
|
@@ -26,7 +26,7 @@ async function join(opts) {
|
|
|
26
26
|
const token = opts.token || process.env.CONVENE_JOIN_TOKEN || proj?.joinToken;
|
|
27
27
|
if (!token)
|
|
28
28
|
(0, ctx_1.die)('no join token — pass --token <cvj_…>, set CONVENE_JOIN_TOKEN, or ask an owner for one');
|
|
29
|
-
const cfg = (0, config_1.
|
|
29
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj); // redeem against the pinned bus unless --base-url overrides
|
|
30
30
|
const baseUrl = (opts.baseUrl || cfg.baseUrl).replace(/\/$/, '');
|
|
31
31
|
const handle = opts.handle || (0, git_1.deriveHandle)(top ?? undefined);
|
|
32
32
|
const email = opts.email || (0, git_1.gitUserEmail)(top ?? undefined) || undefined;
|
package/dist/commands/notify.js
CHANGED
|
@@ -21,6 +21,7 @@ exports.notifyPush = notifyPush;
|
|
|
21
21
|
const config_1 = require("../config");
|
|
22
22
|
const git_1 = require("../git");
|
|
23
23
|
const api_1 = require("../api");
|
|
24
|
+
const cache_1 = require("../cache");
|
|
24
25
|
const exit_1 = require("../exit");
|
|
25
26
|
const isZero = (sha) => /^0+$/.test(sha);
|
|
26
27
|
/** Parse git's pre-push stdin into the non-deletion refs being pushed. */
|
|
@@ -109,10 +110,11 @@ function readStdin(timeoutMs) {
|
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
112
|
async function run(opts) {
|
|
112
|
-
const cfg = (0, config_1.resolveConfig)();
|
|
113
113
|
const top = (0, git_1.gitToplevel)();
|
|
114
|
+
const proj = (0, config_1.loadProjectConfig)(top);
|
|
115
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj); // honor a committed host pin for the push notify
|
|
114
116
|
const cwd = top || process.cwd();
|
|
115
|
-
const slug = opts.project ||
|
|
117
|
+
const slug = opts.project || proj?.slug || null;
|
|
116
118
|
if (!slug)
|
|
117
119
|
return; // not a bus repo — silent no-op, exactly like `convene fetch`
|
|
118
120
|
const stdin = await readStdin(1500);
|
|
@@ -142,8 +144,14 @@ async function run(opts) {
|
|
|
142
144
|
const session = top ? (0, git_1.sessionId)(cfg.member, top) : `${cfg.member}/cli`;
|
|
143
145
|
const api = new api_1.ConveneApi(cfg.baseUrl, cfg.apiKey, session, cfg.tool);
|
|
144
146
|
const res = await api.post(slug, { type: 'status', body }, idem, 4000);
|
|
145
|
-
if (res.ok
|
|
146
|
-
|
|
147
|
+
if (res.ok) {
|
|
148
|
+
// Advance the shared broadcast cursor to the pushed tip so a turn-end `convene
|
|
149
|
+
// wrap` (Stop hook) doesn't double-post a "wrapped" status for the same commit
|
|
150
|
+
// the bus just heard about via this push.
|
|
151
|
+
(0, cache_1.writeLastBroadcastSha)(slug, refs[0].localSha);
|
|
152
|
+
if (res.json?.message?.short_id) {
|
|
153
|
+
process.stdout.write(`convene: posted [STATUS] ${res.json.message.short_id} — ${body}\n`);
|
|
154
|
+
}
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
async function notifyPush(opts) {
|
|
@@ -466,7 +466,7 @@ async function offboard(opts) {
|
|
|
466
466
|
if (!opts.yes && !dryRun && !process.stdout.isTTY) {
|
|
467
467
|
(0, ctx_1.die)('refusing to off-board non-interactively without confirmation — re-run with `--yes` to confirm removing Convene from THIS repo.');
|
|
468
468
|
}
|
|
469
|
-
const cfg = (0, config_1.
|
|
469
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj); // revoke against the bound bus, not a stale ambient host
|
|
470
470
|
const baseUrl = cfg.baseUrl;
|
|
471
471
|
const slug = proj.slug;
|
|
472
472
|
// 1. Optional server-side token revoke FIRST (before deleting local files, so a
|
|
@@ -486,6 +486,31 @@ async function offboard(opts) {
|
|
|
486
486
|
: `⚠ could not revoke the join token (${r.error}) — it is owner-only; revoke from the dashboard if needed.`);
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
|
+
// 1.5 Optional owner hard-delete of the project server-side (--delete-project). This
|
|
490
|
+
// PERMANENTLY removes the project + ALL its messages for EVERY member (owner-only,
|
|
491
|
+
// typed confirm = the slug itself). Warn (don't fail) on 403/error, then continue
|
|
492
|
+
// with the local off-board — the reversible default is to leave the project intact.
|
|
493
|
+
if (opts.deleteProject) {
|
|
494
|
+
if (dryRun) {
|
|
495
|
+
log(`· would PERMANENTLY delete the project "${slug}" server-side (--delete-project).`);
|
|
496
|
+
}
|
|
497
|
+
else if (!cfg.apiKey) {
|
|
498
|
+
log('⚠ --delete-project: not logged in; skipping the server-side delete.');
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const api = new api_1.ConveneApi(baseUrl, cfg.apiKey, cfg.member ? (0, git_1.sessionId)(cfg.member, top) : null, cfg.tool);
|
|
502
|
+
const r = await api.deleteProjectOwner(slug, slug, 8000);
|
|
503
|
+
if (r.ok) {
|
|
504
|
+
log(`✓ PERMANENTLY deleted the project "${slug}" server-side (all members + messages; slug tombstoned).`);
|
|
505
|
+
}
|
|
506
|
+
else if (r.status === 403) {
|
|
507
|
+
log(`⚠ --delete-project: only an owner can delete "${slug}" — left it intact; off-boarding locally only.`);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
log(`⚠ --delete-project: could not delete "${slug}" (${r.error}) — left it intact; off-boarding locally only.`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
489
514
|
// 2. Local footprint removal.
|
|
490
515
|
const touched = [];
|
|
491
516
|
// Read the adopted-practices manifest BEFORE any deletion — delPath('.convene')
|
|
@@ -557,4 +582,13 @@ async function offboard(opts) {
|
|
|
557
582
|
log('The shared `convene fetch` hook was kept so your OTHER Convene repos keep working — re-run');
|
|
558
583
|
log('with `--remove-global` if this was your last Convene repo and you want the machine fully clean.');
|
|
559
584
|
}
|
|
585
|
+
// Reclaim guidance: unless the project was just deleted, off-board is LOCAL-ONLY — the
|
|
586
|
+
// project + your membership still exist server-side. This is the clarity that the
|
|
587
|
+
// orphaned-off-board incident lacked (people assumed off-board tore down the server side).
|
|
588
|
+
if (!opts.deleteProject) {
|
|
589
|
+
log('');
|
|
590
|
+
log(`Server-side, the project "${slug}" and your membership are untouched — off-board removed Convene`);
|
|
591
|
+
log('from THIS checkout only. To reconnect later: restore .convene/project.json from git history, then');
|
|
592
|
+
log('run `convene reclaim` (re-grants your membership via the committed token).');
|
|
593
|
+
}
|
|
560
594
|
}
|
|
@@ -43,7 +43,7 @@ async function override(id, opts = {}) {
|
|
|
43
43
|
const tok = (0, cache_1.writeOverrideToken)(slug, id, reason);
|
|
44
44
|
// 2) Best-effort attributed [STATUS] to the bus. FAIL-OPEN: a bus failure warns
|
|
45
45
|
// but never blocks — the local token already stands.
|
|
46
|
-
const cfg = (0, config_1.
|
|
46
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
47
47
|
const member = cfg.member;
|
|
48
48
|
const session = member && top ? (0, git_1.sessionId)(member, top) : null;
|
|
49
49
|
let posted = false;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reclaim = reclaim;
|
|
4
|
+
/**
|
|
5
|
+
* `convene reclaim` — self-recovery for an owner/member locked out of a project they
|
|
6
|
+
* can still READ (they hold the committed cvj_ token in .convene/project.json). It
|
|
7
|
+
* re-grants your own membership server-side. Like `join`, the committed token proves
|
|
8
|
+
* "can read the repo" and grants MEMBER only; OWNER is restored ONLY when the server
|
|
9
|
+
* has a recorded prior-owner row for you (your membership was soft-removed, not erased).
|
|
10
|
+
*
|
|
11
|
+
* This is the recurrence-fix for the orphaned-off-board incident: a returning owner no
|
|
12
|
+
* longer dead-ends on a 403, they run `convene reclaim`. If only `member` comes back,
|
|
13
|
+
* an owner/superadmin must promote you (the dashboard repair path).
|
|
14
|
+
*/
|
|
15
|
+
const brand_1 = require("../brand");
|
|
16
|
+
const api_1 = require("../api");
|
|
17
|
+
const config_1 = require("../config");
|
|
18
|
+
const git_1 = require("../git");
|
|
19
|
+
const hook_1 = require("../hook");
|
|
20
|
+
const githook_1 = require("../githook");
|
|
21
|
+
const ctx_1 = require("../ctx");
|
|
22
|
+
const log = (m) => process.stdout.write(m + '\n');
|
|
23
|
+
async function reclaim(opts) {
|
|
24
|
+
const top = (0, git_1.gitToplevel)();
|
|
25
|
+
const proj = (0, config_1.loadProjectConfig)(top);
|
|
26
|
+
const slug = opts.slug || proj?.slug;
|
|
27
|
+
if (!slug)
|
|
28
|
+
(0, ctx_1.die)('no project — run inside a `convene init`-ed repo, or pass --slug <slug>');
|
|
29
|
+
const token = opts.token || process.env.CONVENE_JOIN_TOKEN || proj?.joinToken;
|
|
30
|
+
if (!token) {
|
|
31
|
+
(0, ctx_1.die)('no join token — reclaim needs the committed token from .convene/project.json (restore it from git history if off-board removed it), or pass --token <cvj_…>');
|
|
32
|
+
}
|
|
33
|
+
const cfg = (0, config_1.resolveConfig)();
|
|
34
|
+
const baseUrl = (opts.baseUrl || cfg.baseUrl).replace(/\/$/, '');
|
|
35
|
+
const handle = opts.handle || (0, git_1.deriveHandle)(top ?? undefined);
|
|
36
|
+
const email = opts.email || (0, git_1.gitUserEmail)(top ?? undefined) || undefined;
|
|
37
|
+
// Authenticated mode if we already have a key (re-grants the existing identity, and
|
|
38
|
+
// can restore owner from a prior-owner row); unauthenticated mints a fresh identity
|
|
39
|
+
// (member only — a new identity has no prior-owner row to match).
|
|
40
|
+
const api = new api_1.ConveneApi(baseUrl, cfg.apiKey ?? null);
|
|
41
|
+
const res = await api.reclaim(slug, { token, handle, email, display_name: handle, tool: 'cli' }, 10_000);
|
|
42
|
+
if (res.status === 409 && res.json?.code === 'SLUG_REBOUND') {
|
|
43
|
+
(0, ctx_1.die)('this token predates the current project at this slug — a different project now owns it. Ask a superadmin to repair, or re-onboard with `convene init`.');
|
|
44
|
+
}
|
|
45
|
+
if (res.status === 409)
|
|
46
|
+
(0, ctx_1.die)(`the handle "${handle}" is already taken — re-run with --handle <unique-handle>, or \`convene login\` with your existing key first`);
|
|
47
|
+
if (res.status === 401)
|
|
48
|
+
(0, ctx_1.die)('the committed join token is invalid, expired, or revoked — ask an owner/superadmin for a fresh one (or use the dashboard repair path).');
|
|
49
|
+
if (!res.ok && res.status !== 200)
|
|
50
|
+
(0, ctx_1.die)(`reclaim failed (${res.status}): ${res.error ?? 'unknown error'}`);
|
|
51
|
+
const memberHandle = res.json?.member?.handle ?? handle;
|
|
52
|
+
const grantedRole = res.json?.project?.role ?? 'member';
|
|
53
|
+
const ownerRestored = res.json?.owner_restored === true;
|
|
54
|
+
if (res.json?.api_key) {
|
|
55
|
+
(0, config_1.saveFileConfig)({ apiKey: res.json.api_key, baseUrl, member: memberHandle });
|
|
56
|
+
log(`Reclaimed "${slug}" as ${memberHandle} (new identity, role: ${grantedRole}). Logged in (config 0600).`);
|
|
57
|
+
}
|
|
58
|
+
else if (res.json?.already) {
|
|
59
|
+
log(`Already an active member of "${slug}" as ${memberHandle} (role: ${grantedRole}). Nothing to reclaim.`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
log(`Reclaimed "${slug}" as ${memberHandle} (role: ${grantedRole}).`);
|
|
63
|
+
}
|
|
64
|
+
if (ownerRestored) {
|
|
65
|
+
log('✓ Owner restored — the server had a recorded prior-owner row for you.');
|
|
66
|
+
}
|
|
67
|
+
else if (grantedRole !== 'owner') {
|
|
68
|
+
log('You were re-granted MEMBER. If you should be an owner, ask an existing owner or a');
|
|
69
|
+
log('superadmin to promote you (superadmin dashboard → project → "Make owner").');
|
|
70
|
+
}
|
|
71
|
+
// Same hook re-registration tail as `join`, so the recovered checkout is fully wired.
|
|
72
|
+
const hook = (0, hook_1.ensureHookRegistered)();
|
|
73
|
+
log(hook === 'registered'
|
|
74
|
+
? 'Registered the convene fetch hook in ~/.claude/settings.json.'
|
|
75
|
+
: hook === 'already'
|
|
76
|
+
? 'Hook already registered.'
|
|
77
|
+
: 'Could not auto-register the hook — run `convene doctor --fix` or add it manually.');
|
|
78
|
+
if (top) {
|
|
79
|
+
const pr = (0, hook_1.ensureProjectHookRegistered)(top);
|
|
80
|
+
if (pr === 'registered')
|
|
81
|
+
log('Wrote committed project hook (.claude/settings.json) — commit it so teammates auto-connect.');
|
|
82
|
+
const gh = (0, githook_1.installGitHooks)(top);
|
|
83
|
+
if (gh.status === 'installed' || gh.status === 'updated') {
|
|
84
|
+
log('Installed the git pre-push hook (.githooks/pre-push) — pushes auto-post a [STATUS].');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
log(`You are operational on ${brand_1.BRAND.product}. Try: convene whoami`);
|
|
88
|
+
}
|
package/dist/commands/rotate.js
CHANGED
|
@@ -21,10 +21,10 @@ async function rotateJoinToken(opts) {
|
|
|
21
21
|
const top = (0, git_1.gitToplevel)();
|
|
22
22
|
if (!top)
|
|
23
23
|
(0, ctx_1.die)('not a git repository — run inside a repo');
|
|
24
|
-
const
|
|
24
|
+
const existing = (0, config_1.loadProjectConfig)(top);
|
|
25
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, existing); // mint/revoke against the pinned bus
|
|
25
26
|
if (!cfg.apiKey)
|
|
26
27
|
(0, ctx_1.die)('not logged in — run `convene login`');
|
|
27
|
-
const existing = (0, config_1.loadProjectConfig)(top);
|
|
28
28
|
const slug = opts.slug || existing?.slug;
|
|
29
29
|
if (!slug)
|
|
30
30
|
(0, ctx_1.die)('no project — run from a repo with .convene/project.json, or pass --slug');
|
|
@@ -39,8 +39,15 @@ async function rotateJoinToken(opts) {
|
|
|
39
39
|
if (!minted.ok || !minted.json?.join_token)
|
|
40
40
|
(0, ctx_1.die)(`could not mint a new join token: ${minted.error}`);
|
|
41
41
|
const newToken = minted.json.join_token;
|
|
42
|
-
// 2. write project.json (so we never lose a working token even if revoke fails)
|
|
42
|
+
// 2. write project.json (so we never lose a working token even if revoke fails).
|
|
43
|
+
// Preserve every OTHER committed field (bestPractices manifest, host-pin
|
|
44
|
+
// binding, …) — only the token changes here. Strip the stale `schema` so
|
|
45
|
+
// writeProjectConfig re-derives it (round-trip-safe; same discipline as
|
|
46
|
+
// writeManifest/writeBinding). Without this, rotating the token would silently
|
|
47
|
+
// un-pin the repo and drop its adopted practices.
|
|
48
|
+
const { schema: _schema, ...rest } = existing ?? {};
|
|
43
49
|
const projFile = (0, config_1.writeProjectConfig)(top, {
|
|
50
|
+
...rest,
|
|
44
51
|
slug: slug,
|
|
45
52
|
displayName: existing?.displayName || slug,
|
|
46
53
|
joinToken: newToken,
|
|
@@ -22,6 +22,7 @@ exports.sessionStart = sessionStart;
|
|
|
22
22
|
const node_child_process_1 = require("node:child_process");
|
|
23
23
|
const git_1 = require("../git");
|
|
24
24
|
const config_1 = require("../config");
|
|
25
|
+
const binding_local_1 = require("../binding-local");
|
|
25
26
|
const cache_1 = require("../cache");
|
|
26
27
|
const worktree_1 = require("./worktree");
|
|
27
28
|
const api_1 = require("../api");
|
|
@@ -123,7 +124,14 @@ async function run(opts) {
|
|
|
123
124
|
if (!proj?.slug)
|
|
124
125
|
return; // not on the bus → silent no-op
|
|
125
126
|
const slug = proj.slug;
|
|
126
|
-
|
|
127
|
+
// Host-pin drift banner — LOCAL ONLY, emitted BEFORE the network call so it
|
|
128
|
+
// survives an offline/DEGRADED boot (where the catch-up block is suppressed) and
|
|
129
|
+
// never adds boot latency. Fail-open: localBindingDrift returns null on no binding
|
|
130
|
+
// or any error, and the whole run() is inside sessionStart's try/catch + watchdog.
|
|
131
|
+
const drift = (0, binding_local_1.localBindingDrift)(top, proj);
|
|
132
|
+
if (drift)
|
|
133
|
+
emit(drift);
|
|
134
|
+
const cfg = (0, config_1.resolveConfigForRepo)(top, proj);
|
|
127
135
|
if (!cfg.apiKey || !cfg.member)
|
|
128
136
|
return; // not authenticated → silent (fail-open)
|
|
129
137
|
const member = cfg.member;
|