ghostrail 0.8.0 → 0.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/cli/commands.d.ts +32 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +409 -5
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +49 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +4 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard/page.d.ts +10 -0
- package/dist/dashboard/page.d.ts.map +1 -0
- package/dist/dashboard/page.js +94 -0
- package/dist/dashboard/page.js.map +1 -0
- package/dist/dashboard/routes.d.ts +48 -0
- package/dist/dashboard/routes.d.ts.map +1 -0
- package/dist/dashboard/routes.js +102 -0
- package/dist/dashboard/routes.js.map +1 -0
- package/dist/dashboard/server.d.ts +43 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +110 -0
- package/dist/dashboard/server.js.map +1 -0
- 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 +71 -0
- package/dist/service/manager.d.ts.map +1 -0
- package/dist/service/manager.js +125 -0
- package/dist/service/manager.js.map +1 -0
- package/dist/service/unit.d.ts +71 -0
- package/dist/service/unit.d.ts.map +1 -0
- package/dist/service/unit.js +138 -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 {
|
|
@@ -50,8 +51,38 @@ interface BoardFlags {
|
|
|
50
51
|
export declare function parseBoardFlags(argv: readonly string[], defaults: BoardFlags): BoardFlags;
|
|
51
52
|
/** `ghostrail board`: serve the run store as a dashboard. Runs until stopped. */
|
|
52
53
|
export declare function cmdBoard(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, cwd?: string): Promise<number>;
|
|
54
|
+
/** The cross-ghostrail dashboard's default port, one above the board's. */
|
|
55
|
+
export declare const DEFAULT_DASHBOARD_PORT = 5051;
|
|
56
|
+
/**
|
|
57
|
+
* `ghostrail dashboard`: serve every registered ghostrail.
|
|
58
|
+
*
|
|
59
|
+
* This is what the dashboard's service unit invokes, the way `watch` is what a
|
|
60
|
+
* ghostrail's unit invokes. Running it by hand works, but `ghostrail start` is
|
|
61
|
+
* how most people will meet it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function cmdDashboard(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, manager?: ServiceManager): Promise<number>;
|
|
53
64
|
/** `ghostrail artifacts`: list the shared artifacts available in the global store. */
|
|
54
65
|
export declare function cmdArtifacts(argv: readonly string[], streams: CommandStreams, env?: CommandEnv): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* How a registered ghostrail stands on disk. A stale entry is reported and
|
|
68
|
+
* never auto-pruned: an unmounted drive or a moved repo does not mean the
|
|
69
|
+
* operator stopped wanting that ghostrail, and quietly deleting their config to
|
|
70
|
+
* tidy a listing would be presumptuous.
|
|
71
|
+
*/
|
|
72
|
+
export type GhostrailHealth = "ok" | "no config" | "missing";
|
|
73
|
+
/** Check one registered path without trusting the registry's own claims. */
|
|
74
|
+
export declare function ghostrailHealth(path: string): Promise<GhostrailHealth>;
|
|
75
|
+
/** `ghostrail ls`: the ghostrails this account knows about. */
|
|
76
|
+
export declare function cmdLs(_argv: readonly string[], streams: CommandStreams, env?: CommandEnv): Promise<number>;
|
|
77
|
+
/**
|
|
78
|
+
* `ghostrail start`: choose which ghostrails should be running, record that in
|
|
79
|
+
* the registry, and make the services match.
|
|
80
|
+
*/
|
|
81
|
+
export declare function cmdStart(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, ask?: Asker, isTty?: boolean, manager?: ServiceManager): Promise<number>;
|
|
82
|
+
/** `ghostrail stop`: take ghostrails down without changing what should run. */
|
|
83
|
+
export declare function cmdStop(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, manager?: ServiceManager): Promise<number>;
|
|
84
|
+
/** `ghostrail status`: what each ghostrail is doing, and what it should be doing. */
|
|
85
|
+
export declare function cmdStatus(argv: readonly string[], streams: CommandStreams, env?: CommandEnv, manager?: ServiceManager): Promise<number>;
|
|
55
86
|
/** The quickstart templates bundled with the package. */
|
|
56
87
|
export declare const TEMPLATES: readonly ["code-local", "content-loop"];
|
|
57
88
|
/**
|
|
@@ -62,7 +93,7 @@ export declare const TEMPLATES: readonly ["code-local", "content-loop"];
|
|
|
62
93
|
* takes the template changes this repo never edited while naming what a
|
|
63
94
|
* customized file is missing.
|
|
64
95
|
*/
|
|
65
|
-
export declare function cmdInit(argv: readonly string[], streams: CommandStreams, cwd?: string): Promise<number>;
|
|
96
|
+
export declare function cmdInit(argv: readonly string[], streams: CommandStreams, cwd?: string, env?: CommandEnv): Promise<number>;
|
|
66
97
|
interface SkillFlags {
|
|
67
98
|
scope: Scope;
|
|
68
99
|
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":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAgEtD,OAAO,EAGL,KAAK,cAAc,EAMpB,MAAM,qBAAqB,CAAC;AAU7B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAE1D,OAAO,EAAE,KAAK,KAAK,EAAqC,MAAM,aAAa,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,YAAY,GAAG,YAAY,CAc/F;AAED,UAAU,UAAU;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CACnC;AA2HD,iDAAiD;AACjD,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,GAAG,SAAS,CAoB/E;AAED,sFAAsF;AACtF,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CAgCjB;AAED,sFAAsF;AACtF,wBAAsB,SAAS,CAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAED,gFAAgF;AAChF,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,YAAY,EAAE,OAAO,CAAC;IACtB,gFAAgF;IAChF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,QAAQ,EAAE,UAAU,GAAG,UAAU,CAyBzF;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CACtB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CA2EjB;AAED,2EAA2E;AAC3E,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAmB3C;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,OAAO,GAAE,cAAyD,GACjE,OAAO,CAAC,MAAM,CAAC,CA6CjB;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D,4EAA4E;AAC5E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAG5E;AAED,+DAA+D;AAC/D,wBAAsB,KAAK,CACzB,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAiFD;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,GAAE,KAAqB,EAC1B,KAAK,GAAE,OAAsC,EAC7C,OAAO,GAAE,cAAyD,GACjE,OAAO,CAAC,MAAM,CAAC,CAwGjB;AAED,+EAA+E;AAC/E,wBAAsB,OAAO,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,OAAO,GAAE,cAAyD,GACjE,OAAO,CAAC,MAAM,CAAC,CAkDjB;AAED,qFAAqF;AACrF,wBAAsB,SAAS,CAC7B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,OAAO,GAAE,cAAyD,GACjE,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAED,yDAAyD;AACzD,eAAO,MAAM,SAAS,yCAA0C,CAAC;AAkTjE;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,SAAgB,EACnB,GAAG,GAAE,UAAwB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAiHjB;AAED,UAAU,UAAU;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,GAAG,EAAE,OAAO,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA8BnE;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,GAClB,OAAO,CAAC,MAAM,CAAC,CAoDjB;AAiBD;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,GAAG,SAAgB,EACnB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,MAAM,CAAC,CAWjB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,IAAI,CAAC,CA8Cf;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,IAAI,GAAE,cAAmB,GACxB,OAAO,CAAC,MAAM,CAAC,CAuDjB;AAqBD,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACtD,gEAAgE;IAChE,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrD,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACjE;AAiFD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAoDjB;AAED,4EAA4E;AAC5E,MAAM,WAAW,WAAY,SAAQ,gBAAgB;IACnD,qEAAqE;IACrE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,+CAA+C;IAC/C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iEAAiE;IACjE,GAAG,CAAC,EAAE,KAAK,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAwBD,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,cAAc,EACvB,GAAG,GAAE,UAAwB,EAC7B,IAAI,GAAE,gBAAqB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB"}
|
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";
|
|
@@ -10,7 +10,9 @@ import { createBoardServer, isLoopback } from "../board/server.js";
|
|
|
10
10
|
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
|
+
import { createDashboardServer } from "../dashboard/index.js";
|
|
13
14
|
import { STORE_DIR, buildLoopDeps, buildRespondDeps, buildTriageDeps, resolveRepoSlug, } from "../factory/build.js";
|
|
15
|
+
import { findByPath, upsert } from "../global/registry.js";
|
|
14
16
|
import { GlobalStore, resolveGlobalDir } from "../global/store.js";
|
|
15
17
|
import { DEFAULT_FACTORY_IMAGE, dockerBuildArgs, dockerDir, parseImageFlags, } from "../image/build.js";
|
|
16
18
|
import { extractPlaceholders, isClean, missingSignals, repoPathFor, } from "../init/drift.js";
|
|
@@ -20,6 +22,7 @@ import { runTick } from "../loop/engine.js";
|
|
|
20
22
|
import { watchLoop } from "../loop/watch.js";
|
|
21
23
|
import { respondTick } from "../respond/respond.js";
|
|
22
24
|
import { GLOBAL_SECRETS_FILE, appendPlaceholders, applyOverrides, launchEditor as defaultLaunchEditor, loadEnvOverrides, missingSecrets, readSecretsFile, redactSecrets, renderFile, resolveEditor, secretValuesOf, writeSecretsFile, } from "../secrets/index.js";
|
|
25
|
+
import { DASHBOARD_UNIT, renderDashboardUnit, stopTimeoutSeconds, systemdManager, unitName, } from "../service/index.js";
|
|
23
26
|
import { applySkillPlan, describePlan, pathCtx, planSkillInstall, uninstallSkill, } from "../skill/install.js";
|
|
24
27
|
import { FileStore } from "../store/file-store.js";
|
|
25
28
|
import { triageTick } from "../triage/engine.js";
|
|
@@ -363,6 +366,71 @@ export function cmdBoard(argv, streams, env = process.env, cwd = process.cwd())
|
|
|
363
366
|
});
|
|
364
367
|
});
|
|
365
368
|
}
|
|
369
|
+
/** The cross-ghostrail dashboard's default port, one above the board's. */
|
|
370
|
+
export const DEFAULT_DASHBOARD_PORT = 5051;
|
|
371
|
+
/** Build the dashboard server's dependencies from the real environment. */
|
|
372
|
+
function dashboardDeps(env, manager) {
|
|
373
|
+
const store = globalStoreFor(env);
|
|
374
|
+
return {
|
|
375
|
+
readRegistry: () => store.readRegistry(),
|
|
376
|
+
writeRegistry: (entries) => store.writeRegistry(entries),
|
|
377
|
+
health: ghostrailHealth,
|
|
378
|
+
states: async (entries) => {
|
|
379
|
+
if ((await manager.kind()) !== "systemd")
|
|
380
|
+
return new Map();
|
|
381
|
+
const units = entries.map((e) => unitName(e.name, e.path));
|
|
382
|
+
const byUnit = await manager.states(units);
|
|
383
|
+
// Keyed by path, since that is a ghostrail's identity everywhere else.
|
|
384
|
+
return new Map(entries.map((e, i) => [e.path, byUnit.get(units[i]) ?? "unknown"]));
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* `ghostrail dashboard`: serve every registered ghostrail.
|
|
390
|
+
*
|
|
391
|
+
* This is what the dashboard's service unit invokes, the way `watch` is what a
|
|
392
|
+
* ghostrail's unit invokes. Running it by hand works, but `ghostrail start` is
|
|
393
|
+
* how most people will meet it.
|
|
394
|
+
*/
|
|
395
|
+
export function cmdDashboard(argv, streams, env = process.env, manager = systemdManager(env)) {
|
|
396
|
+
const flags = parseBoardFlags(argv, {
|
|
397
|
+
repo: ".",
|
|
398
|
+
port: DEFAULT_DASHBOARD_PORT,
|
|
399
|
+
bind: "127.0.0.1",
|
|
400
|
+
portExplicit: false,
|
|
401
|
+
noToken: false,
|
|
402
|
+
...(env.GHOSTRAIL_BOARD_TOKEN === undefined ? {} : { token: env.GHOSTRAIL_BOARD_TOKEN }),
|
|
403
|
+
});
|
|
404
|
+
const unauthenticated = flags.noToken && flags.token === undefined;
|
|
405
|
+
if (unauthenticated && !isLoopback(flags.bind)) {
|
|
406
|
+
streams.err(`serving the dashboard unauthenticated on ${flags.bind}: anyone who can reach this host can read your run history and change which ghostrails run\n`);
|
|
407
|
+
}
|
|
408
|
+
const server = createDashboardServer(dashboardDeps(env, manager), {
|
|
409
|
+
bind: flags.bind,
|
|
410
|
+
...(flags.token === undefined ? {} : { token: flags.token }),
|
|
411
|
+
});
|
|
412
|
+
const candidates = flags.portExplicit
|
|
413
|
+
? [flags.port]
|
|
414
|
+
: portCandidates(flags.port, PORT_FALLBACK_ATTEMPTS);
|
|
415
|
+
return new Promise((resolve) => {
|
|
416
|
+
let index = 0;
|
|
417
|
+
server.on("error", (error) => {
|
|
418
|
+
const next = candidates[index + 1];
|
|
419
|
+
if (isAddressInUse(error) && next !== undefined) {
|
|
420
|
+
index++;
|
|
421
|
+
server.listen(next, flags.bind);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
streams.err(`dashboard error: ${error.message}\n`);
|
|
425
|
+
resolve(1);
|
|
426
|
+
});
|
|
427
|
+
const first = candidates[0] ?? flags.port;
|
|
428
|
+
server.listen(first, flags.bind, () => {
|
|
429
|
+
const bound = candidates[index] ?? first;
|
|
430
|
+
streams.out(`ghostrail dashboard on http://${flags.bind}:${bound}/\n`);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
366
434
|
/** `ghostrail artifacts`: list the shared artifacts available in the global store. */
|
|
367
435
|
export async function cmdArtifacts(argv, streams, env = process.env) {
|
|
368
436
|
const dir = resolveGlobalDir({ GHOSTRAIL_HOME: env.GHOSTRAIL_HOME, XDG_CONFIG_HOME: env.XDG_CONFIG_HOME }, homedir());
|
|
@@ -375,6 +443,293 @@ export async function cmdArtifacts(argv, streams, env = process.env) {
|
|
|
375
443
|
streams.out(`artifacts:\n${names.map((name) => ` ${name}`).join("\n")}\n`);
|
|
376
444
|
return 0;
|
|
377
445
|
}
|
|
446
|
+
/** Check one registered path without trusting the registry's own claims. */
|
|
447
|
+
export async function ghostrailHealth(path) {
|
|
448
|
+
if (!(await pathExists(path)))
|
|
449
|
+
return "missing";
|
|
450
|
+
return (await pathExists(join(path, "ghostrail.toml"))) ? "ok" : "no config";
|
|
451
|
+
}
|
|
452
|
+
/** `ghostrail ls`: the ghostrails this account knows about. */
|
|
453
|
+
export async function cmdLs(_argv, streams, env = process.env) {
|
|
454
|
+
const store = globalStoreFor(env);
|
|
455
|
+
const entries = await store.readRegistry();
|
|
456
|
+
if (entries === undefined) {
|
|
457
|
+
streams.err(`cannot parse ${store.registryPath()}\n`);
|
|
458
|
+
streams.err("fix or remove it; a registry that does not parse is treated as none at all\n");
|
|
459
|
+
return 1;
|
|
460
|
+
}
|
|
461
|
+
if (entries.length === 0) {
|
|
462
|
+
streams.out("no ghostrails registered yet\n");
|
|
463
|
+
streams.out("`ghostrail init <template>` registers a new one, and\n");
|
|
464
|
+
streams.out("`ghostrail init <template> --update` adopts a repo set up before the registry\n");
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
const rows = await Promise.all(entries.map(async (entry) => ({ entry, health: await ghostrailHealth(entry.path) })));
|
|
468
|
+
const width = Math.max(...rows.map((row) => row.entry.name.length));
|
|
469
|
+
streams.out(`${store.registryPath()}\n\n`);
|
|
470
|
+
for (const { entry, health } of rows) {
|
|
471
|
+
const watch = entry.watch ? "watch" : " off";
|
|
472
|
+
const note = health === "ok" ? "" : ` (${health})`;
|
|
473
|
+
streams.out(` ${watch} ${entry.name.padEnd(width)} ${entry.path}${note}\n`);
|
|
474
|
+
}
|
|
475
|
+
if (rows.some((row) => row.health !== "ok")) {
|
|
476
|
+
streams.out("\nsome entries no longer resolve. remove one by deleting its block from\n");
|
|
477
|
+
streams.out(`${store.registryPath()}\n`);
|
|
478
|
+
}
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
/** Resolve the absolute path of the running ghostrail binary, for a unit file. */
|
|
482
|
+
function ghostrailExecPath() {
|
|
483
|
+
return process.argv[1] ?? "ghostrail";
|
|
484
|
+
}
|
|
485
|
+
/** A ghostrail's configured agent timeout, for the unit's stop timeout. */
|
|
486
|
+
async function agentTimeoutMs(repo) {
|
|
487
|
+
const raw = await readIfPresent(join(repo, "ghostrail.toml"));
|
|
488
|
+
if (raw !== undefined) {
|
|
489
|
+
const parsed = parseConfig(raw);
|
|
490
|
+
if (parsed.ok)
|
|
491
|
+
return parsed.value.guardrails.timeoutMs;
|
|
492
|
+
}
|
|
493
|
+
return 30 * 60_000;
|
|
494
|
+
}
|
|
495
|
+
function parseSelectFlags(argv) {
|
|
496
|
+
const flags = {
|
|
497
|
+
all: false,
|
|
498
|
+
names: [],
|
|
499
|
+
interval: "5m",
|
|
500
|
+
yes: false,
|
|
501
|
+
noDashboard: false,
|
|
502
|
+
};
|
|
503
|
+
for (let i = 0; i < argv.length; i++) {
|
|
504
|
+
const arg = argv[i];
|
|
505
|
+
const next = argv[i + 1];
|
|
506
|
+
if (arg === "--all" || arg === "-a")
|
|
507
|
+
flags.all = true;
|
|
508
|
+
else if ((arg === "--name" || arg === "-n") && next !== undefined) {
|
|
509
|
+
flags.names.push(next);
|
|
510
|
+
i++;
|
|
511
|
+
}
|
|
512
|
+
else if ((arg === "--interval" || arg === "-i") && next !== undefined) {
|
|
513
|
+
flags.interval = next;
|
|
514
|
+
i++;
|
|
515
|
+
}
|
|
516
|
+
else if (arg === "--yes" || arg === "-y")
|
|
517
|
+
flags.yes = true;
|
|
518
|
+
else if (arg === "--no-dashboard")
|
|
519
|
+
flags.noDashboard = true;
|
|
520
|
+
}
|
|
521
|
+
return flags;
|
|
522
|
+
}
|
|
523
|
+
/** Load the registry, reporting the two failure shapes distinctly. */
|
|
524
|
+
async function loadRegistry(store, streams) {
|
|
525
|
+
const entries = await store.readRegistry();
|
|
526
|
+
if (entries === undefined) {
|
|
527
|
+
streams.err(`cannot parse ${store.registryPath()}\n`);
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
if (entries.length === 0) {
|
|
531
|
+
streams.out("no ghostrails registered yet\n");
|
|
532
|
+
streams.out("`ghostrail init <template>` registers one, and\n");
|
|
533
|
+
streams.out("`ghostrail init <template> --update` adopts an existing repo\n");
|
|
534
|
+
}
|
|
535
|
+
return entries;
|
|
536
|
+
}
|
|
537
|
+
/** Say what platform we are on when there is no service manager to use. */
|
|
538
|
+
function explainNoManager(kind, streams, interval) {
|
|
539
|
+
if (kind === "launchd") {
|
|
540
|
+
streams.err("this machine uses launchd, which ghostrail cannot install services for yet\n");
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
streams.err("no user service manager found (looked for systemd)\n");
|
|
544
|
+
}
|
|
545
|
+
streams.err(`run a ghostrail by hand instead:\n cd <repo> && ghostrail watch --interval ${interval}\n`);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* `ghostrail start`: choose which ghostrails should be running, record that in
|
|
549
|
+
* the registry, and make the services match.
|
|
550
|
+
*/
|
|
551
|
+
export async function cmdStart(argv, streams, env = process.env, ask = askOnTerminal, isTty = process.stdin.isTTY === true, manager = systemdManager(env)) {
|
|
552
|
+
const flags = parseSelectFlags(argv);
|
|
553
|
+
const store = globalStoreFor(env);
|
|
554
|
+
const entries = await loadRegistry(store, streams);
|
|
555
|
+
if (entries === undefined)
|
|
556
|
+
return 1;
|
|
557
|
+
if (entries.length === 0)
|
|
558
|
+
return 0;
|
|
559
|
+
// Which ghostrails should be watched after this command.
|
|
560
|
+
let chosen;
|
|
561
|
+
if (flags.all) {
|
|
562
|
+
chosen = [...entries];
|
|
563
|
+
}
|
|
564
|
+
else if (flags.names.length > 0) {
|
|
565
|
+
const wanted = new Set(flags.names);
|
|
566
|
+
chosen = entries.filter((e) => wanted.has(e.name) || wanted.has(e.path));
|
|
567
|
+
const missing = flags.names.filter((n) => !entries.some((e) => e.name === n || e.path === n));
|
|
568
|
+
for (const name of missing)
|
|
569
|
+
streams.err(`no registered ghostrail named ${name}\n`);
|
|
570
|
+
if (missing.length > 0)
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
else if (isTty && !flags.yes) {
|
|
574
|
+
streams.out("Which ghostrails do you want to start?\n");
|
|
575
|
+
chosen = [];
|
|
576
|
+
for (const entry of entries) {
|
|
577
|
+
// Pre-answered from `watch`, so the prompt reflects the standing decision
|
|
578
|
+
// rather than asking it fresh every time.
|
|
579
|
+
if (await ask(` ${entry.name} (${entry.path})`, entry.watch))
|
|
580
|
+
chosen.push(entry);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
chosen = entries.filter((e) => e.watch);
|
|
585
|
+
}
|
|
586
|
+
const chosenPaths = new Set(chosen.map((e) => e.path));
|
|
587
|
+
// The selection IS the watch flag. One piece of state, so the CLI, the
|
|
588
|
+
// dashboard, and the units cannot disagree about what should be running.
|
|
589
|
+
const updated = entries.map((e) => ({ ...e, watch: chosenPaths.has(e.path) }));
|
|
590
|
+
await store.writeRegistry(updated);
|
|
591
|
+
const kind = await manager.kind();
|
|
592
|
+
if (kind !== "systemd") {
|
|
593
|
+
explainNoManager(kind, streams, flags.interval);
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
let failed = false;
|
|
597
|
+
for (const entry of updated) {
|
|
598
|
+
const unit = unitName(entry.name, entry.path);
|
|
599
|
+
if (!entry.watch) {
|
|
600
|
+
const error = await manager.disable(unit);
|
|
601
|
+
if (error === undefined)
|
|
602
|
+
streams.out(` stopped ${entry.name}\n`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if ((await ghostrailHealth(entry.path)) !== "ok") {
|
|
606
|
+
streams.err(` skipped ${entry.name}: ${entry.path} has no ghostrail.toml\n`);
|
|
607
|
+
failed = true;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const spec = {
|
|
611
|
+
name: entry.name,
|
|
612
|
+
path: entry.path,
|
|
613
|
+
exec: ghostrailExecPath(),
|
|
614
|
+
interval: flags.interval,
|
|
615
|
+
stopTimeoutSec: stopTimeoutSeconds(await agentTimeoutMs(entry.path)),
|
|
616
|
+
};
|
|
617
|
+
const { outcome, file } = await manager.install(spec);
|
|
618
|
+
if (outcome === "hand-edited") {
|
|
619
|
+
streams.err(` skipped ${entry.name}: ${file} was edited; remove it to regenerate\n`);
|
|
620
|
+
failed = true;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const error = await manager.enable(unit);
|
|
624
|
+
if (error !== undefined) {
|
|
625
|
+
streams.err(` failed ${entry.name}: ${error}\n`);
|
|
626
|
+
failed = true;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
streams.out(` started ${entry.name} (${outcome === "written" ? "new unit" : "unit unchanged"})\n`);
|
|
630
|
+
}
|
|
631
|
+
// The dashboard is how you see what any of this is doing, so it comes up with
|
|
632
|
+
// the ghostrails rather than being a command to remember.
|
|
633
|
+
if (!flags.noDashboard) {
|
|
634
|
+
const outcome = await manager.installRaw(DASHBOARD_UNIT, renderDashboardUnit(ghostrailExecPath(), DEFAULT_DASHBOARD_PORT, "127.0.0.1"));
|
|
635
|
+
if (outcome === "hand-edited") {
|
|
636
|
+
streams.err(" skipped dashboard: its unit was edited; remove it to regenerate\n");
|
|
637
|
+
failed = true;
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
const error = await manager.enable(DASHBOARD_UNIT);
|
|
641
|
+
if (error === undefined) {
|
|
642
|
+
streams.out(` started dashboard http://127.0.0.1:${DEFAULT_DASHBOARD_PORT}/\n`);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
streams.err(` failed dashboard: ${error}\n`);
|
|
646
|
+
failed = true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
streams.out("\n`watch` ticks the fix queue only; `respond` and `triage` still need their own schedule\n");
|
|
651
|
+
return failed ? 1 : 0;
|
|
652
|
+
}
|
|
653
|
+
/** `ghostrail stop`: take ghostrails down without changing what should run. */
|
|
654
|
+
export async function cmdStop(argv, streams, env = process.env, manager = systemdManager(env)) {
|
|
655
|
+
const flags = parseSelectFlags(argv);
|
|
656
|
+
const store = globalStoreFor(env);
|
|
657
|
+
const entries = await loadRegistry(store, streams);
|
|
658
|
+
if (entries === undefined)
|
|
659
|
+
return 1;
|
|
660
|
+
if (entries.length === 0)
|
|
661
|
+
return 0;
|
|
662
|
+
const wanted = new Set(flags.names);
|
|
663
|
+
const targets = flags.names.length > 0
|
|
664
|
+
? entries.filter((e) => wanted.has(e.name) || wanted.has(e.path))
|
|
665
|
+
: entries.filter((e) => flags.all || e.watch);
|
|
666
|
+
if (targets.length === 0) {
|
|
667
|
+
streams.out("nothing to stop\n");
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
const kind = await manager.kind();
|
|
671
|
+
if (kind !== "systemd") {
|
|
672
|
+
explainNoManager(kind, streams, flags.interval);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
// Say this before the first stop, not after: an in-flight tick is not
|
|
676
|
+
// interrupted, so this can sit for minutes and look hung.
|
|
677
|
+
streams.out("stopping; a ghostrail mid-tick finishes its run first, which can take minutes\n");
|
|
678
|
+
let failed = false;
|
|
679
|
+
for (const entry of targets) {
|
|
680
|
+
const error = await manager.disable(unitName(entry.name, entry.path));
|
|
681
|
+
if (error !== undefined) {
|
|
682
|
+
streams.err(` failed ${entry.name}: ${error}\n`);
|
|
683
|
+
failed = true;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
streams.out(` stopped ${entry.name}\n`);
|
|
687
|
+
}
|
|
688
|
+
// The dashboard came up with them, so it goes down with them. Leaving it
|
|
689
|
+
// serving a board of things that are not running would be its own confusion.
|
|
690
|
+
if (!flags.noDashboard) {
|
|
691
|
+
const error = await manager.disable(DASHBOARD_UNIT);
|
|
692
|
+
if (error === undefined)
|
|
693
|
+
streams.out(" stopped dashboard\n");
|
|
694
|
+
else {
|
|
695
|
+
streams.err(` failed dashboard: ${error}\n`);
|
|
696
|
+
failed = true;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Deliberately does not touch `watch`: "not running now" and "should not run"
|
|
700
|
+
// are different statements, and only the operator makes the second one.
|
|
701
|
+
return failed ? 1 : 0;
|
|
702
|
+
}
|
|
703
|
+
/** `ghostrail status`: what each ghostrail is doing, and what it should be doing. */
|
|
704
|
+
export async function cmdStatus(argv, streams, env = process.env, manager = systemdManager(env)) {
|
|
705
|
+
const store = globalStoreFor(env);
|
|
706
|
+
const entries = await loadRegistry(store, streams);
|
|
707
|
+
if (entries === undefined)
|
|
708
|
+
return 1;
|
|
709
|
+
if (entries.length === 0)
|
|
710
|
+
return 0;
|
|
711
|
+
const kind = await manager.kind();
|
|
712
|
+
const units = entries.map((e) => unitName(e.name, e.path));
|
|
713
|
+
const states = kind === "systemd" ? await manager.states(units) : new Map();
|
|
714
|
+
const width = Math.max(...entries.map((e) => e.name.length));
|
|
715
|
+
for (const [i, entry] of entries.entries()) {
|
|
716
|
+
const unit = units[i];
|
|
717
|
+
const state = kind === "systemd" ? (states.get(unit) ?? "unknown") : "unknown";
|
|
718
|
+
const health = await ghostrailHealth(entry.path);
|
|
719
|
+
const wants = entry.watch ? "watch" : " off";
|
|
720
|
+
const note = health === "ok" ? "" : ` (${health})`;
|
|
721
|
+
streams.out(` ${wants} ${state.padEnd(8)} ${entry.name.padEnd(width)} ${entry.path}${note}\n`);
|
|
722
|
+
}
|
|
723
|
+
if (kind === "systemd") {
|
|
724
|
+
// Listed with the ghostrails so "what is running" has one answer, not two.
|
|
725
|
+
const dash = (await manager.states([DASHBOARD_UNIT])).get(DASHBOARD_UNIT) ?? "unknown";
|
|
726
|
+
streams.out(`\n ${dash.padEnd(8)} dashboard http://127.0.0.1:${DEFAULT_DASHBOARD_PORT}/\n`);
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
streams.out("\nno supported service manager here, so nothing is reported as running\n");
|
|
730
|
+
}
|
|
731
|
+
return 0;
|
|
732
|
+
}
|
|
378
733
|
/** The quickstart templates bundled with the package. */
|
|
379
734
|
export const TEMPLATES = ["code-local", "content-loop"];
|
|
380
735
|
function templatesDir() {
|
|
@@ -478,6 +833,54 @@ async function reportTemplateDiff(template, srcDir, repo, streams) {
|
|
|
478
833
|
}
|
|
479
834
|
return 0;
|
|
480
835
|
}
|
|
836
|
+
/** The global store for this environment. */
|
|
837
|
+
function globalStoreFor(env) {
|
|
838
|
+
return new GlobalStore(resolveGlobalDir({ GHOSTRAIL_HOME: env.GHOSTRAIL_HOME, XDG_CONFIG_HOME: env.XDG_CONFIG_HOME }, homedir()));
|
|
839
|
+
}
|
|
840
|
+
/** A ghostrail's display name: `[factory].name`, else the directory. */
|
|
841
|
+
async function ghostrailName(repo) {
|
|
842
|
+
const raw = await readIfPresent(join(repo, "ghostrail.toml"));
|
|
843
|
+
if (raw !== undefined) {
|
|
844
|
+
const parsed = parseConfig(raw);
|
|
845
|
+
if (parsed.ok && parsed.value.factory.name.length > 0)
|
|
846
|
+
return parsed.value.factory.name;
|
|
847
|
+
}
|
|
848
|
+
return basename(repo);
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Record this repo in the ghostrails registry, so `start` and the board have
|
|
852
|
+
* something to list. Adding only: an entry that already exists is left exactly
|
|
853
|
+
* as it is, because re-running `init --update` must not switch `watch` back on
|
|
854
|
+
* for a ghostrail the operator deliberately turned off.
|
|
855
|
+
*
|
|
856
|
+
* Never fails the command. Scaffolding the repo is the job; registering it is a
|
|
857
|
+
* convenience, and aborting a good scaffold over an unwritable config dir would
|
|
858
|
+
* be the worse trade.
|
|
859
|
+
*/
|
|
860
|
+
async function registerGhostrail(repo, env, streams) {
|
|
861
|
+
const store = globalStoreFor(env);
|
|
862
|
+
const path = resolve(repo);
|
|
863
|
+
try {
|
|
864
|
+
const entries = await store.readRegistry();
|
|
865
|
+
if (entries === undefined) {
|
|
866
|
+
streams.err(`note: ${store.registryPath()} could not be parsed, so this ghostrail was not registered\n`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (findByPath(entries, path) !== undefined)
|
|
870
|
+
return;
|
|
871
|
+
const entry = {
|
|
872
|
+
name: await ghostrailName(repo),
|
|
873
|
+
path,
|
|
874
|
+
added: new Date().toISOString().slice(0, 10),
|
|
875
|
+
watch: true,
|
|
876
|
+
};
|
|
877
|
+
await store.writeRegistry(upsert(entries, entry));
|
|
878
|
+
streams.out(`registered ghostrail "${entry.name}" (${path})\n`);
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
streams.err(`note: could not register this ghostrail: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
481
884
|
/**
|
|
482
885
|
* Prompt paths a config file names, by role. `undefined` when the file cannot
|
|
483
886
|
* be read or parsed, which is a different thing from a config that names no
|
|
@@ -507,7 +910,7 @@ async function promptPathsAt(configPath) {
|
|
|
507
910
|
* fields the template gained are named, because that is what every hand port has
|
|
508
911
|
* actually consisted of, and the skill places them in the repo's own wording.
|
|
509
912
|
*/
|
|
510
|
-
async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
913
|
+
async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
|
|
511
914
|
const manifest = await readManifest(repo);
|
|
512
915
|
const rels = await scaffoldFiles(srcDir);
|
|
513
916
|
const templatePrompts = (await promptPathsAt(join(srcDir, "ghostrail.toml"))) ?? {};
|
|
@@ -570,6 +973,7 @@ async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
|
570
973
|
if (Object.keys(written).length > 0) {
|
|
571
974
|
await writeManifest(repo, mergeManifest(manifest, template, VERSION, written));
|
|
572
975
|
}
|
|
976
|
+
await registerGhostrail(repo, env, streams);
|
|
573
977
|
streams.out("\n");
|
|
574
978
|
if (took > 0)
|
|
575
979
|
streams.out(`took ${took} file(s) you had not edited\n`);
|
|
@@ -588,7 +992,7 @@ async function applyTemplateUpdate(template, srcDir, repo, streams) {
|
|
|
588
992
|
* takes the template changes this repo never edited while naming what a
|
|
589
993
|
* customized file is missing.
|
|
590
994
|
*/
|
|
591
|
-
export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
995
|
+
export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.env) {
|
|
592
996
|
let template;
|
|
593
997
|
let repo = cwd;
|
|
594
998
|
let force = false;
|
|
@@ -631,7 +1035,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
|
631
1035
|
const srcDir = join(templatesDir(), template);
|
|
632
1036
|
if (update) {
|
|
633
1037
|
try {
|
|
634
|
-
return await applyTemplateUpdate(template, srcDir, repo, streams);
|
|
1038
|
+
return await applyTemplateUpdate(template, srcDir, repo, streams, env);
|
|
635
1039
|
}
|
|
636
1040
|
catch (error) {
|
|
637
1041
|
streams.err(`init --update failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -691,7 +1095,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd()) {
|
|
|
691
1095
|
streams.out(`, kept ${skipped.length} existing (${skipped.join(", ")}; use --force to overwrite)`);
|
|
692
1096
|
}
|
|
693
1097
|
streams.out("\n");
|
|
694
|
-
|
|
1098
|
+
await registerGhostrail(repo, env, streams);
|
|
695
1099
|
streams.out("next: `ghostrail skill` installs the /ghostrail skill for your coding agent,\n");
|
|
696
1100
|
streams.out(" then run `/ghostrail customize` there to tailor the prompts to this repo\n");
|
|
697
1101
|
return 0;
|