gitdog 0.0.0-stage → 0.1.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 +76 -2
- package/dist/api-catalog.json +432 -0
- package/dist/api-keys.d.ts +15 -0
- package/dist/api-keys.js +93 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +243 -0
- package/dist/catalog.d.ts +11 -0
- package/dist/catalog.js +8 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +31 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +109 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +678 -0
- package/dist/projects.d.ts +55 -0
- package/dist/projects.js +349 -0
- package/dist/storage.d.ts +24 -0
- package/dist/storage.js +147 -0
- package/dist/ui.d.ts +17 -0
- package/dist/ui.js +155 -0
- package/package.json +54 -4
package/dist/commands.js
ADDED
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
import { GitdogApi } from "./client.js";
|
|
6
|
+
import { login, logout, openBrowser } from "./auth.js";
|
|
7
|
+
import { API_CATALOG, publicOperation } from "./catalog.js";
|
|
8
|
+
import { BOOLEAN_SETTINGS, STYLE_SETTINGS, ProjectContext, limitParser, } from "./projects.js";
|
|
9
|
+
import { DEFAULT_API_URL, DEFAULT_SITE_URL, SessionStore, validateUrl, configDirectory, exists, } from "./storage.js";
|
|
10
|
+
import { readProjectKey, saveProjectKey, projectHttpUrl } from "./api-keys.js";
|
|
11
|
+
import { Ui, VERSION, asRecord, integer } from "./ui.js";
|
|
12
|
+
const text = (value) => typeof value === "string" ? value : undefined;
|
|
13
|
+
const number = (value) => typeof value === "number" ? value : undefined;
|
|
14
|
+
const flag = (value) => value === true;
|
|
15
|
+
const compact = (object) => Object.fromEntries(Object.entries(object).filter((entry) => entry[1] !== undefined));
|
|
16
|
+
export async function jsonArguments(value) {
|
|
17
|
+
const content = value.startsWith("@")
|
|
18
|
+
? await readFile(resolve(value.slice(1)), "utf8")
|
|
19
|
+
: value;
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = JSON.parse(content);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error("--data must be a JSON object, or @path/to/file.json.");
|
|
26
|
+
}
|
|
27
|
+
const object = asRecord(parsed);
|
|
28
|
+
if (!object)
|
|
29
|
+
throw new Error("--data must be a JSON object.");
|
|
30
|
+
return object;
|
|
31
|
+
}
|
|
32
|
+
export function createProgram(dependencies = {}) {
|
|
33
|
+
const program = new Command();
|
|
34
|
+
program
|
|
35
|
+
.name("gitdog")
|
|
36
|
+
.description("Your Gitdog workspace, from the terminal.")
|
|
37
|
+
.version(VERSION)
|
|
38
|
+
.option("-P, --project <alias|owner/repo|id>", "Project for this command")
|
|
39
|
+
.option("--config <path>", "Path to gitdog.json")
|
|
40
|
+
.option("--key-file <path>", "Use a private project API key file instead of an owner session")
|
|
41
|
+
.option("--api-url <url>", "Use a custom Gitdog backend (or GITDOG_API_URL)")
|
|
42
|
+
.option("--site-url <url>", "Use a custom Gitdog website (or GITDOG_SITE_URL)")
|
|
43
|
+
.option("--json", "Output JSON without banners, tables or prompts")
|
|
44
|
+
.option("-y, --yes", "Confirm the requested operation without prompting")
|
|
45
|
+
.showHelpAfterError()
|
|
46
|
+
.showSuggestionAfterError()
|
|
47
|
+
.addHelpText("after", `\nGetting started:\n gitdog login\n gitdog init\n gitdog config set requireApproval true\n gitdog config push --dry-run\n gitdog config push\n\nFirebase-style aliases also work: projects:list, projects:create, config:push.\nRun gitdog <command> --help for details.`);
|
|
48
|
+
async function context(command) {
|
|
49
|
+
const options = command.optsWithGlobals();
|
|
50
|
+
const ui = dependencies.ui ?? new Ui(flag(options.json));
|
|
51
|
+
const keyFile = text(options.keyFile) ?? process.env.GITDOG_API_KEY_FILE;
|
|
52
|
+
const key = keyFile ? await readProjectKey(keyFile) : undefined;
|
|
53
|
+
let session;
|
|
54
|
+
if (!dependencies.api) {
|
|
55
|
+
try {
|
|
56
|
+
session = await new SessionStore().read();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* Authenticated commands report corrupt sessions; login can replace them. */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const apiUrl = validateUrl(text(options.apiUrl) ??
|
|
63
|
+
process.env.GITDOG_API_URL ??
|
|
64
|
+
key?.apiUrl ??
|
|
65
|
+
session?.apiUrl ??
|
|
66
|
+
DEFAULT_API_URL, "API URL");
|
|
67
|
+
const siteUrl = validateUrl(text(options.siteUrl) ??
|
|
68
|
+
process.env.GITDOG_SITE_URL ??
|
|
69
|
+
session?.siteUrl ??
|
|
70
|
+
DEFAULT_SITE_URL, "Site URL");
|
|
71
|
+
const api = dependencies.api ?? new GitdogApi(apiUrl, siteUrl, undefined, keyFile);
|
|
72
|
+
return {
|
|
73
|
+
api,
|
|
74
|
+
apiUrl,
|
|
75
|
+
siteUrl,
|
|
76
|
+
ui,
|
|
77
|
+
options,
|
|
78
|
+
project: new ProjectContext(api, ui, text(options.project), text(options.config)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function command(parent, signature, description, handler) {
|
|
82
|
+
const cmd = parent.command(signature).description(description);
|
|
83
|
+
cmd.action(async (...values) => {
|
|
84
|
+
const current = values.at(-1);
|
|
85
|
+
const args = values.slice(0, -2);
|
|
86
|
+
const ctx = await context(current);
|
|
87
|
+
await handler(ctx, args, ctx.options);
|
|
88
|
+
});
|
|
89
|
+
return cmd;
|
|
90
|
+
}
|
|
91
|
+
const service = (parent, signature, description, kind, path, build = () => ({}), confirm) => command(parent, signature, description, async ({ api, ui }, args, opts) => {
|
|
92
|
+
const data = build(args, opts);
|
|
93
|
+
if (confirm)
|
|
94
|
+
await ui.confirm(confirm, flag(opts.yes));
|
|
95
|
+
const result = await api.call(kind, path, data);
|
|
96
|
+
if (result === null && /getById$/.test(path))
|
|
97
|
+
throw new Error("This item does not exist or is not accessible.");
|
|
98
|
+
ui.output(result);
|
|
99
|
+
});
|
|
100
|
+
program.action(() => {
|
|
101
|
+
const ui = dependencies.ui ?? new Ui(flag(program.opts().json));
|
|
102
|
+
ui.banner();
|
|
103
|
+
if (ui.json)
|
|
104
|
+
ui.output({
|
|
105
|
+
name: "gitdog",
|
|
106
|
+
version: VERSION,
|
|
107
|
+
next: ["gitdog login", "gitdog init", "gitdog --help"],
|
|
108
|
+
});
|
|
109
|
+
else
|
|
110
|
+
ui.output("Get started\n 1. gitdog login Sign in with GitHub\n 2. gitdog init Connect a project\n 3. gitdog status Check your assistant\n\nRun gitdog --help to explore all commands.");
|
|
111
|
+
});
|
|
112
|
+
command(program, "login", "Sign in with GitHub using your browser", async ({ apiUrl, siteUrl, ui }, _args, opts) => login(apiUrl, siteUrl, ui, opts.browser === false)).option("--no-browser", "Print the sign-in URL without opening a browser");
|
|
113
|
+
command(program, "logout", "Revoke this session and remove saved credentials", async ({ ui }, _args, opts) => logout(ui, flag(opts.local))).option("--local", "Remove local credentials without contacting the server");
|
|
114
|
+
service(program, "whoami", "Show your signed-in Gitdog account", "query", "users/query:getCurrentUser");
|
|
115
|
+
command(program, "init [repository]", "Create or connect a project and write gitdog.json", async ({ project, ui, apiUrl, siteUrl }, args, opts) => {
|
|
116
|
+
if (!dependencies.api &&
|
|
117
|
+
!process.env.GITDOG_TOKEN &&
|
|
118
|
+
!opts.keyFile &&
|
|
119
|
+
!process.env.GITDOG_API_KEY_FILE &&
|
|
120
|
+
!process.env.GITDOG_API_KEY &&
|
|
121
|
+
!(await new SessionStore().read())) {
|
|
122
|
+
if (!process.stdin.isTTY || ui.json)
|
|
123
|
+
throw new Error("Run gitdog login before gitdog init.");
|
|
124
|
+
await login(apiUrl, siteUrl, ui);
|
|
125
|
+
}
|
|
126
|
+
await project.init(args[0], text(opts.alias) ?? "default", flag(opts.force));
|
|
127
|
+
})
|
|
128
|
+
.option("--alias <name>", "Local project alias", "default")
|
|
129
|
+
.option("--force", "Replace an existing local project configuration");
|
|
130
|
+
command(program, "use [project]", "Select the default local project or add an alias", async ({ project }, args, opts) => project.use(args[0], text(opts.add), flag(opts.list)))
|
|
131
|
+
.option("--add <alias>", "Add owner/repo as an alias and select it")
|
|
132
|
+
.option("--list", "List local aliases");
|
|
133
|
+
command(program, "status", "Show active project, indexing and pending approvals", async ({ api, project, ui }) => {
|
|
134
|
+
const repo = await project.repository();
|
|
135
|
+
const [indexing, pending] = await Promise.all([
|
|
136
|
+
api.call("query", "repoIndexing/query:getIndexingStatus", {
|
|
137
|
+
repositoryId: repo._id,
|
|
138
|
+
}),
|
|
139
|
+
api.call("query", "tickets/query:listPendingApproval", {
|
|
140
|
+
githubRepo: repo.githubRepo,
|
|
141
|
+
limit: 20,
|
|
142
|
+
offset: 0,
|
|
143
|
+
}),
|
|
144
|
+
]);
|
|
145
|
+
ui.output({
|
|
146
|
+
project: repo.githubRepo,
|
|
147
|
+
active: repo.isActive,
|
|
148
|
+
indexing,
|
|
149
|
+
pending,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
command(program, "open", "Open the active project in Gitdog", async ({ project, ui, siteUrl }, _args, opts) => {
|
|
153
|
+
const repo = await project.repository();
|
|
154
|
+
const url = `${siteUrl}/repositories/${repo.githubRepo.split("/").map(encodeURIComponent).join("/")}`;
|
|
155
|
+
if (!flag(opts.print) && !ui.json)
|
|
156
|
+
await openBrowser(url);
|
|
157
|
+
ui.output({ url });
|
|
158
|
+
}).option("--print", "Print the URL without opening a browser");
|
|
159
|
+
const keys = program
|
|
160
|
+
.command("keys")
|
|
161
|
+
.description("Create, list, check and revoke project API keys");
|
|
162
|
+
command(keys, "create", "Issue a project API key and save it to a private file", async ({ api, project, ui, apiUrl }, _args, opts) => {
|
|
163
|
+
const repository = await project.repository();
|
|
164
|
+
const out = text(opts.out);
|
|
165
|
+
if (out && (await exists(resolve(out))))
|
|
166
|
+
throw new Error("The output file already exists. Choose a new private path.");
|
|
167
|
+
const name = text(opts.name) ?? "CLI key";
|
|
168
|
+
const scopes = flag(opts.write) ? ["read", "write"] : ["read"];
|
|
169
|
+
const issued = await api.call("action", "apiKeys/action:create", {
|
|
170
|
+
repositoryId: repository._id,
|
|
171
|
+
name,
|
|
172
|
+
scopes,
|
|
173
|
+
expiresInDays: number(opts.expiresIn) ?? 30,
|
|
174
|
+
});
|
|
175
|
+
const keyFile = out
|
|
176
|
+
? resolve(out)
|
|
177
|
+
: join(configDirectory(), "keys", `${issued.keyId}.json`);
|
|
178
|
+
try {
|
|
179
|
+
if (!/^gdk_[a-f0-9]{64}$/.test(issued.key) ||
|
|
180
|
+
!/^[a-z0-9]{20,64}$/.test(issued.keyId))
|
|
181
|
+
throw new Error("The server returned invalid API key material.");
|
|
182
|
+
await saveProjectKey(keyFile, {
|
|
183
|
+
version: 1,
|
|
184
|
+
apiKey: issued.key,
|
|
185
|
+
keyId: issued.keyId,
|
|
186
|
+
apiUrl,
|
|
187
|
+
httpUrl: projectHttpUrl(apiUrl),
|
|
188
|
+
repositoryId: repository._id,
|
|
189
|
+
name,
|
|
190
|
+
scopes: issued.scopes,
|
|
191
|
+
expiresAt: issued.expiresAt,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
try {
|
|
196
|
+
await api.call("mutation", "apiKeys/mutation:revoke", {
|
|
197
|
+
keyId: issued.keyId,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
throw new Error(`Could not save or revoke the new key. Revoke key ${issued.keyId} with an owner session.`);
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
ui.output({
|
|
206
|
+
keyId: issued.keyId,
|
|
207
|
+
prefix: issued.prefix,
|
|
208
|
+
project: repository.githubRepo,
|
|
209
|
+
scopes: issued.scopes,
|
|
210
|
+
expiresAt: issued.expiresAt,
|
|
211
|
+
keyFile,
|
|
212
|
+
});
|
|
213
|
+
ui.info(`Key saved privately. Use: gitdog --key-file ${JSON.stringify(keyFile)} projects list`);
|
|
214
|
+
})
|
|
215
|
+
.option("--name <name>", "Key name", "CLI key")
|
|
216
|
+
.option("--write", "Grant project settings and ticket write access (read access is always included)")
|
|
217
|
+
.option("--expires-in <days>", "Expiry in days (1–365)", (value) => integer(value, 1, 365), 30)
|
|
218
|
+
.option("--out <path>", "Private key file path; defaults to the Gitdog credentials directory");
|
|
219
|
+
command(keys, "list [repository]", "List key metadata without exposing secrets", async ({ api, project, ui }, args, opts) => {
|
|
220
|
+
const repository = await project.repository(args[0]);
|
|
221
|
+
ui.output(await api.call("query", "apiKeys/query:list", {
|
|
222
|
+
repositoryId: repository._id,
|
|
223
|
+
paginationOpts: {
|
|
224
|
+
numItems: number(opts.limit) ?? 20,
|
|
225
|
+
cursor: text(opts.cursor) ?? null,
|
|
226
|
+
},
|
|
227
|
+
}));
|
|
228
|
+
})
|
|
229
|
+
.option("--limit <n>", "Page size (1–100)", (value) => integer(value, 1, 100), 20)
|
|
230
|
+
.option("--cursor <cursor>", "Continue a key metadata page");
|
|
231
|
+
command(keys, "check", "Verify a project API key with a live authenticated request", async ({ api, ui }, _args, opts) => {
|
|
232
|
+
if (!opts.keyFile &&
|
|
233
|
+
!process.env.GITDOG_API_KEY_FILE &&
|
|
234
|
+
!process.env.GITDOG_API_KEY &&
|
|
235
|
+
!dependencies.api)
|
|
236
|
+
throw new Error("Select --key-file, GITDOG_API_KEY_FILE or GITDOG_API_KEY first.");
|
|
237
|
+
const projects = await api.call("query", "repositories/query:list");
|
|
238
|
+
ui.output({
|
|
239
|
+
authenticated: true,
|
|
240
|
+
projects: projects.map((item) => ({
|
|
241
|
+
id: item._id,
|
|
242
|
+
repository: item.githubRepo,
|
|
243
|
+
})),
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
command(keys, "revoke <id>", "Revoke a key immediately and remove its default local copy", async ({ api, ui }, args, opts) => {
|
|
247
|
+
if (!/^[a-z0-9]{20,64}$/.test(args[0]))
|
|
248
|
+
throw new Error("Use an API key ID from keys list, not a key secret.");
|
|
249
|
+
await ui.confirm(`Revoke project API key ${args[0]}?`, flag(opts.yes));
|
|
250
|
+
const result = await api.call("mutation", "apiKeys/mutation:revoke", { keyId: args[0] });
|
|
251
|
+
if (/^[a-z0-9]{20,64}$/.test(result.keyId))
|
|
252
|
+
await rm(join(configDirectory(), "keys", `${result.keyId}.json`), {
|
|
253
|
+
force: true,
|
|
254
|
+
});
|
|
255
|
+
ui.output(result);
|
|
256
|
+
});
|
|
257
|
+
const projects = program
|
|
258
|
+
.command("projects")
|
|
259
|
+
.alias("repos")
|
|
260
|
+
.description("Create, connect and manage repository projects");
|
|
261
|
+
service(projects, "list", "List owned and shared Gitdog projects", "query", "repositories/query:list");
|
|
262
|
+
service(projects, "available", "List a page of repositories available to connect", "action", "github/action:listUserRepositories", (_args, opts) => compact({
|
|
263
|
+
cursor: text(opts.cursor),
|
|
264
|
+
reconnectRepositoryId: text(opts.reconnect),
|
|
265
|
+
}))
|
|
266
|
+
.option("--cursor <cursor>", "Continue from the returned continueCursor")
|
|
267
|
+
.option("--reconnect <id>", "List candidates for an existing disconnected project");
|
|
268
|
+
command(projects, "create <repository>", "Create a Gitdog project from an authorized GitHub repository", async ({ project, ui }, args) => ui.output(await project.connect(args[0])));
|
|
269
|
+
command(projects, "get [repository]", "Show a project and its settings", async ({ project, ui }, args) => ui.output(await project.repository(args[0])));
|
|
270
|
+
command(projects, "reconnect [repository]", "Reconnect an existing project's GitHub App access", async ({ project, ui }, args) => {
|
|
271
|
+
const repo = await project.repository(args[0]);
|
|
272
|
+
ui.output(await project.connect(repo.githubRepo, repo._id));
|
|
273
|
+
});
|
|
274
|
+
command(projects, "sync [repository]", "Synchronize repository files and refresh the index", async ({ api, project, ui }, args) => {
|
|
275
|
+
const repo = await project.repository(args[0]);
|
|
276
|
+
const result = await api.call("action", "repositories/action:syncRepository", { repositoryId: repo._id });
|
|
277
|
+
if (asRecord(result)?.success === false)
|
|
278
|
+
throw new Error(String(asRecord(result)?.message ?? "Repository sync could not start."));
|
|
279
|
+
ui.output(result);
|
|
280
|
+
});
|
|
281
|
+
for (const enabled of [true, false]) {
|
|
282
|
+
command(projects, enabled ? "enable [repository]" : "disable [repository]", enabled ? "Enable project automation" : "Pause project automation", async ({ api, project, ui }, args, opts) => {
|
|
283
|
+
const repo = await project.repository(args[0]);
|
|
284
|
+
if (repo.isActive !== enabled) {
|
|
285
|
+
await ui.confirm(`${enabled ? "Enable" : "Pause"} automation for ${repo.githubRepo}?`, flag(opts.yes));
|
|
286
|
+
await api.call("mutation", "repositories/mutation:toggleActive", {
|
|
287
|
+
repositoryId: repo._id,
|
|
288
|
+
isActive: enabled,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const current = await project.repository(repo.githubRepo);
|
|
292
|
+
if (current.isActive !== enabled)
|
|
293
|
+
throw new Error("Project activity changed during verification. Check projects get before retrying.");
|
|
294
|
+
ui.output({ project: current.githubRepo, active: current.isActive });
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
command(projects, "delete [repository]", "Disconnect a Gitdog project and remove its cached data", async ({ api, project, ui }, args, opts) => {
|
|
298
|
+
const repo = await project.repository(args[0]);
|
|
299
|
+
await ui.confirm(`Disconnect ${repo.githubRepo} and remove Gitdog cached data? The GitHub repository is preserved.`, flag(opts.yes));
|
|
300
|
+
ui.output(await api.call("mutation", "repositories/mutation:remove", {
|
|
301
|
+
repositoryId: repo._id,
|
|
302
|
+
}));
|
|
303
|
+
});
|
|
304
|
+
const config = program
|
|
305
|
+
.command("config")
|
|
306
|
+
.description("Manage all issue and discussion automation settings");
|
|
307
|
+
command(config, "keys", "List all supported automation settings", async ({ ui }) => ui.output([
|
|
308
|
+
...BOOLEAN_SETTINGS.map((name) => ({ name, type: "boolean" })),
|
|
309
|
+
...STYLE_SETTINGS.map((name) => ({ name, type: "string" })),
|
|
310
|
+
]));
|
|
311
|
+
command(config, "get [key]", "Read local settings (or current remote settings)", async ({ project }, args, opts) => project.configGet(args[0], flag(opts.remote))).option("--remote", "Read settings from Gitdog");
|
|
312
|
+
command(config, "set <key> <value>", "Set one local setting; use config push to apply", async ({ project }, args) => project.configSet(args[0], args[1]));
|
|
313
|
+
command(config, "pull", "Replace local settings with remote settings", async ({ project }, _args, opts) => project.configPull(flag(opts.yes)));
|
|
314
|
+
command(config, "push", "Preview and apply local settings to Gitdog", async ({ project }, _args, opts) => project.configPush(flag(opts.dryRun), flag(opts.yes))).option("--dry-run", "Show changes without applying them");
|
|
315
|
+
const indexing = program
|
|
316
|
+
.command("index")
|
|
317
|
+
.description("Inspect repository indexing and cached files");
|
|
318
|
+
command(indexing, "status", "Show indexing progress; optionally wait for completion", async ({ api, project, ui }, _args, opts) => {
|
|
319
|
+
const repo = await project.repository();
|
|
320
|
+
const deadline = Date.now() + (number(opts.timeout) ?? 300) * 1000;
|
|
321
|
+
let last = "";
|
|
322
|
+
do {
|
|
323
|
+
const remaining = deadline - Date.now();
|
|
324
|
+
if (flag(opts.wait) && remaining <= 0)
|
|
325
|
+
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check gitdog index status.");
|
|
326
|
+
const state = await api.call("query", "repoIndexing/query:getIndexingStatus", { repositoryId: repo._id }, flag(opts.wait) ? { timeoutMs: remaining } : undefined);
|
|
327
|
+
const record = asRecord(state);
|
|
328
|
+
if (!record)
|
|
329
|
+
throw new Error("Indexing status is not available.");
|
|
330
|
+
if (!flag(opts.wait)) {
|
|
331
|
+
ui.output(state);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const fingerprint = JSON.stringify(state);
|
|
335
|
+
if (fingerprint !== last) {
|
|
336
|
+
ui.info(`Index: ${String(record.status)} / Search: ${String(record.ragStatus)}`);
|
|
337
|
+
last = fingerprint;
|
|
338
|
+
}
|
|
339
|
+
if (record.status === "failed" || record.ragStatus === "failed")
|
|
340
|
+
throw new Error("Indexing failed. Run gitdog index logs for details.");
|
|
341
|
+
if (record.status === "completed" &&
|
|
342
|
+
["completed", "skipped"].includes(String(record.ragStatus))) {
|
|
343
|
+
ui.output(state);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (Date.now() >= deadline)
|
|
347
|
+
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check gitdog index status.");
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(2000, Math.max(1, deadline - Date.now()))));
|
|
349
|
+
} while (true);
|
|
350
|
+
})
|
|
351
|
+
.option("--wait", "Wait for indexing and search readiness")
|
|
352
|
+
.option("--timeout <seconds>", "Maximum wait (1–900 seconds)", (value) => integer(value, 1, 900), 300);
|
|
353
|
+
for (const [name, path, description] of [
|
|
354
|
+
["logs", "getIndexingLogs", "Show logs for the current indexing run"],
|
|
355
|
+
["files", "getFileList", "List cached repository files"],
|
|
356
|
+
])
|
|
357
|
+
command(indexing, name, description, async ({ api, project, ui }, _args, opts) => ui.output(await api.call("query", `repoIndexing/query:${path}`, {
|
|
358
|
+
repositoryId: (await project.repository())._id,
|
|
359
|
+
limit: number(opts.limit) ?? 100,
|
|
360
|
+
}))).option("--limit <n>", "Maximum records (1–500)", limitParser, 100);
|
|
361
|
+
command(indexing, "file <path>", "Read a cached repository file", async ({ api, project, ui }, args) => ui.output(await api.call("query", "repoIndexing/query:getFileContent", {
|
|
362
|
+
repositoryId: (await project.repository())._id,
|
|
363
|
+
filePath: args[0],
|
|
364
|
+
})));
|
|
365
|
+
command(indexing, "restart", "Restart repository indexing", async ({ api, project, ui }, _args, opts) => {
|
|
366
|
+
const repo = await project.repository();
|
|
367
|
+
await ui.confirm(`Restart indexing for ${repo.githubRepo}?`, flag(opts.yes));
|
|
368
|
+
ui.output(await api.call("mutation", "repoIndexing/mutation:initializeIndexing", {
|
|
369
|
+
repositoryId: repo._id,
|
|
370
|
+
}));
|
|
371
|
+
});
|
|
372
|
+
command(indexing, "learnings", "Count trusted repository learnings", async ({ api, project, ui }) => ui.output({
|
|
373
|
+
count: await api.call("query", "repoLearnings/query:countByRepository", { repositoryId: (await project.repository())._id }),
|
|
374
|
+
}));
|
|
375
|
+
const tickets = program
|
|
376
|
+
.command("tickets")
|
|
377
|
+
.description("Browse issues, discussions and pending approvals");
|
|
378
|
+
for (const pending of [false, true]) {
|
|
379
|
+
command(tickets, pending ? "pending" : "list", pending
|
|
380
|
+
? "List tickets awaiting approval"
|
|
381
|
+
: "List issues and discussions", async ({ api, project, ui }, _args, opts) => {
|
|
382
|
+
const githubRepo = flag(opts.all)
|
|
383
|
+
? undefined
|
|
384
|
+
: (await project.repository()).githubRepo;
|
|
385
|
+
ui.output(await api.call("query", `tickets/query:${pending ? "listPendingApproval" : "list"}`, compact({
|
|
386
|
+
githubRepo,
|
|
387
|
+
...(!pending ? { status: text(opts.status) } : {}),
|
|
388
|
+
ticketType: text(opts.type),
|
|
389
|
+
search: text(opts.search),
|
|
390
|
+
limit: number(opts.limit),
|
|
391
|
+
offset: number(opts.offset),
|
|
392
|
+
})));
|
|
393
|
+
})
|
|
394
|
+
.option("--all", "List across all projects instead of the active project")
|
|
395
|
+
.addOption(new Option("--status <status>", "Ticket status (list only)").choices([
|
|
396
|
+
"open",
|
|
397
|
+
"closed",
|
|
398
|
+
]))
|
|
399
|
+
.addOption(new Option("--type <type>", "Ticket type").choices([
|
|
400
|
+
"issue",
|
|
401
|
+
"discussion",
|
|
402
|
+
]))
|
|
403
|
+
.option("--search <text>", "Search title, repository or GitHub number")
|
|
404
|
+
.option("--limit <n>", "Page size (1–500)", limitParser, 20)
|
|
405
|
+
.option("--offset <n>", "Number of records to skip", (value) => integer(value, 0), 0);
|
|
406
|
+
}
|
|
407
|
+
service(tickets, "get <id>", "Show a ticket and its actions", "query", "tickets/query:getById", ([id]) => ({ id }));
|
|
408
|
+
service(tickets, "delete <id>", "Soft-delete a ticket in Gitdog", "mutation", "tickets/mutation:softDelete", ([id]) => ({ id }), "Delete this Gitdog ticket?");
|
|
409
|
+
service(tickets, "restore <id>", "Restore a soft-deleted ticket by ID", "mutation", "tickets/mutation:restore", ([id]) => ({ id }));
|
|
410
|
+
const actions = program
|
|
411
|
+
.command("actions")
|
|
412
|
+
.description("Review and approve assistant actions");
|
|
413
|
+
service(actions, "list <ticket-id>", "List actions for a ticket", "query", "ticketActions/query:listByTicket", ([ticketId]) => ({ ticketId }));
|
|
414
|
+
service(actions, "get <id>", "Show one assistant action", "query", "ticketActions/query:getById", ([id]) => ({ id }));
|
|
415
|
+
service(actions, "approve <id>", "Approve and execute an action on GitHub", "action", "ticketActions/action:approve", ([actionId]) => ({ actionId }), "Approve this action and execute it on GitHub?");
|
|
416
|
+
service(actions, "reject <id>", "Reject an assistant action", "action", "ticketActions/action:reject", ([actionId]) => ({ actionId }), "Reject this assistant action?");
|
|
417
|
+
service(actions, "request-changes <id> <feedback>", "Request a revised assistant response", "action", "ticketActions/action:requestChanges", ([actionId, feedback]) => ({ actionId, feedback }), "Request a new response using this feedback?");
|
|
418
|
+
const notifications = program
|
|
419
|
+
.command("notifications")
|
|
420
|
+
.description("Read workspace notifications");
|
|
421
|
+
command(notifications, "list", "List recent or unread notifications", async ({ api, ui }, _args, opts) => ui.output(await api.call("query", flag(opts.unread)
|
|
422
|
+
? "notifications/query:listUnread"
|
|
423
|
+
: "notifications/query:list", flag(opts.unread) ? {} : { limit: number(opts.limit) ?? 50 })))
|
|
424
|
+
.option("--unread", "Show the 20 most recent unread notifications")
|
|
425
|
+
.option("--limit <n>", "Maximum records (1–500)", limitParser, 50);
|
|
426
|
+
service(notifications, "count", "Show unread count (capped at 100)", "query", "notifications/query:getUnreadCount");
|
|
427
|
+
service(notifications, "read <id>", "Mark a notification as read", "mutation", "notifications/mutation:markAsRead", ([notificationId]) => ({ notificationId }));
|
|
428
|
+
service(notifications, "read-all", "Mark all unread notifications as read", "mutation", "notifications/mutation:markAllAsRead");
|
|
429
|
+
const team = program
|
|
430
|
+
.command("team")
|
|
431
|
+
.description("Manage project members and invitations");
|
|
432
|
+
command(team, "members", "List project members", async ({ api, project, ui }) => ui.output(await api.call("query", "repositoryMembers/query:list", {
|
|
433
|
+
repositoryId: (await project.repository())._id,
|
|
434
|
+
})));
|
|
435
|
+
command(team, "role", "Show your role in this project", async ({ api, project, ui }) => ui.output(await api.call("query", "repositoryMembers/query:getMyRole", {
|
|
436
|
+
repositoryId: (await project.repository())._id,
|
|
437
|
+
})));
|
|
438
|
+
command(team, "invite <github-username>", "Invite a GitHub user as a project viewer", async ({ api, project, ui }, args, opts) => {
|
|
439
|
+
const repo = await project.repository();
|
|
440
|
+
await ui.confirm(`Invite ${args[0]} as a viewer of ${repo.githubRepo}?`, flag(opts.yes));
|
|
441
|
+
ui.output(await api.call("action", "invitations/action:create", {
|
|
442
|
+
repositoryId: repo._id,
|
|
443
|
+
inviteeGithubUsername: args[0],
|
|
444
|
+
role: "viewer",
|
|
445
|
+
}));
|
|
446
|
+
});
|
|
447
|
+
command(team, "invitations", "List this project's invitations", async ({ api, project, ui }, _args, opts) => ui.output(await api.call("query", flag(opts.pending)
|
|
448
|
+
? "invitations/query:getPendingForRepository"
|
|
449
|
+
: "invitations/query:listForRepository", { repositoryId: (await project.repository())._id }))).option("--pending", "Show only pending project invitations");
|
|
450
|
+
service(team, "remove <member-id>", "Remove a project member", "mutation", "repositoryMembers/mutation:remove", ([memberId]) => ({ memberId }), "Remove this project member?");
|
|
451
|
+
command(team, "leave", "Leave a shared project", async ({ api, project, ui }, _args, opts) => {
|
|
452
|
+
const repo = await project.repository();
|
|
453
|
+
await ui.confirm(`Leave ${repo.githubRepo}?`, flag(opts.yes));
|
|
454
|
+
ui.output(await api.call("mutation", "repositoryMembers/mutation:leave", {
|
|
455
|
+
repositoryId: repo._id,
|
|
456
|
+
}));
|
|
457
|
+
});
|
|
458
|
+
const invitations = program
|
|
459
|
+
.command("invitations")
|
|
460
|
+
.description("Manage incoming invitations");
|
|
461
|
+
service(invitations, "list", "List incoming pending invitations", "query", "invitations/query:listPending");
|
|
462
|
+
for (const name of ["accept", "reject", "cancel"])
|
|
463
|
+
service(invitations, `${name} <id>`, `${name[0].toUpperCase() + name.slice(1)} a project invitation`, "mutation", `invitations/mutation:${name}`, ([invitationId]) => ({ invitationId }), `${name[0].toUpperCase() + name.slice(1)} this invitation?`);
|
|
464
|
+
const usage = program
|
|
465
|
+
.command("usage")
|
|
466
|
+
.description("Inspect token usage and billing estimates");
|
|
467
|
+
service(usage, "summary", "Show current billing-period usage", "query", "tokenUsage/query:getCurrentMonthUsage");
|
|
468
|
+
service(usage, "projects", "Show usage grouped by repository", "query", "tokenUsage/query:getUsageByRepository");
|
|
469
|
+
service(usage, "estimate", "Show estimated overage and billing costs", "query", "subscriptions/query:calculateOverage");
|
|
470
|
+
service(usage, "quota", "Check whether your account can perform an action", "query", "subscriptions/query:canPerformAction");
|
|
471
|
+
const billing = program
|
|
472
|
+
.command("billing")
|
|
473
|
+
.description("Inspect your plan and open secure checkout");
|
|
474
|
+
service(billing, "status", "Show current subscription", "query", "subscriptions/query:getCurrent");
|
|
475
|
+
service(billing, "pricing", "Show plans and token-pack pricing", "query", "subscriptions/query:getPricingInfo");
|
|
476
|
+
command(billing, "checkout [pack]", "Create a checkout link for Pro or a token pack", async ({ api, ui }, args, opts) => {
|
|
477
|
+
const pack = args[0] ?? "pro";
|
|
478
|
+
if (!["pro", "small", "large"].includes(pack))
|
|
479
|
+
throw new Error("Choose pro, small or large.");
|
|
480
|
+
const result = asRecord(await api.call("action", pack === "pro"
|
|
481
|
+
? "subscriptions/action:createCheckoutUrl"
|
|
482
|
+
: "subscriptions/action:createTokenPackCheckoutUrl", pack === "pro" ? { plan: "pro" } : { packSize: pack }));
|
|
483
|
+
if (typeof result?.checkoutUrl !== "string" ||
|
|
484
|
+
new URL(result.checkoutUrl).protocol !== "https:")
|
|
485
|
+
throw new Error("Gitdog did not return a valid checkout URL.");
|
|
486
|
+
if (flag(opts.open) && !ui.json)
|
|
487
|
+
await openBrowser(result.checkoutUrl);
|
|
488
|
+
ui.output(result);
|
|
489
|
+
}).option("--open", "Open checkout in a browser; payment still requires checkout confirmation");
|
|
490
|
+
const account = program
|
|
491
|
+
.command("account")
|
|
492
|
+
.description("View your account and manage consent");
|
|
493
|
+
service(account, "profile", "Show your account profile", "query", "users/query:getCurrentUser");
|
|
494
|
+
service(account, "sync", "Refresh your GitHub profile", "action", "users/action:ensureProfile");
|
|
495
|
+
command(account, "consent", "Open the account consent screen", async ({ ui, siteUrl }, _args, opts) => {
|
|
496
|
+
const url = `${siteUrl}/legal/consent`;
|
|
497
|
+
if (!ui.json && !flag(opts.print))
|
|
498
|
+
await openBrowser(url);
|
|
499
|
+
ui.output({
|
|
500
|
+
url,
|
|
501
|
+
message: "Review and confirm account eligibility and policies in your browser.",
|
|
502
|
+
});
|
|
503
|
+
}).option("--print", "Print the URL without opening a browser");
|
|
504
|
+
const github = program
|
|
505
|
+
.command("github")
|
|
506
|
+
.description("Manage GitHub App installations and repository access");
|
|
507
|
+
service(github, "installations", "List bound GitHub App installations", "query", "github/query:getUserInstallations");
|
|
508
|
+
command(github, "install", "Open GitHub App installation and permission selection", async ({ ui }, _args, opts) => {
|
|
509
|
+
const slug = text(opts.app) ?? "gitdogbot";
|
|
510
|
+
if (!/^[a-z\d][a-z\d-]{0,99}$/.test(slug))
|
|
511
|
+
throw new Error("Invalid GitHub App slug.");
|
|
512
|
+
const url = `https://github.com/apps/${slug}/installations/new`;
|
|
513
|
+
if (!ui.json && !flag(opts.print))
|
|
514
|
+
await openBrowser(url);
|
|
515
|
+
ui.output({
|
|
516
|
+
url,
|
|
517
|
+
next: "After GitHub authorization, run gitdog github recover, then gitdog projects available.",
|
|
518
|
+
});
|
|
519
|
+
})
|
|
520
|
+
.option("--app <slug>", "GitHub App slug (use gitdogbot-dev for development)", "gitdogbot")
|
|
521
|
+
.option("--print", "Print the installation URL");
|
|
522
|
+
command(github, "recover", "Recover installation bindings after GitHub authorization", async ({ api, ui }) => {
|
|
523
|
+
for (let count = 0; count < 500; count++) {
|
|
524
|
+
const result = asRecord(await api.call("action", "github/action:recoverOAuthInstallation"));
|
|
525
|
+
if (result?.status !== "recovered") {
|
|
526
|
+
ui.output({ ...result, recoveredCount: count });
|
|
527
|
+
if (result?.status === "reauth_required")
|
|
528
|
+
throw new Error("Refresh GitHub authorization with gitdog login, then run github recover again.");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
ui.info(`Recovered installation ${String(result.installationId)}.`);
|
|
532
|
+
}
|
|
533
|
+
throw new Error("Installation recovery reached its safety limit. Check github installations before retrying.");
|
|
534
|
+
});
|
|
535
|
+
service(github, "bind <installation-id>", "Refresh a GitHub installation binding", "action", "github/action:saveInstallation", ([id]) => ({ installationId: integer(id) }));
|
|
536
|
+
service(github, "retry <installation-id>", "Retry repository-access reconciliation", "action", "github/action:retryRepositoryAccessReconciliation", ([id]) => ({ installationId: integer(id) }));
|
|
537
|
+
const admin = program
|
|
538
|
+
.command("admin")
|
|
539
|
+
.description("Administrator operations (server-enforced access)");
|
|
540
|
+
const adminLimit = (value) => integer(value, 1, 100);
|
|
541
|
+
service(admin, "status", "Check administrator access", "query", "admin/query:isAdmin");
|
|
542
|
+
service(admin, "stats", "Show assistant workflow statistics", "query", "admin/query:getAiLogStats");
|
|
543
|
+
service(admin, "logs", "List assistant logs", "query", "admin/query:listAiLogs", (_args, opts) => compact({
|
|
544
|
+
limit: number(opts.limit),
|
|
545
|
+
cursor: text(opts.cursor),
|
|
546
|
+
workflowType: text(opts.workflow),
|
|
547
|
+
status: text(opts.status),
|
|
548
|
+
}))
|
|
549
|
+
.option("--limit <n>", "Page size (1–100)", adminLimit, 20)
|
|
550
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
551
|
+
.option("--workflow <type>", "Workflow type")
|
|
552
|
+
.option("--status <status>", "Workflow status");
|
|
553
|
+
service(admin, "payments", "List payment logs", "query", "admin/paymentQuery:listPaymentLogs", (_args, opts) => compact({
|
|
554
|
+
limit: number(opts.limit),
|
|
555
|
+
cursor: text(opts.cursor),
|
|
556
|
+
statusFilter: text(opts.status),
|
|
557
|
+
eventTypeFilter: text(opts.event),
|
|
558
|
+
}))
|
|
559
|
+
.option("--limit <n>", "Page size (1–100)", adminLimit, 20)
|
|
560
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
561
|
+
.option("--status <status>", "Payment status")
|
|
562
|
+
.option("--event <type>", "Payment event type");
|
|
563
|
+
service(admin, "subscriptions", "List subscriptions", "query", "admin/paymentQuery:listSubscriptions", (_args, opts) => ({ limit: number(opts.limit) ?? 20 })).option("--limit <n>", "Maximum records (1–100)", adminLimit, 20);
|
|
564
|
+
command(admin, "call <operation>", "Run an advanced admin operation with explicit JSON arguments", async ({ api, ui }, args, opts) => {
|
|
565
|
+
const operation = publicOperation(args[0]);
|
|
566
|
+
if (operation.path === "apiKeys/action:create")
|
|
567
|
+
throw new Error("Use keys create to issue API keys securely into a private file.");
|
|
568
|
+
if (!operation.path.startsWith("admin/"))
|
|
569
|
+
throw new Error("Use gitdog api call for non-admin operations.");
|
|
570
|
+
const data = await jsonArguments(text(opts.data) ?? "{}");
|
|
571
|
+
if (operation.kind !== "query") {
|
|
572
|
+
if (!ui.json)
|
|
573
|
+
ui.output({ operation: operation.path, arguments: data });
|
|
574
|
+
await ui.confirm(`Run ${operation.path}?`, flag(opts.yes));
|
|
575
|
+
}
|
|
576
|
+
ui.output(await api.call(operation.kind, operation.path, data));
|
|
577
|
+
}).option("--data <json|@file>", "Exact arguments; see api describe <operation>", "{}");
|
|
578
|
+
const advanced = program
|
|
579
|
+
.command("api")
|
|
580
|
+
.description("Discover and call every public Gitdog API operation");
|
|
581
|
+
command(advanced, "list", "List the complete public feature inventory", async ({ ui }, _args, opts) => ui.output(API_CATALOG.filter((entry) => !opts.filter || entry.path.includes(String(opts.filter))))).option("--filter <text>", "Filter by feature or operation name");
|
|
582
|
+
command(advanced, "describe <operation>", "Show the server validator for an operation", async ({ ui }, args) => ui.output(publicOperation(args[0])));
|
|
583
|
+
command(advanced, "call <operation>", "Call a public operation using exact JSON arguments", async ({ api, ui }, args, opts) => {
|
|
584
|
+
const operation = publicOperation(args[0]);
|
|
585
|
+
if (operation.path === "apiKeys/action:create")
|
|
586
|
+
throw new Error("Use keys create to issue API keys securely into a private file.");
|
|
587
|
+
const data = await jsonArguments(text(opts.data) ?? "{}");
|
|
588
|
+
if (operation.kind !== "query") {
|
|
589
|
+
if (!ui.json)
|
|
590
|
+
ui.output({ operation: operation.path, arguments: data });
|
|
591
|
+
await ui.confirm(`Run ${operation.path}?`, flag(opts.yes));
|
|
592
|
+
}
|
|
593
|
+
ui.output(await api.call(operation.kind, operation.path, data));
|
|
594
|
+
}).option("--data <json|@file>", "Exact arguments; use @file to avoid shell escaping", "{}");
|
|
595
|
+
command(program, "doctor", "Check runtime, authentication and local project configuration", async ({ apiUrl, siteUrl, project, ui }, _args, opts) => {
|
|
596
|
+
const keyFile = text(opts.keyFile) ?? process.env.GITDOG_API_KEY_FILE;
|
|
597
|
+
const keyMode = Boolean(keyFile || process.env.GITDOG_API_KEY);
|
|
598
|
+
const metadata = keyFile ? await readProjectKey(keyFile) : undefined;
|
|
599
|
+
const session = keyMode ? undefined : await new SessionStore().read();
|
|
600
|
+
let configuration;
|
|
601
|
+
try {
|
|
602
|
+
const local = await project.config();
|
|
603
|
+
configuration = {
|
|
604
|
+
path: local.path,
|
|
605
|
+
defaultProject: local.value.defaultProject,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
configuration = { message: error.message };
|
|
610
|
+
}
|
|
611
|
+
ui.output({
|
|
612
|
+
version: VERSION,
|
|
613
|
+
node: process.version,
|
|
614
|
+
apiUrl,
|
|
615
|
+
siteUrl,
|
|
616
|
+
authentication: keyMode
|
|
617
|
+
? "project API key"
|
|
618
|
+
: process.env.GITDOG_TOKEN
|
|
619
|
+
? "environment token"
|
|
620
|
+
: session
|
|
621
|
+
? "saved session"
|
|
622
|
+
: "not logged in",
|
|
623
|
+
apiKey: metadata
|
|
624
|
+
? {
|
|
625
|
+
keyId: metadata.keyId,
|
|
626
|
+
repositoryId: metadata.repositoryId,
|
|
627
|
+
scopes: metadata.scopes,
|
|
628
|
+
expiresAt: metadata.expiresAt,
|
|
629
|
+
}
|
|
630
|
+
: undefined,
|
|
631
|
+
configuration,
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
return program;
|
|
635
|
+
}
|
|
636
|
+
export function normalizeArgv(argv) {
|
|
637
|
+
const result = [...argv];
|
|
638
|
+
const groups = new Set([
|
|
639
|
+
"keys",
|
|
640
|
+
"projects",
|
|
641
|
+
"repos",
|
|
642
|
+
"config",
|
|
643
|
+
"tickets",
|
|
644
|
+
"actions",
|
|
645
|
+
"notifications",
|
|
646
|
+
"team",
|
|
647
|
+
"invitations",
|
|
648
|
+
"usage",
|
|
649
|
+
"billing",
|
|
650
|
+
"account",
|
|
651
|
+
"github",
|
|
652
|
+
"index",
|
|
653
|
+
"admin",
|
|
654
|
+
"api",
|
|
655
|
+
]);
|
|
656
|
+
const valueOptions = new Set([
|
|
657
|
+
"--project",
|
|
658
|
+
"-P",
|
|
659
|
+
"--config",
|
|
660
|
+
"--key-file",
|
|
661
|
+
"--api-url",
|
|
662
|
+
"--site-url",
|
|
663
|
+
]);
|
|
664
|
+
for (let index = 0; index < result.length; index++) {
|
|
665
|
+
const value = result[index];
|
|
666
|
+
if (valueOptions.has(value)) {
|
|
667
|
+
index++;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (value.startsWith("-"))
|
|
671
|
+
continue;
|
|
672
|
+
const [group, command, ...rest] = value.split(":");
|
|
673
|
+
if (groups.has(group) && command && rest.length === 0)
|
|
674
|
+
result.splice(index, 1, group, command);
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|