@theholocron/holocron-plugin-github 2.0.0-alpha.7 → 2.0.0-alpha.70

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 CHANGED
@@ -13,8 +13,10 @@ against the GitHub REST API:
13
13
 
14
14
  ## Install
15
15
 
16
+ <!-- prettier-ignore -->
16
17
  ```bash
17
18
  pnpm add -D @theholocron/holocron-plugin-github@alpha
19
+
18
20
  ```
19
21
 
20
22
  ## Auth
@@ -33,17 +35,19 @@ toggles, etc.) — silent fallback would surface as mysterious 403s.
33
35
 
34
36
  ## Config
35
37
 
38
+ <!-- prettier-ignore -->
36
39
  ```jsonc
37
40
  // holocron.config.json
38
41
  {
39
- "providers": {
40
- "source": "github",
41
- "ci": "github",
42
- "secrets": "github",
43
- "environments": "github",
44
- "issues": ["github", { "labels": { "inProgress": "status:in-progress", "inReview": "status:in-review" } }],
45
- },
42
+ "providers": {
43
+ "source": "github",
44
+ "ci": "github",
45
+ "secrets": "github",
46
+ "environments": "github",
47
+ "issues": ["github", { "labels": { "inProgress": "status:in-progress", "inReview": "status:in-review" } }],
48
+ },
46
49
  }
50
+
47
51
  ```
48
52
 
49
53
  ## Status
