azdo-cli 0.2.0-001-azdo-cli-base.5 → 0.2.0-002-get-item-command.10
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 +47 -54
- package/dist/index.js +270 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,79 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# azdo-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A command-line interface for Azure DevOps.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/azdo-cli)
|
|
6
|
+
[](LICENSE)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
[](https://goreportcard.com/report/github.com/steveyegge/beads)
|
|
9
|
-
[](https://github.com/steveyegge/beads/releases)
|
|
10
|
-
[](https://www.npmjs.com/package/@beads/bd)
|
|
11
|
-
[](https://pypi.org/project/beads-mcp/)
|
|
8
|
+
## Installation
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g azdo-cli
|
|
12
|
+
```
|
|
14
13
|
|
|
15
|
-
##
|
|
14
|
+
## Usage
|
|
16
15
|
|
|
17
16
|
```bash
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Initialize in YOUR project
|
|
22
|
-
cd your-project
|
|
23
|
-
bd init
|
|
17
|
+
# Show help
|
|
18
|
+
azdo --help
|
|
24
19
|
|
|
25
|
-
#
|
|
26
|
-
|
|
20
|
+
# Show version
|
|
21
|
+
azdo --version
|
|
22
|
+
azdo -v
|
|
27
23
|
```
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
## Current Status
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
This project is in early development (v0.2.0). The base CLI scaffold is in place with support for `--help` and `--version`. Azure DevOps commands (work items, pipelines, repos, etc.) will be added in future releases.
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
* **Agent-Optimized:** JSON output, dependency tracking, and auto-ready task detection.
|
|
35
|
-
* **Zero Conflict:** Hash-based IDs (`bd-a1b2`) prevent merge collisions in multi-agent/multi-branch workflows.
|
|
36
|
-
* **Invisible Infrastructure:** SQLite local cache for speed; background daemon for auto-sync.
|
|
37
|
-
* **Compaction:** Semantic "memory decay" summarizes old closed tasks to save context window.
|
|
29
|
+
## Development
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| Command | Action |
|
|
42
|
-
| --- | --- |
|
|
43
|
-
| `bd ready` | List tasks with no open blockers. |
|
|
44
|
-
| `bd create "Title" -p 0` | Create a P0 task. |
|
|
45
|
-
| `bd update <id> --claim` | Atomically claim a task (sets assignee + in_progress). |
|
|
46
|
-
| `bd dep add <child> <parent>` | Link tasks (blocks, related, parent-child). |
|
|
47
|
-
| `bd show <id>` | View task details and audit trail. |
|
|
31
|
+
### Prerequisites
|
|
48
32
|
|
|
49
|
-
|
|
33
|
+
- Node.js LTS (20+)
|
|
34
|
+
- npm
|
|
50
35
|
|
|
51
|
-
|
|
36
|
+
### Setup
|
|
52
37
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/alkampfergit/azdo-cli.git
|
|
40
|
+
cd azdo-cli
|
|
41
|
+
npm install
|
|
42
|
+
```
|
|
58
43
|
|
|
59
|
-
|
|
44
|
+
### Scripts
|
|
60
45
|
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
| Command | Description |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `npm run build` | Build the CLI with tsup |
|
|
49
|
+
| `npm test` | Run tests with vitest |
|
|
50
|
+
| `npm run lint` | Lint source files with ESLint |
|
|
51
|
+
| `npm run typecheck` | Type-check with tsc (no emit) |
|
|
52
|
+
| `npm run format` | Check formatting with Prettier |
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
### Tech Stack
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
56
|
+
- **TypeScript 5.x** (strict mode, ES modules)
|
|
57
|
+
- **commander.js** — CLI framework
|
|
58
|
+
- **tsup** — Bundler (single-file ESM output)
|
|
59
|
+
- **vitest** — Test runner
|
|
60
|
+
- **ESLint + Prettier** — Linting and formatting
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
### Branch Strategy
|
|
71
63
|
|
|
72
|
-
|
|
64
|
+
This project follows **GitFlow**:
|
|
73
65
|
|
|
74
|
-
|
|
66
|
+
- `master` — stable releases
|
|
67
|
+
- `develop` — integration branch
|
|
68
|
+
- `feature/*` — feature branches off `develop`
|
|
75
69
|
|
|
76
|
-
##
|
|
70
|
+
## License
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
* [](https://deepwiki.com/steveyegge/beads)
|
|
72
|
+
[MIT](LICENSE)
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -11,9 +11,277 @@ var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
var pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf-8"));
|
|
12
12
|
var version = pkg.version;
|
|
13
13
|
|
|
14
|
+
// src/commands/get-item.ts
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
|
|
17
|
+
// src/services/azdo-client.ts
|
|
18
|
+
async function getWorkItem(context, id, pat) {
|
|
19
|
+
const url = `https://dev.azure.com/${context.org}/${context.project}/_apis/wit/workitems/${id}?api-version=7.1`;
|
|
20
|
+
const token = Buffer.from(`:${pat}`).toString("base64");
|
|
21
|
+
let response;
|
|
22
|
+
try {
|
|
23
|
+
response = await fetch(url, {
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Basic ${token}`
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error("NETWORK_ERROR");
|
|
30
|
+
}
|
|
31
|
+
if (response.status === 401) throw new Error("AUTH_FAILED");
|
|
32
|
+
if (response.status === 403) throw new Error("PERMISSION_DENIED");
|
|
33
|
+
if (response.status === 404) throw new Error("NOT_FOUND");
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`HTTP_${response.status}`);
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
return {
|
|
39
|
+
id: data.id,
|
|
40
|
+
rev: data.rev,
|
|
41
|
+
title: data.fields["System.Title"],
|
|
42
|
+
state: data.fields["System.State"],
|
|
43
|
+
type: data.fields["System.WorkItemType"],
|
|
44
|
+
assignedTo: data.fields["System.AssignedTo"]?.displayName ?? null,
|
|
45
|
+
description: data.fields["System.Description"] ?? null,
|
|
46
|
+
areaPath: data.fields["System.AreaPath"],
|
|
47
|
+
iterationPath: data.fields["System.IterationPath"],
|
|
48
|
+
url: data._links.html.href
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/services/auth.ts
|
|
53
|
+
import { createInterface } from "readline";
|
|
54
|
+
|
|
55
|
+
// src/services/credential-store.ts
|
|
56
|
+
import { Entry } from "@napi-rs/keyring";
|
|
57
|
+
var SERVICE = "azdo-cli";
|
|
58
|
+
var ACCOUNT = "pat";
|
|
59
|
+
async function getPat() {
|
|
60
|
+
try {
|
|
61
|
+
const entry = new Entry(SERVICE, ACCOUNT);
|
|
62
|
+
return entry.getPassword();
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function storePat(pat) {
|
|
68
|
+
try {
|
|
69
|
+
const entry = new Entry(SERVICE, ACCOUNT);
|
|
70
|
+
entry.setPassword(pat);
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/services/auth.ts
|
|
76
|
+
async function promptForPat() {
|
|
77
|
+
if (!process.stdin.isTTY) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve2) => {
|
|
81
|
+
const rl = createInterface({
|
|
82
|
+
input: process.stdin,
|
|
83
|
+
output: process.stderr
|
|
84
|
+
});
|
|
85
|
+
process.stderr.write("Enter your Azure DevOps PAT: ");
|
|
86
|
+
process.stdin.setRawMode(true);
|
|
87
|
+
process.stdin.resume();
|
|
88
|
+
let pat = "";
|
|
89
|
+
const onData = (key) => {
|
|
90
|
+
const ch = key.toString("utf8");
|
|
91
|
+
if (ch === "") {
|
|
92
|
+
process.stdin.setRawMode(false);
|
|
93
|
+
process.stdin.removeListener("data", onData);
|
|
94
|
+
rl.close();
|
|
95
|
+
process.stderr.write("\n");
|
|
96
|
+
resolve2(null);
|
|
97
|
+
} else if (ch === "\r" || ch === "\n") {
|
|
98
|
+
process.stdin.setRawMode(false);
|
|
99
|
+
process.stdin.removeListener("data", onData);
|
|
100
|
+
rl.close();
|
|
101
|
+
process.stderr.write("\n");
|
|
102
|
+
resolve2(pat);
|
|
103
|
+
} else if (ch === "\x7F" || ch === "\b") {
|
|
104
|
+
if (pat.length > 0) {
|
|
105
|
+
pat = pat.slice(0, -1);
|
|
106
|
+
process.stderr.write("\b \b");
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
pat += ch;
|
|
110
|
+
process.stderr.write("*");
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
process.stdin.on("data", onData);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function resolvePat() {
|
|
117
|
+
const envPat = process.env.AZDO_PAT;
|
|
118
|
+
if (envPat) {
|
|
119
|
+
return { pat: envPat, source: "env" };
|
|
120
|
+
}
|
|
121
|
+
const storedPat = await getPat();
|
|
122
|
+
if (storedPat !== null) {
|
|
123
|
+
return { pat: storedPat, source: "credential-store" };
|
|
124
|
+
}
|
|
125
|
+
const promptedPat = await promptForPat();
|
|
126
|
+
if (promptedPat !== null) {
|
|
127
|
+
await storePat(promptedPat);
|
|
128
|
+
return { pat: promptedPat, source: "prompt" };
|
|
129
|
+
}
|
|
130
|
+
throw new Error(
|
|
131
|
+
"Authentication cancelled. Set AZDO_PAT environment variable or run again to enter a PAT."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/services/git-remote.ts
|
|
136
|
+
import { execSync } from "child_process";
|
|
137
|
+
var patterns = [
|
|
138
|
+
// HTTPS (current): https://dev.azure.com/{org}/{project}/_git/{repo}
|
|
139
|
+
/^https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/.+$/,
|
|
140
|
+
// HTTPS (legacy + DefaultCollection): https://{org}.visualstudio.com/DefaultCollection/{project}/_git/{repo}
|
|
141
|
+
/^https?:\/\/([^.]+)\.visualstudio\.com\/DefaultCollection\/([^/]+)\/_git\/.+$/,
|
|
142
|
+
// HTTPS (legacy): https://{org}.visualstudio.com/{project}/_git/{repo}
|
|
143
|
+
/^https?:\/\/([^.]+)\.visualstudio\.com\/([^/]+)\/_git\/.+$/,
|
|
144
|
+
// SSH (current): git@ssh.dev.azure.com:v3/{org}/{project}/{repo}
|
|
145
|
+
/^git@ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/.+$/,
|
|
146
|
+
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}
|
|
147
|
+
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/.+$/
|
|
148
|
+
];
|
|
149
|
+
function parseAzdoRemote(url) {
|
|
150
|
+
for (const pattern of patterns) {
|
|
151
|
+
const match = url.match(pattern);
|
|
152
|
+
if (match) {
|
|
153
|
+
return { org: match[1], project: match[2] };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
function detectAzdoContext() {
|
|
159
|
+
let remoteUrl;
|
|
160
|
+
try {
|
|
161
|
+
remoteUrl = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
164
|
+
}
|
|
165
|
+
const context = parseAzdoRemote(remoteUrl);
|
|
166
|
+
if (!context) {
|
|
167
|
+
throw new Error('Git remote "origin" is not an Azure DevOps URL. Provide --org and --project explicitly.');
|
|
168
|
+
}
|
|
169
|
+
return context;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/commands/get-item.ts
|
|
173
|
+
function stripHtml(html) {
|
|
174
|
+
let text = html;
|
|
175
|
+
text = text.replace(/<br\s*\/?>/gi, "\n");
|
|
176
|
+
text = text.replace(/<\/?(p|div)>/gi, "\n");
|
|
177
|
+
text = text.replace(/<li>/gi, "\n");
|
|
178
|
+
text = text.replace(/<[^>]*>/g, "");
|
|
179
|
+
text = text.replace(/&/g, "&");
|
|
180
|
+
text = text.replace(/</g, "<");
|
|
181
|
+
text = text.replace(/>/g, ">");
|
|
182
|
+
text = text.replace(/"/g, '"');
|
|
183
|
+
text = text.replace(/'/g, "'");
|
|
184
|
+
text = text.replace(/ /g, " ");
|
|
185
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
186
|
+
return text.trim();
|
|
187
|
+
}
|
|
188
|
+
function formatWorkItem(workItem, short) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
const label = (name) => name.padEnd(13);
|
|
191
|
+
lines.push(`${label("ID:")}${workItem.id}`);
|
|
192
|
+
lines.push(`${label("Type:")}${workItem.type}`);
|
|
193
|
+
lines.push(`${label("Title:")}${workItem.title}`);
|
|
194
|
+
lines.push(`${label("State:")}${workItem.state}`);
|
|
195
|
+
lines.push(`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`);
|
|
196
|
+
if (!short) {
|
|
197
|
+
lines.push(`${label("Area:")}${workItem.areaPath}`);
|
|
198
|
+
lines.push(`${label("Iteration:")}${workItem.iterationPath}`);
|
|
199
|
+
}
|
|
200
|
+
lines.push(`${label("URL:")}${workItem.url}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
const descriptionText = workItem.description ? stripHtml(workItem.description) : "";
|
|
203
|
+
if (short) {
|
|
204
|
+
const descLines = descriptionText.split("\n").filter((l) => l.trim() !== "");
|
|
205
|
+
const firstThree = descLines.slice(0, 3);
|
|
206
|
+
const truncated = descLines.length > 3;
|
|
207
|
+
const descSummary = firstThree.join("\n") + (truncated ? "\n..." : "");
|
|
208
|
+
lines.push(`${label("Description:")}${descSummary}`);
|
|
209
|
+
} else {
|
|
210
|
+
lines.push("Description:");
|
|
211
|
+
lines.push(descriptionText);
|
|
212
|
+
}
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
function createGetItemCommand() {
|
|
216
|
+
const command = new Command("get-item");
|
|
217
|
+
command.description("Retrieve an Azure DevOps work item by ID").argument("<id>", "work item ID").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").option("--short", "show abbreviated output").action(
|
|
218
|
+
async (idStr, options) => {
|
|
219
|
+
const id = parseInt(idStr, 10);
|
|
220
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`Error: Work item ID must be a positive integer. Got: "${idStr}"
|
|
223
|
+
`
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const hasOrg = options.org !== void 0;
|
|
228
|
+
const hasProject = options.project !== void 0;
|
|
229
|
+
if (hasOrg !== hasProject) {
|
|
230
|
+
process.stderr.write(
|
|
231
|
+
"Error: --org and --project must both be provided, or both omitted.\n"
|
|
232
|
+
);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
let context;
|
|
236
|
+
try {
|
|
237
|
+
if (options.org && options.project) {
|
|
238
|
+
context = { org: options.org, project: options.project };
|
|
239
|
+
} else {
|
|
240
|
+
context = detectAzdoContext();
|
|
241
|
+
}
|
|
242
|
+
const credential = await resolvePat();
|
|
243
|
+
const workItem = await getWorkItem(context, id, credential.pat);
|
|
244
|
+
const output = formatWorkItem(workItem, options.short ?? false);
|
|
245
|
+
process.stdout.write(output + "\n");
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
248
|
+
const msg = error.message;
|
|
249
|
+
if (msg === "AUTH_FAILED") {
|
|
250
|
+
process.stderr.write(
|
|
251
|
+
'Error: Authentication failed. Check that your PAT is valid and has the "Work Items (read)" scope.\n'
|
|
252
|
+
);
|
|
253
|
+
} else if (msg === "NOT_FOUND") {
|
|
254
|
+
process.stderr.write(
|
|
255
|
+
`Error: Work item ${id} not found in ${context.org}/${context.project}.
|
|
256
|
+
`
|
|
257
|
+
);
|
|
258
|
+
} else if (msg === "PERMISSION_DENIED") {
|
|
259
|
+
process.stderr.write(
|
|
260
|
+
`Error: Access denied. Your PAT may lack permissions for project "${context.project}".
|
|
261
|
+
`
|
|
262
|
+
);
|
|
263
|
+
} else if (msg === "NETWORK_ERROR") {
|
|
264
|
+
process.stderr.write(
|
|
265
|
+
"Error: Could not connect to Azure DevOps. Check your network connection.\n"
|
|
266
|
+
);
|
|
267
|
+
} else if (msg.includes("Not in a git repository") || msg.includes("is not an Azure DevOps URL") || msg.includes("Authentication cancelled")) {
|
|
268
|
+
process.stderr.write(`Error: ${msg}
|
|
269
|
+
`);
|
|
270
|
+
} else {
|
|
271
|
+
process.stderr.write(`Error: ${msg}
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
return command;
|
|
279
|
+
}
|
|
280
|
+
|
|
14
281
|
// src/index.ts
|
|
15
|
-
var program = new
|
|
282
|
+
var program = new Command2();
|
|
16
283
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
284
|
+
program.addCommand(createGetItemCommand());
|
|
17
285
|
program.showHelpAfterError();
|
|
18
286
|
program.parse();
|
|
19
287
|
if (process.argv.length <= 2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "azdo-cli",
|
|
3
|
-
"version": "0.2.0-
|
|
3
|
+
"version": "0.2.0-002-get-item-command.10",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@napi-rs/keyring": "^1.2.0",
|
|
25
26
|
"commander": "^14.0.3"
|
|
26
27
|
},
|
|
27
28
|
"devDependencies": {
|