@halyard/cli 0.1.1 → 0.2.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/README.md +115 -0
- package/dist/cli.js +30 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.d.ts.map +1 -0
- package/dist/commands/api-keys.js +90 -0
- package/dist/commands/api-keys.js.map +1 -0
- package/dist/commands/hook.d.ts +6 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +106 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +170 -65
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.d.ts.map +1 -0
- package/dist/commands/orgs.js +102 -0
- package/dist/commands/orgs.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +29 -6
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/sessions.d.ts +3 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +139 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +58 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +171 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +51 -49
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/users.d.ts +13 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +205 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/whoami.d.ts +21 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +77 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/lib/api-client.d.ts +64 -2
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +32 -7
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/auth.d.ts +43 -6
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +106 -25
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/config-files.d.ts +6 -0
- package/dist/lib/config-files.d.ts.map +1 -0
- package/dist/lib/config-files.js +27 -0
- package/dist/lib/config-files.js.map +1 -0
- package/dist/lib/config.d.ts +13 -7
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +22 -8
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot-export.d.ts +31 -0
- package/dist/lib/copilot-export.d.ts.map +1 -0
- package/dist/lib/copilot-export.js +129 -0
- package/dist/lib/copilot-export.js.map +1 -0
- package/dist/lib/discovery.d.ts +60 -2
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +187 -15
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/hook-discovery.d.ts +43 -0
- package/dist/lib/hook-discovery.d.ts.map +1 -0
- package/dist/lib/hook-discovery.js +90 -0
- package/dist/lib/hook-discovery.js.map +1 -0
- package/dist/lib/hook-input.d.ts +36 -0
- package/dist/lib/hook-input.d.ts.map +1 -0
- package/dist/lib/hook-input.js +161 -0
- package/dist/lib/hook-input.js.map +1 -0
- package/dist/lib/hook-runner.d.ts +48 -0
- package/dist/lib/hook-runner.d.ts.map +1 -0
- package/dist/lib/hook-runner.js +306 -0
- package/dist/lib/hook-runner.js.map +1 -0
- package/dist/lib/http.d.ts +18 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +54 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/opencode-export.d.ts +22 -0
- package/dist/lib/opencode-export.d.ts.map +1 -0
- package/dist/lib/opencode-export.js +53 -0
- package/dist/lib/opencode-export.js.map +1 -0
- package/dist/lib/output.d.ts +27 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +74 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/preflight.d.ts +35 -0
- package/dist/lib/preflight.d.ts.map +1 -0
- package/dist/lib/preflight.js +54 -0
- package/dist/lib/preflight.js.map +1 -0
- package/dist/lib/repo-binding.d.ts +43 -0
- package/dist/lib/repo-binding.d.ts.map +1 -0
- package/dist/lib/repo-binding.js +158 -0
- package/dist/lib/repo-binding.js.map +1 -0
- package/dist/lib/repo-identity.d.ts +29 -0
- package/dist/lib/repo-identity.d.ts.map +1 -0
- package/dist/lib/repo-identity.js +113 -0
- package/dist/lib/repo-identity.js.map +1 -0
- package/dist/lib/setup-hooks.d.ts +41 -0
- package/dist/lib/setup-hooks.d.ts.map +1 -0
- package/dist/lib/setup-hooks.js +276 -0
- package/dist/lib/setup-hooks.js.map +1 -0
- package/dist/lib/setup.d.ts +47 -0
- package/dist/lib/setup.d.ts.map +1 -0
- package/dist/lib/setup.js +179 -0
- package/dist/lib/setup.js.map +1 -0
- package/dist/lib/skill-batches.d.ts +16 -0
- package/dist/lib/skill-batches.d.ts.map +1 -0
- package/dist/lib/skill-batches.js +35 -0
- package/dist/lib/skill-batches.js.map +1 -0
- package/dist/lib/skill-files.d.ts +4 -0
- package/dist/lib/skill-files.d.ts.map +1 -0
- package/dist/lib/skill-files.js +36 -0
- package/dist/lib/skill-files.js.map +1 -0
- package/dist/lib/skill-fingerprint.d.ts +55 -0
- package/dist/lib/skill-fingerprint.d.ts.map +1 -0
- package/dist/lib/skill-fingerprint.js +204 -0
- package/dist/lib/skill-fingerprint.js.map +1 -0
- package/dist/lib/skills-discovery.d.ts +57 -0
- package/dist/lib/skills-discovery.d.ts.map +1 -0
- package/dist/lib/skills-discovery.js +157 -0
- package/dist/lib/skills-discovery.js.map +1 -0
- package/dist/lib/skills-sync.d.ts +66 -0
- package/dist/lib/skills-sync.d.ts.map +1 -0
- package/dist/lib/skills-sync.js +251 -0
- package/dist/lib/skills-sync.js.map +1 -0
- package/dist/lib/sync-runner.d.ts +56 -0
- package/dist/lib/sync-runner.d.ts.map +1 -0
- package/dist/lib/sync-runner.js +106 -0
- package/dist/lib/sync-runner.js.map +1 -0
- package/dist/lib/sync-state.d.ts +36 -6
- package/dist/lib/sync-state.d.ts.map +1 -1
- package/dist/lib/sync-state.js +41 -11
- package/dist/lib/sync-state.js.map +1 -1
- package/package.json +16 -8
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type HookClient, type HookEvent } from './hook-input.js';
|
|
2
|
+
import { type TargetOrg } from './preflight.js';
|
|
3
|
+
/**
|
|
4
|
+
* `halyard hook`: the uploader every client's lifecycle hook calls.
|
|
5
|
+
*
|
|
6
|
+
* - `end` uploads the transcript that just finished (Claude: plus its
|
|
7
|
+
* subagent transcripts; Codex: the rollout matched by thread id).
|
|
8
|
+
* - `sweep` uploads this repo's earlier transcripts that never made it up
|
|
9
|
+
* (run detached from a `start` event by the command layer).
|
|
10
|
+
*
|
|
11
|
+
* Org binding: a repo's `.halyard.json` decides which org its sessions
|
|
12
|
+
* belong to. The hook resolves credentials the same way `sync` and `push`
|
|
13
|
+
* do (HALYARD_TOKEN → per-org login for the bound org → default login) and
|
|
14
|
+
* refuses login credentials for another org.
|
|
15
|
+
*
|
|
16
|
+
* Why session end and not every turn: the ingest endpoint keeps the FIRST
|
|
17
|
+
* upload it sees for a session id, so a growing transcript uploaded per turn
|
|
18
|
+
* would freeze at turn one.
|
|
19
|
+
*
|
|
20
|
+
* Never throws for expected conditions — every skip reason goes to
|
|
21
|
+
* ~/.halyard/hooks.log — and never writes to stdout (clients parse it).
|
|
22
|
+
*/
|
|
23
|
+
export interface HookRunOptions {
|
|
24
|
+
/** Raw JSON the client piped to the hook; may be empty. */
|
|
25
|
+
input: string;
|
|
26
|
+
client?: HookClient;
|
|
27
|
+
event?: HookEvent;
|
|
28
|
+
root?: string;
|
|
29
|
+
transcript?: string;
|
|
30
|
+
dryRun: boolean;
|
|
31
|
+
apiUrl: string;
|
|
32
|
+
halyardDir: string;
|
|
33
|
+
homeDir: string;
|
|
34
|
+
/** Should be process.env in production: HALYARD_TOKEN found in the repo .env is exported into it. */
|
|
35
|
+
env: NodeJS.ProcessEnv;
|
|
36
|
+
cwd: string;
|
|
37
|
+
/** Injectable for tests (defaults to the shared upload preflight). */
|
|
38
|
+
resolveTarget?: (apiUrl: string) => Promise<TargetOrg>;
|
|
39
|
+
}
|
|
40
|
+
export declare class HookLogger {
|
|
41
|
+
private readonly file;
|
|
42
|
+
private readonly tag;
|
|
43
|
+
constructor(file: string, tag: string);
|
|
44
|
+
log(message: string): void;
|
|
45
|
+
private rotate;
|
|
46
|
+
}
|
|
47
|
+
export declare function runHook(opts: HookRunOptions): Promise<void>;
|
|
48
|
+
//# sourceMappingURL=hook-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-runner.d.ts","sourceRoot":"","sources":["../../src/lib/hook-runner.ts"],"names":[],"mappings":"AAsBA,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAYjE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,qGAAqG;IACrG,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,sEAAsE;IACtE,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAA;CACvD;AAMD,qBAAa,UAAU;IAEnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBADH,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM;IAG9B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAW1B,OAAO,CAAC,MAAM;CAWf;AAeD,wBAAsB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDjE"}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, mkdirSync, readFileSync, rmdirSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
4
|
+
import { uploadSession } from './api-client.js';
|
|
5
|
+
import { getAuthHeaders, getCredentials } from './auth.js';
|
|
6
|
+
import { formatError } from './config-files.js';
|
|
7
|
+
import { claudeSessionFiles, findCodexRollout, sweepForClient, } from './hook-discovery.js';
|
|
8
|
+
import { detectClient, isNonEmptyFile, normalizeEvent, parseHookInput, providerForClient, readDotenvVar, resolveProjectRoot, } from './hook-input.js';
|
|
9
|
+
import { resolveTargetOrg } from './preflight.js';
|
|
10
|
+
import { findRepoBinding, gitToplevel } from './repo-binding.js';
|
|
11
|
+
import { runSkillsSync } from './skills-sync.js';
|
|
12
|
+
import { runSync } from './sync-runner.js';
|
|
13
|
+
import { hashFileContents, isUploaded, markUploaded, readSyncState, writeSyncState, } from './sync-state.js';
|
|
14
|
+
const LOG_MAX_BYTES = 1_048_576;
|
|
15
|
+
const LOG_KEEP_BYTES = 262_144;
|
|
16
|
+
const LOCK_STALE_MS = 10 * 60 * 1000;
|
|
17
|
+
export class HookLogger {
|
|
18
|
+
file;
|
|
19
|
+
tag;
|
|
20
|
+
constructor(file, tag) {
|
|
21
|
+
this.file = file;
|
|
22
|
+
this.tag = tag;
|
|
23
|
+
}
|
|
24
|
+
log(message) {
|
|
25
|
+
try {
|
|
26
|
+
mkdirSync(dirname(this.file), { recursive: true });
|
|
27
|
+
this.rotate();
|
|
28
|
+
const stamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
29
|
+
appendFileSync(this.file, `${stamp} [${this.tag}] ${message}\n`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Logging must never fail the hook.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
rotate() {
|
|
36
|
+
let size = 0;
|
|
37
|
+
try {
|
|
38
|
+
size = statSync(this.file).size;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (size <= LOG_MAX_BYTES)
|
|
44
|
+
return;
|
|
45
|
+
const content = readFileSync(this.file);
|
|
46
|
+
writeFileSync(this.file, content.subarray(content.length - LOG_KEEP_BYTES));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runHook(opts) {
|
|
50
|
+
if (opts.env['HALYARD_SESSION_UPLOAD'] === '0')
|
|
51
|
+
return;
|
|
52
|
+
const parsed = parseHookInput(opts.input);
|
|
53
|
+
const event = opts.event ?? normalizeEvent(parsed.event);
|
|
54
|
+
if (!event)
|
|
55
|
+
return;
|
|
56
|
+
const transcript = opts.transcript || parsed.transcriptPath || opts.env['CURSOR_TRANSCRIPT_PATH'] || '';
|
|
57
|
+
const client = opts.client ?? detectClient(transcript, opts.env);
|
|
58
|
+
const logger = new HookLogger(join(opts.halyardDir, 'hooks.log'), `${client}/${event}`);
|
|
59
|
+
const projectRoot = resolveProjectRoot([
|
|
60
|
+
opts.root,
|
|
61
|
+
opts.env['HALYARD_PROJECT_ROOT'],
|
|
62
|
+
opts.env['CLAUDE_PROJECT_DIR'],
|
|
63
|
+
opts.env['CURSOR_PROJECT_DIR'],
|
|
64
|
+
opts.env['GEMINI_PROJECT_DIR'],
|
|
65
|
+
opts.env['CODEX_CWD'],
|
|
66
|
+
parsed.cwd,
|
|
67
|
+
opts.cwd,
|
|
68
|
+
], opts.cwd);
|
|
69
|
+
const ctx = {
|
|
70
|
+
opts,
|
|
71
|
+
client,
|
|
72
|
+
event,
|
|
73
|
+
projectRoot,
|
|
74
|
+
binding: findRepoBinding(projectRoot),
|
|
75
|
+
sessionId: parsed.sessionId,
|
|
76
|
+
transcript,
|
|
77
|
+
logger,
|
|
78
|
+
};
|
|
79
|
+
if (!ensureToken(ctx))
|
|
80
|
+
return;
|
|
81
|
+
const target = await resolveTarget(ctx);
|
|
82
|
+
if (!target)
|
|
83
|
+
return;
|
|
84
|
+
let authHeaders;
|
|
85
|
+
try {
|
|
86
|
+
authHeaders = await getAuthHeaders(opts.apiUrl, ctx.binding);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
logger.log(`skip: ${formatError(err)}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const upload = { ...ctx, authHeaders, stateKey: target.stateKey };
|
|
93
|
+
logger.log(`root=${projectRoot} org=${target.label} transcript=${transcript || '<none>'} session=${parsed.sessionId || '<none>'}`);
|
|
94
|
+
const plan = event === 'end' ? endPlan(upload) : sweepForClient(client, sweepContext(upload));
|
|
95
|
+
await executePlan(upload, plan);
|
|
96
|
+
if (event === 'sweep')
|
|
97
|
+
await sweepSkills(upload);
|
|
98
|
+
logger.log(`${event} finished`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Session start also lifts this repo's committed skills (`.claude/skills`,
|
|
102
|
+
* `.agents/skills`, ...) into the bound org. Project scope only: personal
|
|
103
|
+
* `~/.claude/skills` are synced by `halyard skills sync`, never by a hook
|
|
104
|
+
* firing inside whichever org's repo happens to be open. The ledger makes
|
|
105
|
+
* this a no-op when nothing changed. HALYARD_SKILLS_SYNC=0 disables it.
|
|
106
|
+
*/
|
|
107
|
+
async function sweepSkills(ctx) {
|
|
108
|
+
if (ctx.opts.env['HALYARD_SKILLS_SYNC'] === '0')
|
|
109
|
+
return;
|
|
110
|
+
const tag = 'skills[project]';
|
|
111
|
+
// Only a real repository is a project root. The hook's project dir falls
|
|
112
|
+
// back to the raw cwd, and scanning `~` as a "project" would upload the
|
|
113
|
+
// user's personal skill directories as repo skills.
|
|
114
|
+
const projectRoot = ctx.binding?.rootDir ?? gitToplevel(ctx.projectRoot);
|
|
115
|
+
if (!projectRoot) {
|
|
116
|
+
ctx.logger.log(`${tag} skip: ${ctx.projectRoot} is not a git repository`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const summary = await runSkillsSync({
|
|
121
|
+
homeDir: ctx.opts.homeDir,
|
|
122
|
+
projectRoot,
|
|
123
|
+
scope: 'project',
|
|
124
|
+
stateKey: ctx.stateKey,
|
|
125
|
+
authHeaders: ctx.authHeaders,
|
|
126
|
+
apiUrl: ctx.opts.apiUrl,
|
|
127
|
+
dryRun: ctx.opts.dryRun,
|
|
128
|
+
force: false,
|
|
129
|
+
log: (line) => ctx.logger.log(`${tag} ${line.trim()}`),
|
|
130
|
+
});
|
|
131
|
+
ctx.logger.log(`${tag} found=${summary.found} created=${summary.created} updated=${summary.updated} unchanged=${summary.unchanged} skipped=${summary.skipped} failed=${summary.failed}`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
ctx.logger.log(`${tag} failed: ${formatError(err)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Token resolution: environment → repo .env → `halyard login` credentials. */
|
|
138
|
+
function ensureToken(ctx) {
|
|
139
|
+
const env = ctx.opts.env;
|
|
140
|
+
if (!env['HALYARD_TOKEN']) {
|
|
141
|
+
const fromRepo = readDotenvVar(join(ctx.projectRoot, '.env'), 'HALYARD_TOKEN');
|
|
142
|
+
if (fromRepo)
|
|
143
|
+
env['HALYARD_TOKEN'] = fromRepo;
|
|
144
|
+
}
|
|
145
|
+
if (env['HALYARD_TOKEN'] || getCredentials(ctx.binding))
|
|
146
|
+
return true;
|
|
147
|
+
ctx.logger.log(`skip: not authenticated (run \`halyard login\` in ${ctx.projectRoot}, or set HALYARD_TOKEN in the environment or ${ctx.projectRoot}/.env)`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Where uploads will land, and whether that agrees with the repo binding.
|
|
152
|
+
* Mirrors `runPreflight` for sync/push, except that a mismatch skips instead
|
|
153
|
+
* of failing: a hook must never break the agent.
|
|
154
|
+
*/
|
|
155
|
+
async function resolveTarget(ctx) {
|
|
156
|
+
let target;
|
|
157
|
+
try {
|
|
158
|
+
target = await (ctx.opts.resolveTarget ?? resolveTargetOrg)(ctx.opts.apiUrl);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
ctx.logger.log(`skip: could not resolve the destination org (${formatError(err)})`);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (!ctx.binding)
|
|
165
|
+
return target;
|
|
166
|
+
if (target.slug === null) {
|
|
167
|
+
ctx.logger.log(`note: repo is bound to '${ctx.binding.org}' but an API key's org cannot be verified; uploading to the key's org`);
|
|
168
|
+
return target;
|
|
169
|
+
}
|
|
170
|
+
if (target.slug === ctx.binding.org)
|
|
171
|
+
return target;
|
|
172
|
+
ctx.logger.log(`skip: credentials are for org '${target.slug}' but this repo is bound to '${ctx.binding.org}' (${ctx.binding.rootDir}/.halyard.json). Run \`halyard login\` inside the repo, or set HALYARD_TOKEN to a ${ctx.binding.org} API key in ${ctx.projectRoot}/.env`);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function sweepContext(ctx) {
|
|
176
|
+
return {
|
|
177
|
+
homeDir: ctx.opts.homeDir,
|
|
178
|
+
projectRoot: ctx.projectRoot,
|
|
179
|
+
sessionId: ctx.sessionId,
|
|
180
|
+
transcriptPath: ctx.transcript,
|
|
181
|
+
event: ctx.event,
|
|
182
|
+
env: ctx.opts.env,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function endPlan(ctx) {
|
|
186
|
+
if (ctx.transcript && isNonEmptyFile(ctx.transcript)) {
|
|
187
|
+
const files = ctx.client === 'claude' ? claudeSessionFiles(ctx.transcript) : [ctx.transcript];
|
|
188
|
+
const provider = providerForClient(ctx.client);
|
|
189
|
+
return { kind: 'files', targets: files.map((file) => ({ file, provider })) };
|
|
190
|
+
}
|
|
191
|
+
if (ctx.client === 'codex') {
|
|
192
|
+
const rollout = findCodexRollout(ctx.opts.homeDir, ctx.sessionId);
|
|
193
|
+
if (rollout)
|
|
194
|
+
return { kind: 'files', targets: [{ file: rollout, provider: 'codex' }] };
|
|
195
|
+
}
|
|
196
|
+
return sweepForClient(ctx.client, sweepContext(ctx));
|
|
197
|
+
}
|
|
198
|
+
/** The live session is uploaded at its own end (first upload wins server-side). */
|
|
199
|
+
function isLiveSession(ctx, session) {
|
|
200
|
+
if (!ctx.sessionId)
|
|
201
|
+
return false;
|
|
202
|
+
if (ctx.client === 'codex')
|
|
203
|
+
return session.filePath.endsWith(`-${ctx.sessionId}.jsonl`);
|
|
204
|
+
return session.sessionId === ctx.sessionId || session.filePath.includes(`/${ctx.sessionId}/`);
|
|
205
|
+
}
|
|
206
|
+
async function executePlan(ctx, plan) {
|
|
207
|
+
if (plan.kind === 'sync') {
|
|
208
|
+
const tag = `sync[${plan.provider}${plan.scoped ? ', repo-scoped' : ''}]`;
|
|
209
|
+
const summary = await runSync({
|
|
210
|
+
providerFilter: plan.provider,
|
|
211
|
+
scopeRootDir: plan.scoped ? (ctx.binding?.rootDir ?? ctx.projectRoot) : undefined,
|
|
212
|
+
exclude: (session) => isLiveSession(ctx, session),
|
|
213
|
+
stateKey: ctx.stateKey,
|
|
214
|
+
apiUrl: ctx.opts.apiUrl,
|
|
215
|
+
authHeaders: ctx.authHeaders,
|
|
216
|
+
dryRun: ctx.opts.dryRun,
|
|
217
|
+
log: (line) => ctx.logger.log(`${tag} ${line.trim()}`),
|
|
218
|
+
});
|
|
219
|
+
ctx.logger.log(`${tag} found=${summary.found} matched=${summary.matched} unresolved=${summary.unresolved} uploaded=${summary.uploaded} skipped=${summary.skipped} failed=${summary.failed}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (plan.targets.length === 0) {
|
|
223
|
+
ctx.logger.log('nothing to upload');
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const target of plan.targets) {
|
|
227
|
+
await pushFile(ctx, target);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function lockPath(halyardDir, file) {
|
|
231
|
+
const key = createHash('sha1').update(file).digest('hex').slice(0, 16);
|
|
232
|
+
return join(halyardDir, 'locks', key);
|
|
233
|
+
}
|
|
234
|
+
/** A per-file directory lock: safe for two hooks (repo + plugin) to race. A
|
|
235
|
+
* stale lock (crashed hook) is reclaimed after ten minutes. */
|
|
236
|
+
function acquireLock(halyardDir, file) {
|
|
237
|
+
const lock = lockPath(halyardDir, file);
|
|
238
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
239
|
+
try {
|
|
240
|
+
mkdirSync(lock);
|
|
241
|
+
return lock;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// fall through to the stale check
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
if (Date.now() - statSync(lock).mtimeMs < LOCK_STALE_MS)
|
|
248
|
+
return null;
|
|
249
|
+
rmdirSync(lock);
|
|
250
|
+
mkdirSync(lock);
|
|
251
|
+
return lock;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function releaseLock(lock) {
|
|
258
|
+
try {
|
|
259
|
+
rmdirSync(lock);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// already gone
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** The session id is the file name without extension — the same rule
|
|
266
|
+
* `halyard sync` uses, so hook uploads and later manual syncs dedupe together. */
|
|
267
|
+
async function pushFile(ctx, target) {
|
|
268
|
+
const { file, provider } = target;
|
|
269
|
+
if (!isNonEmptyFile(file))
|
|
270
|
+
return;
|
|
271
|
+
const content = readFileSync(file);
|
|
272
|
+
const hash = hashFileContents(content);
|
|
273
|
+
const state = readSyncState();
|
|
274
|
+
if (isUploaded(state, ctx.stateKey, file, hash)) {
|
|
275
|
+
ctx.logger.log(`skip (already uploaded): ${file}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const lock = acquireLock(ctx.opts.halyardDir, file);
|
|
279
|
+
if (!lock) {
|
|
280
|
+
ctx.logger.log(`skip (another hook is uploading): ${file}`);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
if (ctx.opts.dryRun) {
|
|
285
|
+
ctx.logger.log(`dry-run: would push [${provider}] ${file} (${content.length} bytes)`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const sessionId = basename(file, extname(file));
|
|
289
|
+
const result = await uploadSession(content, basename(file), provider, sessionId, ctx.authHeaders, ctx.opts.apiUrl);
|
|
290
|
+
markUploaded(state, ctx.stateKey, file, {
|
|
291
|
+
sessionId: result.sessionId,
|
|
292
|
+
uploadedAt: new Date().toISOString(),
|
|
293
|
+
size: content.length,
|
|
294
|
+
hash,
|
|
295
|
+
});
|
|
296
|
+
writeSyncState(state);
|
|
297
|
+
ctx.logger.log(`push[${provider}] ${result.status}: ${file} (${content.length} bytes)`);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
ctx.logger.log(`push[${provider}] failed: ${file} :: ${formatError(err)}`);
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
releaseLock(lock);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=hook-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook-runner.js","sourceRoot":"","sources":["../../src/lib/hook-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare class ApiError extends Error {
|
|
2
|
+
status: number;
|
|
3
|
+
body: string;
|
|
4
|
+
constructor(message: string, status: number, body: string);
|
|
5
|
+
}
|
|
6
|
+
export interface ApiFetchOptions {
|
|
7
|
+
method?: string;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
/** Injectable fetch for tests */
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Authenticated JSON request against the Halyard API.
|
|
14
|
+
* Wraps getAuthHeaders (token refresh + HALYARD_TOKEN handling) and maps
|
|
15
|
+
* error responses to actionable messages in one place.
|
|
16
|
+
*/
|
|
17
|
+
export declare function apiFetch<T>(apiUrl: string, path: string, options?: ApiFetchOptions): Promise<T>;
|
|
18
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAEA,qBAAa,QAAS,SAAQ,KAAK;IACjC,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;gBAEA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAM1D;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iCAAiC;IACjC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;CACzB;AAeD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,CAAC,CAAC,CAmCZ"}
|
package/dist/lib/http.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { getAuthHeaders } from './auth.js';
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'ApiError';
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** Extract the `{ error }` field every API route uses, falling back to raw text. */
|
|
13
|
+
function extractErrorMessage(body) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(body);
|
|
16
|
+
if (typeof parsed.error === 'string' && parsed.error.length > 0) {
|
|
17
|
+
return parsed.error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// not JSON
|
|
22
|
+
}
|
|
23
|
+
return body;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Authenticated JSON request against the Halyard API.
|
|
27
|
+
* Wraps getAuthHeaders (token refresh + HALYARD_TOKEN handling) and maps
|
|
28
|
+
* error responses to actionable messages in one place.
|
|
29
|
+
*/
|
|
30
|
+
export async function apiFetch(apiUrl, path, options = {}) {
|
|
31
|
+
const { method = 'GET', body, fetchImpl = fetch } = options;
|
|
32
|
+
const authHeaders = await getAuthHeaders(apiUrl);
|
|
33
|
+
const headers = { ...authHeaders };
|
|
34
|
+
if (body !== undefined) {
|
|
35
|
+
headers['Content-Type'] = 'application/json';
|
|
36
|
+
}
|
|
37
|
+
const res = await fetchImpl(`${apiUrl}${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers,
|
|
40
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const rawBody = await res.text();
|
|
44
|
+
if (res.status === 401) {
|
|
45
|
+
throw new ApiError('Authentication failed or token revoked. Run `halyard login` (or check HALYARD_TOKEN).', res.status, rawBody);
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 403) {
|
|
48
|
+
throw new ApiError(`Permission denied: ${extractErrorMessage(rawBody)}`, res.status, rawBody);
|
|
49
|
+
}
|
|
50
|
+
throw new ApiError(`Request failed (${res.status}): ${extractErrorMessage(rawBody)}`, res.status, rawBody);
|
|
51
|
+
}
|
|
52
|
+
return (await res.json());
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/lib/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,MAAM,CAAQ;IACd,IAAI,CAAQ;IAEZ,YAAY,OAAe,EAAE,MAAc,EAAE,IAAY;QACvD,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AASD,oFAAoF;AACpF,SAAS,mBAAmB,CAAC,IAAY;IACvC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAA;QACtD,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChE,OAAO,MAAM,CAAC,KAAK,CAAA;QACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,WAAW;IACb,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,MAAc,EACd,IAAY,EACZ,UAA2B,EAAE;IAE7B,MAAM,EAAE,MAAM,GAAG,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,KAAK,EAAE,GAAG,OAAO,CAAA;IAC3D,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,CAAA;IAEhD,MAAM,OAAO,GAA2B,EAAE,GAAG,WAAW,EAAE,CAAA;IAC1D,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,CAAC,cAAc,CAAC,GAAG,kBAAkB,CAAA;IAC9C,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,EAAE,EAAE;QAC9C,MAAM;QACN,OAAO;QACP,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;KAC5D,CAAC,CAAA;IAEF,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;QAChC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CAChB,uFAAuF,EACvF,GAAG,CAAC,MAAM,EACV,OAAO,CACR,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CAAC,sBAAsB,mBAAmB,CAAC,OAAO,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC/F,CAAC;QACD,MAAM,IAAI,QAAQ,CAChB,mBAAmB,GAAG,CAAC,MAAM,MAAM,mBAAmB,CAAC,OAAO,CAAC,EAAE,EACjE,GAAG,CAAC,MAAM,EACV,OAAO,CACR,CAAA;IACH,CAAC;IAED,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAA;AAChC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode (opencode.ai / sst) integration.
|
|
3
|
+
*
|
|
4
|
+
* OpenCode ≥1.x stores sessions in a SQLite database
|
|
5
|
+
* (`~/.local/share/opencode/opencode.db`) rather than per-session files.
|
|
6
|
+
* Instead of parsing their schema ourselves, we drive the `opencode` CLI:
|
|
7
|
+
* `opencode db "<sql>" --format json` for listing and `opencode export
|
|
8
|
+
* <sessionID>` for the full self-describing session document — both stable,
|
|
9
|
+
* first-class commands, and the export stays valid across their internal
|
|
10
|
+
* schema migrations.
|
|
11
|
+
*
|
|
12
|
+
* Everything here degrades to "no sessions" when opencode isn't installed
|
|
13
|
+
* or the commands fail — discovery must never break `halyard sync` for the
|
|
14
|
+
* other providers.
|
|
15
|
+
*/
|
|
16
|
+
export interface OpencodeSessionRef {
|
|
17
|
+
id: string;
|
|
18
|
+
timeUpdated: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function listOpencodeSessions(): OpencodeSessionRef[];
|
|
21
|
+
export declare function exportOpencodeSession(sessionId: string): Buffer;
|
|
22
|
+
//# sourceMappingURL=opencode-export.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-export.d.ts","sourceRoot":"","sources":["../../src/lib/opencode-export.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,EAAE,MAAM,CAAA;CACpB;AAWD,wBAAgB,oBAAoB,IAAI,kBAAkB,EAAE,CA2B3D;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAU/D"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
const LIST_QUERY = 'SELECT s.id AS id, s.time_updated AS time_updated FROM session s WHERE EXISTS (SELECT 1 FROM message m WHERE m.session_id = s.id)';
|
|
6
|
+
function opencodeDataDir() {
|
|
7
|
+
if (process.env.OPENCODE_DATA_DIR)
|
|
8
|
+
return process.env.OPENCODE_DATA_DIR;
|
|
9
|
+
const xdg = process.env.XDG_DATA_HOME;
|
|
10
|
+
return xdg ? join(xdg, 'opencode') : join(homedir(), '.local', 'share', 'opencode');
|
|
11
|
+
}
|
|
12
|
+
export function listOpencodeSessions() {
|
|
13
|
+
// Cheap pre-check: no database file means opencode was never used here.
|
|
14
|
+
if (!existsSync(join(opencodeDataDir(), 'opencode.db')))
|
|
15
|
+
return [];
|
|
16
|
+
let stdout;
|
|
17
|
+
try {
|
|
18
|
+
stdout = execFileSync('opencode', ['db', LIST_QUERY, '--format', 'json'], {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
21
|
+
timeout: 30_000,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const rows = JSON.parse(stdout);
|
|
29
|
+
if (!Array.isArray(rows))
|
|
30
|
+
return [];
|
|
31
|
+
return rows
|
|
32
|
+
.filter((r) => typeof r.id === 'string')
|
|
33
|
+
.map((r) => ({
|
|
34
|
+
id: r.id,
|
|
35
|
+
timeUpdated: typeof r.time_updated === 'number' ? r.time_updated : 0,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function exportOpencodeSession(sessionId) {
|
|
43
|
+
const stdout = execFileSync('opencode', ['export', sessionId], {
|
|
44
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
45
|
+
timeout: 60_000,
|
|
46
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
if (stdout.length === 0) {
|
|
49
|
+
throw new Error(`opencode export produced no output for session ${sessionId}`);
|
|
50
|
+
}
|
|
51
|
+
return stdout;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=opencode-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-export.js","sourceRoot":"","sources":["../../src/lib/opencode-export.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAuBjD,MAAM,UAAU,GACd,mIAAmI,CAAA;AAErI,SAAS,eAAe;IACtB,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAA;IACvE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAA;IACrC,OAAO,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,CAAC,CAAA;AACrF,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,wEAAwE;IACxE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,aAAa,CAAC,CAAC;QAAE,OAAO,EAAE,CAAA;IAElE,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE;YACxE,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;YACnC,OAAO,EAAE,MAAM;SAChB,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAA+C,CAAA;QAC7E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,CAAA;QACnC,OAAO,IAAI;aACR,MAAM,CAAC,CAAC,CAAC,EAA6C,EAAE,CAAC,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC;aAClF,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,EAAE,EAAE,CAAC,CAAC,EAAE;YACR,WAAW,EAAE,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;SACrE,CAAC,CAAC,CAAA;IACP,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,SAAiB;IACrD,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE;QAC7D,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;QACnC,OAAO,EAAE,MAAM;QACf,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;KAC5B,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,kDAAkD,SAAS,EAAE,CAAC,CAAA;IAChF,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface Column {
|
|
2
|
+
key: string;
|
|
3
|
+
header: string;
|
|
4
|
+
format?: (value: unknown) => string;
|
|
5
|
+
}
|
|
6
|
+
/** Render rows as a plain-ASCII table (space-padded columns, no borders). */
|
|
7
|
+
export declare function renderTable(rows: Record<string, unknown>[], columns: Column[]): string;
|
|
8
|
+
export declare function printTable(rows: Record<string, unknown>[], columns: Column[]): void;
|
|
9
|
+
export declare function printJson(data: unknown): void;
|
|
10
|
+
/** Format an ISO date for table cells; `-` for null/undefined. */
|
|
11
|
+
export declare function formatDate(value: unknown): string;
|
|
12
|
+
export interface ConfirmOptions {
|
|
13
|
+
force?: boolean;
|
|
14
|
+
/** Injectable prompt for tests; returns the raw answer string. */
|
|
15
|
+
promptImpl?: (question: string) => Promise<string>;
|
|
16
|
+
/** Injectable TTY check for tests. */
|
|
17
|
+
isTty?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Gate for destructive operations: prompt y/N by default, bypass with --force.
|
|
21
|
+
* When stdin is not a TTY, --force is required so scripts fail fast instead of
|
|
22
|
+
* hanging on an invisible prompt.
|
|
23
|
+
*/
|
|
24
|
+
export declare function confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
25
|
+
/** Print an error and exit non-zero. */
|
|
26
|
+
export declare function fail(message: string): never;
|
|
27
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAA;CACpC;AASD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAmBtF;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAEnF;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE7C;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMjD;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,kEAAkE;IAClE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAClD,sCAAsC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED;;;;GAIG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,OAAO,CAAC,CAsB7F;AAED,wCAAwC;AACxC,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAG3C"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
function cell(row, column) {
|
|
3
|
+
const value = row[column.key];
|
|
4
|
+
if (column.format)
|
|
5
|
+
return column.format(value);
|
|
6
|
+
if (value === null || value === undefined || value === '')
|
|
7
|
+
return '-';
|
|
8
|
+
return String(value);
|
|
9
|
+
}
|
|
10
|
+
/** Render rows as a plain-ASCII table (space-padded columns, no borders). */
|
|
11
|
+
export function renderTable(rows, columns) {
|
|
12
|
+
const widths = columns.map((col) => Math.max(col.header.length, ...rows.map((row) => cell(row, col).length)));
|
|
13
|
+
const lines = [
|
|
14
|
+
columns
|
|
15
|
+
.map((col, i) => col.header.padEnd(widths[i]))
|
|
16
|
+
.join(' ')
|
|
17
|
+
.trimEnd(),
|
|
18
|
+
];
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
lines.push(columns
|
|
21
|
+
.map((col, i) => cell(row, col).padEnd(widths[i]))
|
|
22
|
+
.join(' ')
|
|
23
|
+
.trimEnd());
|
|
24
|
+
}
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
export function printTable(rows, columns) {
|
|
28
|
+
console.log(renderTable(rows, columns));
|
|
29
|
+
}
|
|
30
|
+
export function printJson(data) {
|
|
31
|
+
console.log(JSON.stringify(data, null, 2));
|
|
32
|
+
}
|
|
33
|
+
/** Format an ISO date for table cells; `-` for null/undefined. */
|
|
34
|
+
export function formatDate(value) {
|
|
35
|
+
if (!value)
|
|
36
|
+
return '-';
|
|
37
|
+
const date = new Date(String(value));
|
|
38
|
+
if (Number.isNaN(date.getTime()))
|
|
39
|
+
return '-';
|
|
40
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
41
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Gate for destructive operations: prompt y/N by default, bypass with --force.
|
|
45
|
+
* When stdin is not a TTY, --force is required so scripts fail fast instead of
|
|
46
|
+
* hanging on an invisible prompt.
|
|
47
|
+
*/
|
|
48
|
+
export async function confirm(message, options = {}) {
|
|
49
|
+
if (options.force)
|
|
50
|
+
return true;
|
|
51
|
+
const isTty = options.isTty ?? process.stdin.isTTY === true;
|
|
52
|
+
if (!isTty && !options.promptImpl) {
|
|
53
|
+
console.error(`${message}\nRefusing to prompt without a TTY. Pass --force to proceed.`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const ask = options.promptImpl ??
|
|
57
|
+
(async (question) => {
|
|
58
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
try {
|
|
60
|
+
return await rl.question(question);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
rl.close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const answer = await ask(`${message} [y/N] `);
|
|
67
|
+
return answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes';
|
|
68
|
+
}
|
|
69
|
+
/** Print an error and exit non-zero. */
|
|
70
|
+
export function fail(message) {
|
|
71
|
+
console.error(message);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=output.js.map
|