package/dist/index.d.mts CHANGED
@@ -1,62 +1,8 @@
1
- import { Auth, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LifecycleResult, LifecycleSlot, RepoRef, RepoSettings, Ruleset, SecretScope, Secrets, Source, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
1
+ import { GitHubClient, createGitHubClient } from "@theholocron/github-client";
2
+ import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
2
3
 
3
4
  //#region src/auth.d.ts
4
- /**
5
- * Token resolution for the GitHub plugin. See README §Auth.
6
- *
7
- * Resolution order (matches the standard 4-step precedence set by
8
- * `.notes/tech-auth-bootstrap.spec.md`):
9
- * 1. explicit `token` argument (from `--token` flag)
10
- * 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
11
- * 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
12
- * 4. keyring (com.theholocron.cli / "github")
13
- * 5. AuthError naming all four options + the bootstrap hint
14
- *
15
- * No `gh auth token` fallback by design — it usually has narrower
16
- * scopes than admin commands need.
17
- */
18
- declare class AuthError extends Error {
19
- name: string;
20
- }
21
- interface ResolveTokenInput {
22
- /** From `--token` CLI flag. */
23
- cliToken?: string;
24
- /** Env vars; passed in for testability. Defaults to `process.env`. */
25
- env?: NodeJS.ProcessEnv;
26
- /** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
27
- keyring?: (provider: string) => string | null;
28
- }
29
- declare function resolveToken(input?: ResolveTokenInput): string;
30
- //#endregion
31
- //#region src/rest.d.ts
32
- /**
33
- * Thin REST wrapper around api.github.com.
34
- *
35
- * Adapted from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
36
- * `request<T>` method into a standalone class so each capability can
37
- * hold a reference and share the same auth/error handling.
38
- *
39
- * Errors throw `ProviderApiError` from `@theholocron/cli`, which the
40
- * orchestrator catches to soft-skip rather than abort the whole pipeline.
41
- */
42
- interface RestClientOptions {
43
- token: string;
44
- fetch?: typeof fetch;
45
- baseUrl?: string;
46
- }
47
- interface RequestOptions {
48
- method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
49
- body?: unknown;
50
- /** Skip JSON parse when true (204 No Content endpoints). */
51
- expectNoContent?: boolean;
52
- }
53
- declare class GitHubRestClient {
54
- private readonly token;
55
- private readonly fetchImpl;
56
- readonly baseUrl: string;
57
- constructor(opts: RestClientOptions);
58
- request<T>(path: string, opts?: RequestOptions): Promise<T>;
59
- }
5
+ declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
60
6
  //#endregion
61
7
  //#region src/sodium.d.ts
62
8
  /**
@@ -68,19 +14,18 @@ declare function encryptSecret(publicKeyBase64: string, value: string): Promise<
68
14
  //#region src/capabilities/source.d.ts
69
15
  interface SourceOptions {
70
16
  repo: string;
71
- /** Absolute path to the repo root. Workflow file ops are scoped here. */
72
17
  repoRoot: string;
73
18
  }
74
19
  declare class GitHubSource implements Source {
75
- private readonly rest;
20
+ private readonly client;
76
21
  readonly key: "source";
77
22
  readonly providerName = "github";
78
23
  private readonly owner;
79
24
  private readonly name;
80
- private readonly repoPath;
25
+ private readonly repo;
81
26
  private readonly repoRoot;
82
27
  private readonly workflowDir;
83
- constructor(rest: GitHubRestClient, opts: SourceOptions);
28
+ constructor(client: GitHubClient, opts: SourceOptions);
84
29
  whoami(): Promise<{
85
30
  login: string;
86
31
  }>;
@@ -94,6 +39,7 @@ declare class GitHubSource implements Source {
94
39
  enableAutomatedSecurityFixes(): Promise<void>;
95
40
  enableSecretScanning(): Promise<void>;
96
41
  enableCodeScanning(): Promise<string>;
42
+ disableDefaultCodeScanning(): Promise<void>;
97
43
  enableDependencyGraph(): Promise<void>;
98
44
  enablePrivateVulnerabilityReporting(): Promise<void>;
99
45
  listWorkflowFiles(): Promise<string[]>;
@@ -101,6 +47,11 @@ declare class GitHubSource implements Source {
101
47
  writeWorkflowFile(name: string, contents: string): Promise<void>;
102
48
  removeWorkflowFile(name: string): Promise<void>;
103
49
  writeRepoFile(path: string, contents: string): Promise<void>;
50
+ syncLabels(canonical: ReadonlyArray<LabelDef>, stale: ReadonlyArray<string>): Promise<string>;
51
+ syncProperties(values: Record<string, string>): Promise<string>;
52
+ syncTeams(teams: TeamEntry$1[]): Promise<string>;
53
+ syncTopics(topics: string[]): Promise<string>;
54
+ syncDescription(description: string): Promise<string>;
104
55
  }
105
56
  //#endregion
106
57
  //#region src/capabilities/secrets.d.ts
@@ -108,16 +59,14 @@ interface SecretsOptions {
108
59
  repo: string;
109
60
  }
110
61
  declare class GitHubSecrets implements Secrets {
111
- private readonly rest;
62
+ private readonly client;
63
+ private readonly opts;
112
64
  readonly key: "secrets";
113
65
  readonly providerName = "github";
114
- private readonly repoBase;
115
- constructor(rest: GitHubRestClient, opts: SecretsOptions);
66
+ constructor(client: GitHubClient, opts: SecretsOptions);
116
67
  listSecrets(scope: SecretScope): Promise<string[]>;
117
68
  setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
118
69
  deleteSecret(scope: SecretScope, name: string): Promise<void>;
119
- /** Endpoint root for the given scope. */
120
- private scopeBase;
121
70
  }
122
71
  //#endregion
123
72
  //#region src/capabilities/environments.d.ts
@@ -125,11 +74,11 @@ interface EnvironmentsOptions {
125
74
  repo: string;
126
75
  }
127
76
  declare class GitHubEnvironments implements Environments {
128
- private readonly rest;
77
+ private readonly client;
78
+ private readonly opts;
129
79
  readonly key: "environments";
130
80
  readonly providerName = "github";
131
- private readonly base;
132
- constructor(rest: GitHubRestClient, opts: EnvironmentsOptions);
81
+ constructor(client: GitHubClient, opts: EnvironmentsOptions);
133
82
  listEnvironments(): Promise<Environment[]>;
134
83
  upsertEnvironment(env: Environment): Promise<void>;
135
84
  deleteEnvironment(name: string): Promise<void>;
@@ -141,19 +90,13 @@ interface CiOptions {
141
90
  repo: string;
142
91
  }
143
92
  declare class GitHubCi implements Ci {
144
- private readonly rest;
93
+ private readonly client;
94
+ private readonly opts;
145
95
  readonly key: "ci";
146
96
  readonly providerName = "github";
147
- private readonly base;
148
- constructor(rest: GitHubRestClient, opts: CiOptions);
97
+ constructor(client: GitHubClient, opts: CiOptions);
149
98
  listRuns(filter?: CiRunFilter): Promise<CiRun[]>;
150
99
  getRun(id: string | number): Promise<CiRun>;
151
- /**
152
- * GitHub reports run state as `status` plus `conclusion` (the latter
153
- * is set once `status === 'completed'`). Holocron's `CiRunStatus`
154
- * collapses these onto one axis — when complete, the conclusion is
155
- * the meaningful value.
156
- */
157
100
  private mapRun;
158
101
  private mapConclusion;
159
102
  }
@@ -165,9 +108,7 @@ interface IssuesOptions {
165
108
  * Lifecycle slot → status label name. Optional at construction; if
166
109
  * omitted, capability methods that need labels (`transition`,
167
110
  * `doctor`) degrade gracefully — `transition` throws a clear error,
168
- * `doctor` reports the two label-backed slots as unresolved. This
169
- * lets the plugin load in configs where the operator hasn't picked
170
- * labels yet without failing the whole runtime.
111
+ * `doctor` reports the two label-backed slots as unresolved.
171
112
  */
172
113
  labels?: {
173
114
  inProgress: string;
@@ -175,14 +116,14 @@ interface IssuesOptions {
175
116
  };
176
117
  }
177
118
  declare class GitHubIssues implements Issues {
178
- private readonly rest;
119
+ private readonly client;
179
120
  readonly key: "issues";
180
121
  readonly providerName = "github";
181
122
  private readonly owner;
182
123
  private readonly repoName;
183
- private readonly base;
124
+ private readonly repo;
184
125
  private readonly labels;
185
- constructor(rest: GitHubRestClient, opts: IssuesOptions);
126
+ constructor(client: GitHubClient, opts: IssuesOptions);
186
127
  getMyself(): Promise<TrackerUser>;
187
128
  search(filter: IssueSearchFilter): Promise<Issue[]>;
188
129
  get(key: string): Promise<Issue>;
@@ -202,6 +143,30 @@ declare class GitHubIssues implements Issues {
202
143
  private mapIssue;
203
144
  }
204
145
  //#endregion
146
+ //#region src/capabilities/labels.d.ts
147
+ interface CanonicalLabel {
148
+ readonly name: string;
149
+ readonly color: string;
150
+ readonly description: string;
151
+ }
152
+ /**
153
+ * Idempotently synchronise a repo's labels to the canonical set.
154
+ *
155
+ * - Creates labels that are missing.
156
+ * - PATCHes labels whose color or description has drifted.
157
+ * - DELETEs labels listed in `stale`.
158
+ * - Leaves unrecognised labels that aren't in `stale` untouched.
159
+ */
160
+ declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
161
+ //#endregion
162
+ //#region src/capabilities/teams.d.ts
163
+ interface NormalizedTeamEntry {
164
+ slug: string;
165
+ permission: TeamPermission;
166
+ }
167
+ declare function normalizeTeamEntry(entry: TeamEntry): NormalizedTeamEntry;
168
+ declare function syncTeams(client: GitHubClient, repo: string, teams: TeamEntry[]): Promise<string>;
169
+ //#endregion
205
170
  //#region src/verify-token.d.ts
206
171
  /**
207
172
  * `verifyToken` — plugin-level export used by `holocron auth set` +
@@ -247,7 +212,7 @@ interface GitHubPluginOptions extends ResolveTokenInput {
247
212
  }
248
213
  interface PluginContext {
249
214
  options: GitHubPluginOptions;
250
- rest: GitHubRestClient;
215
+ client: GitHubClient;
251
216
  repo: string;
252
217
  repoRoot: string;
253
218
  }
@@ -275,4 +240,4 @@ declare function createPlugin(options: GitHubPluginOptions): {
275
240
  */
276
241
  declare const AUTH_HINT: string;
277
242
  //#endregion
278
- export { AUTH_HINT, type Auth, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubRestClient, GitHubSecrets, GitHubSource, PluginContext, ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, verifyToken };
243
+ export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, type NormalizedTeamEntry, PluginContext, type ResolveTokenInput, type TeamEntry, type TeamPermission, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveToken, secrets, source, syncLabels, syncTeams, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,77 +1,37 @@
1
1
  import { createRequire } from "node:module";
2
- import { ProviderApiError, getToken } from "@theholocron/cli";
2
+ import { createGitHubClient, createGitHubClient as createGitHubClient$1 } from "@theholocron/github-client";
3
+ import { AuthError, createResolveToken } from "@theholocron/cli";
3
4
  import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
5
  import { dirname, join } from "node:path";
5
6
  //#region src/auth.ts
6
- /**
7
- * Token resolution for the GitHub plugin. See README §Auth.
8
- *
9
- * Resolution order (matches the standard 4-step precedence set by
10
- * `.notes/tech-auth-bootstrap.spec.md`):
11
- * 1. explicit `token` argument (from `--token` flag)
12
- * 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
13
- * 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
14
- * 4. keyring (com.theholocron.cli / "github")
15
- * 5. AuthError naming all four options + the bootstrap hint
16
- *
17
- * No `gh auth token` fallback by design — it usually has narrower
18
- * scopes than admin commands need.
19
- */
20
- var AuthError = class extends Error {
21
- name = "AuthError";
22
- };
23
- function resolveToken(input = {}) {
24
- const env = input.env ?? process.env;
25
- const keyring = input.keyring ?? getToken;
26
- const token = input.cliToken || env.HOLOCRON_GH_TOKEN || env.GITHUB_TOKEN || keyring("github");
27
- if (!token) throw new AuthError("no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>");
28
- return token;
29
- }
30
- //#endregion
31
- //#region src/repo.ts
32
- var RepoError = class extends Error {
33
- name = "RepoError";
34
- };
35
- function parseRepo(repo) {
36
- if (!repo) throw new RepoError("repo is required; pass { repo: \"owner/name\" } in plugin options");
37
- const [owner, name] = repo.split("/");
38
- if (!owner || !name) throw new RepoError(`invalid repo "${repo}" — expected "owner/name"`);
39
- return {
40
- owner,
41
- name
42
- };
43
- }
7
+ const resolveToken = createResolveToken({
8
+ envName: "HOLOCRON_GH_TOKEN",
9
+ vendorEnvName: "GITHUB_TOKEN",
10
+ keyringService: "github",
11
+ errorMessage: "no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>"
12
+ });
44
13
  //#endregion
45
14
  //#region src/capabilities/ci.ts
46
15
  var GitHubCi = class {
47
- rest;
16
+ client;
17
+ opts;
48
18
  key = "ci";
49
19
  providerName = "github";
50
- base;
51
- constructor(rest, opts) {
52
- this.rest = rest;
53
- const { owner, name } = parseRepo(opts.repo);
54
- this.base = `/repos/${owner}/${name}/actions/runs`;
20
+ constructor(client, opts) {
21
+ this.client = client;
22
+ this.opts = opts;
55
23
  }
56
24
  async listRuns(filter) {
57
- const params = new URLSearchParams();
58
- if (filter?.branch) params.set("branch", filter.branch);
59
- if (filter?.limit) params.set("per_page", String(filter.limit));
60
- if (filter?.status) params.set("status", filter.status);
61
- const qs = params.toString();
62
- const path = qs ? `${this.base}?${qs}` : this.base;
63
- return (await this.rest.request(path)).workflow_runs.map((r) => this.mapRun(r));
25
+ return (await this.client.workflows.listRuns(this.opts.repo, {
26
+ branch: filter?.branch,
27
+ limit: filter?.limit,
28
+ status: filter?.status
29
+ })).map((r) => this.mapRun(r));
64
30
  }
65
31
  async getRun(id) {
66
- const raw = await this.rest.request(`${this.base}/${id}`);
32
+ const raw = await this.client.workflows.getRun(this.opts.repo, id);
67
33
  return this.mapRun(raw);
68
34
  }
69
- /**
70
- * GitHub reports run state as `status` plus `conclusion` (the latter
71
- * is set once `status === 'completed'`). Holocron's `CiRunStatus`
72
- * collapses these onto one axis — when complete, the conclusion is
73
- * the meaningful value.
74
- */
75
35
  mapRun(raw) {
76
36
  const status = raw.status === "completed" && raw.conclusion ? this.mapConclusion(raw.conclusion) : raw.status;
77
37
  return {
@@ -98,17 +58,16 @@ var GitHubCi = class {
98
58
  //#endregion
99
59
  //#region src/capabilities/environments.ts
100
60
  var GitHubEnvironments = class {
101
- rest;
61
+ client;
62
+ opts;
102
63
  key = "environments";
103
64
  providerName = "github";
104
- base;
105
- constructor(rest, opts) {
106
- this.rest = rest;
107
- const { owner, name } = parseRepo(opts.repo);
108
- this.base = `/repos/${owner}/${name}/environments`;
65
+ constructor(client, opts) {
66
+ this.client = client;
67
+ this.opts = opts;
109
68
  }
110
69
  async listEnvironments() {
111
- return (await this.rest.request(this.base)).environments.map((e) => ({
70
+ return (await this.client.environments.listEnvironments(this.opts.repo)).map((e) => ({
112
71
  name: e.name,
113
72
  waitTimer: e.wait_timer,
114
73
  preventSelfReview: e.prevent_self_review,
@@ -123,16 +82,10 @@ var GitHubEnvironments = class {
123
82
  type: r.type,
124
83
  id: r.id
125
84
  }));
126
- await this.rest.request(`${this.base}/${encodeURIComponent(env.name)}`, {
127
- method: "PUT",
128
- body
129
- });
85
+ await this.client.environments.upsertEnvironment(this.opts.repo, env.name, body);
130
86
  }
131
87
  async deleteEnvironment(name) {
132
- await this.rest.request(`${this.base}/${encodeURIComponent(name)}`, {
133
- method: "DELETE",
134
- expectNoContent: true
135
- });
88
+ await this.client.environments.deleteEnvironment(this.opts.repo, name);
136
89
  }
137
90
  flattenReviewers(rules) {
138
91
  if (!rules) return void 0;
@@ -147,41 +100,51 @@ var GitHubEnvironments = class {
147
100
  //#endregion
148
101
  //#region src/capabilities/issues.ts
149
102
  var GitHubIssues = class {
150
- rest;
103
+ client;
151
104
  key = "issues";
152
105
  providerName = "github";
153
106
  owner;
154
107
  repoName;
155
- base;
108
+ repo;
156
109
  labels;
157
- constructor(rest, opts) {
158
- this.rest = rest;
159
- const { owner, name } = parseRepo(opts.repo);
110
+ constructor(client, opts) {
111
+ this.client = client;
112
+ const [owner = "", repoName = ""] = opts.repo.split("/", 2);
160
113
  this.owner = owner;
161
- this.repoName = name;
162
- this.base = `/repos/${owner}/${name}`;
114
+ this.repoName = repoName;
115
+ this.repo = opts.repo;
163
116
  this.labels = opts.labels;
164
117
  }
165
118
  async getMyself() {
166
- return mapUser(await this.rest.request("/user"));
119
+ const raw = await this.client.user.getCurrentUser();
120
+ return {
121
+ id: raw.login,
122
+ displayName: raw.name ?? raw.login,
123
+ ...raw.email ? { emailAddress: raw.email } : {}
124
+ };
167
125
  }
168
126
  async search(filter) {
169
- const params = new URLSearchParams({
127
+ const params = {
170
128
  state: filter.openOnly ? "open" : "all",
171
129
  sort: "updated",
172
130
  direction: "desc",
173
- per_page: String(filter.limit ?? 50),
131
+ per_page: filter.limit ?? 50,
174
132
  filter: "all"
175
- });
176
- if (filter.assignee === "currentUser") {
177
- const me = await this.getMyself();
178
- params.set("assignee", me.id);
179
- } else if (filter.assignee) params.set("assignee", filter.assignee);
180
- return (await this.rest.request(`${this.base}/issues?${params.toString()}`)).filter((i) => !i.pull_request).map((i) => this.mapIssue(i));
133
+ };
134
+ if (filter.assignee === "currentUser") params.assignee = (await this.getMyself()).id;
135
+ else if (filter.assignee) params.assignee = filter.assignee;
136
+ return (await this.client.issues.listIssues(this.repo, {
137
+ state: params.state,
138
+ sort: params.sort,
139
+ direction: params.direction,
140
+ per_page: Number(params.per_page),
141
+ filter: params.filter,
142
+ assignee: params.assignee
143
+ })).filter((i) => !i.pull_request).map((i) => this.mapIssue(i));
181
144
  }
182
145
  async get(key) {
183
146
  const number = parseIssueNumber(key, this.owner, this.repoName);
184
- const raw = await this.rest.request(`${this.base}/issues/${number}`);
147
+ const raw = await this.client.issues.getIssue(this.repo, number);
185
148
  return this.mapIssue(raw);
186
149
  }
187
150
  async create(input) {
@@ -189,14 +152,11 @@ var GitHubIssues = class {
189
152
  if (input.body) body.body = input.body;
190
153
  if (input.labels?.length) body.labels = input.labels;
191
154
  if (input.milestone) body.milestone = await this.resolveMilestone(input.milestone);
192
- return { key: `#${(await this.rest.request(`${this.base}/issues`, {
193
- method: "POST",
194
- body
195
- })).number}` };
155
+ return { key: `#${(await this.client.issues.createIssue(this.repo, body)).number}` };
196
156
  }
197
157
  async transition(key, slot) {
198
158
  const number = parseIssueNumber(key, this.owner, this.repoName);
199
- const issue = await this.rest.request(`${this.base}/issues/${number}`);
159
+ const issue = await this.client.issues.getIssue(this.repo, number);
200
160
  const currentStatusLabels = issue.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
201
161
  if (slot === "done") {
202
162
  if (issue.state === "closed") return {
@@ -205,12 +165,9 @@ var GitHubIssues = class {
205
165
  via: "already closed"
206
166
  };
207
167
  await this.removeStatusLabels(number, currentStatusLabels);
208
- await this.rest.request(`${this.base}/issues/${number}`, {
209
- method: "PATCH",
210
- body: {
211
- state: "closed",
212
- state_reason: "completed"
213
- }
168
+ await this.client.issues.updateIssue(this.repo, number, {
169
+ state: "closed",
170
+ state_reason: "completed"
214
171
  });
215
172
  return {
216
173
  transitioned: true,
@@ -227,16 +184,10 @@ var GitHubIssues = class {
227
184
  status: `open + ${targetLabel}`,
228
185
  via: "already there"
229
186
  };
230
- if (!alreadyOpen) await this.rest.request(`${this.base}/issues/${number}`, {
231
- method: "PATCH",
232
- body: { state: "open" }
233
- });
187
+ if (!alreadyOpen) await this.client.issues.updateIssue(this.repo, number, { state: "open" });
234
188
  const toRemove = currentStatusLabels.filter((n) => n !== targetLabel);
235
189
  if (toRemove.length) await this.removeStatusLabels(number, toRemove);
236
- if (!alreadyLabeled) await this.rest.request(`${this.base}/issues/${number}/labels`, {
237
- method: "POST",
238
- body: { labels: [targetLabel] }
239
- });
190
+ if (!alreadyLabeled) await this.client.issues.addLabels(this.repo, number, [targetLabel]);
240
191
  return {
241
192
  transitioned: true,
242
193
  status: `open + ${targetLabel}`,
@@ -245,15 +196,12 @@ var GitHubIssues = class {
245
196
  }
246
197
  async comment(key, body) {
247
198
  const number = parseIssueNumber(key, this.owner, this.repoName);
248
- await this.rest.request(`${this.base}/issues/${number}/comments`, {
249
- method: "POST",
250
- body: { body }
251
- });
199
+ await this.client.issues.createComment(this.repo, number, body);
252
200
  }
253
201
  async doctor() {
254
202
  const me = await this.getMyself();
255
- const repo = await this.rest.request(this.base);
256
- const allLabels = await this.rest.request(`${this.base}/labels?per_page=100`);
203
+ const repo = await this.client.repos.getRepo(this.repo);
204
+ const allLabels = await this.client.labels.listLabels(this.repo);
257
205
  const labelNames = new Set(allLabels.map((l) => l.name));
258
206
  const statuses = [
259
207
  {
@@ -320,12 +268,12 @@ var GitHubIssues = class {
320
268
  }
321
269
  async resolveMilestone(ref) {
322
270
  if (/^\d+$/.test(ref)) return parseInt(ref, 10);
323
- const match = (await this.rest.request(`${this.base}/milestones?state=all&per_page=100`)).find((m) => m.title.toLowerCase() === ref.toLowerCase());
271
+ const match = (await this.client.issues.listMilestones(this.repo, { state: "all" })).find((m) => m.title.toLowerCase() === ref.toLowerCase());
324
272
  if (!match) throw new Error(`Milestone "${ref}" not found in ${this.owner}/${this.repoName}. Use the numeric id or an exact title.`);
325
273
  return match.number;
326
274
  }
327
275
  async removeStatusLabels(number, labels) {
328
- for (const label of labels) await this.rest.request(`${this.base}/issues/${number}/labels/${encodeURIComponent(label)}`, { method: "DELETE" });
276
+ for (const label of labels) await this.client.issues.removeLabel(this.repo, number, label);
329
277
  }
330
278
  mapIssue(raw) {
331
279
  const statusLabels = raw.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
@@ -346,17 +294,16 @@ var GitHubIssues = class {
346
294
  ...raw.body ? { body: raw.body } : {},
347
295
  status: statusName,
348
296
  statusCategory: category,
349
- assignee: raw.assignee ? mapUser(raw.assignee) : null,
297
+ assignee: raw.assignee ? {
298
+ id: raw.assignee.login,
299
+ displayName: raw.assignee.name ?? raw.assignee.login,
300
+ ...raw.assignee.email ? { emailAddress: raw.assignee.email } : {}
301
+ } : null,
350
302
  updated: raw.updated_at,
351
303
  url: raw.html_url
352
304
  };
353
305
  }
354
306
  };
355
- /**
356
- * Accept "#42", "42", or "owner/repo#42" and return the numeric id.
357
- * Cross-repo refs are allowed in input but the adapter is bound to one
358
- * repo — we error if the owner/repo doesn't match.
359
- */
360
307
  function parseIssueNumber(key, owner, repoName) {
361
308
  const trimmed = key.trim();
362
309
  const crossMatch = trimmed.match(/^([^/]+)\/([^#]+)#(\d+)$/);
@@ -368,13 +315,6 @@ function parseIssueNumber(key, owner, repoName) {
368
315
  if (!/^\d+$/.test(num)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
369
316
  return parseInt(num, 10);
370
317
  }
371
- function mapUser(raw) {
372
- return {
373
- id: raw.login,
374
- displayName: raw.name ?? raw.login,
375
- ...raw.email ? { emailAddress: raw.email } : {}
376
- };
377
- }
378
318
  //#endregion
379
319
  //#region src/sodium.ts
380
320
  /**
@@ -410,84 +350,135 @@ async function encryptSecret(publicKeyBase64, value) {
410
350
  //#endregion
411
351
  //#region src/capabilities/secrets.ts
412
352
  var GitHubSecrets = class {
413
- rest;
353
+ client;
354
+ opts;
414
355
  key = "secrets";
415
356
  providerName = "github";
416
- repoBase;
417
- constructor(rest, opts) {
418
- this.rest = rest;
419
- const { owner, name } = parseRepo(opts.repo);
420
- this.repoBase = `/repos/${owner}/${name}`;
357
+ constructor(client, opts) {
358
+ this.client = client;
359
+ this.opts = opts;
421
360
  }
422
361
  async listSecrets(scope) {
423
- const base = this.scopeBase(scope);
424
- return (await this.rest.request(`${base}/secrets`)).secrets.map((s) => s.name);
362
+ return this.client.secrets.listSecrets(this.opts.repo, toGHScope(scope));
425
363
  }
426
364
  async setSecret(scope, name, value) {
427
- const base = this.scopeBase(scope);
428
- const pk = await this.rest.request(`${base}/secrets/public-key`);
365
+ const ghScope = toGHScope(scope);
366
+ const pk = await this.client.secrets.getPublicKey(this.opts.repo, ghScope);
429
367
  const body = {
430
368
  encrypted_value: await encryptSecret(pk.key, value),
431
369
  key_id: pk.key_id
432
370
  };
433
371
  if (scope.kind === "organization") body.visibility = "all";
434
- await this.rest.request(`${base}/secrets/${name}`, {
435
- method: "PUT",
436
- body,
437
- expectNoContent: true
438
- });
372
+ await this.client.secrets.putSecret(this.opts.repo, ghScope, name, body);
439
373
  }
440
374
  async deleteSecret(scope, name) {
441
- const base = this.scopeBase(scope);
442
- await this.rest.request(`${base}/secrets/${name}`, {
443
- method: "DELETE",
444
- expectNoContent: true
445
- });
446
- }
447
- /** Endpoint root for the given scope. */
448
- scopeBase(scope) {
449
- switch (scope.kind) {
450
- case "repo": return `${this.repoBase}/actions`;
451
- case "environment": return `${this.repoBase}/environments/${scope.name}`;
452
- case "organization": return `/orgs/${scope.name}/actions`;
453
- }
375
+ await this.client.secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
454
376
  }
455
377
  };
378
+ function toGHScope(scope) {
379
+ if (scope.kind === "organization") return {
380
+ kind: "organization",
381
+ org: scope.name
382
+ };
383
+ return scope;
384
+ }
456
385
  //#endregion
457
- //#region src/capabilities/source.ts
386
+ //#region src/capabilities/labels.ts
458
387
  /**
459
- * `source` capability for GitHub.
388
+ * Idempotently synchronise a repo's labels to the canonical set.
460
389
  *
461
- * Ported from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
462
- * with two changes for v2:
463
- *
464
- * - Repo coords are bound at construction time (not passed per call)
465
- * - Workflow files are local-fs operations (not GH API) — they live
466
- * in the consumer's `.github/workflows/` directory
390
+ * - Creates labels that are missing.
391
+ * - PATCHes labels whose color or description has drifted.
392
+ * - DELETEs labels listed in `stale`.
393
+ * - Leaves unrecognised labels that aren't in `stale` untouched.
467
394
  */
395
+ async function syncLabels(client, repo, canonical, stale) {
396
+ const existing = await client.labels.listLabels(repo);
397
+ const existingMap = new Map(existing.map((l) => [l.name.toLowerCase(), l]));
398
+ let created = 0;
399
+ let updated = 0;
400
+ let deleted = 0;
401
+ for (const label of canonical) {
402
+ const current = existingMap.get(label.name);
403
+ if (!current) {
404
+ await client.labels.createLabel(repo, {
405
+ name: label.name,
406
+ color: label.color,
407
+ description: label.description
408
+ });
409
+ created++;
410
+ } else if (current.color !== label.color || (current.description ?? "") !== label.description) {
411
+ await client.labels.updateLabel(repo, label.name, {
412
+ color: label.color,
413
+ description: label.description
414
+ });
415
+ updated++;
416
+ }
417
+ }
418
+ for (const staleName of stale) if (existingMap.has(staleName)) {
419
+ await client.labels.deleteLabel(repo, staleName);
420
+ deleted++;
421
+ }
422
+ return `${created} created, ${updated} updated, ${deleted} deleted`;
423
+ }
424
+ //#endregion
425
+ //#region src/capabilities/properties.ts
426
+ async function syncProperties(client, repo, values) {
427
+ await client.properties.setProperties(repo, values);
428
+ return `${Object.keys(values).length} properties set`;
429
+ }
430
+ //#endregion
431
+ //#region src/capabilities/teams.ts
432
+ function normalizeTeamEntry(entry) {
433
+ if (typeof entry === "string") return {
434
+ slug: entry,
435
+ permission: "push"
436
+ };
437
+ return entry;
438
+ }
439
+ async function syncTeams(client, repo, teams) {
440
+ const [org = "", name = ""] = repo.split("/", 2);
441
+ const normalized = teams.map(normalizeTeamEntry);
442
+ const results = await Promise.allSettled(normalized.map(({ slug, permission }) => client.teams.addRepo(org, slug, org, name, permission)));
443
+ const succeeded = results.filter((r) => r.status === "fulfilled").length;
444
+ const failedSlugs = normalized.filter((_, i) => results[i]?.status === "rejected").map(({ slug }) => slug);
445
+ if (failedSlugs.length > 0) {
446
+ if (succeeded === 0) throw new Error(`all teams failed: ${failedSlugs.join(", ")}`);
447
+ return `${succeeded} synced, failed: ${failedSlugs.join(", ")}`;
448
+ }
449
+ return `${succeeded} team${succeeded === 1 ? "" : "s"} synced`;
450
+ }
451
+ //#endregion
452
+ //#region src/capabilities/topics.ts
453
+ async function syncTopics(client, repo, topics) {
454
+ await client.topics.setTopics(repo, topics);
455
+ return `${topics.length} topics set`;
456
+ }
457
+ //#endregion
458
+ //#region src/capabilities/source.ts
468
459
  var GitHubSource = class {
469
- rest;
460
+ client;
470
461
  key = "source";
471
462
  providerName = "github";
472
463
  owner;
473
464
  name;
474
- repoPath;
465
+ repo;
475
466
  repoRoot;
476
467
  workflowDir;
477
- constructor(rest, opts) {
478
- this.rest = rest;
479
- const { owner, name } = parseRepo(opts.repo);
468
+ constructor(client, opts) {
469
+ this.client = client;
470
+ const [owner = "", name = ""] = opts.repo.split("/", 2);
480
471
  this.owner = owner;
481
472
  this.name = name;
482
- this.repoPath = `/repos/${owner}/${name}`;
473
+ this.repo = opts.repo;
483
474
  this.repoRoot = opts.repoRoot;
484
475
  this.workflowDir = join(opts.repoRoot, ".github", "workflows");
485
476
  }
486
477
  async whoami() {
487
- return { login: (await this.rest.request("/user")).login };
478
+ return { login: (await this.client.user.getCurrentUser()).login };
488
479
  }
489
480
  async getRepo() {
490
- const data = await this.rest.request(this.repoPath);
481
+ const data = await this.client.repos.getRepo(this.repo);
491
482
  return {
492
483
  owner: this.owner,
493
484
  name: this.name,
@@ -495,79 +486,40 @@ var GitHubSource = class {
495
486
  };
496
487
  }
497
488
  async listRulesets() {
498
- return this.rest.request(`${this.repoPath}/rulesets`);
489
+ return this.client.rulesets.listRulesets(this.repo);
499
490
  }
500
491
  async createRuleset(payload) {
501
- return this.rest.request(`${this.repoPath}/rulesets`, {
502
- method: "POST",
503
- body: payload
504
- });
492
+ return this.client.rulesets.createRuleset(this.repo, payload);
505
493
  }
506
494
  async updateRuleset(id, payload) {
507
- return this.rest.request(`${this.repoPath}/rulesets/${id}`, {
508
- method: "PUT",
509
- body: payload
510
- });
495
+ return this.client.rulesets.updateRuleset(this.repo, id, payload);
511
496
  }
512
497
  async updateRepoSettings(settings) {
513
- await this.rest.request(this.repoPath, {
514
- method: "PATCH",
515
- body: settings
516
- });
498
+ await this.client.repos.updateRepo(this.repo, settings);
517
499
  }
518
500
  async protectBranch(branch, payload) {
519
- await this.rest.request(`${this.repoPath}/branches/${encodeURIComponent(branch)}/protection`, {
520
- method: "PUT",
521
- body: payload
522
- });
501
+ await this.client.branches.protectBranch(this.repo, branch, payload);
523
502
  }
524
503
  async enableVulnerabilityAlerts() {
525
- await this.rest.request(`${this.repoPath}/vulnerability-alerts`, {
526
- method: "PUT",
527
- expectNoContent: true
528
- });
504
+ await this.client.security.enableVulnerabilityAlerts(this.repo);
529
505
  }
530
506
  async enableAutomatedSecurityFixes() {
531
- await this.rest.request(`${this.repoPath}/automated-security-fixes`, {
532
- method: "PUT",
533
- expectNoContent: true
534
- });
507
+ await this.client.security.enableAutomatedSecurityFixes(this.repo);
535
508
  }
536
509
  async enableSecretScanning() {
537
- await this.rest.request(this.repoPath, {
538
- method: "PATCH",
539
- body: { security_and_analysis: {
540
- secret_scanning: { status: "enabled" },
541
- secret_scanning_push_protection: { status: "enabled" },
542
- secret_scanning_validity_checks: { status: "enabled" },
543
- secret_scanning_non_provider_patterns: { status: "enabled" }
544
- } }
545
- });
510
+ await this.client.security.enableSecretScanning(this.repo);
546
511
  }
547
512
  async enableCodeScanning() {
548
- return `run ${(await this.rest.request(`${this.repoPath}/code-scanning/default-setup`, {
549
- method: "PATCH",
550
- body: {
551
- state: "configured",
552
- query_suite: "extended",
553
- threat_model: "remote_and_local"
554
- }
555
- })).run_id}`;
513
+ return `run ${(await this.client.security.enableCodeScanning(this.repo)).run_id}`;
514
+ }
515
+ async disableDefaultCodeScanning() {
516
+ await this.client.security.disableDefaultCodeScanning(this.repo);
556
517
  }
557
518
  async enableDependencyGraph() {
558
- await this.rest.request(this.repoPath, {
559
- method: "PATCH",
560
- body: { security_and_analysis: {
561
- dependency_graph: { status: "enabled" },
562
- dependency_graph_autosubmit_action: { status: "enabled" }
563
- } }
564
- });
519
+ await this.client.security.enableDependencyGraph(this.repo);
565
520
  }
566
521
  async enablePrivateVulnerabilityReporting() {
567
- await this.rest.request(`${this.repoPath}/private-vulnerability-reporting`, {
568
- method: "PUT",
569
- expectNoContent: true
570
- });
522
+ await this.client.security.enablePrivateVulnerabilityReporting(this.repo);
571
523
  }
572
524
  async listWorkflowFiles() {
573
525
  try {
@@ -602,6 +554,22 @@ var GitHubSource = class {
602
554
  await ensureDir(dirname(full));
603
555
  await writeFile(full, contents, "utf8");
604
556
  }
557
+ async syncLabels(canonical, stale) {
558
+ return syncLabels(this.client, this.repo, canonical, stale);
559
+ }
560
+ async syncProperties(values) {
561
+ return syncProperties(this.client, this.repo, values);
562
+ }
563
+ async syncTeams(teams) {
564
+ return syncTeams(this.client, this.repo, teams);
565
+ }
566
+ async syncTopics(topics) {
567
+ return syncTopics(this.client, this.repo, topics);
568
+ }
569
+ async syncDescription(description) {
570
+ await this.client.repos.updateRepo(this.repo, { description });
571
+ return "description updated";
572
+ }
605
573
  };
606
574
  function isENOENT(err) {
607
575
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
@@ -618,55 +586,6 @@ async function ensureDir(path) {
618
586
  }
619
587
  }
620
588
  //#endregion
621
- //#region src/rest.ts
622
- /**
623
- * Thin REST wrapper around api.github.com.
624
- *
625
- * Adapted from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
626
- * `request<T>` method into a standalone class so each capability can
627
- * hold a reference and share the same auth/error handling.
628
- *
629
- * Errors throw `ProviderApiError` from `@theholocron/cli`, which the
630
- * orchestrator catches to soft-skip rather than abort the whole pipeline.
631
- */
632
- var GitHubRestClient = class {
633
- token;
634
- fetchImpl;
635
- baseUrl;
636
- constructor(opts) {
637
- this.token = opts.token;
638
- this.fetchImpl = opts.fetch ?? globalThis.fetch;
639
- let url = opts.baseUrl ?? "https://api.github.com";
640
- while (url.endsWith("/")) url = url.slice(0, -1);
641
- this.baseUrl = url;
642
- }
643
- async request(path, opts = {}) {
644
- const url = `${this.baseUrl}${path}`;
645
- const headers = {
646
- accept: "application/vnd.github+json",
647
- authorization: `Bearer ${this.token}`,
648
- "x-github-api-version": "2022-11-28"
649
- };
650
- const init = {
651
- method: opts.method ?? "GET",
652
- headers
653
- };
654
- if (opts.body !== void 0) {
655
- headers["content-type"] = "application/json";
656
- init.body = JSON.stringify(opts.body);
657
- }
658
- const res = await this.fetchImpl(url, init);
659
- if (!res.ok) {
660
- const body = await res.text().catch(() => "");
661
- throw new ProviderApiError(`GitHub ${init.method} ${path} → ${res.status}`, res.status, body);
662
- }
663
- if (opts.expectNoContent || res.status === 204) return;
664
- const text = await res.text();
665
- if (!text) return void 0;
666
- return JSON.parse(text);
667
- }
668
- };
669
- //#endregion
670
589
  //#region src/verify-token.ts
671
590
  /**
672
591
  * `verifyToken` — plugin-level export used by `holocron auth set` +
@@ -679,12 +598,13 @@ var GitHubRestClient = class {
679
598
  * what we don't have yet at bootstrap time.
680
599
  */
681
600
  async function verifyToken(token, opts = {}) {
682
- const restOpts = { token };
683
- if (opts.baseUrl !== void 0) restOpts.baseUrl = opts.baseUrl;
684
- if (opts.fetch !== void 0) restOpts.fetch = opts.fetch;
685
- const rest = new GitHubRestClient(restOpts);
601
+ const client = createGitHubClient({
602
+ token,
603
+ baseUrl: opts.baseUrl,
604
+ fetch: opts.fetch
605
+ });
686
606
  try {
687
- const me = await rest.request("/user");
607
+ const me = await client.user.getCurrentUser();
688
608
  return {
689
609
  ok: true,
690
610
  subject: `user @ ${me.login ?? me.email ?? "unknown"}`
@@ -701,7 +621,7 @@ async function verifyToken(token, opts = {}) {
701
621
  function createContext(options) {
702
622
  return {
703
623
  options,
704
- rest: new GitHubRestClient({
624
+ client: createGitHubClient$1({
705
625
  token: resolveToken(options),
706
626
  baseUrl: options.baseUrl,
707
627
  fetch: options.fetch
@@ -711,24 +631,24 @@ function createContext(options) {
711
631
  };
712
632
  }
713
633
  function source(ctx) {
714
- return new GitHubSource(ctx.rest, {
634
+ return new GitHubSource(ctx.client, {
715
635
  repo: ctx.repo,
716
636
  repoRoot: ctx.repoRoot
717
637
  });
718
638
  }
719
639
  function secrets(ctx) {
720
- return new GitHubSecrets(ctx.rest, { repo: ctx.repo });
640
+ return new GitHubSecrets(ctx.client, { repo: ctx.repo });
721
641
  }
722
642
  function environments(ctx) {
723
- return new GitHubEnvironments(ctx.rest, { repo: ctx.repo });
643
+ return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
724
644
  }
725
645
  function ci(ctx) {
726
- return new GitHubCi(ctx.rest, { repo: ctx.repo });
646
+ return new GitHubCi(ctx.client, { repo: ctx.repo });
727
647
  }
728
648
  function issues(ctx) {
729
649
  const opts = { repo: ctx.repo };
730
650
  if (ctx.options.labels !== void 0) opts.labels = ctx.options.labels;
731
- return new GitHubIssues(ctx.rest, opts);
651
+ return new GitHubIssues(ctx.client, opts);
732
652
  }
733
653
  function createPlugin(options) {
734
654
  const ctx = createContext(options);
@@ -751,4 +671,4 @@ function createPlugin(options) {
751
671
  */
752
672
  const AUTH_HINT = "generate a Personal Access Token at https://github.com/settings/tokens (scopes: repo, admin:repo_hook — plus admin:org for org-level ops), then run: holocron auth set github <PAT>";
753
673
  //#endregion
754
- export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubRestClient, GitHubSecrets, GitHubSource, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, verifyToken };
674
+ export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveToken, secrets, source, syncLabels, syncTeams, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.70",
4
4
  "description": "Holocron plugin for GitHub. Implements source, ci, secrets, environments, and issues capabilities against the GitHub REST API.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-github#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -21,24 +21,30 @@
21
21
  }
22
22
  },
23
23
  "peerDependencies": {
24
- "@theholocron/cli": "2.0.0-alpha.7"
24
+ "@theholocron/github-client": "^1.1.0",
25
+ "@theholocron/cli": "2.0.0-alpha.70"
25
26
  },
26
27
  "dependencies": {
27
28
  "libsodium-wrappers": "^0.7.15"
28
29
  },
29
30
  "devDependencies": {
30
- "@theholocron/eslint-config": "^4.1.0",
31
- "@theholocron/tsconfig": "^4.1.0",
32
- "@tsconfig/node-lts": "^24.0.0",
31
+ "@theholocron/eslint-config": "^7.3.0",
32
+ "@theholocron/github-client": "^1.1.0",
33
+ "@theholocron/tsconfig": "^7.3.0",
34
+ "@theholocron/tsdown-config": "^7.3.0",
35
+ "@theholocron/vitest-config": "^7.3.0",
33
36
  "@types/libsodium-wrappers": "^0.7.14",
34
- "@vitest/coverage-v8": "^3.2.6",
35
- "eslint": "^9.36.0",
36
- "globals": "^16.5.0",
37
- "typescript": "^5.9.3",
38
- "vitest": "^3.2.6",
37
+ "@types/node": "^26",
38
+ "@vitest/coverage-v8": "^4.1.10",
39
+ "@vitest/eslint-plugin": "^1.6.23",
40
+ "eslint": "^10.7.0",
41
+ "eslint-plugin-n": "^18.2.2",
42
+ "globals": "^17.7.0",
39
43
  "tsdown": "^0.22.3",
40
44
  "tsx": "^4.22.4",
41
- "@theholocron/cli": "2.0.0-alpha.7"
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.1.10",
47
+ "@theholocron/cli": "2.0.0-alpha.70"
42
48
  },
43
49
  "publishConfig": {
44
50
  "access": "public"