codiedev 0.9.64 → 0.9.66
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/captureLog.d.ts +1 -1
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +14 -35
- package/dist/commands/doctor.js +0 -34
- package/dist/hook.js +16 -1
- package/dist/launcher.d.ts +20 -0
- package/dist/launcher.js +167 -0
- package/dist/mcp.js +0 -337
- package/dist/selfUpdate.d.ts +49 -0
- package/dist/selfUpdate.js +390 -0
- package/dist/utils.d.ts +4 -4
- package/dist/utils.js +84 -8
- package/package.json +1 -1
- package/dist/commands/promote.d.ts +0 -1
- package/dist/commands/promote.js +0 -53
- package/dist/commands/react.d.ts +0 -2
- package/dist/commands/react.js +0 -40
- package/dist/commands/reverseTicket.d.ts +0 -2
- package/dist/commands/reverseTicket.js +0 -477
- package/dist/prUrl.d.ts +0 -9
- package/dist/prUrl.js +0 -33
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Background self-update for the hook path.
|
|
3
|
+
//
|
|
4
|
+
// Onboarded developers never run `connect`/`doctor`/`codie` again — only the
|
|
5
|
+
// harness hooks fire — so the interactive nudges in upgradeNudge.ts never
|
|
6
|
+
// reach them. This module lets any hook invocation spawn a detached child
|
|
7
|
+
// (at most once per day) that upgrades the exact install the hook is running
|
|
8
|
+
// from. Harness configs bake the absolute path to dist/hook.js, so the next
|
|
9
|
+
// hook call picks up the new code with no config change.
|
|
10
|
+
//
|
|
11
|
+
// Split like versionCheck.ts: `planSelfUpdate` is pure and unit-tested; the IO
|
|
12
|
+
// (state file, lock, registry fetch, package-manager spawn) is below it.
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.planSelfUpdate = planSelfUpdate;
|
|
48
|
+
exports.isSelfUpdateDue = isSelfUpdateDue;
|
|
49
|
+
exports.readSelfUpdateState = readSelfUpdateState;
|
|
50
|
+
exports.maybeSpawnSelfUpdate = maybeSpawnSelfUpdate;
|
|
51
|
+
exports.findInstalledHookScript = findInstalledHookScript;
|
|
52
|
+
exports.runSelfUpdate = runSelfUpdate;
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const path = __importStar(require("path"));
|
|
55
|
+
const child_process_1 = require("child_process");
|
|
56
|
+
const version_1 = require("./version");
|
|
57
|
+
const versionCheck_1 = require("./versionCheck");
|
|
58
|
+
const configDir_1 = require("./configDir");
|
|
59
|
+
const captureLog_1 = require("./captureLog");
|
|
60
|
+
const launcher_1 = require("./launcher");
|
|
61
|
+
const PKG = "codiedev";
|
|
62
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
63
|
+
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
64
|
+
const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
65
|
+
const FETCH_TIMEOUT_MS = 5_000;
|
|
66
|
+
/**
|
|
67
|
+
* Decide which package manager owns this install, from where hook.js really
|
|
68
|
+
* lives. We only ever upgrade the install we are running from: running
|
|
69
|
+
* whatever `npm` is first on PATH would, under nvm/Homebrew mixes, upgrade a
|
|
70
|
+
* different global tree than the one the harness config points at.
|
|
71
|
+
*/
|
|
72
|
+
function planSelfUpdate(deps) {
|
|
73
|
+
const { env } = deps;
|
|
74
|
+
if (env.CODIEDEV_NO_UPDATE_CHECK || env.CODIEDEV_NO_AUTO_UPDATE) {
|
|
75
|
+
return { kind: "skip", reason: "disabled" };
|
|
76
|
+
}
|
|
77
|
+
if (env.CI || env.CONTINUOUS_INTEGRATION || env.GITHUB_ACTIONS || env.BUILD_NUMBER) {
|
|
78
|
+
return { kind: "skip", reason: "ci" };
|
|
79
|
+
}
|
|
80
|
+
if (deps.isContainer)
|
|
81
|
+
return { kind: "skip", reason: "container" };
|
|
82
|
+
const p = deps.hookScriptReal.replace(/\\/g, "/");
|
|
83
|
+
if (/\/(?:_npx|\.npm\/_npx)\//.test(p))
|
|
84
|
+
return { kind: "skip", reason: "npx" };
|
|
85
|
+
const pkgDir = path.dirname(path.dirname(deps.hookScriptReal));
|
|
86
|
+
if (!deps.isWritable(pkgDir))
|
|
87
|
+
return { kind: "skip", reason: "readonly" };
|
|
88
|
+
// pnpm puts every global install in a fresh hashed dir
|
|
89
|
+
// (global/v11/<hash>/node_modules/codiedev). That is only safe to upgrade
|
|
90
|
+
// when the hook runs through the stable launcher, which runSelfUpdate
|
|
91
|
+
// re-points at the new dir; a direct-path config would stay on the old copy.
|
|
92
|
+
if (/\/\.pnpm\/|\/pnpm\/global\/|\/global\/v\d+\/[^/]+\/node_modules\/codiedev\/|\/store\/v\d+\/links\//.test(p)) {
|
|
93
|
+
return deps.viaLauncher ? { kind: "pnpm" } : { kind: "skip", reason: "pnpm-direct" };
|
|
94
|
+
}
|
|
95
|
+
// $BUN_INSTALL/install/global — ~/.bun by default, but BUN_INSTALL is often
|
|
96
|
+
// customized, so match the layout rather than the directory name.
|
|
97
|
+
if (/\/install\/global\/node_modules\/codiedev\/dist\/hook\.js$/.test(p))
|
|
98
|
+
return { kind: "bun" };
|
|
99
|
+
// npm global: <prefix>/lib/node_modules/codiedev/dist/hook.js on unix,
|
|
100
|
+
// <prefix>/node_modules/codiedev/dist/hook.js on Windows.
|
|
101
|
+
const unix = p.match(/^(.*)\/lib\/node_modules\/codiedev\/dist\/hook\.js$/);
|
|
102
|
+
if (unix && deps.platform !== "win32")
|
|
103
|
+
return { kind: "npm", prefix: unix[1] };
|
|
104
|
+
const win = p.match(/^(.*)\/node_modules\/codiedev\/dist\/hook\.js$/);
|
|
105
|
+
if (win && deps.platform === "win32")
|
|
106
|
+
return { kind: "npm", prefix: win[1] };
|
|
107
|
+
// A repo checkout, a project-local install, or a layout we don't know.
|
|
108
|
+
return { kind: "skip", reason: "unmanaged" };
|
|
109
|
+
}
|
|
110
|
+
function isSelfUpdateDue(state, nowMs) {
|
|
111
|
+
return !state || nowMs - state.lastAttemptAt > DAY_MS;
|
|
112
|
+
}
|
|
113
|
+
function statePath() {
|
|
114
|
+
return path.join((0, configDir_1.codiedevStateDir)(), ".self-update.json");
|
|
115
|
+
}
|
|
116
|
+
function lockPath() {
|
|
117
|
+
return path.join((0, configDir_1.codiedevStateDir)(), ".self-update.lock");
|
|
118
|
+
}
|
|
119
|
+
function readSelfUpdateState() {
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(fs.readFileSync(statePath(), "utf8"));
|
|
122
|
+
return typeof parsed.lastAttemptAt === "number" ? parsed : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function writeState(state) {
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync((0, configDir_1.codiedevStateDir)(), { recursive: true });
|
|
131
|
+
fs.writeFileSync(statePath(), JSON.stringify(state), "utf8");
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Called on every hook invocation, so the not-due path is one small file read.
|
|
140
|
+
* Stamps the attempt before spawning so a burst of parallel hooks spawns one
|
|
141
|
+
* child, not one each; the child's lock covers whatever still slips through.
|
|
142
|
+
*/
|
|
143
|
+
function maybeSpawnSelfUpdate(hookEntry) {
|
|
144
|
+
try {
|
|
145
|
+
if (!hookEntry || process.env.CODIEDEV_NO_AUTO_UPDATE || process.env.CODIEDEV_NO_UPDATE_CHECK)
|
|
146
|
+
return;
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (!isSelfUpdateDue(readSelfUpdateState(), now))
|
|
149
|
+
return;
|
|
150
|
+
if (!writeState({ lastAttemptAt: now, lastResult: "spawned" }))
|
|
151
|
+
return;
|
|
152
|
+
const child = (0, child_process_1.spawn)(process.execPath, [hookEntry, "self-update"], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
// detached on Windows opens a console window unless hidden.
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
});
|
|
158
|
+
child.unref();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Best-effort; a failed spawn retries tomorrow.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function acquireLock() {
|
|
165
|
+
const lock = lockPath();
|
|
166
|
+
try {
|
|
167
|
+
fs.mkdirSync(lock);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
try {
|
|
172
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
|
|
173
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
174
|
+
fs.mkdirSync(lock);
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Lost the race to another child.
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function detectContainer(env) {
|
|
185
|
+
if (env.KUBERNETES_SERVICE_HOST)
|
|
186
|
+
return true;
|
|
187
|
+
// /run/.containerenv is podman's marker (e.g. Sonifi's dev containers);
|
|
188
|
+
// cgroup v2 hosts report a bare "0::/" there, so the cgroup probe misses it.
|
|
189
|
+
if (fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv"))
|
|
190
|
+
return true;
|
|
191
|
+
try {
|
|
192
|
+
return /docker|containerd|kubepods|lxc/.test(fs.readFileSync("/proc/1/cgroup", "utf8"));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function registryBase(env) {
|
|
199
|
+
const configured = env.npm_config_registry || env.NPM_CONFIG_REGISTRY;
|
|
200
|
+
return (configured || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
201
|
+
}
|
|
202
|
+
async function fetchLatest(env) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
205
|
+
try {
|
|
206
|
+
const res = await fetch(`${registryBase(env)}/${PKG}/latest`, {
|
|
207
|
+
headers: { Accept: "application/json" },
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok)
|
|
211
|
+
return null;
|
|
212
|
+
const json = (await res.json());
|
|
213
|
+
return typeof json.version === "string" ? json.version : null;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function installCommand(plan, version, env) {
|
|
223
|
+
const spec = `${PKG}@${version}`;
|
|
224
|
+
// Pass an env-level registry override explicitly so the install always
|
|
225
|
+
// hits the registry fetchLatest just read from (not every manager honors
|
|
226
|
+
// npm_config_registry for global installs).
|
|
227
|
+
const configured = env.npm_config_registry || env.NPM_CONFIG_REGISTRY;
|
|
228
|
+
const registry = configured ? ["--registry", registryBase(env)] : [];
|
|
229
|
+
switch (plan.kind) {
|
|
230
|
+
case "npm": {
|
|
231
|
+
const args = ["i", "-g", "--prefix", plan.prefix, "--no-fund", "--no-audit", ...registry, spec];
|
|
232
|
+
// Run the npm that ships beside the node running us (it owns this
|
|
233
|
+
// prefix even when PATH resolves a different node), through its JS
|
|
234
|
+
// entry. That avoids spawning npm.cmd, which on Windows needs a shell
|
|
235
|
+
// and would split a prefix like C:\Users\First Last\… on the space.
|
|
236
|
+
const execDir = path.dirname(process.execPath);
|
|
237
|
+
const npmCli = [
|
|
238
|
+
path.join(execDir, "node_modules", "npm", "bin", "npm-cli.js"),
|
|
239
|
+
path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
240
|
+
].find((p) => fs.existsSync(p));
|
|
241
|
+
if (npmCli)
|
|
242
|
+
return { cmd: process.execPath, args: [npmCli, ...args] };
|
|
243
|
+
return { cmd: process.platform === "win32" ? "npm.cmd" : "npm", args, shell: process.platform === "win32" };
|
|
244
|
+
}
|
|
245
|
+
case "pnpm":
|
|
246
|
+
// pnpm on Windows is usually a .cmd shim; these args carry no paths.
|
|
247
|
+
return { cmd: "pnpm", args: ["add", "-g", ...registry, spec], shell: process.platform === "win32" };
|
|
248
|
+
case "bun":
|
|
249
|
+
return { cmd: "bun", args: ["add", "-g", ...registry, spec] };
|
|
250
|
+
default:
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function readVersionAt(hookScript) {
|
|
255
|
+
try {
|
|
256
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(path.dirname(path.dirname(hookScript)), "package.json"), "utf8"));
|
|
257
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Where the just-installed copy lives. npm/bun reinstall in place. pnpm's
|
|
265
|
+
* `root -g` is <root>/node_modules up to v10 but the bare global/v11 parent
|
|
266
|
+
* from v11 on, with each install in a hashed child — so check both shapes and
|
|
267
|
+
* take the copy that is actually at the version we just installed.
|
|
268
|
+
*/
|
|
269
|
+
function findInstalledHookScript(plan, hookEntry, version, pnpmRoot = pnpmGlobalRoot) {
|
|
270
|
+
if (plan.kind !== "pnpm")
|
|
271
|
+
return readVersionAt(hookEntry) === version ? hookEntry : null;
|
|
272
|
+
const root = pnpmRoot();
|
|
273
|
+
if (!root)
|
|
274
|
+
return null;
|
|
275
|
+
const candidates = [path.join(root, PKG, "dist", "hook.js")];
|
|
276
|
+
try {
|
|
277
|
+
for (const entry of fs.readdirSync(root)) {
|
|
278
|
+
candidates.push(path.join(root, entry, "node_modules", PKG, "dist", "hook.js"));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// root vanished between calls
|
|
283
|
+
}
|
|
284
|
+
for (const candidate of candidates) {
|
|
285
|
+
if (readVersionAt(candidate) !== version)
|
|
286
|
+
continue;
|
|
287
|
+
try {
|
|
288
|
+
return fs.realpathSync(candidate);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// dangling link; keep looking
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
function pnpmGlobalRoot() {
|
|
297
|
+
const root = (0, child_process_1.spawnSync)("pnpm", ["root", "-g"], {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
timeout: 30_000,
|
|
300
|
+
shell: process.platform === "win32",
|
|
301
|
+
windowsHide: true,
|
|
302
|
+
});
|
|
303
|
+
const dir = root.status === 0 ? root.stdout.trim() : "";
|
|
304
|
+
return dir || null;
|
|
305
|
+
}
|
|
306
|
+
/** Body of the detached `codiedev-hook self-update` child. Never throws. */
|
|
307
|
+
async function runSelfUpdate(hookEntry) {
|
|
308
|
+
const env = process.env;
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const finish = (lastResult, extra = {}) => {
|
|
311
|
+
const state = { lastAttemptAt: now, lastResult, fromVersion: version_1.CLI_VERSION, ...extra };
|
|
312
|
+
writeState(state);
|
|
313
|
+
(0, captureLog_1.appendCaptureLog)({
|
|
314
|
+
stage: "self-update",
|
|
315
|
+
sessionId: undefined,
|
|
316
|
+
outcome: lastResult.startsWith("error") ? "ERROR" : "ok",
|
|
317
|
+
detail: `result=${lastResult} from=${version_1.CLI_VERSION} to=${extra.toVersion ?? "-"} elapsedMs=${Date.now() - now}`,
|
|
318
|
+
});
|
|
319
|
+
return state;
|
|
320
|
+
};
|
|
321
|
+
let hookScriptReal;
|
|
322
|
+
try {
|
|
323
|
+
hookScriptReal = fs.realpathSync(hookEntry);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return finish("skip:unresolved");
|
|
327
|
+
}
|
|
328
|
+
const plan = planSelfUpdate({
|
|
329
|
+
hookScriptReal,
|
|
330
|
+
env,
|
|
331
|
+
platform: process.platform,
|
|
332
|
+
isContainer: detectContainer(env),
|
|
333
|
+
viaLauncher: Boolean(env.CODIEDEV_VIA_LAUNCHER),
|
|
334
|
+
isWritable: (dir) => {
|
|
335
|
+
try {
|
|
336
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
if (plan.kind === "skip")
|
|
345
|
+
return finish(`skip:${plan.reason}`);
|
|
346
|
+
if (!acquireLock()) {
|
|
347
|
+
// Another child is mid-install and owns the state file; overwriting it
|
|
348
|
+
// here would report "skip:locked" while that install is still running.
|
|
349
|
+
(0, captureLog_1.appendCaptureLog)({ stage: "self-update", sessionId: undefined, outcome: "ok", detail: "result=skip:locked" });
|
|
350
|
+
return { lastAttemptAt: now, lastResult: "skip:locked" };
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const latest = await fetchLatest(env);
|
|
354
|
+
if (!latest)
|
|
355
|
+
return finish("error:registry");
|
|
356
|
+
if ((0, versionCheck_1.compareSemver)(version_1.CLI_VERSION, latest) !== -1)
|
|
357
|
+
return finish("current", { toVersion: latest });
|
|
358
|
+
const install = installCommand(plan, latest, env);
|
|
359
|
+
if (!install)
|
|
360
|
+
return finish("skip:no-installer");
|
|
361
|
+
const result = (0, child_process_1.spawnSync)(install.cmd, install.args, {
|
|
362
|
+
stdio: "ignore",
|
|
363
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
364
|
+
env: { ...env, CODIEDEV_NO_AUTO_UPDATE: "1" },
|
|
365
|
+
shell: install.shell ?? false,
|
|
366
|
+
windowsHide: true,
|
|
367
|
+
});
|
|
368
|
+
if (result.error || result.status !== 0) {
|
|
369
|
+
return finish(`error:install-exit-${result.status ?? "spawn"}`, { toVersion: latest });
|
|
370
|
+
}
|
|
371
|
+
// Re-point the launcher. npm/bun reinstall in place; pnpm lands in a new
|
|
372
|
+
// hashed dir. Only switch once the new copy is on disk at the new version,
|
|
373
|
+
// so a half-finished install can never strand the hooks.
|
|
374
|
+
const installed = findInstalledHookScript(plan, hookScriptReal, latest);
|
|
375
|
+
if (!installed) {
|
|
376
|
+
return finish("error:not-found-after-install", { toVersion: latest });
|
|
377
|
+
}
|
|
378
|
+
if (!(0, launcher_1.recordCurrentInstall)(installed))
|
|
379
|
+
return finish("error:record", { toVersion: latest });
|
|
380
|
+
return finish("updated", { toVersion: latest });
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
try {
|
|
384
|
+
fs.rmSync(lockPath(), { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// A stale lock self-expires after LOCK_STALE_MS.
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -120,6 +120,9 @@ export declare function ensureDiscoveryHookInstalled(): void;
|
|
|
120
120
|
export declare function ensureBackupHookInstalled(): void;
|
|
121
121
|
export declare function ensureLimitHookInstalled(): void;
|
|
122
122
|
export declare function ensureAllCodiedevHooksInstalled(): void;
|
|
123
|
+
/** Exported for unit testing. */
|
|
124
|
+
export declare function hasDirectHookPath(configText: string): boolean;
|
|
125
|
+
export declare function migrateHooksToLauncher(): string[];
|
|
123
126
|
export declare function ensureSessionStartHookInstalled(): void;
|
|
124
127
|
export declare function installClaudeCodeInstructions(): void;
|
|
125
128
|
export declare function installCodexInstructions(): void;
|
|
@@ -144,10 +147,6 @@ export declare function installCursorMcp(): void;
|
|
|
144
147
|
* Claude / Cursor: top-level key is `servers` (not `mcpServers`). On first
|
|
145
148
|
* Copilot Chat run after install, VS Code prompts the user to trust the
|
|
146
149
|
* server — that's expected and documented in the connect success message.
|
|
147
|
-
*
|
|
148
|
-
* Reverse-ticket and the other on-demand verbs work in Copilot identically
|
|
149
|
-
* to Claude Code since the MCP server's reverse_ticket handler is already
|
|
150
|
-
* 100% diff-driven (no transcript dependency).
|
|
151
150
|
*/
|
|
152
151
|
export declare function installVSCodeMcp(): void;
|
|
153
152
|
/**
|
|
@@ -356,4 +355,5 @@ interface DeferredUploadDeps extends UploadDeps {
|
|
|
356
355
|
}
|
|
357
356
|
export declare function runDeferredUpload(jobFile: string, delayMs: number, deps: DeferredUploadDeps): Promise<void>;
|
|
358
357
|
export declare function transcriptReadSkipReason(err: unknown): string;
|
|
358
|
+
export declare function isCodexRolloutPath(transcriptPath: string | undefined): boolean;
|
|
359
359
|
export declare function shouldReportCaptureSkip(final: boolean | undefined): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -71,6 +71,8 @@ exports.ensureDiscoveryHookInstalled = ensureDiscoveryHookInstalled;
|
|
|
71
71
|
exports.ensureBackupHookInstalled = ensureBackupHookInstalled;
|
|
72
72
|
exports.ensureLimitHookInstalled = ensureLimitHookInstalled;
|
|
73
73
|
exports.ensureAllCodiedevHooksInstalled = ensureAllCodiedevHooksInstalled;
|
|
74
|
+
exports.hasDirectHookPath = hasDirectHookPath;
|
|
75
|
+
exports.migrateHooksToLauncher = migrateHooksToLauncher;
|
|
74
76
|
exports.ensureSessionStartHookInstalled = ensureSessionStartHookInstalled;
|
|
75
77
|
exports.installClaudeCodeInstructions = installClaudeCodeInstructions;
|
|
76
78
|
exports.installCodexInstructions = installCodexInstructions;
|
|
@@ -119,6 +121,7 @@ exports.readLastCapture = readLastCapture;
|
|
|
119
121
|
exports.runCaptureUpload = runCaptureUpload;
|
|
120
122
|
exports.runDeferredUpload = runDeferredUpload;
|
|
121
123
|
exports.transcriptReadSkipReason = transcriptReadSkipReason;
|
|
124
|
+
exports.isCodexRolloutPath = isCodexRolloutPath;
|
|
122
125
|
exports.shouldReportCaptureSkip = shouldReportCaptureSkip;
|
|
123
126
|
const fs = __importStar(require("fs"));
|
|
124
127
|
const transcriptModels_1 = require("./transcriptModels");
|
|
@@ -130,6 +133,7 @@ const captureLog_1 = require("./captureLog");
|
|
|
130
133
|
const configDir_1 = require("./configDir");
|
|
131
134
|
Object.defineProperty(exports, "codiedevStateDir", { enumerable: true, get: function () { return configDir_1.codiedevStateDir; } });
|
|
132
135
|
const codexHookTrust_1 = require("./codexHookTrust");
|
|
136
|
+
const launcher_1 = require("./launcher");
|
|
133
137
|
const teamSkillContext_1 = require("./teamSkillContext");
|
|
134
138
|
const nativeSteering_1 = require("./nativeSteering");
|
|
135
139
|
const copilotHooks_1 = require("./copilotHooks");
|
|
@@ -1019,7 +1023,10 @@ function resolveHookInvocation(subcommand, preferredNodePath) {
|
|
|
1019
1023
|
const hookScript = path.join(distDir, "hook.js");
|
|
1020
1024
|
if (fs.existsSync(hookScript)) {
|
|
1021
1025
|
const nodePath = resolveHookNodePath(preferredNodePath);
|
|
1022
|
-
|
|
1026
|
+
// Bake the stable launcher, not this install's path, so later
|
|
1027
|
+
// upgrades (including pnpm's hashed dirs) never strand the hook.
|
|
1028
|
+
const launcher = (0, launcher_1.ensureLauncher)(hookScript);
|
|
1029
|
+
return { command: nodePath, args: [launcher ?? hookScript, subcommand] };
|
|
1023
1030
|
}
|
|
1024
1031
|
}
|
|
1025
1032
|
}
|
|
@@ -1392,6 +1399,44 @@ function ensureAllCodiedevHooksInstalled() {
|
|
|
1392
1399
|
// Best-effort — never block a session on self-heal.
|
|
1393
1400
|
}
|
|
1394
1401
|
}
|
|
1402
|
+
// Matches a hook config that bakes an install's own dist/hook.js rather than
|
|
1403
|
+
// the stable launcher (see launcher.ts). Runs on raw file text, where JSON
|
|
1404
|
+
// doubles Windows separators (codiedev\\dist\\hook.js), hence `+`.
|
|
1405
|
+
const DIRECT_HOOK_PATH = /codiedev[^"'\s/\\]*[\\/]+dist[\\/]+hook\.js/;
|
|
1406
|
+
/** Exported for unit testing. */
|
|
1407
|
+
function hasDirectHookPath(configText) {
|
|
1408
|
+
return DIRECT_HOOK_PATH.test(configText);
|
|
1409
|
+
}
|
|
1410
|
+
// Move configs written before the launcher existed onto it, by re-running each
|
|
1411
|
+
// harness's own installer (which, for Codex, also re-trusts the new commands).
|
|
1412
|
+
// Only harnesses that already carry a direct-path codiedev hook are touched,
|
|
1413
|
+
// each in isolation. Bails when the launcher can't be written, so a read-only
|
|
1414
|
+
// state dir doesn't rewrite every config on every hook call.
|
|
1415
|
+
function migrateHooksToLauncher() {
|
|
1416
|
+
const probe = resolveHookInvocation("session-start");
|
|
1417
|
+
if (!(0, launcher_1.usesLauncher)(probe.args[0]))
|
|
1418
|
+
return [];
|
|
1419
|
+
const targets = [
|
|
1420
|
+
["claude-code", CLAUDE_SETTINGS_PATH, installHook],
|
|
1421
|
+
["codex", CODEX_HOOKS_PATH, installCodexHook],
|
|
1422
|
+
["cursor", CURSOR_HOOKS_PATH, installCursorHook],
|
|
1423
|
+
["copilot", copilotHooksPath(), installCopilotHook],
|
|
1424
|
+
["opencode", OPENCODE_PLUGIN_PATH, installOpenCodeCapturePlugin],
|
|
1425
|
+
];
|
|
1426
|
+
const migrated = [];
|
|
1427
|
+
for (const [name, file, install] of targets) {
|
|
1428
|
+
try {
|
|
1429
|
+
if (!fs.existsSync(file) || !hasDirectHookPath(fs.readFileSync(file, "utf8")))
|
|
1430
|
+
continue;
|
|
1431
|
+
install();
|
|
1432
|
+
migrated.push(name);
|
|
1433
|
+
}
|
|
1434
|
+
catch {
|
|
1435
|
+
// Best-effort per harness; the next direct-path hook call retries.
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return migrated;
|
|
1439
|
+
}
|
|
1395
1440
|
function hasCodiedevHook(hooks, eventName) {
|
|
1396
1441
|
const entries = hooks[eventName] ?? [];
|
|
1397
1442
|
// A malformed (non-array) event value would make `.some` throw and — under
|
|
@@ -1457,14 +1502,12 @@ thought, use the \`codiedev\` CLI via Bash:**
|
|
|
1457
1502
|
| "any messages?" / "check my inbox" / "what did a teammate say?" | \`codiedev inbox\` |
|
|
1458
1503
|
| "read that reply" / "mark that ping read" | \`codiedev read <ping-id>\` |
|
|
1459
1504
|
| "note that X is a follow-up" / "remember X" | \`codiedev note "<text>"\` |
|
|
1460
|
-
| "promote the extracted spec" | \`codiedev promote <artifact-id>\` |
|
|
1461
1505
|
| "create a ticket" / "scope this into tickets" / "ticket this for me" | \`codiedev create-ticket [optional-inline-prompt]\` |
|
|
1462
1506
|
| "share with a teammate" / "give someone access" (no notification) | \`codiedev share <key> <name> [--role read\\|edit]\` |
|
|
1463
1507
|
| "send this to a teammate" / "ask someone to look at this" | \`codiedev send <key> <name> ["<msg>"]\` |
|
|
1464
1508
|
| "find something about X" / "has anyone solved Y?" | \`codiedev search "<query>" [--limit N]\` |
|
|
1465
1509
|
| "why did we…?" / "what does the team know about X?" | \`codiedev ask "<question>"\` (synthesized answer + citations) |
|
|
1466
1510
|
| "show my library" / "what artifacts exist?" | \`codiedev library [--scope mine\\|shared\\|all] [--kind doc\\|skill]\` |
|
|
1467
|
-
| "react 🛠 to that post" / "mark as used" | \`codiedev react <postId> <emoji>\` |
|
|
1468
1511
|
|
|
1469
1512
|
**Just push the file — we'll figure out where it goes.** Every artifact
|
|
1470
1513
|
is classified into one of two kinds at upload time:
|
|
@@ -1661,10 +1704,6 @@ function installCursorMcp() {
|
|
|
1661
1704
|
* Claude / Cursor: top-level key is `servers` (not `mcpServers`). On first
|
|
1662
1705
|
* Copilot Chat run after install, VS Code prompts the user to trust the
|
|
1663
1706
|
* server — that's expected and documented in the connect success message.
|
|
1664
|
-
*
|
|
1665
|
-
* Reverse-ticket and the other on-demand verbs work in Copilot identically
|
|
1666
|
-
* to Claude Code since the MCP server's reverse_ticket handler is already
|
|
1667
|
-
* 100% diff-driven (no transcript dependency).
|
|
1668
1707
|
*/
|
|
1669
1708
|
function installVSCodeMcp() {
|
|
1670
1709
|
if (!fs.existsSync(VSCODE_USER_DIR)) {
|
|
@@ -2132,7 +2171,37 @@ function parseClaudeUsage(content) {
|
|
|
2132
2171
|
}
|
|
2133
2172
|
const sum = (field) => models.reduce((total, usage) => total + usage[field], 0);
|
|
2134
2173
|
const cacheReadTokens = sum("cacheReadTokens"), cacheCreationTokens = sum("cacheCreationTokens");
|
|
2135
|
-
|
|
2174
|
+
const stats = { inputTokens: sum("inputTokens"), outputTokens: sum("outputTokens"),
|
|
2175
|
+
cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
|
|
2176
|
+
return model ? stats : fromClaudeCostState(content, stats);
|
|
2177
|
+
}
|
|
2178
|
+
// Some Claude Code transcripts carry no assistant records, only a closing
|
|
2179
|
+
// `cost-state` whose `modelUsage` is keyed by model id. Without it these
|
|
2180
|
+
// sessions upload with no model and trip harness health model_missing.
|
|
2181
|
+
function fromClaudeCostState(content, stats) {
|
|
2182
|
+
let usage;
|
|
2183
|
+
for (const line of content.split("\n")) {
|
|
2184
|
+
if (!line.includes('"cost-state"'))
|
|
2185
|
+
continue;
|
|
2186
|
+
try {
|
|
2187
|
+
const e = JSON.parse(line);
|
|
2188
|
+
if (e?.type === "cost-state" && e.modelUsage && typeof e.modelUsage === "object")
|
|
2189
|
+
usage = e.modelUsage;
|
|
2190
|
+
}
|
|
2191
|
+
catch { /* partial records */ }
|
|
2192
|
+
}
|
|
2193
|
+
const n = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
2194
|
+
const entries = Object.entries(usage ?? {}).filter(([id, u]) => id.trim() && !id.startsWith("<") && u && typeof u === "object");
|
|
2195
|
+
if (!entries.length)
|
|
2196
|
+
return stats;
|
|
2197
|
+
// A session that switched models names the one that did most of the work.
|
|
2198
|
+
const [model] = entries.reduce((best, entry) => n(entry[1].outputTokens) > n(best[1].outputTokens) ? entry : best);
|
|
2199
|
+
// Assistant records, when present, already counted tokens; never add twice.
|
|
2200
|
+
if (stats.inputTokens || stats.outputTokens || stats.cacheTokens)
|
|
2201
|
+
return { ...stats, model };
|
|
2202
|
+
const total = (field) => entries.reduce((sum, [, u]) => sum + n(u[field]), 0);
|
|
2203
|
+
const cacheReadTokens = total("cacheReadInputTokens"), cacheCreationTokens = total("cacheCreationInputTokens");
|
|
2204
|
+
return { inputTokens: total("inputTokens"), outputTokens: total("outputTokens"),
|
|
2136
2205
|
cacheTokens: cacheReadTokens + cacheCreationTokens, cacheReadTokens, cacheCreationTokens, model };
|
|
2137
2206
|
}
|
|
2138
2207
|
function parseCodexUsage(content) {
|
|
@@ -3046,6 +3115,13 @@ function transcriptReadSkipReason(err) {
|
|
|
3046
3115
|
return "no-transcript";
|
|
3047
3116
|
return `transcript-unreadable:${code || "unknown"}`;
|
|
3048
3117
|
}
|
|
3118
|
+
// Codex's Stop hook also runs the plain `backup` command for artifact backup.
|
|
3119
|
+
// Its transcript is a Codex rollout, already captured per turn by
|
|
3120
|
+
// capture-codex; parsing it as Claude Code would upload an empty duplicate
|
|
3121
|
+
// session with no model under source claude-code.
|
|
3122
|
+
function isCodexRolloutPath(transcriptPath) {
|
|
3123
|
+
return Boolean(transcriptPath && /^rollout-.*\.jsonl$/.test(path.basename(transcriptPath)));
|
|
3124
|
+
}
|
|
3049
3125
|
// Per-turn snapshots are backups, not session endings. Reporting their local
|
|
3050
3126
|
// read/empty condition as a session skip would hide or close coverage while the
|
|
3051
3127
|
// agent is still running; only final/legacy capture paths emit the skip ping.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.66",
|
|
4
4
|
"description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codiedev": "dist/cli.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runPromote(args: string[]): Promise<void>;
|
package/dist/commands/promote.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runPromote = runPromote;
|
|
4
|
-
const shared_1 = require("./shared");
|
|
5
|
-
function parseArgs(args) {
|
|
6
|
-
let artifactId;
|
|
7
|
-
let keyOverride;
|
|
8
|
-
for (let i = 0; i < args.length; i++) {
|
|
9
|
-
const a = args[i];
|
|
10
|
-
if ((a === "--key" || a === "-k") && i + 1 < args.length) {
|
|
11
|
-
keyOverride = args[++i];
|
|
12
|
-
}
|
|
13
|
-
else if (a.startsWith("--key=")) {
|
|
14
|
-
keyOverride = a.slice("--key=".length);
|
|
15
|
-
}
|
|
16
|
-
else if (!a.startsWith("--") && !artifactId) {
|
|
17
|
-
artifactId = a;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (!artifactId) {
|
|
21
|
-
console.error("Usage: codiedev promote <extracted-artifact-id> [--key spec-foo.md]");
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
return { artifactId, keyOverride };
|
|
25
|
-
}
|
|
26
|
-
async function runPromote(args) {
|
|
27
|
-
const { artifactId, keyOverride } = parseArgs(args);
|
|
28
|
-
const config = (0, shared_1.requireConfig)();
|
|
29
|
-
const start = Date.now();
|
|
30
|
-
try {
|
|
31
|
-
const res = await (0, shared_1.apiRequest)("POST", "/api/cli/promote", {
|
|
32
|
-
config,
|
|
33
|
-
body: { artifactId, keyOverride },
|
|
34
|
-
});
|
|
35
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
36
|
-
tool: "codiedev_promote",
|
|
37
|
-
ok: true,
|
|
38
|
-
latencyMs: Date.now() - start,
|
|
39
|
-
});
|
|
40
|
-
console.log(`✓ Promoted to authored artifact ${res.key}`);
|
|
41
|
-
console.log(` id: ${res.artifactId}`);
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
(0, shared_1.recordAgentEvent)(config, {
|
|
45
|
-
tool: "codiedev_promote",
|
|
46
|
-
ok: false,
|
|
47
|
-
latencyMs: Date.now() - start,
|
|
48
|
-
error: err.message,
|
|
49
|
-
});
|
|
50
|
-
console.error(`Promote failed: ${err.message}`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
}
|
package/dist/commands/react.d.ts
DELETED