ghostrail 0.21.0 → 0.23.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/board/page.d.ts.map +1 -1
- package/dist/board/page.js +71 -13
- package/dist/board/page.js.map +1 -1
- package/dist/board/server.d.ts +10 -0
- package/dist/board/server.d.ts.map +1 -1
- package/dist/board/server.js +24 -0
- package/dist/board/server.js.map +1 -1
- package/dist/cli/commands.d.ts +74 -2
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +183 -18
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +11 -2
- package/dist/commands/help.js.map +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +19 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +13 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/dashboard/page.d.ts +0 -14
- package/dist/dashboard/page.d.ts.map +1 -1
- package/dist/dashboard/page.js +107 -9
- package/dist/dashboard/page.js.map +1 -1
- package/dist/dashboard/routes.d.ts +4 -0
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +4 -0
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/dashboard/server.d.ts +29 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +75 -4
- package/dist/dashboard/server.js.map +1 -1
- package/dist/factory/build.d.ts +6 -0
- package/dist/factory/build.d.ts.map +1 -1
- package/dist/factory/build.js +23 -0
- package/dist/factory/build.js.map +1 -1
- package/dist/loop/branch.d.ts +8 -1
- package/dist/loop/branch.d.ts.map +1 -1
- package/dist/loop/branch.js +14 -2
- package/dist/loop/branch.js.map +1 -1
- package/dist/loop/engine.d.ts.map +1 -1
- package/dist/loop/engine.js +57 -5
- package/dist/loop/engine.js.map +1 -1
- package/dist/loop/next-tick.d.ts +43 -5
- package/dist/loop/next-tick.d.ts.map +1 -1
- package/dist/loop/next-tick.js +68 -18
- package/dist/loop/next-tick.js.map +1 -1
- package/dist/loop/ports.d.ts +4 -1
- package/dist/loop/ports.d.ts.map +1 -1
- package/dist/loop/recurrence.d.ts +43 -0
- package/dist/loop/recurrence.d.ts.map +1 -0
- package/dist/loop/recurrence.js +92 -0
- package/dist/loop/recurrence.js.map +1 -0
- package/dist/loop/schedule.d.ts +11 -2
- package/dist/loop/schedule.d.ts.map +1 -1
- package/dist/loop/schedule.js +3 -1
- package/dist/loop/schedule.js.map +1 -1
- package/dist/publish/gh.d.ts +43 -2
- package/dist/publish/gh.d.ts.map +1 -1
- package/dist/publish/gh.js +80 -2
- package/dist/publish/gh.js.map +1 -1
- package/dist/publish/githost.d.ts +18 -1
- package/dist/publish/githost.d.ts.map +1 -1
- package/dist/publish/githost.js +13 -0
- package/dist/publish/githost.js.map +1 -1
- package/dist/publish/index.d.ts +1 -1
- package/dist/publish/index.d.ts.map +1 -1
- package/dist/publish/index.js +1 -1
- package/dist/publish/index.js.map +1 -1
- package/dist/release/index.d.ts +2 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +2 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/release.d.ts +67 -0
- package/dist/release/release.d.ts.map +1 -0
- package/dist/release/release.js +193 -0
- package/dist/release/release.js.map +1 -0
- package/dist/respond/ci-fix-queue.d.ts +40 -0
- package/dist/respond/ci-fix-queue.d.ts.map +1 -0
- package/dist/respond/ci-fix-queue.js +74 -0
- package/dist/respond/ci-fix-queue.js.map +1 -0
- package/dist/respond/index.d.ts +2 -1
- package/dist/respond/index.d.ts.map +1 -1
- package/dist/respond/index.js +2 -1
- package/dist/respond/index.js.map +1 -1
- package/dist/respond/merge.d.ts.map +1 -1
- package/dist/respond/merge.js +14 -1
- package/dist/respond/merge.js.map +1 -1
- package/dist/respond/respond.d.ts +26 -1
- package/dist/respond/respond.d.ts.map +1 -1
- package/dist/respond/respond.js +154 -9
- package/dist/respond/respond.js.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/skill/ghostrail/SKILL.md +15 -4
- package/templates/code/ghostrail.toml +3 -0
- package/templates/code/prompts/release.md +40 -0
- package/templates/code/prompts/respond.md +2 -1
- package/templates/content/prompts/respond.md +3 -1
package/dist/cli/commands.js
CHANGED
|
@@ -12,7 +12,7 @@ import { parseDuration } from "../config/duration.js";
|
|
|
12
12
|
import { parseConfig, parseMergedConfig } from "../config/load.js";
|
|
13
13
|
import { MERGE_TRIGGER_COMMENT, createDashboardServer, defaultStateDir, } from "../dashboard/index.js";
|
|
14
14
|
import { hasFailure, renderDoctorJson, renderDoctorText, runDoctor, } from "../doctor/index.js";
|
|
15
|
-
import { buildLoopDeps, buildRespondDeps, buildTriageDeps, repoGhRunner, resolveRepoSlug, } from "../factory/build.js";
|
|
15
|
+
import { buildLoopDeps, buildReleaseDeps, buildRespondDeps, buildTriageDeps, repoGhRunner, resolveRepoSlug, } from "../factory/build.js";
|
|
16
16
|
import { describeMigration, migrateLegacyStore, resolveWorkRepo, stateDirFor, } from "../factory/state.js";
|
|
17
17
|
import { entryKey, findByPath, findForRepoResolving, replaceEntry, upsert, } from "../global/registry.js";
|
|
18
18
|
import { canonicalRepoId, localRepoId, repoKey } from "../global/repo-id.js";
|
|
@@ -28,19 +28,21 @@ import { concurrencyAdvice } from "../loop/advice.js";
|
|
|
28
28
|
import { fileControl } from "../loop/control.js";
|
|
29
29
|
import { readControl } from "../loop/control.js";
|
|
30
30
|
import { runTick } from "../loop/engine.js";
|
|
31
|
-
import { writeNextTickAt } from "../loop/next-tick.js";
|
|
31
|
+
import { clearTickStarted, markTickStarted, writeNextTickAt } from "../loop/next-tick.js";
|
|
32
32
|
import { duePasses, } from "../loop/schedule.js";
|
|
33
33
|
import { appendLog } from "../loop/tick-log.js";
|
|
34
34
|
import { watchLoop } from "../loop/watch.js";
|
|
35
35
|
import { GhHost, parseRepoSlug } from "../publish/gh.js";
|
|
36
|
+
import { releaseNow, releaseTick, requestRelease } from "../release/release.js";
|
|
36
37
|
import { resolveIgnoredAuthors } from "../respond/authors.js";
|
|
38
|
+
import { FileCiFixQueueStore } from "../respond/ci-fix-queue.js";
|
|
37
39
|
import { respondTick } from "../respond/respond.js";
|
|
38
40
|
import { GLOBAL_SECRETS_FILE, appendPlaceholders, applyOverrides, launchEditor as defaultLaunchEditor, loadEnvOverrides, missingSecrets, readSecretsFile, redactSecrets, renderFile, resolveEditor, secretValuesOf, writeSecretsFile, } from "../secrets/index.js";
|
|
39
41
|
import { DASHBOARD_UNIT, renderDashboardUnit, servicePath, stopTimeoutSeconds, systemdManager, unitName, } from "../service/index.js";
|
|
40
42
|
import { applySkillPlan, describePlan, pathCtx, planSkillInstall, uninstallSkill, } from "../skill/install.js";
|
|
41
43
|
import { FileStore } from "../store/file-store.js";
|
|
42
44
|
import { triageTick } from "../triage/engine.js";
|
|
43
|
-
import { VERSION } from "../version.js";
|
|
45
|
+
import { STABILITY_NOTICE, VERSION } from "../version.js";
|
|
44
46
|
import { askOnTerminal, parseInstallOffers } from "./prompt.js";
|
|
45
47
|
import { describeStalledUpdate } from "./update-diagnosis.js";
|
|
46
48
|
/**
|
|
@@ -187,8 +189,13 @@ async function resolveClaudeProfile(env) {
|
|
|
187
189
|
* Shared setup for the run/watch loops: apply secrets, load the merged config,
|
|
188
190
|
* require the Linear key, and assemble the loop deps. Prints errors and returns
|
|
189
191
|
* undefined on failure.
|
|
192
|
+
*
|
|
193
|
+
* `loggerFor` is the seam TJ-1682 turned on: the logger the deps are built with
|
|
194
|
+
* is decided here, because only here is the state directory known, and a caller
|
|
195
|
+
* that builds its own afterwards cannot reach the engine's narration. Exported
|
|
196
|
+
* so that invariant can be tested directly rather than inferred from a tick.
|
|
190
197
|
*/
|
|
191
|
-
async function prepareLoop(flags, streams, env) {
|
|
198
|
+
export async function prepareLoop(flags, streams, env, loggerFor = (_dir, secrets) => makeLogger(streams, secrets)) {
|
|
192
199
|
const secrets = await applySecrets(flags, env);
|
|
193
200
|
const loaded = await loadConfig(flags, streams, env);
|
|
194
201
|
if (loaded === undefined)
|
|
@@ -212,6 +219,9 @@ async function prepareLoop(flags, streams, env) {
|
|
|
212
219
|
// of one repo running overlapping ticks: while the store lived inside each
|
|
213
220
|
// checkout they took different locks and nothing serialized them.
|
|
214
221
|
const lock = new FileLock(join(state.dir, "lock"), config.guardrails.claimTtlMs);
|
|
222
|
+
// Keyed on the state dir because that is what a persisting logger needs and
|
|
223
|
+
// only `prepareLoop` knows it: the caller cannot build this itself (TJ-1682).
|
|
224
|
+
const logger = loggerFor(state.dir, secrets);
|
|
215
225
|
const ctx = {
|
|
216
226
|
repoPath: flags.repo,
|
|
217
227
|
...(work.path === undefined ? {} : { workRepoPath: work.path }),
|
|
@@ -227,10 +237,10 @@ async function prepareLoop(flags, streams, env) {
|
|
|
227
237
|
...(claudeProfileDir === undefined ? {} : { claudeProfileDir }),
|
|
228
238
|
...(promptSourceDir === undefined ? {} : { promptSourceDir }),
|
|
229
239
|
...(repoSlug === undefined ? {} : { repoSlug }),
|
|
230
|
-
logger
|
|
240
|
+
logger,
|
|
231
241
|
};
|
|
232
242
|
const deps = buildLoopDeps(config, ctx);
|
|
233
|
-
return { config, deps, secrets, stateDir: state.dir, ctx };
|
|
243
|
+
return { config, deps, secrets, stateDir: state.dir, ctx, logger };
|
|
234
244
|
}
|
|
235
245
|
/** `ghostrail run`: drain eligible work once. */
|
|
236
246
|
export async function cmdRun(argv, streams, env = process.env, cwd = process.cwd()) {
|
|
@@ -243,12 +253,14 @@ export async function cmdRun(argv, streams, env = process.env, cwd = process.cwd
|
|
|
243
253
|
return result.results.some((r) => r.state === "failed") ? 1 : 0;
|
|
244
254
|
}
|
|
245
255
|
/**
|
|
246
|
-
* Parse `ghostrail watch` options: `--interval`/`-i <duration>` (default `60s`)
|
|
247
|
-
*
|
|
256
|
+
* Parse `ghostrail watch` options: `--interval`/`-i <duration>` (default `60s`),
|
|
257
|
+
* an optional `--max-ticks <n>`, and `--self-update-restart`. Returns undefined
|
|
258
|
+
* for a malformed interval.
|
|
248
259
|
*/
|
|
249
260
|
export function parseWatchFlags(argv) {
|
|
250
261
|
let intervalLabel = "60s";
|
|
251
262
|
let maxTicks;
|
|
263
|
+
let selfUpdateRestart = false;
|
|
252
264
|
for (let i = 0; i < argv.length; i++) {
|
|
253
265
|
const arg = argv[i];
|
|
254
266
|
const next = argv[i + 1];
|
|
@@ -262,13 +274,51 @@ export function parseWatchFlags(argv) {
|
|
|
262
274
|
maxTicks = n;
|
|
263
275
|
i++;
|
|
264
276
|
}
|
|
277
|
+
else if (arg === "--self-update-restart") {
|
|
278
|
+
selfUpdateRestart = true;
|
|
279
|
+
}
|
|
265
280
|
}
|
|
266
281
|
const intervalMs = parseDuration(intervalLabel);
|
|
267
282
|
if (intervalMs === null)
|
|
268
283
|
return undefined;
|
|
269
|
-
return
|
|
270
|
-
|
|
271
|
-
|
|
284
|
+
return {
|
|
285
|
+
intervalMs,
|
|
286
|
+
intervalLabel,
|
|
287
|
+
...(maxTicks === undefined ? {} : { maxTicks }),
|
|
288
|
+
...(selfUpdateRestart ? { selfUpdateRestart: true } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Detached so the relaunch survives this process exiting right after. */
|
|
292
|
+
function defaultRestart(watchArgv) {
|
|
293
|
+
spawn("ghostrail", ["watch", ...watchArgv], { stdio: "inherit", detached: true }).unref();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* `--self-update-restart`'s whole behavior: called between ticks (never
|
|
297
|
+
* mid-tick, since `watchLoop` awaits `tick()` to completion first). Checks
|
|
298
|
+
* whether a newer ghostrail is published; if not, this is a no-op — refusing
|
|
299
|
+
* to start an update *is* the "pause ticks" the feature asks for, since the
|
|
300
|
+
* caller simply lets the loop continue. If a newer version exists, updates the
|
|
301
|
+
* same way `ghostrail self-update` does and, only on success, relaunches a
|
|
302
|
+
* detached process with the exact argv `watch` was given. A failed update is
|
|
303
|
+
* reported and left running on the current version rather than restarted.
|
|
304
|
+
*/
|
|
305
|
+
export async function maybeSelfUpdateAndRestart(watchArgv, streams, deps = {}) {
|
|
306
|
+
const current = deps.currentVersion ?? VERSION;
|
|
307
|
+
const latestVersion = deps.latestVersion ?? registryLatestVersion;
|
|
308
|
+
const selfUpdate = deps.selfUpdate ?? cmdSelfUpdate;
|
|
309
|
+
const restart = deps.restart ?? defaultRestart;
|
|
310
|
+
const latest = await latestVersion();
|
|
311
|
+
if (latest === undefined || latest === current)
|
|
312
|
+
return false;
|
|
313
|
+
streams.err(`self-update-restart: ${current} -> ${latest} available; updating\n`);
|
|
314
|
+
const code = await selfUpdate([], streams);
|
|
315
|
+
if (code !== 0) {
|
|
316
|
+
streams.err("self-update-restart: update failed; staying on the running version\n");
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
streams.err(`self-update-restart: restarting: ghostrail watch ${watchArgv.join(" ")}\n`);
|
|
320
|
+
restart(watchArgv);
|
|
321
|
+
return true;
|
|
272
322
|
}
|
|
273
323
|
/**
|
|
274
324
|
* The passes this config can run under `watch`: the fix queue always, plus
|
|
@@ -293,6 +343,10 @@ function schedulablePasses(setup, streams) {
|
|
|
293
343
|
streams.err(`not watching triage: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
294
344
|
}
|
|
295
345
|
}
|
|
346
|
+
if (setup.config.release !== undefined) {
|
|
347
|
+
const { deps, config: releaseConfig } = buildReleaseDeps(setup.config, setup.ctx);
|
|
348
|
+
passes.release = () => releaseTick(deps, releaseConfig);
|
|
349
|
+
}
|
|
296
350
|
return passes;
|
|
297
351
|
}
|
|
298
352
|
/** The intervals for the passes that are actually wired up. */
|
|
@@ -304,11 +358,14 @@ function intervalsOf(config, passes) {
|
|
|
304
358
|
if (passes.triage !== undefined && config.triage?.everyMs !== undefined) {
|
|
305
359
|
intervals.triage = config.triage.everyMs;
|
|
306
360
|
}
|
|
361
|
+
if (passes.release !== undefined) {
|
|
362
|
+
intervals.release = true;
|
|
363
|
+
}
|
|
307
364
|
return intervals;
|
|
308
365
|
}
|
|
309
|
-
/** The ", plus respond
|
|
366
|
+
/** The ", plus respond and triage" tail of the watch banner. */
|
|
310
367
|
function describeSchedule(passes) {
|
|
311
|
-
const extra = ["respond", "triage"].filter((pass) => passes[pass] !== undefined);
|
|
368
|
+
const extra = ["respond", "triage", "release"].filter((pass) => passes[pass] !== undefined);
|
|
312
369
|
return extra.length === 0 ? "" : `, plus ${extra.join(" and ")}`;
|
|
313
370
|
}
|
|
314
371
|
export async function cmdWatch(argv, streams, env = process.env, cwd = process.cwd()) {
|
|
@@ -318,16 +375,29 @@ export async function cmdWatch(argv, streams, env = process.env, cwd = process.c
|
|
|
318
375
|
streams.err('invalid --interval (use e.g. "30s", "5m", "1h")\n');
|
|
319
376
|
return 1;
|
|
320
377
|
}
|
|
321
|
-
|
|
378
|
+
// The persisting logger has to be the one the loop deps are built with, not a
|
|
379
|
+
// second one made afterwards: the engine narrates through `ctx.logger`, and a
|
|
380
|
+
// logger `watch` keeps to itself only ever sees pause, resume, and a thrown
|
|
381
|
+
// tick, which is why the log stayed empty (TJ-1682).
|
|
382
|
+
const setup = await prepareLoop(flags, streams, env, (dir, secrets) => watchLogger(streams, secrets, dir));
|
|
322
383
|
if (setup === undefined)
|
|
323
384
|
return 1;
|
|
324
385
|
const controller = new AbortController();
|
|
325
386
|
const onSignal = () => controller.abort();
|
|
326
387
|
process.once("SIGINT", onSignal);
|
|
327
388
|
process.once("SIGTERM", onSignal);
|
|
328
|
-
const logger =
|
|
389
|
+
const logger = setup.logger;
|
|
390
|
+
// A process killed mid-tick leaves its marker behind; starting up is the one
|
|
391
|
+
// moment we can be certain no tick of ours is running (TJ-1683).
|
|
392
|
+
try {
|
|
393
|
+
await clearTickStarted(setup.stateDir);
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Display state only. Never worth refusing to start over.
|
|
397
|
+
}
|
|
329
398
|
const passes = schedulablePasses(setup, streams);
|
|
330
399
|
streams.err(`watching every ${watch.intervalLabel}${describeSchedule(passes)}; press Ctrl-C to stop\n`);
|
|
400
|
+
let restarting = false;
|
|
331
401
|
try {
|
|
332
402
|
const controlDir = setup.stateDir;
|
|
333
403
|
// In memory, for the life of this process: a restart re-runs every pass
|
|
@@ -340,6 +410,15 @@ export async function cmdWatch(argv, streams, env = process.env, cwd = process.c
|
|
|
340
410
|
isEnabled: async () => (await readControl(controlDir)).enabled,
|
|
341
411
|
...(watch.maxTicks === undefined ? {} : { maxTicks: watch.maxTicks }),
|
|
342
412
|
tick: async () => {
|
|
413
|
+
// Best-effort and for display only (specs/dashboard-log.spec.md): a
|
|
414
|
+
// tick that outlasts the interval is ordinary here, and without this
|
|
415
|
+
// the countdown reaches zero and has nothing left to say (TJ-1683).
|
|
416
|
+
try {
|
|
417
|
+
await markTickStarted(setup.stateDir, Date.now());
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// the countdown is a convenience, not something the loop depends on
|
|
421
|
+
}
|
|
343
422
|
const due = duePasses({
|
|
344
423
|
now: Date.now(),
|
|
345
424
|
intervals: intervalsOf(setup.config, passes),
|
|
@@ -351,7 +430,11 @@ export async function cmdWatch(argv, streams, env = process.env, cwd = process.c
|
|
|
351
430
|
continue;
|
|
352
431
|
try {
|
|
353
432
|
const result = await run();
|
|
354
|
-
streams.out
|
|
433
|
+
// Through the logger, not `streams.out`: this is the most useful
|
|
434
|
+
// line a tick produces, and writing it straight to stdout kept it
|
|
435
|
+
// out of the log the dashboard shows (TJ-1682). `watchLogger`
|
|
436
|
+
// redacts, so this no longer redacts a second time.
|
|
437
|
+
logger.info(`${pass}: ${JSON.stringify(result, null, 2)}`);
|
|
355
438
|
}
|
|
356
439
|
catch (error) {
|
|
357
440
|
// One broken pass must not disable the other two, and must not
|
|
@@ -360,7 +443,8 @@ export async function cmdWatch(argv, streams, env = process.env, cwd = process.c
|
|
|
360
443
|
// an agent-backed pass means spending money in a loop.
|
|
361
444
|
logger.warn(`${pass} pass failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
362
445
|
}
|
|
363
|
-
|
|
446
|
+
// `release` has no interval to stamp (see PassIntervals), same as `fix`.
|
|
447
|
+
if (pass !== "fix" && pass !== "release")
|
|
364
448
|
lastRunAt[pass] = Date.now();
|
|
365
449
|
}
|
|
366
450
|
// Best-effort and for display only (specs/dashboard-log.spec.md): the
|
|
@@ -372,9 +456,17 @@ export async function cmdWatch(argv, streams, env = process.env, cwd = process.c
|
|
|
372
456
|
catch {
|
|
373
457
|
// the countdown is a convenience, not something the loop depends on
|
|
374
458
|
}
|
|
459
|
+
// Checked once per tick, never mid-tick: `watchLoop` awaits this whole
|
|
460
|
+
// callback before starting the next one, so this only ever runs
|
|
461
|
+
// between ticks (specs/self-update-restart.spec.md).
|
|
462
|
+
if (watch.selfUpdateRestart === true) {
|
|
463
|
+
restarting = await maybeSelfUpdateAndRestart(argv, streams);
|
|
464
|
+
if (restarting)
|
|
465
|
+
controller.abort();
|
|
466
|
+
}
|
|
375
467
|
},
|
|
376
468
|
});
|
|
377
|
-
streams.err(`stopped after ${ticks} tick(s)\n`);
|
|
469
|
+
streams.err(restarting ? "stopped for self-update-restart\n" : `stopped after ${ticks} tick(s)\n`);
|
|
378
470
|
return 0;
|
|
379
471
|
}
|
|
380
472
|
finally {
|
|
@@ -471,6 +563,43 @@ export async function cmdRespond(argv, streams, env = process.env, cwd = process
|
|
|
471
563
|
streams.out(`${redactSecrets(JSON.stringify(results, null, 2), secrets)}\n`);
|
|
472
564
|
return results.some((r) => r.state === "failed") ? 1 : 0;
|
|
473
565
|
}
|
|
566
|
+
/** `ghostrail release`: cut one release now. What "release" means is entirely [release].prompt's business. */
|
|
567
|
+
export async function cmdRelease(argv, streams, env = process.env, cwd = process.cwd()) {
|
|
568
|
+
const flags = parseCommandFlags(argv, { config: "ghostrail.toml", repo: cwd });
|
|
569
|
+
const secrets = await applySecrets(flags, env);
|
|
570
|
+
const loaded = await loadConfig(flags, streams, env);
|
|
571
|
+
if (loaded === undefined)
|
|
572
|
+
return 1;
|
|
573
|
+
const { config, promptSourceDir } = loaded;
|
|
574
|
+
if (config.release === undefined) {
|
|
575
|
+
streams.err("no [release] section in the config; add one to use `ghostrail release`\n");
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
const claudeProfileDir = await resolveClaudeProfile(env);
|
|
579
|
+
const releaseState = await prepareState(flags.repo, config, env, streams);
|
|
580
|
+
const releaseWork = await resolveWorkRepo({
|
|
581
|
+
repoPath: flags.repo,
|
|
582
|
+
stateDir: releaseState.dir,
|
|
583
|
+
config,
|
|
584
|
+
env,
|
|
585
|
+
});
|
|
586
|
+
if (releaseWork.note !== undefined)
|
|
587
|
+
streams.err(`${releaseWork.note}\n`);
|
|
588
|
+
// No LINEAR_API_KEY requirement: a release touches no tracker item.
|
|
589
|
+
const { deps, config: releaseConfig } = buildReleaseDeps(config, {
|
|
590
|
+
repoPath: flags.repo,
|
|
591
|
+
...(releaseWork.path === undefined ? {} : { workRepoPath: releaseWork.path }),
|
|
592
|
+
stateDir: releaseState.dir,
|
|
593
|
+
linearApiKey: env.LINEAR_API_KEY ?? "",
|
|
594
|
+
lock: { acquire: async () => true, release: async () => { } },
|
|
595
|
+
...(claudeProfileDir === undefined ? {} : { claudeProfileDir }),
|
|
596
|
+
...(promptSourceDir === undefined ? {} : { promptSourceDir }),
|
|
597
|
+
logger: makeLogger(streams, secrets),
|
|
598
|
+
});
|
|
599
|
+
const result = await releaseNow(deps, releaseConfig);
|
|
600
|
+
streams.out(`${redactSecrets(JSON.stringify(result, null, 2), secrets)}\n`);
|
|
601
|
+
return result.state === "failed" ? 1 : 0;
|
|
602
|
+
}
|
|
474
603
|
/** Parse `board` flags: `--repo`, `--port`, `--bind`, `--token`. */
|
|
475
604
|
export function parseBoardFlags(argv, defaults) {
|
|
476
605
|
const flags = { ...defaults };
|
|
@@ -546,6 +675,10 @@ export async function cmdBoard(argv, streams, env = process.env, cwd = process.c
|
|
|
546
675
|
bind: flags.bind,
|
|
547
676
|
gitHost,
|
|
548
677
|
control: fileControl(store.dir),
|
|
678
|
+
// Same directory `buildRespondDeps` resolves for this repo and config
|
|
679
|
+
// (`stateDirFor`), so a "Fix CI" click here is what the next
|
|
680
|
+
// `ghostrail respond` tick actually reads.
|
|
681
|
+
ciFixQueue: new FileCiFixQueueStore(join(store.dir, "ci-fix-queue.json")),
|
|
549
682
|
...(flags.token === undefined ? {} : { token: flags.token }),
|
|
550
683
|
...(unauthenticated ? { allowNoToken: true } : {}),
|
|
551
684
|
});
|
|
@@ -636,12 +769,23 @@ function dashboardDeps(env, manager) {
|
|
|
636
769
|
// subprocess. A machine with no `gh` and no credentials still serves the
|
|
637
770
|
// whole dashboard, with PR status reported as unavailable.
|
|
638
771
|
prStatus: async (entry, prNumber) => new GhHost(repoGhRunner(entry.path), await slugFor(entry)).getPrStatus(prNumber),
|
|
772
|
+
// The same directory `buildRespondDeps` resolves for this repo and
|
|
773
|
+
// config, so a "Fix CI" click here is what that ghostrail's next
|
|
774
|
+
// `ghostrail respond` tick actually reads.
|
|
775
|
+
ciFixQueue: async (entry) => new FileCiFixQueueStore(join(await defaultStateDir(entry), "ci-fix-queue.json")),
|
|
639
776
|
// Posts the same trigger comment a human typing "resolve conflicts and
|
|
640
777
|
// merge" by hand would; the next respond pass does the actual work (see
|
|
641
778
|
// specs/dashboard-review-mode.spec.md). A machine with no `gh` and no
|
|
642
779
|
// credentials still serves the whole dashboard; the route just answers
|
|
643
780
|
// 503 rather than failing to start.
|
|
644
781
|
mergeTrigger: async (entry, prNumber) => new GhHost(repoGhRunner(entry.path), await slugFor(entry)).postComment(prNumber, MERGE_TRIGGER_COMMENT),
|
|
782
|
+
releaseEnabled: (path) => releaseEnabledFor(path),
|
|
783
|
+
// Queuing a release is a local file write plus a store event, not a `gh`
|
|
784
|
+
// call, so this never needs the "no credentials" fallback mergeTrigger has.
|
|
785
|
+
releaseTrigger: async (entry) => {
|
|
786
|
+
const dir = await defaultStateDir(entry);
|
|
787
|
+
return requestRelease(dir, new FileStore(dir));
|
|
788
|
+
},
|
|
645
789
|
readRegistry: () => store.readRegistry(),
|
|
646
790
|
writeRegistry: (entries) => store.writeRegistry(entries),
|
|
647
791
|
health: ghostrailHealth,
|
|
@@ -728,6 +872,25 @@ export async function ghostrailHealth(path) {
|
|
|
728
872
|
return "missing";
|
|
729
873
|
return (await resolveConfigPath(path)) !== undefined ? "ok" : "no config";
|
|
730
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* Whether a registered ghostrail has `[release]` configured — the dashboard's
|
|
877
|
+
* gate for showing the release button. False for anything that cannot be
|
|
878
|
+
* read or parsed, the same leniency `ghostrailHealth` already has: a config
|
|
879
|
+
* problem hides the button rather than erroring the whole listing.
|
|
880
|
+
*/
|
|
881
|
+
export async function releaseEnabledFor(path) {
|
|
882
|
+
const configPath = await resolveConfigPath(path);
|
|
883
|
+
if (configPath === undefined)
|
|
884
|
+
return false;
|
|
885
|
+
try {
|
|
886
|
+
const toml = await readFile(configPath, "utf8");
|
|
887
|
+
const parsed = parseConfig(toml);
|
|
888
|
+
return parsed.ok && parsed.value.release !== undefined;
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
731
894
|
/** `ghostrail ls`: the ghostrails this account knows about. */
|
|
732
895
|
export async function cmdLs(_argv, streams, env = process.env) {
|
|
733
896
|
const store = globalStoreFor(env);
|
|
@@ -1740,6 +1903,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
|
|
|
1740
1903
|
}
|
|
1741
1904
|
await registerGhostrail(repo, env, streams, { repoint, forceRegister: register });
|
|
1742
1905
|
streams.out(await initNextStepsHint(env, cwd));
|
|
1906
|
+
streams.out(`${STABILITY_NOTICE}\n`);
|
|
1743
1907
|
return 0;
|
|
1744
1908
|
}
|
|
1745
1909
|
/**
|
|
@@ -1877,6 +2041,7 @@ async function npmInstallGlobalResult(version) {
|
|
|
1877
2041
|
* install` gives you exactly x.y.z.
|
|
1878
2042
|
*/
|
|
1879
2043
|
export async function cmdInstall(argv, streams, env = process.env, cwd = process.cwd(), deps = {}) {
|
|
2044
|
+
streams.out(`${STABILITY_NOTICE}\n`);
|
|
1880
2045
|
streams.out(`installing ghostrail@${VERSION} globally...\n`);
|
|
1881
2046
|
const code = await npmInstallGlobal(VERSION);
|
|
1882
2047
|
if (code !== 0) {
|