gitdog 0.1.0 → 0.1.2
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 +23 -23
- package/dist/api-keys.d.ts +2 -0
- package/dist/api-keys.js +47 -3
- package/dist/auth.js +4 -4
- package/dist/catalog.js +1 -1
- package/dist/client.js +5 -5
- package/dist/commands.js +18 -12
- package/dist/projects.js +19 -13
- package/dist/storage.js +51 -4
- package/dist/ui.d.ts +3 -1
- package/dist/ui.js +35 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,36 +10,36 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/gitdog"><img src="https://img.shields.io/npm/v/gitdog?logo=npm&color=CB3837" alt="npm version"></a>
|
|
13
|
-
<a href="https://www.npmjs.com/package/gitdog"><img src="https://img.shields.io/npm/dm/gitdog?color=555" alt="npm downloads"></a>
|
|
14
13
|
<a href="https://github.com/hyodotdev/gitdog-cli/actions/workflows/ci.yml"><img src="https://github.com/hyodotdev/gitdog-cli/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
14
|
+
<a href="https://github.com/hyodotdev/gitdog-cli/actions/workflows/release.yml"><img src="https://github.com/hyodotdev/gitdog-cli/actions/workflows/release.yml/badge.svg" alt="npm release"></a>
|
|
15
15
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D22.14.0-3c873a?logo=nodedotjs&logoColor=white" alt="Node.js 22.14 or later"></a>
|
|
16
16
|
<a href="https://gitdog.dev"><img src="https://img.shields.io/badge/Gitdog-gitdog.dev-111" alt="Gitdog website"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<img src="https://raw.githubusercontent.com/hyodotdev/gitdog-cli/main/.github/assets/terminal.svg" alt="Gitdog terminal: connect a project, configure automation and review pending actions" width="800">
|
|
20
|
+
<img src="https://raw.githubusercontent.com/hyodotdev/gitdog-cli/main/.github/assets/terminal.svg?v=0.1.2" alt="Gitdog terminal: connect a project, configure automation and review pending actions" width="800">
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
Connect repositories, configure Gitdog's issue and discussion assistants, review
|
|
24
24
|
AI actions, and automate project tasks from your terminal or CI.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Run with npx
|
|
27
27
|
|
|
28
|
-
Requires **Node.js 22.14+**.
|
|
28
|
+
Requires **Node.js 22.14+**. No global installation needed.
|
|
29
29
|
|
|
30
30
|
```sh
|
|
31
|
-
|
|
31
|
+
npx gitdog@latest --help
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Quick start
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
gitdog login
|
|
38
|
-
gitdog github install
|
|
39
|
-
gitdog github recover
|
|
40
|
-
gitdog init owner/repo
|
|
41
|
-
gitdog config set requireApproval true
|
|
42
|
-
gitdog config push
|
|
37
|
+
npx gitdog@latest login
|
|
38
|
+
npx gitdog@latest github install
|
|
39
|
+
npx gitdog@latest github recover
|
|
40
|
+
npx gitdog@latest init owner/repo
|
|
41
|
+
npx gitdog@latest config set requireApproval true
|
|
42
|
+
npx gitdog@latest config push
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
GitHub App setup is only needed once. A project connects an existing GitHub
|
|
@@ -47,23 +47,23 @@ repository to Gitdog.
|
|
|
47
47
|
|
|
48
48
|
## Everyday commands
|
|
49
49
|
|
|
50
|
-
| Task | Command
|
|
51
|
-
| ------------------------ |
|
|
52
|
-
| List projects | `gitdog projects list` |
|
|
53
|
-
| Read automation settings | `gitdog config get --remote` |
|
|
54
|
-
| Review pending actions | `gitdog tickets pending` |
|
|
55
|
-
| Check code indexing | `gitdog index status` |
|
|
56
|
-
| List team members | `gitdog team members` |
|
|
57
|
-
| Check usage | `gitdog usage summary` |
|
|
50
|
+
| Task | Command |
|
|
51
|
+
| ------------------------ | --------------------------------------- |
|
|
52
|
+
| List projects | `npx gitdog@latest projects list` |
|
|
53
|
+
| Read automation settings | `npx gitdog@latest config get --remote` |
|
|
54
|
+
| Review pending actions | `npx gitdog@latest tickets pending` |
|
|
55
|
+
| Check code indexing | `npx gitdog@latest index status` |
|
|
56
|
+
| List team members | `npx gitdog@latest team members` |
|
|
57
|
+
| Check usage | `npx gitdog@latest usage summary` |
|
|
58
58
|
|
|
59
|
-
Use `gitdog --help` to explore commands. Add `--json` for scripts.
|
|
59
|
+
Use `npx gitdog@latest --help` to explore commands. Add `--json` for scripts.
|
|
60
60
|
|
|
61
61
|
## Use in CI
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
|
-
gitdog keys create --name ci
|
|
65
|
-
gitdog --key-file /path/to/key.json projects list --json
|
|
66
|
-
gitdog keys revoke KEY_ID --yes
|
|
64
|
+
npx gitdog@latest keys create --name ci
|
|
65
|
+
npx --yes gitdog@0.1.2 --key-file /path/to/key.json projects list --json
|
|
66
|
+
npx gitdog@latest keys revoke KEY_ID --yes
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
`keys create` prints the private key file's path. Keys are project-scoped and
|
package/dist/api-keys.d.ts
CHANGED
|
@@ -11,5 +11,7 @@ export type ProjectKey = {
|
|
|
11
11
|
};
|
|
12
12
|
export declare function projectHttpUrl(apiUrl: string): string;
|
|
13
13
|
export declare function readProjectKey(path: string): Promise<ProjectKey>;
|
|
14
|
+
/** Refuse key paths that Git would track, so secrets are not committed. */
|
|
15
|
+
export declare function assertUntrackedKeyPath(path: string): Promise<void>;
|
|
14
16
|
export declare function saveProjectKey(path: string, key: ProjectKey): Promise<void>;
|
|
15
17
|
export declare function callWithProjectKey<T>(apiUrl: string, keyFile: string | undefined, rawKey: string | undefined, kind: string, path: string, args: unknown, timeoutMs: number): Promise<T>;
|
package/dist/api-keys.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { mkdir, open, readFile, realpath, rm } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
5
|
import { asRecord } from "./ui.js";
|
|
4
|
-
import { validateUrl } from "./storage.js";
|
|
6
|
+
import { exists, validateUrl } from "./storage.js";
|
|
5
7
|
export function projectHttpUrl(apiUrl) {
|
|
6
8
|
return validateUrl(process.env.GITDOG_HTTP_URL ??
|
|
7
9
|
apiUrl.replace(/\.convex\.cloud$/, ".convex.site"), "Project HTTP URL");
|
|
@@ -39,6 +41,48 @@ export async function readProjectKey(path) {
|
|
|
39
41
|
expiresAt: key.expiresAt,
|
|
40
42
|
};
|
|
41
43
|
}
|
|
44
|
+
/** Refuse key paths that Git would track, so secrets are not committed. */
|
|
45
|
+
export async function assertUntrackedKeyPath(path) {
|
|
46
|
+
let directory = dirname(path);
|
|
47
|
+
while (!(await exists(directory))) {
|
|
48
|
+
const parent = dirname(directory);
|
|
49
|
+
if (parent === directory)
|
|
50
|
+
return;
|
|
51
|
+
directory = parent;
|
|
52
|
+
}
|
|
53
|
+
const canonicalDirectory = await realpath(directory);
|
|
54
|
+
const canonicalPath = join(canonicalDirectory, relative(directory, path));
|
|
55
|
+
try {
|
|
56
|
+
const { stdout } = await promisify(execFile)("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
57
|
+
cwd: canonicalDirectory,
|
|
58
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
59
|
+
timeout: 5000,
|
|
60
|
+
});
|
|
61
|
+
if (stdout.trim() === "false")
|
|
62
|
+
return;
|
|
63
|
+
if (stdout.trim() !== "true")
|
|
64
|
+
throw new Error("Unexpected Git work tree result");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
const gitError = error;
|
|
68
|
+
if (gitError.code === 128 &&
|
|
69
|
+
/fatal: not a git repository/i.test(gitError.stderr ?? ""))
|
|
70
|
+
return;
|
|
71
|
+
throw new Error("Could not verify the key path with Git. Fix Git configuration or choose the default private key location.");
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await promisify(execFile)("git", ["check-ignore", "-q", "--", canonicalPath], {
|
|
75
|
+
cwd: canonicalDirectory,
|
|
76
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
77
|
+
timeout: 5000,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.code === 1)
|
|
82
|
+
throw new Error("The key file would be inside a Git repository and not ignored. Choose a path outside the repository or add it to .gitignore.");
|
|
83
|
+
throw new Error("Could not verify Git ignore rules for the key path. No key was issued.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
42
86
|
export async function saveProjectKey(path, key) {
|
|
43
87
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
44
88
|
const handle = await open(path, "wx", 0o600);
|
package/dist/auth.js
CHANGED
|
@@ -112,7 +112,7 @@ export async function createLoginListener(timeoutMs = 300_000) {
|
|
|
112
112
|
response.end("Gitdog received your sign-in. Return to your terminal to finish. You can close this tab.");
|
|
113
113
|
finish(undefined, value);
|
|
114
114
|
});
|
|
115
|
-
const timer = setTimeout(() => finish(new Error("Sign-in timed out. Run gitdog login to try again.")), timeoutMs);
|
|
115
|
+
const timer = setTimeout(() => finish(new Error("Sign-in timed out. Run npx gitdog@latest login to try again.")), timeoutMs);
|
|
116
116
|
try {
|
|
117
117
|
await new Promise((resolve, reject) => {
|
|
118
118
|
server.once("error", reject);
|
|
@@ -195,11 +195,11 @@ export async function login(apiUrl, siteUrl, ui, noBrowser = false) {
|
|
|
195
195
|
}
|
|
196
196
|
catch {
|
|
197
197
|
listener.signal.throwIfAborted();
|
|
198
|
-
throw new Error("Could not finish sign-in. Check that the CLI loopback callback is deployed, then run gitdog login again.");
|
|
198
|
+
throw new Error("Could not finish sign-in. Check that the CLI loopback callback is deployed, then run npx gitdog@latest login again.");
|
|
199
199
|
}
|
|
200
200
|
const tokens = asRecord(exchange)?.tokens;
|
|
201
201
|
if (!validTokens(tokens))
|
|
202
|
-
throw new Error("Sign-in did not return a session. Run gitdog login again.");
|
|
202
|
+
throw new Error("Sign-in did not return a session. Run npx gitdog@latest login again.");
|
|
203
203
|
client.setAuth(tokens.token);
|
|
204
204
|
const user = asRecord(await invoke(client, "query", "users/query:getCurrentUser"));
|
|
205
205
|
if (!user)
|
|
@@ -210,7 +210,7 @@ export async function login(apiUrl, siteUrl, ui, noBrowser = false) {
|
|
|
210
210
|
ui.output(`✓ Signed in as ${String(user.name ?? "GitHub user")}.`);
|
|
211
211
|
if (user.requiresCurrentLegalAttestation)
|
|
212
212
|
ui.info(`Complete account consent at ${siteUrl}/legal/consent before running AI workflows.`);
|
|
213
|
-
ui.info("Next: gitdog init");
|
|
213
|
+
ui.info("Next: npx gitdog@latest init");
|
|
214
214
|
}
|
|
215
215
|
finally {
|
|
216
216
|
await listener.close();
|
package/dist/catalog.js
CHANGED
|
@@ -3,6 +3,6 @@ export const API_CATALOG = catalog;
|
|
|
3
3
|
export function publicOperation(path) {
|
|
4
4
|
const operation = API_CATALOG.find((entry) => entry.path === path);
|
|
5
5
|
if (!operation)
|
|
6
|
-
throw new Error("Unknown public operation. Run gitdog api list. Internal functions cannot be called.");
|
|
6
|
+
throw new Error("Unknown public operation. Run npx gitdog@latest api list. Internal functions cannot be called.");
|
|
7
7
|
return operation;
|
|
8
8
|
}
|
package/dist/client.js
CHANGED
|
@@ -60,15 +60,15 @@ export class GitdogApi {
|
|
|
60
60
|
}
|
|
61
61
|
const initial = await this.store.read();
|
|
62
62
|
if (!initial)
|
|
63
|
-
throw new Error("You are not logged in. Run gitdog login.");
|
|
63
|
+
throw new Error("You are not logged in. Run npx gitdog@latest login.");
|
|
64
64
|
if (initial.apiUrl !== this.apiUrl)
|
|
65
|
-
throw new Error("This session belongs to another API URL. Run gitdog login --api-url <url> first.");
|
|
65
|
+
throw new Error("This session belongs to another API URL. Run npx gitdog@latest login --api-url <url> first.");
|
|
66
66
|
this.session = initial;
|
|
67
67
|
if (tokenExpiresSoon(initial.token)) {
|
|
68
68
|
this.session = await this.store.locked(async () => {
|
|
69
69
|
const current = await this.store.read();
|
|
70
70
|
if (!current || current.apiUrl !== this.apiUrl)
|
|
71
|
-
throw new Error("Session changed. Run gitdog login again.");
|
|
71
|
+
throw new Error("Session changed. Run npx gitdog@latest login again.");
|
|
72
72
|
if (!tokenExpiresSoon(current.token))
|
|
73
73
|
return current;
|
|
74
74
|
let result;
|
|
@@ -78,11 +78,11 @@ export class GitdogApi {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
catch {
|
|
81
|
-
throw new Error("Could not refresh your session. Check your connection or run gitdog login again.");
|
|
81
|
+
throw new Error("Could not refresh your session. Check your connection or run npx gitdog@latest login again.");
|
|
82
82
|
}
|
|
83
83
|
const tokens = asRecord(result)?.tokens;
|
|
84
84
|
if (!validTokens(tokens))
|
|
85
|
-
throw new Error("Your session expired. Run gitdog login again.");
|
|
85
|
+
throw new Error("Your session expired. Run npx gitdog@latest login again.");
|
|
86
86
|
const updated = { ...current, ...tokens };
|
|
87
87
|
await this.store.save(updated);
|
|
88
88
|
return updated;
|
package/dist/commands.js
CHANGED
|
@@ -7,7 +7,7 @@ import { login, logout, openBrowser } from "./auth.js";
|
|
|
7
7
|
import { API_CATALOG, publicOperation } from "./catalog.js";
|
|
8
8
|
import { BOOLEAN_SETTINGS, STYLE_SETTINGS, ProjectContext, limitParser, } from "./projects.js";
|
|
9
9
|
import { DEFAULT_API_URL, DEFAULT_SITE_URL, SessionStore, validateUrl, configDirectory, exists, } from "./storage.js";
|
|
10
|
-
import { readProjectKey, saveProjectKey, projectHttpUrl } from "./api-keys.js";
|
|
10
|
+
import { assertUntrackedKeyPath, readProjectKey, saveProjectKey, projectHttpUrl, } from "./api-keys.js";
|
|
11
11
|
import { Ui, VERSION, asRecord, integer } from "./ui.js";
|
|
12
12
|
const text = (value) => typeof value === "string" ? value : undefined;
|
|
13
13
|
const number = (value) => typeof value === "number" ? value : undefined;
|
|
@@ -44,7 +44,7 @@ export function createProgram(dependencies = {}) {
|
|
|
44
44
|
.option("-y, --yes", "Confirm the requested operation without prompting")
|
|
45
45
|
.showHelpAfterError()
|
|
46
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.`);
|
|
47
|
+
.addHelpText("after", `\nGetting started:\n npx gitdog@latest login\n npx gitdog@latest init\n npx gitdog@latest config set requireApproval true\n npx gitdog@latest config push --dry-run\n npx gitdog@latest config push\n\nFirebase-style aliases also work: projects:list, projects:create, config:push.\nRun npx gitdog@latest <command> --help for details.`);
|
|
48
48
|
async function context(command) {
|
|
49
49
|
const options = command.optsWithGlobals();
|
|
50
50
|
const ui = dependencies.ui ?? new Ui(flag(options.json));
|
|
@@ -104,10 +104,14 @@ export function createProgram(dependencies = {}) {
|
|
|
104
104
|
ui.output({
|
|
105
105
|
name: "gitdog",
|
|
106
106
|
version: VERSION,
|
|
107
|
-
next: [
|
|
107
|
+
next: [
|
|
108
|
+
"npx gitdog@latest login",
|
|
109
|
+
"npx gitdog@latest init",
|
|
110
|
+
"npx gitdog@latest --help",
|
|
111
|
+
],
|
|
108
112
|
});
|
|
109
113
|
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.");
|
|
114
|
+
ui.output("Get started\n 1. npx gitdog@latest login Sign in with GitHub\n 2. npx gitdog@latest init Connect a project\n 3. npx gitdog@latest status Check your assistant\n\nRun npx gitdog@latest --help to explore all commands.");
|
|
111
115
|
});
|
|
112
116
|
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
117
|
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");
|
|
@@ -120,7 +124,7 @@ export function createProgram(dependencies = {}) {
|
|
|
120
124
|
!process.env.GITDOG_API_KEY &&
|
|
121
125
|
!(await new SessionStore().read())) {
|
|
122
126
|
if (!process.stdin.isTTY || ui.json)
|
|
123
|
-
throw new Error("Run gitdog login before gitdog init.");
|
|
127
|
+
throw new Error("Run npx gitdog@latest login before npx gitdog@latest init.");
|
|
124
128
|
await login(apiUrl, siteUrl, ui);
|
|
125
129
|
}
|
|
126
130
|
await project.init(args[0], text(opts.alias) ?? "default", flag(opts.force));
|
|
@@ -164,6 +168,8 @@ export function createProgram(dependencies = {}) {
|
|
|
164
168
|
const out = text(opts.out);
|
|
165
169
|
if (out && (await exists(resolve(out))))
|
|
166
170
|
throw new Error("The output file already exists. Choose a new private path.");
|
|
171
|
+
if (out)
|
|
172
|
+
await assertUntrackedKeyPath(resolve(out));
|
|
167
173
|
const name = text(opts.name) ?? "CLI key";
|
|
168
174
|
const scopes = flag(opts.write) ? ["read", "write"] : ["read"];
|
|
169
175
|
const issued = await api.call("action", "apiKeys/action:create", {
|
|
@@ -210,7 +216,7 @@ export function createProgram(dependencies = {}) {
|
|
|
210
216
|
expiresAt: issued.expiresAt,
|
|
211
217
|
keyFile,
|
|
212
218
|
});
|
|
213
|
-
ui.info(`Key saved privately. Use: gitdog --key-file ${JSON.stringify(keyFile)} projects list`);
|
|
219
|
+
ui.info(`Key saved privately. Use: npx gitdog@latest --key-file ${JSON.stringify(keyFile)} projects list`);
|
|
214
220
|
})
|
|
215
221
|
.option("--name <name>", "Key name", "CLI key")
|
|
216
222
|
.option("--write", "Grant project settings and ticket write access (read access is always included)")
|
|
@@ -322,7 +328,7 @@ export function createProgram(dependencies = {}) {
|
|
|
322
328
|
do {
|
|
323
329
|
const remaining = deadline - Date.now();
|
|
324
330
|
if (flag(opts.wait) && remaining <= 0)
|
|
325
|
-
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check gitdog index status.");
|
|
331
|
+
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check npx gitdog@latest index status.");
|
|
326
332
|
const state = await api.call("query", "repoIndexing/query:getIndexingStatus", { repositoryId: repo._id }, flag(opts.wait) ? { timeoutMs: remaining } : undefined);
|
|
327
333
|
const record = asRecord(state);
|
|
328
334
|
if (!record)
|
|
@@ -337,14 +343,14 @@ export function createProgram(dependencies = {}) {
|
|
|
337
343
|
last = fingerprint;
|
|
338
344
|
}
|
|
339
345
|
if (record.status === "failed" || record.ragStatus === "failed")
|
|
340
|
-
throw new Error("Indexing failed. Run gitdog index logs for details.");
|
|
346
|
+
throw new Error("Indexing failed. Run npx gitdog@latest index logs for details.");
|
|
341
347
|
if (record.status === "completed" &&
|
|
342
348
|
["completed", "skipped"].includes(String(record.ragStatus))) {
|
|
343
349
|
ui.output(state);
|
|
344
350
|
return;
|
|
345
351
|
}
|
|
346
352
|
if (Date.now() >= deadline)
|
|
347
|
-
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check gitdog index status.");
|
|
353
|
+
throw new Error("Timed out waiting for indexing. Indexing continues on the server; check npx gitdog@latest index status.");
|
|
348
354
|
await new Promise((resolve) => setTimeout(resolve, Math.min(2000, Math.max(1, deadline - Date.now()))));
|
|
349
355
|
} while (true);
|
|
350
356
|
})
|
|
@@ -514,7 +520,7 @@ export function createProgram(dependencies = {}) {
|
|
|
514
520
|
await openBrowser(url);
|
|
515
521
|
ui.output({
|
|
516
522
|
url,
|
|
517
|
-
next: "After GitHub authorization, run gitdog github recover, then gitdog projects available.",
|
|
523
|
+
next: "After GitHub authorization, run npx gitdog@latest github recover, then npx gitdog@latest projects available.",
|
|
518
524
|
});
|
|
519
525
|
})
|
|
520
526
|
.option("--app <slug>", "GitHub App slug (use gitdogbot-dev for development)", "gitdogbot")
|
|
@@ -525,7 +531,7 @@ export function createProgram(dependencies = {}) {
|
|
|
525
531
|
if (result?.status !== "recovered") {
|
|
526
532
|
ui.output({ ...result, recoveredCount: count });
|
|
527
533
|
if (result?.status === "reauth_required")
|
|
528
|
-
throw new Error("Refresh GitHub authorization with gitdog login, then run github recover again.");
|
|
534
|
+
throw new Error("Refresh GitHub authorization with npx gitdog@latest login, then run github recover again.");
|
|
529
535
|
return;
|
|
530
536
|
}
|
|
531
537
|
ui.info(`Recovered installation ${String(result.installationId)}.`);
|
|
@@ -566,7 +572,7 @@ export function createProgram(dependencies = {}) {
|
|
|
566
572
|
if (operation.path === "apiKeys/action:create")
|
|
567
573
|
throw new Error("Use keys create to issue API keys securely into a private file.");
|
|
568
574
|
if (!operation.path.startsWith("admin/"))
|
|
569
|
-
throw new Error("Use gitdog api call for non-admin operations.");
|
|
575
|
+
throw new Error("Use npx gitdog@latest api call for non-admin operations.");
|
|
570
576
|
const data = await jsonArguments(text(opts.data) ?? "{}");
|
|
571
577
|
if (operation.kind !== "query") {
|
|
572
578
|
if (!ui.json)
|
package/dist/projects.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { exists, readJson, writeJson } from "./storage.js";
|
|
5
6
|
import { asRecord, integer } from "./ui.js";
|
|
@@ -47,7 +48,7 @@ export function validateSettings(value, complete = false) {
|
|
|
47
48
|
throw new Error(`${key} must be a string.`);
|
|
48
49
|
}
|
|
49
50
|
else
|
|
50
|
-
throw new Error(`Unknown setting: ${key}. Run gitdog config keys.`);
|
|
51
|
+
throw new Error(`Unknown setting: ${key}. Run npx gitdog@latest config keys.`);
|
|
51
52
|
settings[key] = item;
|
|
52
53
|
}
|
|
53
54
|
if (complete &&
|
|
@@ -62,7 +63,7 @@ export function validateProjectConfig(value) {
|
|
|
62
63
|
typeof config.defaultProject !== "string" ||
|
|
63
64
|
!projects ||
|
|
64
65
|
!Object.hasOwn(projects, config.defaultProject))
|
|
65
|
-
throw new Error("Invalid gitdog.json. Run gitdog init in a new directory, or repair the project configuration.");
|
|
66
|
+
throw new Error("Invalid gitdog.json. Run npx gitdog@latest init in a new directory, or repair the project configuration.");
|
|
66
67
|
const result = {};
|
|
67
68
|
for (const [alias, value] of Object.entries(projects)) {
|
|
68
69
|
aliasName(alias);
|
|
@@ -83,13 +84,18 @@ export function validateProjectConfig(value) {
|
|
|
83
84
|
export async function findConfig(explicit, cwd = process.cwd()) {
|
|
84
85
|
if (explicit)
|
|
85
86
|
return resolve(cwd, explicit);
|
|
87
|
+
// Stop at the repository root or home so a file planted in a shared parent
|
|
88
|
+
// directory cannot choose the project.
|
|
86
89
|
let directory = resolve(cwd);
|
|
90
|
+
const home = resolve(homedir());
|
|
87
91
|
while (true) {
|
|
88
92
|
const path = join(directory, "gitdog.json");
|
|
89
93
|
if (await exists(path))
|
|
90
94
|
return path;
|
|
91
95
|
const parent = dirname(directory);
|
|
92
|
-
if (parent === directory
|
|
96
|
+
if (parent === directory ||
|
|
97
|
+
directory === home ||
|
|
98
|
+
(await exists(join(directory, ".git"))))
|
|
93
99
|
return undefined;
|
|
94
100
|
directory = parent;
|
|
95
101
|
}
|
|
@@ -108,10 +114,10 @@ export class ProjectContext {
|
|
|
108
114
|
async config() {
|
|
109
115
|
const path = await findConfig(this.configOption);
|
|
110
116
|
if (!path)
|
|
111
|
-
throw new Error("No gitdog.json found. Run gitdog init, or pass --project owner/repo.");
|
|
117
|
+
throw new Error("No gitdog.json found. Run npx gitdog@latest init, or pass --project owner/repo.");
|
|
112
118
|
const value = await readJson(path);
|
|
113
119
|
if (value === undefined)
|
|
114
|
-
throw new Error(`No project configuration at ${path}. Run gitdog init.`);
|
|
120
|
+
throw new Error(`No project configuration at ${path}. Run npx gitdog@latest init.`);
|
|
115
121
|
return { path, value: validateProjectConfig(value) };
|
|
116
122
|
}
|
|
117
123
|
async target(override) {
|
|
@@ -128,7 +134,7 @@ export class ProjectContext {
|
|
|
128
134
|
const { value } = await this.config();
|
|
129
135
|
const alias = selection ?? value.defaultProject;
|
|
130
136
|
if (!Object.hasOwn(value.projects, alias))
|
|
131
|
-
throw new Error(`Unknown project alias ${alias}. Run gitdog use --list.`);
|
|
137
|
+
throw new Error(`Unknown project alias ${alias}. Run npx gitdog@latest use --list.`);
|
|
132
138
|
return value.projects[alias].repository;
|
|
133
139
|
}
|
|
134
140
|
async repository(override) {
|
|
@@ -137,7 +143,7 @@ export class ProjectContext {
|
|
|
137
143
|
? await this.api.call("query", "repositories/query:get", { owner: target.split("/")[0], repo: target.split("/")[1] })
|
|
138
144
|
: await this.api.call("query", "repositories/query:getById", { repositoryId: target });
|
|
139
145
|
if (!repository)
|
|
140
|
-
throw new Error(`Project ${target} is not connected or accessible. Run gitdog projects create ${target}.`);
|
|
146
|
+
throw new Error(`Project ${target} is not connected or accessible. Run npx gitdog@latest projects create ${target}.`);
|
|
141
147
|
return repository;
|
|
142
148
|
}
|
|
143
149
|
async available(reconnectRepositoryId) {
|
|
@@ -168,7 +174,7 @@ export class ProjectContext {
|
|
|
168
174
|
const available = await this.available(reconnectRepositoryId);
|
|
169
175
|
const matches = available.filter((item) => String(item.fullName).toLowerCase() === name.toLowerCase());
|
|
170
176
|
if (matches.length !== 1)
|
|
171
|
-
throw new Error(`Repository ${name} is unavailable. Run gitdog github install, authorize it, then gitdog github recover. Check projects available.`);
|
|
177
|
+
throw new Error(`Repository ${name} is unavailable. Run npx gitdog@latest github install, authorize it, then npx gitdog@latest github recover. Check projects available.`);
|
|
172
178
|
const item = matches[0];
|
|
173
179
|
if (typeof item.installationId !== "number" || typeof item.id !== "number")
|
|
174
180
|
throw new Error("GitHub returned invalid repository identifiers.");
|
|
@@ -190,7 +196,7 @@ export class ProjectContext {
|
|
|
190
196
|
join(process.cwd(), "gitdog.json");
|
|
191
197
|
const previous = await readJson(path);
|
|
192
198
|
if (previous !== undefined && !force)
|
|
193
|
-
throw new Error(`Project configuration already exists at ${path}. Use gitdog use --add, or --force to replace it.`);
|
|
199
|
+
throw new Error(`Project configuration already exists at ${path}. Use npx gitdog@latest use --add, or --force to replace it.`);
|
|
194
200
|
if (!name) {
|
|
195
201
|
let detected;
|
|
196
202
|
try {
|
|
@@ -225,14 +231,14 @@ export class ProjectContext {
|
|
|
225
231
|
alias,
|
|
226
232
|
config: path,
|
|
227
233
|
});
|
|
228
|
-
this.ui.info("✓ Ready. Try gitdog status, gitdog config get, or gitdog tickets list.");
|
|
234
|
+
this.ui.info("✓ Ready. Try npx gitdog@latest status, npx gitdog@latest config get, or npx gitdog@latest tickets list.");
|
|
229
235
|
}
|
|
230
236
|
async use(selection, add, list = false) {
|
|
231
237
|
const { path, value } = await this.config();
|
|
232
238
|
if (add) {
|
|
233
239
|
aliasName(add);
|
|
234
240
|
if (!selection)
|
|
235
|
-
throw new Error("Use gitdog use owner/repo --add alias.");
|
|
241
|
+
throw new Error("Use npx gitdog@latest use owner/repo --add alias.");
|
|
236
242
|
if (Object.hasOwn(value.projects, add))
|
|
237
243
|
throw new Error(`Alias ${add} already exists. Choose a new alias.`);
|
|
238
244
|
const repo = await this.repository(selection);
|
|
@@ -268,7 +274,7 @@ export class ProjectContext {
|
|
|
268
274
|
: Object.keys(value.projects).find((key) => value.projects[key].repository.toLowerCase() ===
|
|
269
275
|
alias.toLowerCase());
|
|
270
276
|
if (!match)
|
|
271
|
-
throw new Error("This project has no local settings. Add it with gitdog use owner/repo --add alias.");
|
|
277
|
+
throw new Error("This project has no local settings. Add it with npx gitdog@latest use owner/repo --add alias.");
|
|
272
278
|
return { path, value, alias: match, project: value.projects[match] };
|
|
273
279
|
}
|
|
274
280
|
async configGet(key, remote = false) {
|
|
@@ -292,7 +298,7 @@ export class ProjectContext {
|
|
|
292
298
|
local.project.settings = { ...local.project.settings, ...patch };
|
|
293
299
|
await writeJson(local.path, local.value);
|
|
294
300
|
this.ui.output({ saved: true, settings: patch });
|
|
295
|
-
this.ui.info("Saved locally. Run gitdog config push to apply.");
|
|
301
|
+
this.ui.info("Saved locally. Run npx gitdog@latest config push to apply.");
|
|
296
302
|
}
|
|
297
303
|
async configPull(force = false) {
|
|
298
304
|
const local = await this.localProject();
|
package/dist/storage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmod, mkdir, open, readFile, rename, rm, stat, } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, open, readFile, rename, rmdir, rm, stat, } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
@@ -80,7 +80,7 @@ export class SessionStore {
|
|
|
80
80
|
if (!validTokens(value) ||
|
|
81
81
|
typeof item?.apiUrl !== "string" ||
|
|
82
82
|
typeof item.siteUrl !== "string")
|
|
83
|
-
throw new Error("Invalid saved session. Run gitdog logout, then gitdog login.");
|
|
83
|
+
throw new Error("Invalid saved session. Run npx gitdog@latest logout, then npx gitdog@latest login.");
|
|
84
84
|
return {
|
|
85
85
|
...value,
|
|
86
86
|
apiUrl: validateUrl(item.apiUrl, "Saved API URL"),
|
|
@@ -102,13 +102,24 @@ export class SessionStore {
|
|
|
102
102
|
while (!handle) {
|
|
103
103
|
signal?.throwIfAborted();
|
|
104
104
|
if (Date.now() >= deadline)
|
|
105
|
-
throw new Error(`Authentication lock timed out. Check that another Gitdog process is not running; stale lock: ${path}.`);
|
|
105
|
+
throw new Error(`Authentication lock timed out. Check that another Gitdog process is not running; stale lock: ${path} (recovery gate: ${path}.recovery).`);
|
|
106
106
|
try {
|
|
107
|
-
|
|
107
|
+
const created = await open(path, "wx", 0o600);
|
|
108
|
+
try {
|
|
109
|
+
await created.writeFile(String(process.pid));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
await created.close();
|
|
113
|
+
await rm(path, { force: true });
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
handle = created;
|
|
108
117
|
}
|
|
109
118
|
catch (error) {
|
|
110
119
|
if (error.code !== "EEXIST")
|
|
111
120
|
throw error;
|
|
121
|
+
if (await removeAbandonedLock(path))
|
|
122
|
+
continue;
|
|
112
123
|
if (Date.now() >= deadline)
|
|
113
124
|
throw new Error(`Another Gitdog process is updating authentication. Retry later; if it crashed, remove ${path}.`);
|
|
114
125
|
await new Promise((resolve) => setTimeout(resolve, Math.min(100, Math.max(1, deadline - Date.now()))));
|
|
@@ -124,6 +135,42 @@ export class SessionStore {
|
|
|
124
135
|
}
|
|
125
136
|
}
|
|
126
137
|
}
|
|
138
|
+
/** Remove a lock whose recorded owner process no longer exists. */
|
|
139
|
+
async function removeAbandonedLock(path) {
|
|
140
|
+
const recovery = `${path}.recovery`;
|
|
141
|
+
try {
|
|
142
|
+
await mkdir(recovery, { mode: 0o700 });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
// Re-read under the recovery mutex: another waiter may have replaced the
|
|
149
|
+
// dead owner's lock with a live lock before this cleaner acquired the gate.
|
|
150
|
+
const content = (await readFile(path, "utf8")).trim();
|
|
151
|
+
if (!/^[1-9]\d*$/.test(content))
|
|
152
|
+
return false;
|
|
153
|
+
const owner = Number(content);
|
|
154
|
+
if (!Number.isSafeInteger(owner) || owner === process.pid)
|
|
155
|
+
return false;
|
|
156
|
+
try {
|
|
157
|
+
process.kill(owner, 0);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (error.code !== "ESRCH")
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
await rm(path);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
await rmdir(recovery);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
127
174
|
export function tokenExpiresSoon(token) {
|
|
128
175
|
try {
|
|
129
176
|
const payload = asRecord(JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString("utf8")));
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.2";
|
|
2
2
|
export declare const DOG: string;
|
|
3
3
|
export declare function redactSecrets(value: string): string;
|
|
4
4
|
export declare function clean(value: string): string;
|
|
5
|
+
/** Escape characters JSON.stringify leaves raw; the JSON value is unchanged. */
|
|
6
|
+
export declare function safeJson(value: unknown): string;
|
|
5
7
|
export declare class Ui {
|
|
6
8
|
readonly json: boolean;
|
|
7
9
|
constructor(json?: boolean);
|
package/dist/ui.js
CHANGED
|
@@ -1,20 +1,46 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stripVTControlCharacters } from "node:util";
|
|
3
|
-
export const VERSION = "0.1.
|
|
3
|
+
export const VERSION = "0.1.2";
|
|
4
4
|
export const DOG = String.raw `
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
▗██▄▄ ▄▄██▖
|
|
6
|
+
██▀▜██▙▄ ▙▄ ▄▟██▛▀██
|
|
7
|
+
▐█▌ ▀███▄▐██▄ ▄███▀ ▜█▌
|
|
8
|
+
▐█▌ ███████▙ ▝▀▀▀ ▐█▌
|
|
9
|
+
██▌ ▄█████████▌ ▐█▌
|
|
10
|
+
▐█▌ ▄████████████ ▐█▌
|
|
11
|
+
▐█▙▟████████████▌ ▜█▘
|
|
12
|
+
▀▝▀▘ ▀▀███▛
|
|
13
|
+
|
|
14
|
+
▄█▙▖ ▄█▙▖
|
|
15
|
+
▜██▌ ▄▄ ▜██▘
|
|
16
|
+
▄███▛▜███▄
|
|
17
|
+
▗███▌ ▐███▖
|
|
18
|
+
█████▄ ▄█████
|
|
19
|
+
▗▄ ▝▜██▛▀ ▀▜██▛▘ ▄▖
|
|
20
|
+
▐██▄▖ ▗▄██▌
|
|
21
|
+
▟█████▙▄▖ ▗▄▄█████▙
|
|
22
|
+
▐█▛███████████▄▄▄▄▄▄███████████▜█▌
|
|
23
|
+
▀ ▝██████████████████████████▘ ▀
|
|
24
|
+
▝▜██████████████████████▛▘
|
|
25
|
+
▝▜██████████████████▀▘
|
|
26
|
+
▀▀▜█▜██████▛█▛▀▘
|
|
27
|
+
▀▀▀▀
|
|
10
28
|
`;
|
|
11
29
|
export function redactSecrets(value) {
|
|
12
30
|
return value
|
|
13
31
|
.replace(/\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g, "[redacted token]")
|
|
14
32
|
.replace(/\bgdk_[a-f0-9]{64}\b/g, "[redacted API key]");
|
|
15
33
|
}
|
|
34
|
+
// C1 controls and bidirectional overrides can drive or spoof terminals.
|
|
35
|
+
const UNSAFE_UNICODE = /[\u0080-\u009f\u202a-\u202e\u2066-\u2069]/g;
|
|
16
36
|
export function clean(value) {
|
|
17
|
-
return redactSecrets(stripVTControlCharacters(value)
|
|
37
|
+
return redactSecrets(stripVTControlCharacters(value)
|
|
38
|
+
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "")
|
|
39
|
+
.replace(UNSAFE_UNICODE, ""));
|
|
40
|
+
}
|
|
41
|
+
/** Escape characters JSON.stringify leaves raw; the JSON value is unchanged. */
|
|
42
|
+
export function safeJson(value) {
|
|
43
|
+
return redactSecrets(JSON.stringify(value ?? null, null, 2)).replace(UNSAFE_UNICODE, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
18
44
|
}
|
|
19
45
|
export class Ui {
|
|
20
46
|
json;
|
|
@@ -31,7 +57,7 @@ export class Ui {
|
|
|
31
57
|
banner() {
|
|
32
58
|
if (this.json || process.env.GITDOG_NO_BANNER === "1")
|
|
33
59
|
return;
|
|
34
|
-
process.stdout.write(this.color(DOG) + this.color(` G I T D O G / ${VERSION}\n`, 1));
|
|
60
|
+
process.stdout.write(this.color(DOG, 1) + this.color(` G I T D O G / ${VERSION}\n`, 1));
|
|
35
61
|
process.stdout.write(" Your repositories. A very good assistant.\n\n");
|
|
36
62
|
}
|
|
37
63
|
info(message) {
|
|
@@ -40,7 +66,7 @@ export class Ui {
|
|
|
40
66
|
}
|
|
41
67
|
output(value) {
|
|
42
68
|
if (this.json) {
|
|
43
|
-
process.stdout.write(`${
|
|
69
|
+
process.stdout.write(`${safeJson(value)}\n`);
|
|
44
70
|
return;
|
|
45
71
|
}
|
|
46
72
|
if (typeof value === "string") {
|