ghostrail 0.8.0 → 0.9.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/cli/commands.d.ts +22 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +300 -5
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +36 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/global/index.d.ts +1 -0
- package/dist/global/index.d.ts.map +1 -1
- package/dist/global/index.js +1 -0
- package/dist/global/index.js.map +1 -1
- package/dist/global/registry.d.ts +63 -0
- package/dist/global/registry.d.ts.map +1 -0
- package/dist/global/registry.js +120 -0
- package/dist/global/registry.js.map +1 -0
- package/dist/global/store.d.ts +10 -0
- package/dist/global/store.d.ts.map +1 -1
- package/dist/global/store.js +25 -1
- package/dist/global/store.js.map +1 -1
- package/dist/service/index.d.ts +3 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +3 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/manager.d.ts +69 -0
- package/dist/service/manager.d.ts.map +1 -0
- package/dist/service/manager.js +107 -0
- package/dist/service/manager.js.map +1 -0
- package/dist/service/unit.d.ts +60 -0
- package/dist/service/unit.d.ts.map +1 -0
- package/dist/service/unit.js +111 -0
- package/dist/service/unit.js.map +1 -0
- package/package.json +1 -1
package/dist/cli/commands.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExecResult } from "../backend/types.js";
|
|
2
|
+
import { type ServiceManager } from "../service/index.js";
|
|
2
3
|
import type { Scope } from "../vendor/skilltend/paths.js";
|
|
3
4
|
import { type Asker } from "./prompt.js";
|
|
4
5
|
export interface CommandStreams {
|
|
@@ -52,6 +53,26 @@ export declare function parseBoardFlags(argv: readonly string[], defaults: Board
|
|
|
52
53
|
export declare function cmdBoard(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, cwd?: string): Promise<number>;
|
|
53
54
|
/** `ghostrail artifacts`: list the shared artifacts available in the global store. */
|
|
54
55
|
export declare function cmdArtifacts(argv: readonly string[], streams: CommandStreams, env?: CommandEnv): Promise<number>;
|
|
56
|
+
/**
|
|
57
|
+
* How a registered ghostrail stands on disk. A stale entry is reported and
|
|
58
|
+
* never auto-pruned: an unmounted drive or a moved repo does not mean the
|
|
59
|
+
* operator stopped wanting that ghostrail, and quietly deleting their config to
|
|
60
|
+
* tidy a listing would be presumptuous.
|
|
61
|
+
*/
|
|
62
|
+
export type GhostrailHealth = "ok" | "no config" | "missing";
|
|
63
|
+
/** Check one registered path without trusting the registry's own claims. */
|
|
64
|
+
export declare function ghostrailHealth(path: string): Promise<GhostrailHealth>;
|
|
65
|
+
/** `ghostrail ls`: the ghostrails this account knows about. */
|
|
66
|
+
export declare function cmdLs(_argv: readonly string[], streams: CommandStreams, env?: CommandEnv): Promise<number>;
|
|
67
|
+
/**
|
|
68
|
+
* `ghostrail start`: choose which ghostrails should be running, record that in
|
|
69
|
+
* the registry, and make the services match.
|
|
70
|
+
*/
|
|
71
|
+
export declare function cmdStart(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, ask?: Asker, isTty?: boolean, manager?: ServiceManager): Promise<number>;
|
|
72
|
+
/** `ghostrail stop`: take ghostrails down without changing what should run. */
|
|
73
|
+
export declare function cmdStop(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, manager?: ServiceManager): Promise<number>;
|
|
74
|
+
/** `ghostrail status`: what each ghostrail is doing, and what it should be doing. */
|
|
75
|
+
export declare function cmdStatus(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, manager?: ServiceManager): Promise<number>;
|
|
55
76
|
/** The quickstart templates bundled with the package. */
|
|
56
77
|
export declare const TEMPLATES: readonly ["code-local", "content-loop"];
|
|
57
78
|
/**
|
|
@@ -62,7 +83,7 @@ export declare const TEMPLATES: readonly ["code-local", "content-loop"];
|
|
|
62
83
|
* takes the template changes this repo never edited while naming what a
|
|
63
84
|
* customized file is missing.
|
|
64
85
|
*/
|
|
65
|
-
export declare function cmdInit(argv: readonly string[], streams: CommandStreams, cwd?: string): Promise<number>;
|
|
86
|
+
export declare function cmdInit(argv: readonly string[], streams: CommandStreams, cwd?: string, env?: CommandEnv): Promise<number>;
|
|
66
87
|
interface SkillFlags {
|
|
67
88
|
scope: Scope;
|
|
68
89
|
agents: string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/cli/commands.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { access, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, isAbsolute, join, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { spawnCollect } from "../backend/proc.js";
|
|
@@ -11,6 +11,7 @@ import { buildProposal, writeProfile } from "../claude-profile/index.js";
|
|
|
11
11
|
import { parseDuration } from "../config/duration.js";
|
|
12
12
|
import { parseConfig, parseMergedConfig } from "../config/load.js";
|
|
13
13
|
import { STORE_DIR, buildLoopDeps, buildRespondDeps, buildTriageDeps, resolveRepoSlug, } from "../factory/build.js";
|
|
14
|
+
import { findByPath, upsert } from "../global/registry.js";
|
|
14
15
|
import { GlobalStore, resolveGlobalDir } from "../global/store.js";
|
|
15
16
|
import { DEFAULT_FACTORY_IMAGE, dockerBuildArgs, dockerDir, parseImageFlags, } from "../image/build.js";
|
|
16
17
|
import { extractPlaceholders, isClean, missingSignals, repoPathFor, } from "../init/drift.js";
|
|
@@ -20,6 +21,7 @@ import { runTick } from "../loop/engine.js";
|
|
|
20
21
|
import { watchLoop } from "../loop/watch.js";
|
|
21
22
|
import { respondTick } from "../respond/respond.js";
|
|
22
23
|
import { GLOBAL_SECRETS_FILE, appendPlaceholders, applyOverrides, launchEditor as defaultLaunchEditor, loadEnvOverrides, missingSecrets, readSecretsFile, redactSecrets, renderFile, resolveEditor, secretValuesOf, writeSecretsFile, } from "../secrets/index.js";
|
|
24
|
+
import { stopTimeoutSeconds, systemdManager, unitName, } from "../service/index.js";
|
|
23
25
|
import { applySkillPlan, describePlan, pathCtx, planSkillInstall, uninstallSkill, } from "../skill/install.js";
|
|
24
26
|
import { FileStore } from "../store/file-store.js";
|
|
25
27
|
import { triageTick } from "../triage/engine.js";
|
|
@@ -375,6 +377,250 @@ export async function cmdArtifacts(argv, streams, env = process.env) {
|
|
|
375
377
|
streams.out(`artifacts:\n${names.map((name) => ` ${name}`).join("\n")}\n`);
|
|
376
378
|
return 0;
|
|
377
379
|
}
|
|
380
|
+
/** Check one registered path without trusting the registry's own claims. */
|
|
381
|
+
export async function ghostrailHealth(path) {
|
|
382
|
+
if (!(await pathExists(path)))
|
|
383
|
+
return "missing";
|
|
384
|
+
return (await pathExists(join(path, "ghostrail.toml"))) ? "ok" : "no config";
|
|
385
|
+
}
|
|
386
|
+
/** `ghostrail ls`: the ghostrails this account knows about. */
|
|
387
|
+
export async function cmdLs(_argv, streams, env = process.env) {
|
|
388
|
+
const store = globalStoreFor(env);
|
|
389
|
+
const entries = await store.readRegistry();
|
|
390
|
+
if (entries === undefined) {
|
|
391
|
+
streams.err(`cannot parse ${store.registryPath()}\n`);
|
|
392
|
+
streams.err("fix or remove it; a registry that does not parse is treated as none at all\n");
|
|
393
|
+
return 1;
|
|
394
|
+
}
|
|
395
|
+
if (entries.length === 0) {
|
|
396
|
+
streams.out("no ghostrails registered yet\n");
|
|
397
|
+
streams.out("`ghostrail init <template>` registers a new one, and\n");
|
|
398
|
+
streams.out("`ghostrail init <template> --update` adopts a repo set up before the registry\n");
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
const rows = await Promise.all(entries.map(async (entry) => ({ entry, health: await ghostrailHealth(entry.path) })));
|
|
402
|
+
const width = Math.max(...rows.map((row) => row.entry.name.length));
|
|
403
|
+
streams.out(`${store.registryPath()}\n\n`);
|
|
404
|
+
for (const { entry, health } of rows) {
|
|
405
|
+
const watch = entry.watch ? "watch" : " off";
|
|
406
|
+
const note = health === "ok" ? "" : ` (${health})`;
|
|
407
|
+
streams.out(` ${watch} ${entry.name.padEnd(width)} ${entry.path}${note}\n`);
|
|
408
|
+
}
|
|
409
|
+
if (rows.some((row) => row.health !== "ok")) {
|
|
410
|
+
streams.out("\nsome entries no longer resolve. remove one by deleting its block from\n");
|
|
411
|
+
streams.out(`${store.registryPath()}\n`);
|
|
412
|
+
}
|
|
413
|
+
return 0;
|
|
414
|
+
}
|
|
415
|
+
/** Resolve the absolute path of the running ghostrail binary, for a unit file. */
|
|
416
|
+
function ghostrailExecPath() {
|
|
417
|
+
return process.argv[1] ?? "ghostrail";
|
|
418
|
+
}
|
|
419
|
+
/** A ghostrail's configured agent timeout, for the unit's stop timeout. */
|
|
420
|
+
async function agentTimeoutMs(repo) {
|
|
421
|
+
const raw = await readIfPresent(join(repo, "ghostrail.toml"));
|
|
422
|
+
if (raw !== undefined) {
|
|
423
|
+
const parsed = parseConfig(raw);
|
|
424
|
+
if (parsed.ok)
|
|
425
|
+
return parsed.value.guardrails.timeoutMs;
|
|
426
|
+
}
|
|
427
|
+
return 30 * 60_000;
|
|
428
|
+
}
|
|
429
|
+
function parseSelectFlags(argv) {
|
|
430
|
+
const flags = { all: false, names: [], interval: "5m", yes: false };
|
|
431
|
+
for (let i = 0; i < argv.length; i++) {
|
|
432
|
+
const arg = argv[i];
|
|
433
|
+
const next = argv[i + 1];
|
|
434
|
+
if (arg === "--all" || arg === "-a")
|
|
435
|
+
flags.all = true;
|
|
436
|
+
else if ((arg === "--name" || arg === "-n") && next !== undefined) {
|
|
437
|
+
flags.names.push(next);
|
|
438
|
+
i++;
|
|
439
|
+
}
|
|
440
|
+
else if ((arg === "--interval" || arg === "-i") && next !== undefined) {
|
|
441
|
+
flags.interval = next;
|
|
442
|
+
i++;
|
|
443
|
+
}
|
|
444
|
+
else if (arg === "--yes" || arg === "-y")
|
|
445
|
+
flags.yes = true;
|
|
446
|
+
}
|
|
447
|
+
return flags;
|
|
448
|
+
}
|
|
449
|
+
/** Load the registry, reporting the two failure shapes distinctly. */
|
|
450
|
+
async function loadRegistry(store, streams) {
|
|
451
|
+
const entries = await store.readRegistry();
|
|
452
|
+
if (entries === undefined) {
|
|
453
|
+
streams.err(`cannot parse ${store.registryPath()}\n`);
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
if (entries.length === 0) {
|
|
457
|
+
streams.out("no ghostrails registered yet\n");
|
|
458
|
+
streams.out("`ghostrail init <template>` registers one, and\n");
|
|
459
|
+
streams.out("`ghostrail init <template> --update` adopts an existing repo\n");
|
|
460
|
+
}
|
|
461
|
+
return entries;
|
|
462
|
+
}
|
|
463
|
+
/** Say what platform we are on when there is no service manager to use. */
|
|
464
|
+
function explainNoManager(kind, streams, interval) {
|
|
465
|
+
if (kind === "launchd") {
|
|
466
|
+
streams.err("this machine uses launchd, which ghostrail cannot install services for yet\n");
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
streams.err("no user service manager found (looked for systemd)\n");
|
|
470
|
+
}
|
|
471
|
+
streams.err(`run a ghostrail by hand instead:\n cd <repo> && ghostrail watch --interval ${interval}\n`);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* `ghostrail start`: choose which ghostrails should be running, record that in
|
|
475
|
+
* the registry, and make the services match.
|
|
476
|
+
*/
|
|
477
|
+
export async function cmdStart(argv, streams, env = process.env, ask = askOnTerminal, isTty = process.stdin.isTTY === true, manager = systemdManager(env)) {
|
|
478
|
+
const flags = parseSelectFlags(argv);
|
|
479
|
+
const store = globalStoreFor(env);
|
|
480
|
+
const entries = await loadRegistry(store, streams);
|
|
481
|
+
if (entries === undefined)
|
|
482
|
+
return 1;
|
|
483
|
+
if (entries.length === 0)
|
|
484
|
+
return 0;
|
|
485
|
+
// Which ghostrails should be watched after this command.
|
|
486
|
+
let chosen;
|
|
487
|
+
if (flags.all) {
|
|
488
|
+
chosen = [...entries];
|
|
489
|
+
}
|
|
490
|
+
else if (flags.names.length > 0) {
|
|
491
|
+
const wanted = new Set(flags.names);
|
|
492
|
+
chosen = entries.filter((e) => wanted.has(e.name) || wanted.has(e.path));
|
|
493
|
+
const missing = flags.names.filter((n) => !entries.some((e) => e.name === n || e.path === n));
|
|
494
|
+
for (const name of missing)
|
|
495
|
+
streams.err(`no registered ghostrail named ${name}\n`);
|
|
496
|
+
if (missing.length > 0)
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
else if (isTty && !flags.yes) {
|
|
500
|
+
streams.out("Which ghostrails do you want to start?\n");
|
|
501
|
+
chosen = [];
|
|
502
|
+
for (const entry of entries) {
|
|
503
|
+
// Pre-answered from `watch`, so the prompt reflects the standing decision
|
|
504
|
+
// rather than asking it fresh every time.
|
|
505
|
+
if (await ask(` ${entry.name} (${entry.path})`, entry.watch))
|
|
506
|
+
chosen.push(entry);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
chosen = entries.filter((e) => e.watch);
|
|
511
|
+
}
|
|
512
|
+
const chosenPaths = new Set(chosen.map((e) => e.path));
|
|
513
|
+
// The selection IS the watch flag. One piece of state, so the CLI, the
|
|
514
|
+
// dashboard, and the units cannot disagree about what should be running.
|
|
515
|
+
const updated = entries.map((e) => ({ ...e, watch: chosenPaths.has(e.path) }));
|
|
516
|
+
await store.writeRegistry(updated);
|
|
517
|
+
const kind = await manager.kind();
|
|
518
|
+
if (kind !== "systemd") {
|
|
519
|
+
explainNoManager(kind, streams, flags.interval);
|
|
520
|
+
return 1;
|
|
521
|
+
}
|
|
522
|
+
let failed = false;
|
|
523
|
+
for (const entry of updated) {
|
|
524
|
+
const unit = unitName(entry.name, entry.path);
|
|
525
|
+
if (!entry.watch) {
|
|
526
|
+
const error = await manager.disable(unit);
|
|
527
|
+
if (error === undefined)
|
|
528
|
+
streams.out(` stopped ${entry.name}\n`);
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if ((await ghostrailHealth(entry.path)) !== "ok") {
|
|
532
|
+
streams.err(` skipped ${entry.name}: ${entry.path} has no ghostrail.toml\n`);
|
|
533
|
+
failed = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const spec = {
|
|
537
|
+
name: entry.name,
|
|
538
|
+
path: entry.path,
|
|
539
|
+
exec: ghostrailExecPath(),
|
|
540
|
+
interval: flags.interval,
|
|
541
|
+
stopTimeoutSec: stopTimeoutSeconds(await agentTimeoutMs(entry.path)),
|
|
542
|
+
};
|
|
543
|
+
const { outcome, file } = await manager.install(spec);
|
|
544
|
+
if (outcome === "hand-edited") {
|
|
545
|
+
streams.err(` skipped ${entry.name}: ${file} was edited; remove it to regenerate\n`);
|
|
546
|
+
failed = true;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const error = await manager.enable(unit);
|
|
550
|
+
if (error !== undefined) {
|
|
551
|
+
streams.err(` failed ${entry.name}: ${error}\n`);
|
|
552
|
+
failed = true;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
streams.out(` started ${entry.name} (${outcome === "written" ? "new unit" : "unit unchanged"})\n`);
|
|
556
|
+
}
|
|
557
|
+
streams.out("\n`watch` ticks the fix queue only; `respond` and `triage` still need their own schedule\n");
|
|
558
|
+
return failed ? 1 : 0;
|
|
559
|
+
}
|
|
560
|
+
/** `ghostrail stop`: take ghostrails down without changing what should run. */
|
|
561
|
+
export async function cmdStop(argv, streams, env = process.env, manager = systemdManager(env)) {
|
|
562
|
+
const flags = parseSelectFlags(argv);
|
|
563
|
+
const store = globalStoreFor(env);
|
|
564
|
+
const entries = await loadRegistry(store, streams);
|
|
565
|
+
if (entries === undefined)
|
|
566
|
+
return 1;
|
|
567
|
+
if (entries.length === 0)
|
|
568
|
+
return 0;
|
|
569
|
+
const wanted = new Set(flags.names);
|
|
570
|
+
const targets = flags.names.length > 0
|
|
571
|
+
? entries.filter((e) => wanted.has(e.name) || wanted.has(e.path))
|
|
572
|
+
: entries.filter((e) => flags.all || e.watch);
|
|
573
|
+
if (targets.length === 0) {
|
|
574
|
+
streams.out("nothing to stop\n");
|
|
575
|
+
return 0;
|
|
576
|
+
}
|
|
577
|
+
const kind = await manager.kind();
|
|
578
|
+
if (kind !== "systemd") {
|
|
579
|
+
explainNoManager(kind, streams, flags.interval);
|
|
580
|
+
return 1;
|
|
581
|
+
}
|
|
582
|
+
// Say this before the first stop, not after: an in-flight tick is not
|
|
583
|
+
// interrupted, so this can sit for minutes and look hung.
|
|
584
|
+
streams.out("stopping; a ghostrail mid-tick finishes its run first, which can take minutes\n");
|
|
585
|
+
let failed = false;
|
|
586
|
+
for (const entry of targets) {
|
|
587
|
+
const error = await manager.disable(unitName(entry.name, entry.path));
|
|
588
|
+
if (error !== undefined) {
|
|
589
|
+
streams.err(` failed ${entry.name}: ${error}\n`);
|
|
590
|
+
failed = true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
streams.out(` stopped ${entry.name}\n`);
|
|
594
|
+
}
|
|
595
|
+
// Deliberately does not touch `watch`: "not running now" and "should not run"
|
|
596
|
+
// are different statements, and only the operator makes the second one.
|
|
597
|
+
return failed ? 1 : 0;
|
|
598
|
+
}
|
|
599
|
+
/** `ghostrail status`: what each ghostrail is doing, and what it should be doing. */
|
|
600
|
+
export async function cmdStatus(argv, streams, env = process.env, manager = systemdManager(env)) {
|
|
601
|
+
const store = globalStoreFor(env);
|
|
602
|
+
const entries = await loadRegistry(store, streams);
|
|
603
|
+
if (entries === undefined)
|
|
604
|
+
return 1;
|
|
605
|
+
if (entries.length === 0)
|
|
606
|
+
return 0;
|
|
607
|
+
const kind = await manager.kind();
|
|
608
|
+
const units = entries.map((e) => unitName(e.name, e.path));
|
|
609
|
+
const states = kind === "systemd" ? await manager.states(units) : new Map();
|
|
610
|
+
const width = Math.max(...entries.map((e) => e.name.length));
|
|
611
|
+
for (const [i, entry] of entries.entries()) {
|
|
612
|
+
const unit = units[i];
|
|
613
|
+
const state = kind === "systemd" ? (states.get(unit) ?? "unknown") : "unknown";
|
|
614
|
+
const health = await ghostrailHealth(entry.path);
|
|
615
|
+
const wants = entry.watch ? "watch" : " off";
|
|
616
|
+
const note = health === "ok" ? "" : ` (${health})`;
|
|
617
|
+
streams.out(` ${wants} ${state.padEnd(8)} ${entry.name.padEnd(width)} ${entry.path}${note}\n`);
|
|
618
|
+
}
|
|
619
|
+
if (kind !== "systemd") {
|
|
620
|
+
streams.out("\nno supported service manager here, so nothing is reported as running\n");
|
|
621
|
+
}
|
|
622
|
+
return 0;
|
|
623
|
+
}
|
|
378
624
|
/** The quickstart templates bundled with the package. */
|
|
379
625
|
export const TEMPLATES = ["code-local", "content-loop"];
|
|
380
626
|
function templatesDir() {
|
|
@@ -478,6 +724,54 @@ async function reportTemplateDiff(template, srcDir, repo, streams) {
|
|
|
478
724
|
}
|
|
479
725
|
return 0;
|
|
480
726
|
}
|
|
727
|
+
/** The global store for this environment. */
|
|
728
|
+
function globalStoreFor(env) {
|
|
729
|
+
return new GlobalStore(resolveGlobalDir({ GHOSTRAIL_HOME: env.GHOSTRAIL_HOME, XDG_CONFIG_HOME: env.XDG_CONFIG_HOME }, homedir()));
|
|
730
|
+
}
|
|
731
|
+
/** A ghostrail's display name: `[factory].name`, else the directory. */
|
|
732
|
+
async function ghostrailName(repo) {
|
|
733
|
+
const raw = await readIfPresent(join(repo, "ghostrail.toml"));
|
|
734
|
+
if (raw !== undefined) {
|
|
735
|
+
const parsed = parseConfig(raw);
|
|
736
|
+
if (parsed.ok && parsed.value.factory.name.length > 0)
|
|
737
|
+
return parsed.value.factory.name;
|
|
738
|
+
}
|
|
739
|
+
return basename(repo);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Record this repo in the ghostrails registry, so `start` and the board have
|
|
743
|
+
* something to list. Adding only: an entry that already exists is left exactly
|
|
744
|
+
* as it is, because re-running `init --update` must not switch `watch` back on
|
|
745
|
+
* for a ghostrail the operator deliberately turned off.
|
|
746
|
+
*
|
|
747
|
+
* Never fails the command. Scaffolding the repo is the job; registering it is a
|
|
748
|
+
* convenience, and aborting a good scaffold over an unwritable config dir would
|
|
749
|
+
* be the worse trade.
|
|
750
|
+
*/
|
|
751
|
+
async function registerGhostrail(repo, env, streams) {
|
|
752
|
+
const store = globalStoreFor(env);
|
|
753
|
+
const path = resolve(repo);
|
|
754
|
+
try {
|
|
755
|
+
const entries = await store.readRegistry();
|
|
756
|
+
if (entries === undefined) {
|
|
757
|
+
streams.err(`note: ${store.registryPath()} could not be parsed, so this ghostrail was not registered\n`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (findByPath(entries, path) !== undefined)
|
|
761
|
+
return;
|
|
762
|
+
const entry = {
|
|
763
|
+
name: await ghostrailName(repo),
|
|
764
|
+
path,
|
|
765
|
+
added: new Date().toISOString().slice(0, 10),
|
|
766
|
+
watch: true,
|
|
767
|
+
};
|
|
768
|
+
await store.writeRegistry(upsert(entries, entry));
|
|
769
|
+
streams.out(`registered ghostrail "${entry.name}" (${path})\n`);
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
streams.err(`note: could not register this ghostrail: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
481
775
|
/**
|
|
482
776
|
* Prompt paths a config file names, by role. `undefined` when the file cannot
|
|
483
777
|
* be read or parsed, which is a different thing from a config that names no
|
|
@@ -507,7 +801,7 @@ async function promptPathsAt(configPath) {
|
|
|
507
801
|
* fields the template gained are named, because that is what every hand port has
|
|
508
802
|
* actually consisted of, and the skill places them in the repo's own wording.
|
|
509
803
|
*/
|
|
510
|
-
async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
804
|
+
async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
|
|
511
805
|
const manifest = await readManifest(repo);
|
|
512
806
|
const rels = await scaffoldFiles(srcDir);
|
|
513
807
|
const templatePrompts = (await promptPathsAt(join(srcDir, "ghostrail.toml"))) ?? {};
|
|
@@ -570,6 +864,7 @@ async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
|
570
864
|
if (Object.keys(written).length > 0) {
|
|
571
865
|
await writeManifest(repo, mergeManifest(manifest, template, VERSION, written));
|
|
572
866
|
}
|
|
867
|
+
await registerGhostrail(repo, env, streams);
|
|
573
868
|
streams.out("\n");
|
|
574
869
|
if (took > 0)
|
|
575
870
|
streams.out(`took ${took} file(s) you had not edited\n`);
|
|
@@ -588,7 +883,7 @@ async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
|
588
883
|
* takes the template changes this repo never edited while naming what a
|
|
589
884
|
* customized file is missing.
|
|
590
885
|
*/
|
|
591
|
-
export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
886
|
+
export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.env) {
|
|
592
887
|
let template;
|
|
593
888
|
let repo = cwd;
|
|
594
889
|
let force = false;
|
|
@@ -631,7 +926,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
|
631
926
|
const srcDir = join(templatesDir(), template);
|
|
632
927
|
if (update) {
|
|
633
928
|
try {
|
|
634
|
-
return await applyTemplateUpdate(template, srcDir, repo, streams);
|
|
929
|
+
return await applyTemplateUpdate(template, srcDir, repo, streams, env);
|
|
635
930
|
}
|
|
636
931
|
catch (error) {
|
|
637
932
|
streams.err(`init --update failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -691,7 +986,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
|
691
986
|
streams.out(`, kept ${skipped.length} existing (${skipped.join(", ")}; use --force to overwrite)`);
|
|
692
987
|
}
|
|
693
988
|
streams.out("\n");
|
|
694
|
-
|
|
989
|
+
await registerGhostrail(repo, env, streams);
|
|
695
990
|
streams.out("next: `ghostrail skill` installs the /ghostrail skill for your coding agent,\n");
|
|
696
991
|
streams.out(" then run `/ghostrail customize` there to tailor the prompts to this repo\n");
|
|
697
992
|
return 0;
|