azdo-cli 0.2.0-feature-devcontainer.72 → 0.2.0-feature-macsecret.95
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 +82 -0
- package/dist/index.js +473 -97
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Azure DevOps CLI focused on work item read/write workflows.
|
|
|
12
12
|
- Update work item state (`set-state`)
|
|
13
13
|
- Assign and unassign work items (`assign`)
|
|
14
14
|
- Set any work item field by reference name (`set-field`)
|
|
15
|
+
- Create or update Tasks from markdown documents (`upsert`)
|
|
15
16
|
- Read rich-text fields as markdown (`get-md-field`)
|
|
16
17
|
- Set rich-text fields as markdown from inline text, file, or stdin (`set-md-field`)
|
|
17
18
|
- Persist org/project/default fields in local config (`config`)
|
|
@@ -47,6 +48,9 @@ azdo get-item 12345
|
|
|
47
48
|
|
|
48
49
|
# 3) Update state
|
|
49
50
|
azdo set-state 12345 "Active"
|
|
51
|
+
|
|
52
|
+
# 4) Create or update a Task from markdown
|
|
53
|
+
azdo upsert --content $'---\nTitle: Improve markdown import UX\nState: New\n---'
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
## Command Cheat Sheet
|
|
@@ -57,6 +61,7 @@ azdo set-state 12345 "Active"
|
|
|
57
61
|
| `azdo set-state <id> <state>` | Change work item state | `--json`, `--org`, `--project` |
|
|
58
62
|
| `azdo assign <id> [name]` | Assign or unassign owner | `--unassign`, `--json`, `--org`, `--project` |
|
|
59
63
|
| `azdo set-field <id> <field> <value>` | Update any field | `--json`, `--org`, `--project` |
|
|
64
|
+
| `azdo upsert [id]` | Create or update a Task from markdown | `--content`, `--file`, `--json`, `--org`, `--project` |
|
|
60
65
|
| `azdo get-md-field <id> <field>` | Get field as markdown | `--org`, `--project` |
|
|
61
66
|
| `azdo set-md-field <id> <field> [content]` | Set markdown field | `--file`, `--json`, `--org`, `--project` |
|
|
62
67
|
| `azdo config <subcommand>` | Manage saved settings | `set`, `get`, `list`, `unset`, `wizard`, `--json` |
|
|
@@ -119,6 +124,83 @@ azdo set-md-field 12345 System.Description --file ./description.md
|
|
|
119
124
|
cat description.md | azdo set-md-field 12345 System.Description
|
|
120
125
|
```
|
|
121
126
|
|
|
127
|
+
## azdo upsert
|
|
128
|
+
|
|
129
|
+
`azdo upsert` accepts a single markdown task document and either creates a new Azure DevOps Task or updates an existing one. Omit `[id]` to create; pass `[id]` to update that work item in place.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Create from inline content
|
|
133
|
+
azdo upsert --content $'---\nTitle: Improve markdown import UX\nAssigned To: user@example.com\nState: New\n---'
|
|
134
|
+
|
|
135
|
+
# Update from a file
|
|
136
|
+
azdo upsert 12345 --file ./task-import.md
|
|
137
|
+
|
|
138
|
+
# JSON output
|
|
139
|
+
azdo upsert 12345 --content $'---\nSystem.Title: Improve markdown import UX\n---' --json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The command requires exactly one source flag:
|
|
143
|
+
|
|
144
|
+
- `azdo upsert [id] --content <markdown>`
|
|
145
|
+
- `azdo upsert [id] --file <path>`
|
|
146
|
+
|
|
147
|
+
If `--file` succeeds, the source file is deleted after the Azure DevOps write completes. If parsing, validation, or the API call fails, the file is preserved. If deletion fails after a successful write, the command still succeeds and prints a warning.
|
|
148
|
+
|
|
149
|
+
### Task Document Format
|
|
150
|
+
|
|
151
|
+
The document starts with YAML front matter for scalar fields, followed by optional `##` heading sections for markdown rich-text fields.
|
|
152
|
+
|
|
153
|
+
```md
|
|
154
|
+
---
|
|
155
|
+
Title: Improve markdown import UX
|
|
156
|
+
Assigned To: user@example.com
|
|
157
|
+
State: New
|
|
158
|
+
Tags: cli; markdown
|
|
159
|
+
Priority: null
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Description
|
|
163
|
+
|
|
164
|
+
Implement a single-command task import flow.
|
|
165
|
+
|
|
166
|
+
## Acceptance Criteria
|
|
167
|
+
|
|
168
|
+
- Supports create when no ID is passed
|
|
169
|
+
- Supports update when an ID is passed
|
|
170
|
+
- Deletes imported files only after success
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Supported friendly field names:
|
|
174
|
+
|
|
175
|
+
- `Title`
|
|
176
|
+
- `Assigned To` / `assignedTo`
|
|
177
|
+
- `State`
|
|
178
|
+
- `Description`
|
|
179
|
+
- `Acceptance Criteria` / `acceptanceCriteria`
|
|
180
|
+
- `Tags`
|
|
181
|
+
- `Priority`
|
|
182
|
+
|
|
183
|
+
Raw Azure DevOps reference names are also accepted anywhere a field name is expected, for example `System.Title` or `Microsoft.VSTS.Common.AcceptanceCriteria`.
|
|
184
|
+
|
|
185
|
+
Clear semantics:
|
|
186
|
+
|
|
187
|
+
- Scalar YAML fields with `null` or an empty value are treated as clears on update.
|
|
188
|
+
- Rich-text heading sections with an empty body are treated as clears on update.
|
|
189
|
+
- Omitted fields are untouched on update.
|
|
190
|
+
|
|
191
|
+
`--json` output shape:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"action": "created",
|
|
196
|
+
"id": 12345,
|
|
197
|
+
"fields": {
|
|
198
|
+
"System.Title": "Improve markdown import UX",
|
|
199
|
+
"System.Description": "Implement a single-command task import flow."
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
122
204
|
### Configuration
|
|
123
205
|
|
|
124
206
|
```bash
|
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 as
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -55,16 +55,43 @@ async function readResponseMessage(response) {
|
|
|
55
55
|
function normalizeFieldList(fields) {
|
|
56
56
|
return Array.from(new Set(fields.map((f) => f.trim()).filter((f) => f.length > 0)));
|
|
57
57
|
}
|
|
58
|
+
function stringifyFieldValue(value) {
|
|
59
|
+
if (typeof value === "object" && value !== null) {
|
|
60
|
+
return JSON.stringify(value);
|
|
61
|
+
}
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
58
64
|
function buildExtraFields(fields, requested) {
|
|
59
65
|
const result = {};
|
|
60
66
|
for (const name of requested) {
|
|
61
67
|
const val = fields[name];
|
|
62
68
|
if (val !== void 0 && val !== null) {
|
|
63
|
-
result[name] =
|
|
69
|
+
result[name] = stringifyFieldValue(val);
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
return Object.keys(result).length > 0 ? result : null;
|
|
67
73
|
}
|
|
74
|
+
function writeHeaders(pat) {
|
|
75
|
+
return {
|
|
76
|
+
...authHeaders(pat),
|
|
77
|
+
"Content-Type": "application/json-patch+json"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function readWriteResponse(response, errorCode) {
|
|
81
|
+
if (response.status === 400) {
|
|
82
|
+
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
83
|
+
throw new Error(`${errorCode}: ${serverMessage}`);
|
|
84
|
+
}
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`HTTP_${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
return {
|
|
90
|
+
id: data.id,
|
|
91
|
+
rev: data.rev,
|
|
92
|
+
fields: data.fields
|
|
93
|
+
};
|
|
94
|
+
}
|
|
68
95
|
async function getWorkItem(context, id, pat, extraFields) {
|
|
69
96
|
const url = new URL(
|
|
70
97
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
@@ -98,7 +125,7 @@ async function getWorkItem(context, id, pat, extraFields) {
|
|
|
98
125
|
}
|
|
99
126
|
let combinedDescription = null;
|
|
100
127
|
if (descriptionParts.length === 1) {
|
|
101
|
-
combinedDescription = descriptionParts
|
|
128
|
+
combinedDescription = descriptionParts.at(0)?.value ?? null;
|
|
102
129
|
} else if (descriptionParts.length > 1) {
|
|
103
130
|
combinedDescription = descriptionParts.map((p) => `<h3>${p.label}</h3>${p.value}`).join("");
|
|
104
131
|
}
|
|
@@ -137,38 +164,44 @@ async function getWorkItemFieldValue(context, id, pat, fieldName) {
|
|
|
137
164
|
if (value === void 0 || value === null || value === "") {
|
|
138
165
|
return null;
|
|
139
166
|
}
|
|
140
|
-
return
|
|
167
|
+
return stringifyFieldValue(value);
|
|
141
168
|
}
|
|
142
169
|
async function updateWorkItem(context, id, pat, fieldName, operations) {
|
|
170
|
+
const result = await applyWorkItemPatch(context, id, pat, operations);
|
|
171
|
+
const title = result.fields["System.Title"];
|
|
172
|
+
const lastOp = operations.at(-1);
|
|
173
|
+
const fieldValue = lastOp?.value ?? null;
|
|
174
|
+
return {
|
|
175
|
+
id: result.id,
|
|
176
|
+
rev: result.rev,
|
|
177
|
+
title: typeof title === "string" ? title : "",
|
|
178
|
+
fieldName,
|
|
179
|
+
fieldValue
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function createWorkItem(context, workItemType, pat, operations) {
|
|
183
|
+
const url = new URL(
|
|
184
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/$${encodeURIComponent(workItemType)}`
|
|
185
|
+
);
|
|
186
|
+
url.searchParams.set("api-version", "7.1");
|
|
187
|
+
const response = await fetchWithErrors(url.toString(), {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: writeHeaders(pat),
|
|
190
|
+
body: JSON.stringify(operations)
|
|
191
|
+
});
|
|
192
|
+
return readWriteResponse(response, "CREATE_REJECTED");
|
|
193
|
+
}
|
|
194
|
+
async function applyWorkItemPatch(context, id, pat, operations) {
|
|
143
195
|
const url = new URL(
|
|
144
196
|
`https://dev.azure.com/${encodeURIComponent(context.org)}/${encodeURIComponent(context.project)}/_apis/wit/workitems/${id}`
|
|
145
197
|
);
|
|
146
198
|
url.searchParams.set("api-version", "7.1");
|
|
147
199
|
const response = await fetchWithErrors(url.toString(), {
|
|
148
200
|
method: "PATCH",
|
|
149
|
-
headers:
|
|
150
|
-
...authHeaders(pat),
|
|
151
|
-
"Content-Type": "application/json-patch+json"
|
|
152
|
-
},
|
|
201
|
+
headers: writeHeaders(pat),
|
|
153
202
|
body: JSON.stringify(operations)
|
|
154
203
|
});
|
|
155
|
-
|
|
156
|
-
const serverMessage = await readResponseMessage(response) ?? "Unknown error";
|
|
157
|
-
throw new Error(`UPDATE_REJECTED: ${serverMessage}`);
|
|
158
|
-
}
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
throw new Error(`HTTP_${response.status}`);
|
|
161
|
-
}
|
|
162
|
-
const data = await response.json();
|
|
163
|
-
const lastOp = operations[operations.length - 1];
|
|
164
|
-
const fieldValue = lastOp.value ?? null;
|
|
165
|
-
return {
|
|
166
|
-
id: data.id,
|
|
167
|
-
rev: data.rev,
|
|
168
|
-
title: data.fields["System.Title"],
|
|
169
|
-
fieldName,
|
|
170
|
-
fieldValue
|
|
171
|
-
};
|
|
204
|
+
return readWriteResponse(response, "UPDATE_REJECTED");
|
|
172
205
|
}
|
|
173
206
|
|
|
174
207
|
// src/services/auth.ts
|
|
@@ -286,7 +319,7 @@ var patterns = [
|
|
|
286
319
|
];
|
|
287
320
|
function parseAzdoRemote(url) {
|
|
288
321
|
for (const pattern of patterns) {
|
|
289
|
-
const match =
|
|
322
|
+
const match = pattern.exec(url);
|
|
290
323
|
if (match) {
|
|
291
324
|
const project = match[2];
|
|
292
325
|
if (/^DefaultCollection$/i.test(project)) {
|
|
@@ -509,6 +542,24 @@ function validateOrgProjectPair(options) {
|
|
|
509
542
|
process.exit(1);
|
|
510
543
|
}
|
|
511
544
|
}
|
|
545
|
+
function validateSource(options) {
|
|
546
|
+
const hasContent = options.content !== void 0;
|
|
547
|
+
const hasFile = options.file !== void 0;
|
|
548
|
+
if (hasContent === hasFile) {
|
|
549
|
+
process.stderr.write("Error: provide exactly one of --content or --file\n");
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function formatCreateError(err) {
|
|
554
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
555
|
+
const message = error.message.startsWith("CREATE_REJECTED:") ? error.message.replace("CREATE_REJECTED:", "").trim() : error.message;
|
|
556
|
+
const requiredMatches = [...message.matchAll(/field ['"]([^'"]+)['"]/gi)];
|
|
557
|
+
if (requiredMatches.length > 0) {
|
|
558
|
+
const fields = Array.from(new Set(requiredMatches.map((match) => match[1])));
|
|
559
|
+
return `Create rejected: ${message} (fields: ${fields.join(", ")})`;
|
|
560
|
+
}
|
|
561
|
+
return `Create rejected: ${message}`;
|
|
562
|
+
}
|
|
512
563
|
function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
513
564
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
514
565
|
const msg = error.message;
|
|
@@ -535,6 +586,9 @@ function handleCommandError(err, id, context, scope = "write", exit = true) {
|
|
|
535
586
|
} else if (msg.startsWith("BAD_REQUEST:")) {
|
|
536
587
|
const serverMsg = msg.replace("BAD_REQUEST: ", "");
|
|
537
588
|
process.stderr.write(`Error: Request rejected: ${serverMsg}
|
|
589
|
+
`);
|
|
590
|
+
} else if (msg.startsWith("CREATE_REJECTED:")) {
|
|
591
|
+
process.stderr.write(`Error: ${formatCreateError(error)}
|
|
538
592
|
`);
|
|
539
593
|
} else if (msg.startsWith("UPDATE_REJECTED:")) {
|
|
540
594
|
const serverMsg = msg.replace("UPDATE_REJECTED: ", "");
|
|
@@ -561,18 +615,18 @@ function parseRequestedFields(raw) {
|
|
|
561
615
|
}
|
|
562
616
|
function stripHtml(html) {
|
|
563
617
|
let text = html;
|
|
564
|
-
text = text.
|
|
565
|
-
text = text.
|
|
566
|
-
text = text.
|
|
567
|
-
text = text.
|
|
568
|
-
text = text.
|
|
569
|
-
text = text.
|
|
570
|
-
text = text.
|
|
571
|
-
text = text.
|
|
572
|
-
text = text.
|
|
573
|
-
text = text.
|
|
574
|
-
text = text.
|
|
575
|
-
text = text.
|
|
618
|
+
text = text.replaceAll(/<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi, "\n--- $1 ---\n");
|
|
619
|
+
text = text.replaceAll(/<br\s*\/?>/gi, "\n");
|
|
620
|
+
text = text.replaceAll(/<\/?(p|div)>/gi, "\n");
|
|
621
|
+
text = text.replaceAll(/<li>/gi, "\n");
|
|
622
|
+
text = text.replaceAll(/<[^>]*>/g, "");
|
|
623
|
+
text = text.replaceAll("&", "&");
|
|
624
|
+
text = text.replaceAll("<", "<");
|
|
625
|
+
text = text.replaceAll(">", ">");
|
|
626
|
+
text = text.replaceAll(""", '"');
|
|
627
|
+
text = text.replaceAll("'", "'");
|
|
628
|
+
text = text.replaceAll(" ", " ");
|
|
629
|
+
text = text.replaceAll(/\n{3,}/g, "\n\n");
|
|
576
630
|
return text.trim();
|
|
577
631
|
}
|
|
578
632
|
function convertRichText(html, markdown) {
|
|
@@ -593,16 +647,19 @@ function summarizeDescription(text, label) {
|
|
|
593
647
|
return [`${label("Description:")}${firstThree.join("\n")}${suffix}`];
|
|
594
648
|
}
|
|
595
649
|
function formatWorkItem(workItem, short, markdown = false) {
|
|
596
|
-
const lines = [];
|
|
597
650
|
const label = (name) => name.padEnd(13);
|
|
598
|
-
lines
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
651
|
+
const lines = [
|
|
652
|
+
`${label("ID:")}${workItem.id}`,
|
|
653
|
+
`${label("Type:")}${workItem.type}`,
|
|
654
|
+
`${label("Title:")}${workItem.title}`,
|
|
655
|
+
`${label("State:")}${workItem.state}`,
|
|
656
|
+
`${label("Assigned To:")}${workItem.assignedTo ?? "Unassigned"}`
|
|
657
|
+
];
|
|
603
658
|
if (!short) {
|
|
604
|
-
lines.push(
|
|
605
|
-
|
|
659
|
+
lines.push(
|
|
660
|
+
`${label("Area:")}${workItem.areaPath}`,
|
|
661
|
+
`${label("Iteration:")}${workItem.iterationPath}`
|
|
662
|
+
);
|
|
606
663
|
}
|
|
607
664
|
lines.push(`${label("URL:")}${workItem.url}`);
|
|
608
665
|
if (workItem.extraFields) {
|
|
@@ -613,8 +670,7 @@ function formatWorkItem(workItem, short, markdown = false) {
|
|
|
613
670
|
if (short) {
|
|
614
671
|
lines.push(...summarizeDescription(descriptionText, label));
|
|
615
672
|
} else {
|
|
616
|
-
lines.push("Description:");
|
|
617
|
-
lines.push(descriptionText);
|
|
673
|
+
lines.push("Description:", descriptionText);
|
|
618
674
|
}
|
|
619
675
|
return lines.join("\n");
|
|
620
676
|
}
|
|
@@ -628,7 +684,7 @@ function createGetItemCommand() {
|
|
|
628
684
|
try {
|
|
629
685
|
context = resolveContext(options);
|
|
630
686
|
const credential = await resolvePat();
|
|
631
|
-
const fieldsList = options.fields
|
|
687
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(loadConfig().fields) : parseRequestedFields(options.fields);
|
|
632
688
|
const workItem = await getWorkItem(context, id, credential.pat, fieldsList);
|
|
633
689
|
const markdownEnabled = options.markdown ?? loadConfig().markdown ?? false;
|
|
634
690
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
@@ -659,6 +715,63 @@ function createClearPatCommand() {
|
|
|
659
715
|
// src/commands/config.ts
|
|
660
716
|
import { Command as Command3 } from "commander";
|
|
661
717
|
import { createInterface as createInterface2 } from "readline";
|
|
718
|
+
function formatConfigValue(value, unsetFallback = "") {
|
|
719
|
+
if (value === void 0) {
|
|
720
|
+
return unsetFallback;
|
|
721
|
+
}
|
|
722
|
+
return Array.isArray(value) ? value.join(",") : value;
|
|
723
|
+
}
|
|
724
|
+
function writeConfigList(cfg) {
|
|
725
|
+
const keyWidth = 10;
|
|
726
|
+
const valueWidth = 30;
|
|
727
|
+
for (const setting of SETTINGS) {
|
|
728
|
+
const raw = cfg[setting.key];
|
|
729
|
+
const value = formatConfigValue(raw, "(not set)");
|
|
730
|
+
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
731
|
+
process.stdout.write(
|
|
732
|
+
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
733
|
+
`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
737
|
+
if (hasUnset) {
|
|
738
|
+
process.stdout.write(
|
|
739
|
+
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function createAsk(rl) {
|
|
744
|
+
return (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
745
|
+
}
|
|
746
|
+
async function promptForSetting(cfg, setting, ask) {
|
|
747
|
+
const currentDisplay = String(formatConfigValue(cfg[setting.key], ""));
|
|
748
|
+
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
749
|
+
process.stderr.write(`${setting.description}${requiredTag}
|
|
750
|
+
`);
|
|
751
|
+
if (setting.example) {
|
|
752
|
+
process.stderr.write(` Example: ${setting.example}
|
|
753
|
+
`);
|
|
754
|
+
}
|
|
755
|
+
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
756
|
+
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
757
|
+
const trimmed = answer.trim();
|
|
758
|
+
if (trimmed) {
|
|
759
|
+
setConfigValue(setting.key, trimmed);
|
|
760
|
+
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
761
|
+
|
|
762
|
+
`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (currentDisplay) {
|
|
766
|
+
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
767
|
+
|
|
768
|
+
`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
process.stderr.write(` -> Skipped "${setting.key}"
|
|
772
|
+
|
|
773
|
+
`);
|
|
774
|
+
}
|
|
662
775
|
function createConfigCommand() {
|
|
663
776
|
const config = new Command3("config");
|
|
664
777
|
config.description("Manage CLI settings");
|
|
@@ -711,27 +824,9 @@ function createConfigCommand() {
|
|
|
711
824
|
const cfg = loadConfig();
|
|
712
825
|
if (options.json) {
|
|
713
826
|
process.stdout.write(JSON.stringify(cfg) + "\n");
|
|
714
|
-
|
|
715
|
-
const keyWidth = 10;
|
|
716
|
-
const valueWidth = 30;
|
|
717
|
-
for (const setting of SETTINGS) {
|
|
718
|
-
const raw = cfg[setting.key];
|
|
719
|
-
const value = raw === void 0 ? "(not set)" : Array.isArray(raw) ? raw.join(",") : raw;
|
|
720
|
-
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
721
|
-
process.stdout.write(
|
|
722
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
723
|
-
`
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
const hasUnset = SETTINGS.some(
|
|
727
|
-
(s) => s.required && cfg[s.key] === void 0
|
|
728
|
-
);
|
|
729
|
-
if (hasUnset) {
|
|
730
|
-
process.stdout.write(
|
|
731
|
-
'\n* = required but not configured. Run "azdo config wizard" to set up.\n'
|
|
732
|
-
);
|
|
733
|
-
}
|
|
827
|
+
return;
|
|
734
828
|
}
|
|
829
|
+
writeConfigList(cfg);
|
|
735
830
|
});
|
|
736
831
|
const unset = new Command3("unset");
|
|
737
832
|
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
@@ -763,36 +858,11 @@ function createConfigCommand() {
|
|
|
763
858
|
input: process.stdin,
|
|
764
859
|
output: process.stderr
|
|
765
860
|
});
|
|
766
|
-
const ask = (
|
|
861
|
+
const ask = createAsk(rl);
|
|
767
862
|
process.stderr.write("Azure DevOps CLI - Configuration Wizard\n");
|
|
768
863
|
process.stderr.write("=======================================\n\n");
|
|
769
864
|
for (const setting of SETTINGS) {
|
|
770
|
-
|
|
771
|
-
const currentDisplay = current === void 0 ? "" : Array.isArray(current) ? current.join(",") : current;
|
|
772
|
-
const requiredTag = setting.required ? " (required)" : " (optional)";
|
|
773
|
-
process.stderr.write(`${setting.description}${requiredTag}
|
|
774
|
-
`);
|
|
775
|
-
if (setting.example) {
|
|
776
|
-
process.stderr.write(` Example: ${setting.example}
|
|
777
|
-
`);
|
|
778
|
-
}
|
|
779
|
-
const defaultHint = currentDisplay ? ` [${currentDisplay}]` : "";
|
|
780
|
-
const answer = await ask(` ${setting.key}${defaultHint}: `);
|
|
781
|
-
const trimmed = answer.trim();
|
|
782
|
-
if (trimmed) {
|
|
783
|
-
setConfigValue(setting.key, trimmed);
|
|
784
|
-
process.stderr.write(` -> Set "${setting.key}" to "${trimmed}"
|
|
785
|
-
|
|
786
|
-
`);
|
|
787
|
-
} else if (currentDisplay) {
|
|
788
|
-
process.stderr.write(` -> Kept "${setting.key}" as "${currentDisplay}"
|
|
789
|
-
|
|
790
|
-
`);
|
|
791
|
-
} else {
|
|
792
|
-
process.stderr.write(` -> Skipped "${setting.key}"
|
|
793
|
-
|
|
794
|
-
`);
|
|
795
|
-
}
|
|
865
|
+
await promptForSetting(cfg, setting, ask);
|
|
796
866
|
}
|
|
797
867
|
rl.close();
|
|
798
868
|
process.stderr.write("Configuration complete!\n");
|
|
@@ -1047,8 +1117,313 @@ function createSetMdFieldCommand() {
|
|
|
1047
1117
|
return command;
|
|
1048
1118
|
}
|
|
1049
1119
|
|
|
1120
|
+
// src/commands/upsert.ts
|
|
1121
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, unlinkSync } from "fs";
|
|
1122
|
+
import { Command as Command9 } from "commander";
|
|
1123
|
+
|
|
1124
|
+
// src/services/task-document.ts
|
|
1125
|
+
var FIELD_ALIASES = /* @__PURE__ */ new Map([
|
|
1126
|
+
["title", "System.Title"],
|
|
1127
|
+
["assignedto", "System.AssignedTo"],
|
|
1128
|
+
["assigned to", "System.AssignedTo"],
|
|
1129
|
+
["state", "System.State"],
|
|
1130
|
+
["description", "System.Description"],
|
|
1131
|
+
["acceptancecriteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
|
|
1132
|
+
["acceptance criteria", "Microsoft.VSTS.Common.AcceptanceCriteria"],
|
|
1133
|
+
["tags", "System.Tags"],
|
|
1134
|
+
["priority", "Microsoft.VSTS.Common.Priority"]
|
|
1135
|
+
]);
|
|
1136
|
+
var RICH_TEXT_FIELDS = /* @__PURE__ */ new Set([
|
|
1137
|
+
"System.Description",
|
|
1138
|
+
"Microsoft.VSTS.Common.AcceptanceCriteria"
|
|
1139
|
+
]);
|
|
1140
|
+
var REFERENCE_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*(\.[A-Za-z0-9]+)+$/;
|
|
1141
|
+
function normalizeAlias(name) {
|
|
1142
|
+
return name.trim().replaceAll(/\s+/g, " ").toLowerCase();
|
|
1143
|
+
}
|
|
1144
|
+
function parseScalarValue(rawValue, fieldName) {
|
|
1145
|
+
if (rawValue === void 0) {
|
|
1146
|
+
throw new Error(`Malformed YAML front matter: missing value for "${fieldName}"`);
|
|
1147
|
+
}
|
|
1148
|
+
const trimmed = rawValue.trim();
|
|
1149
|
+
if (trimmed === "" || trimmed === "null" || trimmed === "~") {
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
1153
|
+
return trimmed.slice(1, -1);
|
|
1154
|
+
}
|
|
1155
|
+
if (/^[[{]|^[>|]-?$/.test(trimmed)) {
|
|
1156
|
+
throw new Error(`Malformed YAML front matter: unsupported value for "${fieldName}"`);
|
|
1157
|
+
}
|
|
1158
|
+
return trimmed;
|
|
1159
|
+
}
|
|
1160
|
+
function parseFrontMatter(content) {
|
|
1161
|
+
if (!content.startsWith("---")) {
|
|
1162
|
+
return { frontMatter: "", remainder: content };
|
|
1163
|
+
}
|
|
1164
|
+
const frontMatterPattern = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
|
|
1165
|
+
const match = frontMatterPattern.exec(content);
|
|
1166
|
+
if (!match) {
|
|
1167
|
+
throw new Error('Malformed YAML front matter: missing closing "---"');
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
frontMatter: match[1],
|
|
1171
|
+
remainder: content.slice(match[0].length)
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function assertKnownField(name, kind) {
|
|
1175
|
+
const resolved = resolveFieldName(name);
|
|
1176
|
+
if (!resolved) {
|
|
1177
|
+
const prefix = kind === "rich-text" ? "Unknown rich-text field" : "Unknown field";
|
|
1178
|
+
throw new Error(`${prefix}: ${name}`);
|
|
1179
|
+
}
|
|
1180
|
+
if (kind === "rich-text" && !RICH_TEXT_FIELDS.has(resolved)) {
|
|
1181
|
+
throw new Error(`Unknown rich-text field: ${name}`);
|
|
1182
|
+
}
|
|
1183
|
+
return resolved;
|
|
1184
|
+
}
|
|
1185
|
+
function pushField(fields, seen, refName, value, kind) {
|
|
1186
|
+
if (seen.has(refName)) {
|
|
1187
|
+
throw new Error(`Duplicate field: ${refName}`);
|
|
1188
|
+
}
|
|
1189
|
+
seen.add(refName);
|
|
1190
|
+
fields.push({
|
|
1191
|
+
refName,
|
|
1192
|
+
value,
|
|
1193
|
+
op: value === null ? "clear" : "set",
|
|
1194
|
+
kind
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
function parseScalarFields(frontMatter, fields, seen) {
|
|
1198
|
+
if (frontMatter.trim() === "") {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
for (const rawLine of frontMatter.split(/\r?\n/)) {
|
|
1202
|
+
const line = rawLine.trim();
|
|
1203
|
+
if (line === "") {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const separatorIndex = rawLine.indexOf(":");
|
|
1207
|
+
if (separatorIndex <= 0) {
|
|
1208
|
+
throw new Error(`Malformed YAML front matter: ${rawLine.trim()}`);
|
|
1209
|
+
}
|
|
1210
|
+
const rawName = rawLine.slice(0, separatorIndex).trim();
|
|
1211
|
+
const rawValue = rawLine.slice(separatorIndex + 1);
|
|
1212
|
+
const refName = assertKnownField(rawName, "scalar");
|
|
1213
|
+
const value = parseScalarValue(rawValue, rawName);
|
|
1214
|
+
pushField(fields, seen, refName, value, "scalar");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function parseRichTextSections(content, fields, seen) {
|
|
1218
|
+
const normalizedContent = content.replaceAll("\r\n", "\n");
|
|
1219
|
+
const lines = normalizedContent.split("\n");
|
|
1220
|
+
const headings = [];
|
|
1221
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1222
|
+
const line = lines[index];
|
|
1223
|
+
if (!line.startsWith("##")) {
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const headingBody = line.slice(2);
|
|
1227
|
+
if (headingBody.trim() === "" || !headingBody.startsWith(" ") && !headingBody.startsWith(" ")) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
headings.push({
|
|
1231
|
+
lineIndex: index,
|
|
1232
|
+
rawName: headingBody.trim()
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
if (headings.length === 0) {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
for (let index = 0; index < headings[0].lineIndex; index += 1) {
|
|
1239
|
+
if (lines[index].trim() !== "") {
|
|
1240
|
+
throw new Error("Unexpected content before the first markdown heading section");
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
1244
|
+
const { lineIndex, rawName } = headings[index];
|
|
1245
|
+
const refName = assertKnownField(rawName, "rich-text");
|
|
1246
|
+
const bodyStart = lineIndex + 1;
|
|
1247
|
+
const bodyEnd = index + 1 < headings.length ? headings[index + 1].lineIndex : lines.length;
|
|
1248
|
+
const rawBody = lines.slice(bodyStart, bodyEnd).join("\n");
|
|
1249
|
+
const value = rawBody.trim() === "" ? null : rawBody.trimEnd();
|
|
1250
|
+
pushField(fields, seen, refName, value, "rich-text");
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
function resolveFieldName(name) {
|
|
1254
|
+
const trimmed = name.trim();
|
|
1255
|
+
if (trimmed === "") {
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
const alias = FIELD_ALIASES.get(normalizeAlias(trimmed));
|
|
1259
|
+
if (alias) {
|
|
1260
|
+
return alias;
|
|
1261
|
+
}
|
|
1262
|
+
return REFERENCE_NAME_PATTERN.test(trimmed) ? trimmed : null;
|
|
1263
|
+
}
|
|
1264
|
+
function parseTaskDocument(content) {
|
|
1265
|
+
const { frontMatter, remainder } = parseFrontMatter(content);
|
|
1266
|
+
const fields = [];
|
|
1267
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1268
|
+
parseScalarFields(frontMatter, fields, seen);
|
|
1269
|
+
parseRichTextSections(remainder, fields, seen);
|
|
1270
|
+
return { fields };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/commands/upsert.ts
|
|
1274
|
+
function fail2(message) {
|
|
1275
|
+
process.stderr.write(`Error: ${message}
|
|
1276
|
+
`);
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
function loadSourceContent(options) {
|
|
1280
|
+
validateSource(options);
|
|
1281
|
+
if (options.content !== void 0) {
|
|
1282
|
+
return { content: options.content };
|
|
1283
|
+
}
|
|
1284
|
+
const filePath = options.file;
|
|
1285
|
+
if (!existsSync2(filePath)) {
|
|
1286
|
+
fail2(`File not found: ${filePath}`);
|
|
1287
|
+
}
|
|
1288
|
+
try {
|
|
1289
|
+
return {
|
|
1290
|
+
content: readFileSync3(filePath, "utf-8"),
|
|
1291
|
+
sourceFile: filePath
|
|
1292
|
+
};
|
|
1293
|
+
} catch {
|
|
1294
|
+
fail2(`Cannot read file: ${filePath}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function toPatchOperations(fields, action) {
|
|
1298
|
+
const operations = [];
|
|
1299
|
+
for (const field of fields) {
|
|
1300
|
+
if (field.op === "clear") {
|
|
1301
|
+
if (action === "updated") {
|
|
1302
|
+
operations.push({ op: "remove", path: `/fields/${field.refName}` });
|
|
1303
|
+
}
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
operations.push({ op: "add", path: `/fields/${field.refName}`, value: field.value ?? "" });
|
|
1307
|
+
if (field.kind === "rich-text") {
|
|
1308
|
+
operations.push({
|
|
1309
|
+
op: "add",
|
|
1310
|
+
path: `/multilineFieldsFormat/${field.refName}`,
|
|
1311
|
+
value: "Markdown"
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return operations;
|
|
1316
|
+
}
|
|
1317
|
+
function buildAppliedFields(fields) {
|
|
1318
|
+
const applied = {};
|
|
1319
|
+
for (const field of fields) {
|
|
1320
|
+
applied[field.refName] = field.value;
|
|
1321
|
+
}
|
|
1322
|
+
return applied;
|
|
1323
|
+
}
|
|
1324
|
+
function ensureTitleForCreate(fields) {
|
|
1325
|
+
const titleField = fields.find((field) => field.refName === "System.Title");
|
|
1326
|
+
if (!titleField || titleField.op === "clear" || titleField.value === null || titleField.value.trim() === "") {
|
|
1327
|
+
fail2("Title is required when creating a task.");
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function writeSuccess(result, options) {
|
|
1331
|
+
if (options.json) {
|
|
1332
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1333
|
+
`);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const verb = result.action === "created" ? "Created" : "Updated";
|
|
1337
|
+
const fields = Object.keys(result.fields).join(", ");
|
|
1338
|
+
const suffix = fields ? ` (${fields})` : "";
|
|
1339
|
+
process.stdout.write(`${verb} task #${result.id}${suffix}
|
|
1340
|
+
`);
|
|
1341
|
+
}
|
|
1342
|
+
function cleanupSourceFile(sourceFile) {
|
|
1343
|
+
if (!sourceFile) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
try {
|
|
1347
|
+
unlinkSync(sourceFile);
|
|
1348
|
+
} catch {
|
|
1349
|
+
process.stderr.write(`Warning: upsert succeeded but could not delete source file: ${sourceFile}
|
|
1350
|
+
`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function buildUpsertResult(action, writeResult, fields) {
|
|
1354
|
+
const appliedFields = buildAppliedFields(fields);
|
|
1355
|
+
return {
|
|
1356
|
+
action,
|
|
1357
|
+
id: writeResult.id,
|
|
1358
|
+
fields: appliedFields
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
function isUpdateWriteError(err) {
|
|
1362
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NOT_FOUND" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("UPDATE_REJECTED:");
|
|
1363
|
+
}
|
|
1364
|
+
function isCreateWriteError(err) {
|
|
1365
|
+
return err.message === "AUTH_FAILED" || err.message === "PERMISSION_DENIED" || err.message === "NETWORK_ERROR" || err.message.startsWith("BAD_REQUEST:") || err.message.startsWith("HTTP_");
|
|
1366
|
+
}
|
|
1367
|
+
function handleUpsertError(err, id, context) {
|
|
1368
|
+
if (!(err instanceof Error)) {
|
|
1369
|
+
process.stderr.write(`Error: ${String(err)}
|
|
1370
|
+
`);
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
}
|
|
1373
|
+
if (id === void 0 && err.message.startsWith("CREATE_REJECTED:")) {
|
|
1374
|
+
process.stderr.write(`Error: ${formatCreateError(err)}
|
|
1375
|
+
`);
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
if (id !== void 0 && isUpdateWriteError(err)) {
|
|
1379
|
+
handleCommandError(err, id, context, "write");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (id === void 0 && isCreateWriteError(err)) {
|
|
1383
|
+
handleCommandError(err, 0, context, "write");
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
process.stderr.write(`Error: ${err.message}
|
|
1387
|
+
`);
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
function createUpsertCommand() {
|
|
1391
|
+
const command = new Command9("upsert");
|
|
1392
|
+
command.description("Create or update a Task from a markdown document").argument("[id]", "work item ID to update; omit to create a new Task").option("--content <markdown>", "task document content").option("--file <path>", "read task document from file").option("--json", "output result as JSON").option("--org <org>", "Azure DevOps organization").option("--project <project>", "Azure DevOps project").action(async (idStr, options) => {
|
|
1393
|
+
validateOrgProjectPair(options);
|
|
1394
|
+
const id = idStr === void 0 ? void 0 : parseWorkItemId(idStr);
|
|
1395
|
+
const { content, sourceFile } = loadSourceContent(options);
|
|
1396
|
+
let context;
|
|
1397
|
+
try {
|
|
1398
|
+
context = resolveContext(options);
|
|
1399
|
+
const document = parseTaskDocument(content);
|
|
1400
|
+
const action = id === void 0 ? "created" : "updated";
|
|
1401
|
+
if (action === "created") {
|
|
1402
|
+
ensureTitleForCreate(document.fields);
|
|
1403
|
+
}
|
|
1404
|
+
const operations = toPatchOperations(document.fields, action);
|
|
1405
|
+
const credential = await resolvePat();
|
|
1406
|
+
let writeResult;
|
|
1407
|
+
if (action === "created") {
|
|
1408
|
+
writeResult = await createWorkItem(context, "Task", credential.pat, operations);
|
|
1409
|
+
} else {
|
|
1410
|
+
if (id === void 0) {
|
|
1411
|
+
fail2("Work item ID is required for updates.");
|
|
1412
|
+
}
|
|
1413
|
+
writeResult = await applyWorkItemPatch(context, id, credential.pat, operations);
|
|
1414
|
+
}
|
|
1415
|
+
const result = buildUpsertResult(action, writeResult, document.fields);
|
|
1416
|
+
writeSuccess(result, options);
|
|
1417
|
+
cleanupSourceFile(sourceFile);
|
|
1418
|
+
} catch (err) {
|
|
1419
|
+
handleUpsertError(err, id, context);
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
return command;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1050
1425
|
// src/index.ts
|
|
1051
|
-
var program = new
|
|
1426
|
+
var program = new Command10();
|
|
1052
1427
|
program.name("azdo").description("Azure DevOps CLI tool").version(version, "-v, --version");
|
|
1053
1428
|
program.addCommand(createGetItemCommand());
|
|
1054
1429
|
program.addCommand(createClearPatCommand());
|
|
@@ -1058,6 +1433,7 @@ program.addCommand(createAssignCommand());
|
|
|
1058
1433
|
program.addCommand(createSetFieldCommand());
|
|
1059
1434
|
program.addCommand(createGetMdFieldCommand());
|
|
1060
1435
|
program.addCommand(createSetMdFieldCommand());
|
|
1436
|
+
program.addCommand(createUpsertCommand());
|
|
1061
1437
|
program.showHelpAfterError();
|
|
1062
1438
|
program.parse();
|
|
1063
1439
|
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-feature-
|
|
3
|
+
"version": "0.2.0-feature-macsecret.95",
|
|
4
4
|
"description": "Azure DevOps CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"lint": "eslint src/",
|
|
15
15
|
"typecheck": "tsc --noEmit",
|
|
16
16
|
"format": "prettier --check src/",
|
|
17
|
-
"test": "npm run build && vitest run"
|
|
17
|
+
"test": "npm run build && vitest run tests/unit",
|
|
18
|
+
"test:integration": "npm run build && vitest run tests/integration"
|
|
18
19
|
},
|
|
19
20
|
"repository": {
|
|
20
21
|
"type": "git",
|
|
@@ -28,12 +29,12 @@
|
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@eslint/js": "^10.0.1",
|
|
31
|
-
"@types/node": "^25.
|
|
32
|
-
"eslint": "^10.0
|
|
32
|
+
"@types/node": "^25.5.0",
|
|
33
|
+
"eslint": "^10.1.0",
|
|
33
34
|
"prettier": "^3.8.1",
|
|
34
35
|
"tsup": "^8.5.1",
|
|
35
36
|
"typescript": "^5.9.3",
|
|
36
|
-
"typescript-eslint": "^8.
|
|
37
|
-
"vitest": "^4.
|
|
37
|
+
"typescript-eslint": "^8.57.2",
|
|
38
|
+
"vitest": "^4.1.2"
|
|
38
39
|
}
|
|
39
40
|
}
|