@blogic-cz/agent-tools 1.0.0 → 1.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 +12 -0
- package/dist/gh-tool/gist.d.ts +102 -0
- package/dist/gh-tool/gist.d.ts.map +1 -0
- package/dist/gh-tool/text-input.d.ts +1 -0
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/dist/session-tool/index.d.ts.map +1 -1
- package/dist/session-tool/service.d.ts +0 -1
- package/dist/session-tool/service.d.ts.map +1 -1
- package/dist/session-tool/summaries.d.ts +12 -0
- package/dist/session-tool/summaries.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/gh-tool/gist.ts +625 -0
- package/src/gh-tool/index.ts +21 -1
- package/src/gh-tool/text-input.ts +10 -2
- package/src/session-tool/index.ts +11 -6
- package/src/session-tool/service.ts +0 -8
- package/src/session-tool/summaries.ts +19 -0
package/README.md
CHANGED
|
@@ -282,6 +282,18 @@ Two optional scope guards sit on top. A profile keyed `prod`/`production`, or ca
|
|
|
282
282
|
|
|
283
283
|
`azdo-tool` keeps its allowlist of read-only operations (`list`, `run`, `show`, `show-tags`) and rejects `create`/`delete`/`update`/`cancel`/`queue` anywhere in the command. `run` is accepted only in the `pipelines` group — `acr run` and `acr task run` execute arbitrary commands in Azure and are blocked. The `acr` and `account` groups remain reachable from `azdo-tool` for backwards compatibility; new work should use `az-tool` for them. Because those two groups address the Azure platform rather than Azure DevOps, the platform credential rules apply to them here as well — `acr credential show` is refused by both tools. The verb and segment lists behind that live in `src/shared/azure-credentials.ts` so the two tools cannot drift apart.
|
|
284
284
|
|
|
285
|
+
### Gist commands
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
bun gh-tool gist list
|
|
289
|
+
bun gh-tool gist view --id <gist-id>
|
|
290
|
+
bun gh-tool gist create --body "snippet" --filename example.txt
|
|
291
|
+
bun gh-tool gist edit --id <gist-id> --body "updated" --filename example.txt
|
|
292
|
+
bun gh-tool gist delete --id <gist-id> --confirm
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Gists are secret by default. `gist delete` prints a dry run unless `--confirm` is passed. `gist edit` requires a content or mutation flag and never opens an editor.
|
|
296
|
+
|
|
285
297
|
### gh-tool machine contracts
|
|
286
298
|
|
|
287
299
|
`pr view` adds `headSha` and `baseSha`; failed-check evidence adds the same SHA pair. Review summaries, inline comments, and threads add `commitSha` plus `feedbackOrigin`: `current_head` only for an exact `commitSha === headSha`, `pre_existing` for a different known SHA (not an obsolescence verdict), and `unknown` when either SHA is absent. Issue comments always use `commitSha: null` and `feedbackOrigin: unknown`. `review-triage` preserves existing fields and adds `inlineComments` plus per-kind `feedbackOriginCounts`; batch triage returns the same object per PR. `pr request-review --reviewers alice,bob` emits sorted `submittedReviewers` (normalized input), `newlyRequested` and `alreadyPending` (the submitted logins split by whether a pending request already existed, so a fresh re-request is distinguishable from a no-op), and `requestedReviewers` (GitHub-confirmed result). `pr last-human-reviewer` derives `currentRequestedReviewers` from live `reviewRequests` only; timeline events are not replayed because GitHub clears a pending request on review submit without emitting `ReviewRequestRemovedEvent`.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Command } from "effect/unstable/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { GitHubCommandError } from "./errors";
|
|
4
|
+
import { GitHubService } from "./service";
|
|
5
|
+
type GistApiFile = {
|
|
6
|
+
filename: string;
|
|
7
|
+
language: string | null;
|
|
8
|
+
type: string;
|
|
9
|
+
size: number;
|
|
10
|
+
truncated?: boolean;
|
|
11
|
+
content?: string;
|
|
12
|
+
};
|
|
13
|
+
type GistApi = {
|
|
14
|
+
id: string;
|
|
15
|
+
description: string | null;
|
|
16
|
+
public: boolean;
|
|
17
|
+
html_url: string;
|
|
18
|
+
created_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
owner: {
|
|
21
|
+
login: string;
|
|
22
|
+
} | null;
|
|
23
|
+
files: Record<string, GistApiFile>;
|
|
24
|
+
};
|
|
25
|
+
type GistFile = {
|
|
26
|
+
filename: string;
|
|
27
|
+
language: string | null;
|
|
28
|
+
size: number;
|
|
29
|
+
truncated: boolean;
|
|
30
|
+
content?: string;
|
|
31
|
+
};
|
|
32
|
+
type GistDetail = {
|
|
33
|
+
id: string;
|
|
34
|
+
description: string | null;
|
|
35
|
+
public: boolean;
|
|
36
|
+
url: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
updatedAt: string;
|
|
39
|
+
owner: string | null;
|
|
40
|
+
files: GistFile[];
|
|
41
|
+
};
|
|
42
|
+
type GistCreateResult = {
|
|
43
|
+
created: true;
|
|
44
|
+
id: string;
|
|
45
|
+
url: string;
|
|
46
|
+
public: boolean;
|
|
47
|
+
files: string[];
|
|
48
|
+
};
|
|
49
|
+
export declare const toGistDetail: (gist: GistApi, opts: {
|
|
50
|
+
filename: string | null;
|
|
51
|
+
withContent: boolean;
|
|
52
|
+
}) => GistDetail;
|
|
53
|
+
export declare const validateBodyFilename: (body: string | null, filename: string | null, command: string) => GitHubCommandError | null;
|
|
54
|
+
export declare const validateEditInput: (opts: {
|
|
55
|
+
body: string | null;
|
|
56
|
+
filename: string | null;
|
|
57
|
+
description: string | null;
|
|
58
|
+
add: string | null;
|
|
59
|
+
remove: string | null;
|
|
60
|
+
}) => GitHubCommandError | null;
|
|
61
|
+
export declare const createGist: (opts: {
|
|
62
|
+
paths: string[];
|
|
63
|
+
description: string | null;
|
|
64
|
+
public: boolean;
|
|
65
|
+
}) => Effect.Effect<GistCreateResult, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
66
|
+
export declare const gistListCommand: Command.Command<"list", {
|
|
67
|
+
readonly format: "json" | "toon";
|
|
68
|
+
readonly limit: number;
|
|
69
|
+
readonly visibility: Option.Option<"public" | "secret">;
|
|
70
|
+
}, {}, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
71
|
+
export declare const gistViewCommand: Command.Command<"view", {
|
|
72
|
+
readonly filename: Option.Option<string>;
|
|
73
|
+
readonly format: "json" | "toon";
|
|
74
|
+
readonly id: string;
|
|
75
|
+
readonly metadataOnly: boolean;
|
|
76
|
+
}, {}, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
77
|
+
export declare const gistCreateCommand: Command.Command<"create", {
|
|
78
|
+
readonly body: Option.Option<string>;
|
|
79
|
+
readonly bodyFile: Option.Option<string>;
|
|
80
|
+
readonly desc: Option.Option<string>;
|
|
81
|
+
readonly filename: Option.Option<string>;
|
|
82
|
+
readonly files: Option.Option<string>;
|
|
83
|
+
readonly format: "json" | "toon";
|
|
84
|
+
readonly public: boolean;
|
|
85
|
+
}, {}, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
86
|
+
export declare const gistEditCommand: Command.Command<"edit", {
|
|
87
|
+
readonly add: Option.Option<string>;
|
|
88
|
+
readonly body: Option.Option<string>;
|
|
89
|
+
readonly bodyFile: Option.Option<string>;
|
|
90
|
+
readonly desc: Option.Option<string>;
|
|
91
|
+
readonly filename: Option.Option<string>;
|
|
92
|
+
readonly format: "json" | "toon";
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly remove: Option.Option<string>;
|
|
95
|
+
}, {}, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
96
|
+
export declare const gistDeleteCommand: Command.Command<"delete", {
|
|
97
|
+
readonly confirm: boolean;
|
|
98
|
+
readonly format: "json" | "toon";
|
|
99
|
+
readonly id: string;
|
|
100
|
+
}, {}, GitHubCommandError | import("./errors").GitHubNotFoundError | import("./errors").GitHubAuthError, GitHubService>;
|
|
101
|
+
export {};
|
|
102
|
+
//# sourceMappingURL=gist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gist.d.ts","sourceRoot":"","sources":["../../src/gh-tool/gist.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, Schema } from "effect";
|
|
2
2
|
import { GitHubCommandError } from "#gh/errors";
|
|
3
3
|
declare const MissingMode: Schema.Literals<readonly ["error", "null", "default"]>;
|
|
4
|
+
export declare const isSensitivePath: (filePath: string) => boolean;
|
|
4
5
|
type ResolveTextInputOptions = {
|
|
5
6
|
command: string;
|
|
6
7
|
value: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/gh-tool/text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/gh-tool/text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAShD,QAAA,MAAM,WAAW,wDAAgD,CAAC;AAIlE,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,YACmB,CAAC;AAkBpE,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,WAAW,CAAC,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAsFF,eAAO,MAAM,wBAAwB,GACnC,SAAS,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,KACrE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAIuD,CAAC;AAEnG,eAAO,MAAM,wBAAwB,GACnC,SAAS,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,KACrE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,kBAAkB,CAI9C,CAAC;AAEL,eAAO,MAAM,uBAAuB,GAClC,SAAS,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,GAAG;IACvE,YAAY,EAAE,MAAM,CAAC;CACtB,KACA,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAKsD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session-tool/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAQ/C,OAAO,EAAE,aAAa,EAAsB,MAAM,UAAU,CAAC;AAE7D,OAAO,EAAc,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/session-tool/index.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,EAAY,OAAO,EAAQ,MAAM,qBAAqB,CAAC;AAE9D,OAAO,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAQ/C,OAAO,EAAE,aAAa,EAAsB,MAAM,UAAU,CAAC;AAE7D,OAAO,EAAc,cAAc,EAAuB,MAAM,WAAW,CAAC;AAsT5E,eAAO,MAAM,GAAG,8JAEd,CAAC"}
|
|
@@ -3,7 +3,6 @@ import type { MessageSummary, SessionSource, SessionSummary } from "./types";
|
|
|
3
3
|
import { ResolvedPaths } from "./config";
|
|
4
4
|
import { type SessionError } from "./errors";
|
|
5
5
|
export declare const formatDate: (timestamp: number) => string;
|
|
6
|
-
export declare const truncate: (value: string, maxLen: number) => string;
|
|
7
6
|
declare const SessionService_base: Context.ServiceClass<SessionService, "@agent-tools/SessionService", {
|
|
8
7
|
readonly getSessionsForProject: (projectDir: string | null, sources?: ReadonlySet<SessionSource>) => Effect.Effect<Set<string>, SessionError>;
|
|
9
8
|
readonly getPiSessionSummaries: (filterSessions: Set<string> | null) => Effect.Effect<SessionSummary[], SessionError>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/session-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIhD,OAAO,KAAK,EAAE,cAAc,EAAe,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAO1F,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAiD,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAU5F,eAAO,MAAM,UAAU,GAAI,WAAW,MAAM,KAAG,MAW9C,CAAC
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/session-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAIhD,OAAO,KAAK,EAAE,cAAc,EAAe,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAO1F,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAiD,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAU5F,eAAO,MAAM,UAAU,GAAI,WAAW,MAAM,KAAG,MAW9C,CAAC;;oCA6HkC,CAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,OAAO,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;oCACb,CAC9B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,KAC/B,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,YAAY,CAAC;kCACpB,CAC5B,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,EAClC,OAAO,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,YAAY,CAAC;8BACxB,CAAC,SAAS,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,MAAM,KAAK,cAAc,EAAE;;AAd9F,qBAAa,cAAe,SAAQ,mBAgBF;IAChC,MAAM,CAAC,QAAQ,CAAC,KAAK,oDAsPnB;CACH;AAED,eAAO,MAAM,mBAAmB,mDAAuB,CAAC"}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { MessageSummary, SessionSummary } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Shapes a message body for output. A body silently cut to 500 chars was
|
|
4
|
+
* indistinguishable from a short one, so callers read "no match in this snippet" as
|
|
5
|
+
* "not in this session". When the body is cut, say so and report its real length.
|
|
6
|
+
*
|
|
7
|
+
* maxBodyChars <= 0 returns the full body.
|
|
8
|
+
*/
|
|
9
|
+
export declare const shapeBody: (body: string, maxBodyChars: number) => {
|
|
10
|
+
body: string;
|
|
11
|
+
bodyLength?: number;
|
|
12
|
+
truncated?: true;
|
|
13
|
+
};
|
|
2
14
|
export declare const sessionSummariesFromMessages: (summaries: MessageSummary[]) => SessionSummary[];
|
|
3
15
|
export declare const projectSessionFilter: (sessionsBySource: ReadonlyMap<SessionSummary["source"], Set<string>>, source: SessionSummary["source"], allProjects: boolean) => Set<string> | null;
|
|
4
16
|
export declare const sortSessionSummaries: (summaries: SessionSummary[]) => SessionSummary[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summaries.d.ts","sourceRoot":"","sources":["../../src/session-tool/summaries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9D,eAAO,MAAM,4BAA4B,GAAI,WAAW,cAAc,EAAE,KAAG,cAAc,EAwBxF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,kBAAkB,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EACpE,QAAQ,cAAc,CAAC,QAAQ,CAAC,EAChC,aAAa,OAAO,KACnB,GAAG,CAAC,MAAM,CAAC,GAAG,IAA0E,CAAC;AAE5F,eAAO,MAAM,oBAAoB,GAAI,WAAW,cAAc,EAAE,KAAG,cAAc,EAO9E,CAAC"}
|
|
1
|
+
{"version":3,"file":"summaries.d.ts","sourceRoot":"","sources":["../../src/session-tool/summaries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9D;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GACpB,MAAM,MAAM,EACZ,cAAc,MAAM,KACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAOvD,CAAC;AAEF,eAAO,MAAM,4BAA4B,GAAI,WAAW,cAAc,EAAE,KAAG,cAAc,EAwBxF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,kBAAkB,WAAW,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,EACpE,QAAQ,cAAc,CAAC,QAAQ,CAAC,EAChC,aAAa,OAAO,KACnB,GAAG,CAAC,MAAM,CAAC,GAAG,IAA0E,CAAC;AAE5F,eAAO,MAAM,oBAAoB,GAAI,WAAW,cAAc,EAAE,KAAG,cAAc,EAO9E,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { Command, Flag } from "effect/unstable/cli";
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { formatOption, logFormatted } from "#shared";
|
|
6
|
+
import { isSensitivePath, resolveOptionalTextInput } from "#gh/text-input";
|
|
7
|
+
import { GitHubCommandError } from "./errors";
|
|
8
|
+
import { GitHubService } from "./service";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
// `gh gist list/view` have no --json, so reads go through `gh api gists` (REST shape).
|
|
15
|
+
type GistApiFile = {
|
|
16
|
+
filename: string;
|
|
17
|
+
language: string | null;
|
|
18
|
+
type: string;
|
|
19
|
+
size: number;
|
|
20
|
+
truncated?: boolean;
|
|
21
|
+
content?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type GistApi = {
|
|
25
|
+
id: string;
|
|
26
|
+
description: string | null;
|
|
27
|
+
public: boolean;
|
|
28
|
+
html_url: string;
|
|
29
|
+
created_at: string;
|
|
30
|
+
updated_at: string;
|
|
31
|
+
owner: { login: string } | null;
|
|
32
|
+
files: Record<string, GistApiFile>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type GistListItem = {
|
|
36
|
+
id: string;
|
|
37
|
+
description: string | null;
|
|
38
|
+
public: boolean;
|
|
39
|
+
url: string;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
files: string[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type GistFile = {
|
|
46
|
+
filename: string;
|
|
47
|
+
language: string | null;
|
|
48
|
+
size: number;
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
content?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type GistDetail = {
|
|
54
|
+
id: string;
|
|
55
|
+
description: string | null;
|
|
56
|
+
public: boolean;
|
|
57
|
+
url: string;
|
|
58
|
+
createdAt: string;
|
|
59
|
+
updatedAt: string;
|
|
60
|
+
owner: string | null;
|
|
61
|
+
files: GistFile[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type GistCreateResult = {
|
|
65
|
+
created: true;
|
|
66
|
+
id: string;
|
|
67
|
+
url: string;
|
|
68
|
+
public: boolean;
|
|
69
|
+
files: string[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type GistEditResult = {
|
|
73
|
+
edited: true;
|
|
74
|
+
id: string;
|
|
75
|
+
changes: string[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type GistDeleteResult = {
|
|
79
|
+
deleted: boolean;
|
|
80
|
+
id: string;
|
|
81
|
+
dryRun?: true;
|
|
82
|
+
message?: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Helpers
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
const inputError = (message: string, command: string) =>
|
|
90
|
+
new GitHubCommandError({
|
|
91
|
+
message,
|
|
92
|
+
command,
|
|
93
|
+
exitCode: 1,
|
|
94
|
+
stderr: message,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const parsePaths = (value: string, command: string) => {
|
|
98
|
+
const paths = value.split(",").map((path) => path.trim());
|
|
99
|
+
|
|
100
|
+
if (paths.length === 0 || paths.some((path) => path.length === 0)) {
|
|
101
|
+
return Effect.fail(
|
|
102
|
+
inputError(`--files must be comma-separated paths without empty segments: ${value}`, command),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sensitivePath = paths.find((path) => validateFilePath(path, command) !== null);
|
|
107
|
+
if (sensitivePath !== undefined) {
|
|
108
|
+
const validation = validateFilePath(sensitivePath, command);
|
|
109
|
+
if (validation !== null) return Effect.fail(validation);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Effect.succeed(paths);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const validateFilePath = (path: string, command: string) =>
|
|
116
|
+
isSensitivePath(path) ? inputError(`Refusing to read sensitive file: ${path}`, command) : null;
|
|
117
|
+
|
|
118
|
+
// `gh gist create/edit` read content from files only; inline --body is staged in a temp file
|
|
119
|
+
// whose basename becomes the gist filename.
|
|
120
|
+
const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
121
|
+
body: string;
|
|
122
|
+
filename: string;
|
|
123
|
+
command: string;
|
|
124
|
+
}) {
|
|
125
|
+
const directory = yield* Effect.acquireRelease(
|
|
126
|
+
Effect.tryPromise({
|
|
127
|
+
try: async () => {
|
|
128
|
+
const proc = Bun.spawn(
|
|
129
|
+
["mktemp", "-d", join(process.env.TMPDIR ?? "/tmp", "gh-tool-gist-XXXXXX")],
|
|
130
|
+
{ stdout: "pipe", stderr: "pipe" },
|
|
131
|
+
);
|
|
132
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
133
|
+
proc.exited,
|
|
134
|
+
new Response(proc.stdout).text(),
|
|
135
|
+
new Response(proc.stderr).text(),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
if (exitCode !== 0) {
|
|
139
|
+
throw new Error(stderr.trim() || `mktemp exited with code ${exitCode}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return stdout.trim();
|
|
143
|
+
},
|
|
144
|
+
catch: (error) =>
|
|
145
|
+
inputError(
|
|
146
|
+
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
147
|
+
opts.command,
|
|
148
|
+
),
|
|
149
|
+
}),
|
|
150
|
+
(tempDirectory) =>
|
|
151
|
+
Effect.tryPromise({
|
|
152
|
+
try: async () => {
|
|
153
|
+
const proc = Bun.spawn(["rm", "-rf", tempDirectory], {
|
|
154
|
+
stdout: "ignore",
|
|
155
|
+
stderr: "ignore",
|
|
156
|
+
});
|
|
157
|
+
await proc.exited;
|
|
158
|
+
},
|
|
159
|
+
catch: () => undefined,
|
|
160
|
+
}).pipe(Effect.ignore),
|
|
161
|
+
);
|
|
162
|
+
const path = join(directory, opts.filename);
|
|
163
|
+
|
|
164
|
+
yield* Effect.tryPromise({
|
|
165
|
+
try: () => Bun.write(path, opts.body),
|
|
166
|
+
catch: (error) =>
|
|
167
|
+
inputError(
|
|
168
|
+
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
169
|
+
opts.command,
|
|
170
|
+
),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return path;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const toListItem = (gist: GistApi): GistListItem => ({
|
|
177
|
+
id: gist.id,
|
|
178
|
+
description: gist.description,
|
|
179
|
+
public: gist.public,
|
|
180
|
+
url: gist.html_url,
|
|
181
|
+
createdAt: gist.created_at,
|
|
182
|
+
updatedAt: gist.updated_at,
|
|
183
|
+
files: Object.keys(gist.files ?? {}),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const toGistDetail = (
|
|
187
|
+
gist: GistApi,
|
|
188
|
+
opts: { filename: string | null; withContent: boolean },
|
|
189
|
+
): GistDetail => {
|
|
190
|
+
const files = Object.values(gist.files ?? {})
|
|
191
|
+
.filter((file) => opts.filename === null || file.filename === opts.filename)
|
|
192
|
+
.map((file) => {
|
|
193
|
+
const result: GistFile = {
|
|
194
|
+
filename: file.filename,
|
|
195
|
+
language: file.language,
|
|
196
|
+
size: file.size,
|
|
197
|
+
truncated: file.truncated ?? false,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (opts.withContent) {
|
|
201
|
+
result.content = file.content ?? "";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return result;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
id: gist.id,
|
|
209
|
+
description: gist.description,
|
|
210
|
+
public: gist.public,
|
|
211
|
+
url: gist.html_url,
|
|
212
|
+
createdAt: gist.created_at,
|
|
213
|
+
updatedAt: gist.updated_at,
|
|
214
|
+
owner: gist.owner?.login ?? null,
|
|
215
|
+
files,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const validateBodyFilename = (
|
|
220
|
+
body: string | null,
|
|
221
|
+
filename: string | null,
|
|
222
|
+
command: string,
|
|
223
|
+
): GitHubCommandError | null => {
|
|
224
|
+
if (body === null) return null;
|
|
225
|
+
if (filename === null) {
|
|
226
|
+
return inputError("--filename is required with --body/--body-file", command);
|
|
227
|
+
}
|
|
228
|
+
if (filename.length === 0 || filename === "." || filename === ".." || /[\\/]/.test(filename)) {
|
|
229
|
+
return inputError("--filename must be a file name without path separators", command);
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const validateEditInput = (opts: {
|
|
235
|
+
body: string | null;
|
|
236
|
+
filename: string | null;
|
|
237
|
+
description: string | null;
|
|
238
|
+
add: string | null;
|
|
239
|
+
remove: string | null;
|
|
240
|
+
}): GitHubCommandError | null => {
|
|
241
|
+
const command = "gh-tool gist edit";
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
opts.body === null &&
|
|
245
|
+
opts.description === null &&
|
|
246
|
+
opts.add === null &&
|
|
247
|
+
opts.remove === null
|
|
248
|
+
) {
|
|
249
|
+
return inputError(
|
|
250
|
+
"Provide at least one of --body/--body-file (with --filename), --desc, --add, or --remove",
|
|
251
|
+
command,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return validateBodyFilename(opts.body, opts.filename, command);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Internal handlers
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
const listGists = Effect.fn("gist.listGists")(function* (opts: {
|
|
263
|
+
limit: number;
|
|
264
|
+
visibility: string | null;
|
|
265
|
+
}) {
|
|
266
|
+
const gh = yield* GitHubService;
|
|
267
|
+
|
|
268
|
+
const gists = yield* gh.runGhJson<GistApi[]>(["api", `gists?per_page=${opts.limit}`]);
|
|
269
|
+
|
|
270
|
+
return gists
|
|
271
|
+
.filter(
|
|
272
|
+
(gist) =>
|
|
273
|
+
opts.visibility === null || (opts.visibility === "public" ? gist.public : !gist.public),
|
|
274
|
+
)
|
|
275
|
+
.map(toListItem);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const viewGist = Effect.fn("gist.viewGist")(function* (opts: {
|
|
279
|
+
id: string;
|
|
280
|
+
filename: string | null;
|
|
281
|
+
withContent: boolean;
|
|
282
|
+
}) {
|
|
283
|
+
const gh = yield* GitHubService;
|
|
284
|
+
|
|
285
|
+
const gist = yield* gh.runGhJson<GistApi>(["api", `gists/${opts.id}`]);
|
|
286
|
+
|
|
287
|
+
return toGistDetail(gist, { filename: opts.filename, withContent: opts.withContent });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
export const createGist = Effect.fn("gist.createGist")(function* (opts: {
|
|
291
|
+
paths: string[];
|
|
292
|
+
description: string | null;
|
|
293
|
+
public: boolean;
|
|
294
|
+
}) {
|
|
295
|
+
const gh = yield* GitHubService;
|
|
296
|
+
|
|
297
|
+
const args = ["gist", "create", ...opts.paths];
|
|
298
|
+
|
|
299
|
+
if (opts.description !== null) {
|
|
300
|
+
args.push("--desc", opts.description);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (opts.public) {
|
|
304
|
+
args.push("--public");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = yield* gh.runGh(args);
|
|
308
|
+
const url =
|
|
309
|
+
result.stdout
|
|
310
|
+
.trim()
|
|
311
|
+
.split("\n")
|
|
312
|
+
.map((line) => line.trim())
|
|
313
|
+
.findLast((line) => line.startsWith("https://")) ?? "";
|
|
314
|
+
|
|
315
|
+
if (url === "") {
|
|
316
|
+
return yield* Effect.fail(
|
|
317
|
+
inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const created: GistCreateResult = {
|
|
322
|
+
created: true,
|
|
323
|
+
id: url.split("/").at(-1) ?? "",
|
|
324
|
+
url,
|
|
325
|
+
public: opts.public,
|
|
326
|
+
files: opts.paths.map((path) => path.split("/").at(-1) ?? path),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
return created;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
333
|
+
id: string;
|
|
334
|
+
description: string | null;
|
|
335
|
+
add: string | null;
|
|
336
|
+
remove: string | null;
|
|
337
|
+
filename: string | null;
|
|
338
|
+
sourcePath: string | null;
|
|
339
|
+
}) {
|
|
340
|
+
const gh = yield* GitHubService;
|
|
341
|
+
|
|
342
|
+
const args = ["gist", "edit", opts.id];
|
|
343
|
+
const changes: string[] = [];
|
|
344
|
+
|
|
345
|
+
if (opts.sourcePath !== null) {
|
|
346
|
+
args.push(opts.sourcePath);
|
|
347
|
+
changes.push(`content:${opts.filename ?? opts.sourcePath.split("/").at(-1)}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (opts.description !== null) {
|
|
351
|
+
args.push("--desc", opts.description);
|
|
352
|
+
changes.push("description");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (opts.add !== null) {
|
|
356
|
+
args.push("--add", opts.add);
|
|
357
|
+
changes.push(`add:${opts.add.split("/").at(-1)}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (opts.remove !== null) {
|
|
361
|
+
args.push("--remove", opts.remove);
|
|
362
|
+
changes.push(`remove:${opts.remove}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (opts.filename !== null) {
|
|
366
|
+
args.push("--filename", opts.filename);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
yield* gh.runGh(args);
|
|
370
|
+
|
|
371
|
+
const edited: GistEditResult = { edited: true, id: opts.id, changes };
|
|
372
|
+
|
|
373
|
+
return edited;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const deleteGist = Effect.fn("gist.deleteGist")(function* (opts: { id: string; confirm: boolean }) {
|
|
377
|
+
const gh = yield* GitHubService;
|
|
378
|
+
|
|
379
|
+
if (!opts.confirm) {
|
|
380
|
+
const dryRun: GistDeleteResult = {
|
|
381
|
+
deleted: false,
|
|
382
|
+
id: opts.id,
|
|
383
|
+
dryRun: true,
|
|
384
|
+
message: `Would delete gist ${opts.id}. Re-run with --confirm to execute.`,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return dryRun;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
yield* gh.runGh(["gist", "delete", opts.id, "--yes"]);
|
|
391
|
+
|
|
392
|
+
const deleted: GistDeleteResult = { deleted: true, id: opts.id };
|
|
393
|
+
|
|
394
|
+
return deleted;
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Commands
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
export const gistListCommand = Command.make(
|
|
402
|
+
"list",
|
|
403
|
+
{
|
|
404
|
+
format: formatOption,
|
|
405
|
+
limit: Flag.integer("limit").pipe(
|
|
406
|
+
Flag.withDescription("Maximum number of gists to return"),
|
|
407
|
+
Flag.withDefault(10),
|
|
408
|
+
),
|
|
409
|
+
visibility: Flag.choice("visibility", ["public", "secret"]).pipe(
|
|
410
|
+
Flag.withDescription("Filter by visibility: public or secret"),
|
|
411
|
+
Flag.optional,
|
|
412
|
+
),
|
|
413
|
+
},
|
|
414
|
+
({ format, limit, visibility }) =>
|
|
415
|
+
Effect.gen(function* () {
|
|
416
|
+
const requested = Option.getOrNull(visibility);
|
|
417
|
+
|
|
418
|
+
const gists = yield* listGists({ limit, visibility: requested });
|
|
419
|
+
|
|
420
|
+
yield* logFormatted(gists, format);
|
|
421
|
+
}),
|
|
422
|
+
).pipe(Command.withDescription("List your gists (id, description, visibility, file names)"));
|
|
423
|
+
|
|
424
|
+
export const gistViewCommand = Command.make(
|
|
425
|
+
"view",
|
|
426
|
+
{
|
|
427
|
+
filename: Flag.string("filename").pipe(
|
|
428
|
+
Flag.withDescription("Return only this file from the gist"),
|
|
429
|
+
Flag.optional,
|
|
430
|
+
),
|
|
431
|
+
format: formatOption,
|
|
432
|
+
id: Flag.string("id").pipe(Flag.withDescription("Gist id or URL")),
|
|
433
|
+
metadataOnly: Flag.boolean("metadata-only").pipe(
|
|
434
|
+
Flag.withDescription("Omit file contents"),
|
|
435
|
+
Flag.withDefault(false),
|
|
436
|
+
),
|
|
437
|
+
},
|
|
438
|
+
({ filename, format, id, metadataOnly }) =>
|
|
439
|
+
Effect.gen(function* () {
|
|
440
|
+
const gist = yield* viewGist({
|
|
441
|
+
id: id.split("/").at(-1) ?? id,
|
|
442
|
+
filename: Option.getOrNull(filename),
|
|
443
|
+
withContent: !metadataOnly,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
yield* logFormatted(gist, format);
|
|
447
|
+
}),
|
|
448
|
+
).pipe(Command.withDescription("View a gist with file contents"));
|
|
449
|
+
|
|
450
|
+
export const gistCreateCommand = Command.make(
|
|
451
|
+
"create",
|
|
452
|
+
{
|
|
453
|
+
body: Flag.string("body").pipe(
|
|
454
|
+
Flag.withDescription("Inline gist content (requires --filename)"),
|
|
455
|
+
Flag.optional,
|
|
456
|
+
),
|
|
457
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
458
|
+
Flag.withDescription(
|
|
459
|
+
"Read gist content from a file path or '-' for stdin (requires --filename)",
|
|
460
|
+
),
|
|
461
|
+
Flag.optional,
|
|
462
|
+
),
|
|
463
|
+
desc: Flag.string("desc").pipe(Flag.withDescription("Gist description"), Flag.optional),
|
|
464
|
+
filename: Flag.string("filename").pipe(
|
|
465
|
+
Flag.withDescription("File name used for --body/--body-file content"),
|
|
466
|
+
Flag.optional,
|
|
467
|
+
),
|
|
468
|
+
files: Flag.string("files").pipe(
|
|
469
|
+
Flag.withDescription("Comma-separated paths of existing files to upload"),
|
|
470
|
+
Flag.optional,
|
|
471
|
+
),
|
|
472
|
+
format: formatOption,
|
|
473
|
+
public: Flag.boolean("public").pipe(
|
|
474
|
+
Flag.withDescription("Publish as a public gist (gists are secret by default)"),
|
|
475
|
+
Flag.withDefault(false),
|
|
476
|
+
),
|
|
477
|
+
},
|
|
478
|
+
({ body, bodyFile, desc, filename, files, format, public: isPublic }) =>
|
|
479
|
+
Effect.scoped(
|
|
480
|
+
Effect.gen(function* () {
|
|
481
|
+
const command = "gh-tool gist create";
|
|
482
|
+
const filesValue = Option.getOrNull(files);
|
|
483
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
484
|
+
command,
|
|
485
|
+
value: Option.getOrNull(body),
|
|
486
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
487
|
+
valueFlag: "--body",
|
|
488
|
+
fileFlag: "--body-file",
|
|
489
|
+
label: "body",
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (filesValue === null && resolvedBody === null) {
|
|
493
|
+
return yield* Effect.fail(
|
|
494
|
+
inputError("Provide --files, or --body/--body-file with --filename", command),
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const paths: string[] = [];
|
|
499
|
+
|
|
500
|
+
if (filesValue !== null) {
|
|
501
|
+
paths.push(...(yield* parsePaths(filesValue, command)));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (resolvedBody !== null) {
|
|
505
|
+
const name = Option.getOrNull(filename);
|
|
506
|
+
const filenameValidation = validateBodyFilename(resolvedBody, name, command);
|
|
507
|
+
|
|
508
|
+
if (filenameValidation !== null) {
|
|
509
|
+
return yield* Effect.fail(filenameValidation);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
paths.push(yield* stageBody({ body: resolvedBody, filename: name ?? "", command }));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const created = yield* createGist({
|
|
516
|
+
paths,
|
|
517
|
+
description: Option.getOrNull(desc),
|
|
518
|
+
public: isPublic,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
yield* logFormatted(created, format);
|
|
522
|
+
}),
|
|
523
|
+
),
|
|
524
|
+
).pipe(Command.withDescription("Create a gist from files or inline content (secret by default)"));
|
|
525
|
+
|
|
526
|
+
export const gistEditCommand = Command.make(
|
|
527
|
+
"edit",
|
|
528
|
+
{
|
|
529
|
+
add: Flag.string("add").pipe(
|
|
530
|
+
Flag.withDescription("Path of a new file to add to the gist"),
|
|
531
|
+
Flag.optional,
|
|
532
|
+
),
|
|
533
|
+
body: Flag.string("body").pipe(
|
|
534
|
+
Flag.withDescription("Replacement content for --filename"),
|
|
535
|
+
Flag.optional,
|
|
536
|
+
),
|
|
537
|
+
bodyFile: Flag.string("body-file").pipe(
|
|
538
|
+
Flag.withDescription("Read replacement content from a file path or '-' for stdin"),
|
|
539
|
+
Flag.optional,
|
|
540
|
+
),
|
|
541
|
+
desc: Flag.string("desc").pipe(Flag.withDescription("New gist description"), Flag.optional),
|
|
542
|
+
filename: Flag.string("filename").pipe(
|
|
543
|
+
Flag.withDescription("Gist file to replace with --body/--body-file"),
|
|
544
|
+
Flag.optional,
|
|
545
|
+
),
|
|
546
|
+
format: formatOption,
|
|
547
|
+
id: Flag.string("id").pipe(Flag.withDescription("Gist id or URL")),
|
|
548
|
+
remove: Flag.string("remove").pipe(
|
|
549
|
+
Flag.withDescription("File name to remove from the gist"),
|
|
550
|
+
Flag.optional,
|
|
551
|
+
),
|
|
552
|
+
},
|
|
553
|
+
({ add, body, bodyFile, desc, filename, format, id, remove }) =>
|
|
554
|
+
Effect.scoped(
|
|
555
|
+
Effect.gen(function* () {
|
|
556
|
+
const command = "gh-tool gist edit";
|
|
557
|
+
const resolvedBody = yield* resolveOptionalTextInput({
|
|
558
|
+
command,
|
|
559
|
+
value: Option.getOrNull(body),
|
|
560
|
+
fileValue: Option.getOrNull(bodyFile),
|
|
561
|
+
valueFlag: "--body",
|
|
562
|
+
fileFlag: "--body-file",
|
|
563
|
+
label: "body",
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const description = Option.getOrNull(desc);
|
|
567
|
+
const addPath = Option.getOrNull(add);
|
|
568
|
+
const removeName = Option.getOrNull(remove);
|
|
569
|
+
const name = Option.getOrNull(filename);
|
|
570
|
+
|
|
571
|
+
const validation = validateEditInput({
|
|
572
|
+
body: resolvedBody,
|
|
573
|
+
filename: name,
|
|
574
|
+
description,
|
|
575
|
+
add: addPath,
|
|
576
|
+
remove: removeName,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
if (validation !== null) {
|
|
580
|
+
return yield* Effect.fail(validation);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (addPath !== null) {
|
|
584
|
+
const pathValidation = validateFilePath(addPath, command);
|
|
585
|
+
if (pathValidation !== null) {
|
|
586
|
+
return yield* Effect.fail(pathValidation);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const sourcePath =
|
|
591
|
+
resolvedBody === null || name === null
|
|
592
|
+
? null
|
|
593
|
+
: yield* stageBody({ body: resolvedBody, filename: name, command });
|
|
594
|
+
|
|
595
|
+
const edited = yield* editGist({
|
|
596
|
+
id: id.split("/").at(-1) ?? id,
|
|
597
|
+
description,
|
|
598
|
+
add: addPath,
|
|
599
|
+
remove: removeName,
|
|
600
|
+
filename: name,
|
|
601
|
+
sourcePath,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
yield* logFormatted(edited, format);
|
|
605
|
+
}),
|
|
606
|
+
),
|
|
607
|
+
).pipe(Command.withDescription("Edit a gist (never opens an editor — content flags required)"));
|
|
608
|
+
|
|
609
|
+
export const gistDeleteCommand = Command.make(
|
|
610
|
+
"delete",
|
|
611
|
+
{
|
|
612
|
+
confirm: Flag.boolean("confirm").pipe(
|
|
613
|
+
Flag.withDescription("Actually delete the gist (without this flag, only shows dry-run)"),
|
|
614
|
+
Flag.withDefault(false),
|
|
615
|
+
),
|
|
616
|
+
format: formatOption,
|
|
617
|
+
id: Flag.string("id").pipe(Flag.withDescription("Gist id or URL")),
|
|
618
|
+
},
|
|
619
|
+
({ confirm, format, id }) =>
|
|
620
|
+
Effect.gen(function* () {
|
|
621
|
+
const result = yield* deleteGist({ id: id.split("/").at(-1) ?? id, confirm });
|
|
622
|
+
|
|
623
|
+
yield* logFormatted(result, format);
|
|
624
|
+
}),
|
|
625
|
+
).pipe(Command.withDescription("Delete a gist (dry-run by default, use --confirm to execute)"));
|
package/src/gh-tool/index.ts
CHANGED
|
@@ -51,6 +51,13 @@ import {
|
|
|
51
51
|
prWatchCommand,
|
|
52
52
|
} from "./pr/index";
|
|
53
53
|
import { branchRenameCommand } from "./branch";
|
|
54
|
+
import {
|
|
55
|
+
gistCreateCommand,
|
|
56
|
+
gistDeleteCommand,
|
|
57
|
+
gistEditCommand,
|
|
58
|
+
gistListCommand,
|
|
59
|
+
gistViewCommand,
|
|
60
|
+
} from "./gist";
|
|
54
61
|
import {
|
|
55
62
|
releaseCreateCommand,
|
|
56
63
|
releaseDeleteCommand,
|
|
@@ -168,6 +175,17 @@ const releaseCommand = Command.make("release", {}).pipe(
|
|
|
168
175
|
]),
|
|
169
176
|
);
|
|
170
177
|
|
|
178
|
+
const gistCommand = Command.make("gist", {}).pipe(
|
|
179
|
+
Command.withDescription("Gist operations (list, view, create, edit, delete)"),
|
|
180
|
+
Command.withSubcommands([
|
|
181
|
+
gistListCommand,
|
|
182
|
+
gistViewCommand,
|
|
183
|
+
gistCreateCommand,
|
|
184
|
+
gistEditCommand,
|
|
185
|
+
gistDeleteCommand,
|
|
186
|
+
]),
|
|
187
|
+
);
|
|
188
|
+
|
|
171
189
|
const commandsCommand = makeSchemaCommand(() => mainCommand);
|
|
172
190
|
|
|
173
191
|
const mainCommand = Command.make("gh-tool", {}).pipe(
|
|
@@ -200,7 +218,8 @@ WORKFLOW FOR AI AGENTS:
|
|
|
200
218
|
20. Use 'release status' to inspect latest release + repository context
|
|
201
219
|
21. Use 'release create --tag vX.Y.Z --generate-notes' to publish a release
|
|
202
220
|
22. Use 'release edit/view/list/delete' to maintain existing releases
|
|
203
|
-
23. Use 'branch rename --old-name X --new-name Y --confirm' to rename a branch
|
|
221
|
+
23. Use 'branch rename --old-name X --new-name Y --confirm' to rename a branch
|
|
222
|
+
24. Use 'gist list/view' to read gists and 'gist create --files a.ts --desc "..."' to share snippets`,
|
|
204
223
|
),
|
|
205
224
|
Command.withSubcommands([
|
|
206
225
|
prCommand,
|
|
@@ -209,6 +228,7 @@ WORKFLOW FOR AI AGENTS:
|
|
|
209
228
|
branchCommand,
|
|
210
229
|
workflowCommand,
|
|
211
230
|
releaseCommand,
|
|
231
|
+
gistCommand,
|
|
212
232
|
commandsCommand,
|
|
213
233
|
]),
|
|
214
234
|
);
|
|
@@ -3,13 +3,21 @@ import { Effect, Schema } from "effect";
|
|
|
3
3
|
import { GitHubCommandError } from "#gh/errors";
|
|
4
4
|
|
|
5
5
|
const STDIN_SENTINEL = "-";
|
|
6
|
-
const SENSITIVE_PATH_PATTERNS = [
|
|
6
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
7
|
+
/\.env(\..+)?$/,
|
|
8
|
+
/\.envrc$/,
|
|
9
|
+
/\.(pem|key|p12|pfx|cer|crt)$/i,
|
|
10
|
+
/(?:^|[\\/])(credentials?|passwd|shadow)$/i,
|
|
11
|
+
];
|
|
7
12
|
const MissingMode = Schema.Literals(["error", "null", "default"]);
|
|
8
13
|
|
|
9
14
|
const readTextFromStdin = () => Bun.stdin.text();
|
|
10
15
|
|
|
16
|
+
export const isSensitivePath = (filePath: string) =>
|
|
17
|
+
SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
18
|
+
|
|
11
19
|
const readTextFile = (filePath: string) => {
|
|
12
|
-
if (
|
|
20
|
+
if (isSensitivePath(filePath)) {
|
|
13
21
|
return Promise.reject(new Error(`Refusing to read sensitive file: ${filePath}`));
|
|
14
22
|
}
|
|
15
23
|
|
|
@@ -19,9 +19,10 @@ import { makeSchemaCommand, formatOption, formatOutput, logText, VERSION } from
|
|
|
19
19
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
20
20
|
import { ResolvedPaths, ResolvedPathsLayer } from "./config";
|
|
21
21
|
import { SessionStorageNotFoundError } from "./errors";
|
|
22
|
-
import { formatDate, SessionService, SessionServiceLayer
|
|
22
|
+
import { formatDate, SessionService, SessionServiceLayer } from "./service";
|
|
23
23
|
import {
|
|
24
24
|
projectSessionFilter,
|
|
25
|
+
shapeBody,
|
|
25
26
|
sessionSummariesFromMessages,
|
|
26
27
|
sortSessionSummaries,
|
|
27
28
|
} from "./summaries";
|
|
@@ -50,14 +51,14 @@ const buildScopeLabel = (searchAll: boolean, currentDir: string) => {
|
|
|
50
51
|
return `current project (${projectName})`;
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
const mapSummary = (summary: MessageSummary) => {
|
|
54
|
+
const mapSummary = (maxBodyChars: number) => (summary: MessageSummary) => {
|
|
54
55
|
return Effect.gen(function* () {
|
|
55
56
|
const paths = yield* ResolvedPaths;
|
|
56
57
|
return {
|
|
57
58
|
sessionID: summary.sessionID,
|
|
58
59
|
messageID: summary.id,
|
|
59
60
|
title: summary.title,
|
|
60
|
-
|
|
61
|
+
...shapeBody(summary.body, maxBodyChars),
|
|
61
62
|
created: formatDate(summary.created),
|
|
62
63
|
...(summary.source === "opencode"
|
|
63
64
|
? {
|
|
@@ -184,13 +185,17 @@ const searchCommand = Command.make(
|
|
|
184
185
|
Flag.withDefault(false),
|
|
185
186
|
),
|
|
186
187
|
format: formatOption,
|
|
188
|
+
bodyChars: Flag.integer("body-chars").pipe(
|
|
189
|
+
Flag.withDescription("Max message body characters per result (0 = full bodies)"),
|
|
190
|
+
Flag.withDefault(500),
|
|
191
|
+
),
|
|
187
192
|
limit: Flag.integer("limit").pipe(
|
|
188
193
|
Flag.withDescription("Limit result count"),
|
|
189
194
|
Flag.withDefault(10),
|
|
190
195
|
),
|
|
191
196
|
source: sourceOption,
|
|
192
197
|
},
|
|
193
|
-
({ all, format, limit, query, source }) =>
|
|
198
|
+
({ all, bodyChars, format, limit, query, source }) =>
|
|
194
199
|
Effect.gen(function* () {
|
|
195
200
|
const sessionService = yield* SessionService;
|
|
196
201
|
const startTime = Date.now();
|
|
@@ -218,7 +223,7 @@ const searchCommand = Command.make(
|
|
|
218
223
|
const allSummaries = yield* sessionService.getMessageSummaries(sessionFilter);
|
|
219
224
|
const summaries = filterBySource(allSummaries, source);
|
|
220
225
|
const matched = sessionService.searchSummaries(summaries, query);
|
|
221
|
-
const mappedResults = yield* Effect.all(matched.slice(0, limit).map(mapSummary));
|
|
226
|
+
const mappedResults = yield* Effect.all(matched.slice(0, limit).map(mapSummary(bodyChars)));
|
|
222
227
|
|
|
223
228
|
return {
|
|
224
229
|
success: true,
|
|
@@ -287,7 +292,7 @@ const readCommand = Command.make(
|
|
|
287
292
|
onSuccess: (summaries) => {
|
|
288
293
|
const filtered = filterBySource(summaries, source);
|
|
289
294
|
const sessionResults = filtered.filter((summary) => summary.sessionID === session);
|
|
290
|
-
return Effect.all(sessionResults.map(mapSummary)).pipe(
|
|
295
|
+
return Effect.all(sessionResults.map(mapSummary(0))).pipe(
|
|
291
296
|
Effect.map(
|
|
292
297
|
(mapped) =>
|
|
293
298
|
({
|
|
@@ -33,14 +33,6 @@ export const formatDate = (timestamp: number): string => {
|
|
|
33
33
|
});
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
export const truncate = (value: string, maxLen: number): string => {
|
|
37
|
-
if (value.length <= maxLen) {
|
|
38
|
-
return value;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return `${value.slice(0, maxLen - 3)}...`;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
36
|
type FileEntry = { filePath: string; content: string };
|
|
45
37
|
|
|
46
38
|
type SourceFilter = ReadonlySet<SessionSource>;
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import type { MessageSummary, SessionSummary } from "./types";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Shapes a message body for output. A body silently cut to 500 chars was
|
|
5
|
+
* indistinguishable from a short one, so callers read "no match in this snippet" as
|
|
6
|
+
* "not in this session". When the body is cut, say so and report its real length.
|
|
7
|
+
*
|
|
8
|
+
* maxBodyChars <= 0 returns the full body.
|
|
9
|
+
*/
|
|
10
|
+
export const shapeBody = (
|
|
11
|
+
body: string,
|
|
12
|
+
maxBodyChars: number,
|
|
13
|
+
): { body: string; bodyLength?: number; truncated?: true } => {
|
|
14
|
+
if (maxBodyChars <= 0 || body.length <= maxBodyChars) {
|
|
15
|
+
return { body };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const kept = body.slice(0, Math.max(0, maxBodyChars - 3));
|
|
19
|
+
return { body: `${kept}...`, bodyLength: body.length, truncated: true };
|
|
20
|
+
};
|
|
21
|
+
|
|
3
22
|
export const sessionSummariesFromMessages = (summaries: MessageSummary[]): SessionSummary[] => {
|
|
4
23
|
const bySession = new Map<string, SessionSummary>();
|
|
5
24
|
|