azdo-cli 0.5.0-025-multi-org-support.431 → 0.5.0-025-multi-org-support.449
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.
|
@@ -65,6 +65,7 @@ var SETTINGS = [
|
|
|
65
65
|
}
|
|
66
66
|
];
|
|
67
67
|
var VALID_KEYS = SETTINGS.map((s) => s.key);
|
|
68
|
+
var SCOPED_KEYS = ["project", "fields", "markdown"];
|
|
68
69
|
function getConfigPath() {
|
|
69
70
|
return path.join(os.homedir(), ".azdo", "config.json");
|
|
70
71
|
}
|
|
@@ -88,6 +89,20 @@ function loadConfig() {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
function saveConfig(config) {
|
|
92
|
+
if (config.organizations) {
|
|
93
|
+
const normalised = {};
|
|
94
|
+
for (const [k, v] of Object.entries(config.organizations)) {
|
|
95
|
+
if (v && Object.keys(v).length > 0) {
|
|
96
|
+
normalised[k.toLowerCase()] = v;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(normalised).length > 0) {
|
|
100
|
+
config = { ...config, organizations: normalised };
|
|
101
|
+
} else {
|
|
102
|
+
const { organizations: _, ...rest } = config;
|
|
103
|
+
config = rest;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
91
106
|
const configPath = getConfigPath();
|
|
92
107
|
const dir = path.dirname(configPath);
|
|
93
108
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -126,6 +141,115 @@ function unsetConfigValue(key) {
|
|
|
126
141
|
delete config[key];
|
|
127
142
|
saveConfig(config);
|
|
128
143
|
}
|
|
144
|
+
function resolveScopedConfig(org) {
|
|
145
|
+
const config = loadConfig();
|
|
146
|
+
const base = {
|
|
147
|
+
project: config.project,
|
|
148
|
+
fields: config.fields,
|
|
149
|
+
markdown: config.markdown,
|
|
150
|
+
org: config.org
|
|
151
|
+
};
|
|
152
|
+
if (!org) return base;
|
|
153
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
154
|
+
if (!scope) return base;
|
|
155
|
+
return {
|
|
156
|
+
org: config.org,
|
|
157
|
+
project: scope.project ?? config.project,
|
|
158
|
+
fields: scope.fields ?? config.fields,
|
|
159
|
+
markdown: scope.markdown ?? config.markdown
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function validateScopedKey(key) {
|
|
163
|
+
if (!SCOPED_KEYS.includes(key)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Invalid scoped key "${key}". Valid scoped keys: ${SCOPED_KEYS.join(", ")}`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function applyValueToScope(scope, key, value) {
|
|
170
|
+
if (key === "fields") {
|
|
171
|
+
return { ...scope, fields: value.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
172
|
+
}
|
|
173
|
+
if (key === "markdown") {
|
|
174
|
+
if (value !== "true" && value !== "false") {
|
|
175
|
+
throw new Error(`Invalid value "${value}" for markdown. Must be "true" or "false".`);
|
|
176
|
+
}
|
|
177
|
+
return { ...scope, markdown: value === "true" };
|
|
178
|
+
}
|
|
179
|
+
return { ...scope, [key]: value };
|
|
180
|
+
}
|
|
181
|
+
function setOrgScopedValue(org, key, value) {
|
|
182
|
+
validateScopedKey(key);
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const lc = org.toLowerCase();
|
|
185
|
+
const existing = config.organizations?.[lc] ?? {};
|
|
186
|
+
const updated = applyValueToScope(existing, key, value);
|
|
187
|
+
saveConfig({
|
|
188
|
+
...config,
|
|
189
|
+
organizations: { ...config.organizations ?? {}, [lc]: updated }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function unsetOrgScopedValue(org, key) {
|
|
193
|
+
validateScopedKey(key);
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const lc = org.toLowerCase();
|
|
196
|
+
const scope = config.organizations?.[lc];
|
|
197
|
+
if (!scope) return;
|
|
198
|
+
const { [key]: _, ...rest } = scope;
|
|
199
|
+
saveConfig({
|
|
200
|
+
...config,
|
|
201
|
+
organizations: { ...config.organizations ?? {}, [lc]: rest }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function getOrgScopedValue(org, key) {
|
|
205
|
+
validateScopedKey(key);
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
const scope = config.organizations?.[org.toLowerCase()];
|
|
208
|
+
return scope?.[key];
|
|
209
|
+
}
|
|
210
|
+
function readScope(config, name) {
|
|
211
|
+
if (name === "default") {
|
|
212
|
+
const { org: _o, organizations: _orgs, ...defaults } = config;
|
|
213
|
+
return defaults;
|
|
214
|
+
}
|
|
215
|
+
return config.organizations?.[name.toLowerCase()] ?? {};
|
|
216
|
+
}
|
|
217
|
+
function copyOrgScope(from, to, force = false) {
|
|
218
|
+
const config = loadConfig();
|
|
219
|
+
const source = readScope(config, from);
|
|
220
|
+
const toLc = to.toLowerCase();
|
|
221
|
+
const dest = config.organizations?.[toLc] ?? {};
|
|
222
|
+
if (!force) {
|
|
223
|
+
const collisions = Object.keys(source).filter(
|
|
224
|
+
(k) => dest[k] !== void 0
|
|
225
|
+
);
|
|
226
|
+
if (collisions.length > 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Scope "${toLc}" already has values for: ${collisions.join(", ")}. Use --force to overwrite.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
saveConfig({
|
|
233
|
+
...config,
|
|
234
|
+
organizations: {
|
|
235
|
+
...config.organizations ?? {},
|
|
236
|
+
[toLc]: { ...dest, ...source }
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function moveOrgScope(from, to, force = false) {
|
|
241
|
+
copyOrgScope(from, to, force);
|
|
242
|
+
if (from !== "default") {
|
|
243
|
+
deleteOrgScope(from);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function deleteOrgScope(name) {
|
|
247
|
+
const config = loadConfig();
|
|
248
|
+
const lc = name.toLowerCase();
|
|
249
|
+
if (!config.organizations?.[lc]) return;
|
|
250
|
+
const { [lc]: _, ...rest } = config.organizations;
|
|
251
|
+
saveConfig({ ...config, organizations: rest });
|
|
252
|
+
}
|
|
129
253
|
|
|
130
254
|
// src/services/oauth-config.ts
|
|
131
255
|
var DEFAULT_OAUTH_CLIENT_ID = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
|
|
@@ -864,7 +988,7 @@ async function deletePat(org) {
|
|
|
864
988
|
}
|
|
865
989
|
try {
|
|
866
990
|
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
867
|
-
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-
|
|
991
|
+
const { lockPath: lockPath2 } = await import("./oauth-token-refresh-KOVYJAKC.js");
|
|
868
992
|
unlinkSync2(lockPath2(org));
|
|
869
993
|
} catch {
|
|
870
994
|
}
|
|
@@ -1061,6 +1185,13 @@ export {
|
|
|
1061
1185
|
getConfigValue,
|
|
1062
1186
|
setConfigValue,
|
|
1063
1187
|
unsetConfigValue,
|
|
1188
|
+
resolveScopedConfig,
|
|
1189
|
+
setOrgScopedValue,
|
|
1190
|
+
unsetOrgScopedValue,
|
|
1191
|
+
getOrgScopedValue,
|
|
1192
|
+
copyOrgScope,
|
|
1193
|
+
moveOrgScope,
|
|
1194
|
+
deleteOrgScope,
|
|
1064
1195
|
maskedDisplay,
|
|
1065
1196
|
normalizePat,
|
|
1066
1197
|
AZDO_RESOURCE_ID,
|
package/dist/index.js
CHANGED
|
@@ -6,15 +6,19 @@ import {
|
|
|
6
6
|
SETTINGS,
|
|
7
7
|
appendAuthAuditEvent,
|
|
8
8
|
buildScopeString,
|
|
9
|
+
copyOrgScope,
|
|
9
10
|
defaultScopes,
|
|
11
|
+
deleteOrgScope,
|
|
10
12
|
deletePat,
|
|
11
13
|
firstPartyShippedScopes,
|
|
12
14
|
getConfigValue,
|
|
15
|
+
getOrgScopedValue,
|
|
13
16
|
getPat,
|
|
14
17
|
getStoredCredential,
|
|
15
18
|
listOrgsWithStoredPat,
|
|
16
19
|
loadConfig,
|
|
17
20
|
maskedDisplay,
|
|
21
|
+
moveOrgScope,
|
|
18
22
|
normalizePat,
|
|
19
23
|
openUrl,
|
|
20
24
|
probeBackend,
|
|
@@ -22,13 +26,16 @@ import {
|
|
|
22
26
|
readTokenResponse,
|
|
23
27
|
refreshIfNeeded,
|
|
24
28
|
resolveOAuthConfig,
|
|
29
|
+
resolveScopedConfig,
|
|
25
30
|
runAuthCodeFlow,
|
|
26
31
|
setConfigValue,
|
|
32
|
+
setOrgScopedValue,
|
|
27
33
|
storeOAuthCredential,
|
|
28
34
|
storePat,
|
|
29
35
|
tokenResponseToCredential,
|
|
30
|
-
unsetConfigValue
|
|
31
|
-
|
|
36
|
+
unsetConfigValue,
|
|
37
|
+
unsetOrgScopedValue
|
|
38
|
+
} from "./chunk-ISUEWHFA.js";
|
|
32
39
|
|
|
33
40
|
// src/index.ts
|
|
34
41
|
import { Command as Command16 } from "commander";
|
|
@@ -245,11 +252,44 @@ async function fetchWorkItemResponse(context, id, cred, options = {}) {
|
|
|
245
252
|
}
|
|
246
253
|
return await response.json();
|
|
247
254
|
}
|
|
255
|
+
async function getOrgFieldNames(context, cred) {
|
|
256
|
+
const url = new URL(
|
|
257
|
+
`https://dev.azure.com/${encodeURIComponent(context.org)}/_apis/wit/fields`
|
|
258
|
+
);
|
|
259
|
+
url.searchParams.set("api-version", "7.1");
|
|
260
|
+
const response = await fetchWithErrors(url.toString(), { headers: authHeaders(cred) });
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw new Error(`HTTP_${response.status}`);
|
|
263
|
+
}
|
|
264
|
+
const data = await response.json();
|
|
265
|
+
return (data.value ?? []).map((f) => f.referenceName);
|
|
266
|
+
}
|
|
248
267
|
async function getWorkItem(context, id, cred, extraFields) {
|
|
249
268
|
const normalizedExtraFields = extraFields ? normalizeFieldList(extraFields) : [];
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
269
|
+
let effectiveExtraFields = normalizedExtraFields;
|
|
270
|
+
let data;
|
|
271
|
+
if (normalizedExtraFields.length > 0) {
|
|
272
|
+
try {
|
|
273
|
+
data = await fetchWorkItemResponse(context, id, cred, {
|
|
274
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...normalizedExtraFields])
|
|
275
|
+
});
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
278
|
+
if (!msg.includes("TF51535")) throw err;
|
|
279
|
+
const orgFields = new Set(await getOrgFieldNames(context, cred));
|
|
280
|
+
const missing = normalizedExtraFields.filter((f) => !orgFields.has(f));
|
|
281
|
+
effectiveExtraFields = normalizedExtraFields.filter((f) => orgFields.has(f));
|
|
282
|
+
for (const f of missing) {
|
|
283
|
+
process.stderr.write(`Warning: field "${f}" does not exist in this org and will be skipped.
|
|
284
|
+
`);
|
|
285
|
+
}
|
|
286
|
+
data = await fetchWorkItemResponse(context, id, cred, {
|
|
287
|
+
fields: normalizeFieldList([...DEFAULT_FIELDS, ...effectiveExtraFields])
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
data = await fetchWorkItemResponse(context, id, cred, { includeRelations: true });
|
|
292
|
+
}
|
|
253
293
|
const relationsData = normalizedExtraFields.length > 0 ? await fetchWorkItemResponse(context, id, cred, { includeRelations: true }) : data;
|
|
254
294
|
const descriptionParts = [];
|
|
255
295
|
if (data.fields["System.Description"]) {
|
|
@@ -278,7 +318,7 @@ async function getWorkItem(context, id, cred, extraFields) {
|
|
|
278
318
|
areaPath: data.fields["System.AreaPath"],
|
|
279
319
|
iterationPath: data.fields["System.IterationPath"],
|
|
280
320
|
url: data._links.html.href,
|
|
281
|
-
extraFields:
|
|
321
|
+
extraFields: effectiveExtraFields.length > 0 ? buildExtraFields(data.fields, effectiveExtraFields) : null,
|
|
282
322
|
attachments: extractAttachments(relationsData.relations)
|
|
283
323
|
};
|
|
284
324
|
}
|
|
@@ -766,17 +806,22 @@ async function status() {
|
|
|
766
806
|
|
|
767
807
|
// src/services/git-remote.ts
|
|
768
808
|
import { execSync } from "child_process";
|
|
809
|
+
import fs from "fs";
|
|
810
|
+
import path from "path";
|
|
769
811
|
|
|
770
812
|
// src/services/remote-warning.ts
|
|
771
|
-
var WARNING = "azdo: warning: origin includes embedded credentials; consider removing them with 'git remote set-url origin <clean-url>'\n";
|
|
772
813
|
var warned = false;
|
|
773
|
-
function
|
|
814
|
+
function buildWarning(remoteName) {
|
|
815
|
+
return `azdo: warning: ${remoteName} includes embedded credentials; consider removing them with 'git remote set-url ${remoteName} <clean-url>'
|
|
816
|
+
`;
|
|
817
|
+
}
|
|
818
|
+
function noticeCredentialBearingRemote(remoteName = "origin") {
|
|
774
819
|
if (warned) {
|
|
775
820
|
return;
|
|
776
821
|
}
|
|
777
822
|
warned = true;
|
|
778
823
|
try {
|
|
779
|
-
process.stderr.write(
|
|
824
|
+
process.stderr.write(buildWarning(remoteName));
|
|
780
825
|
} catch {
|
|
781
826
|
}
|
|
782
827
|
}
|
|
@@ -794,41 +839,127 @@ var patterns = [
|
|
|
794
839
|
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}[.git]
|
|
795
840
|
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/
|
|
796
841
|
];
|
|
797
|
-
var
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
842
|
+
var httpsEmbeddedSecret = /^https?:\/\/[^:@/]+:[^@/]+@/;
|
|
843
|
+
function parseAllAzdoRemotes(output) {
|
|
844
|
+
const seen = /* @__PURE__ */ new Set();
|
|
845
|
+
const results = [];
|
|
846
|
+
for (const line of output.split("\n")) {
|
|
847
|
+
const trimmed = line.trim();
|
|
848
|
+
if (!trimmed) continue;
|
|
849
|
+
const tabIdx = trimmed.indexOf(" ");
|
|
850
|
+
if (tabIdx === -1) continue;
|
|
851
|
+
const remoteName = trimmed.slice(0, tabIdx);
|
|
852
|
+
if (seen.has(remoteName)) continue;
|
|
853
|
+
const afterTab = trimmed.slice(tabIdx + 1);
|
|
854
|
+
const urlEnd = afterTab.lastIndexOf(" (");
|
|
855
|
+
const url = urlEnd !== -1 ? afterTab.slice(0, urlEnd) : afterTab;
|
|
856
|
+
for (const pattern of patterns) {
|
|
857
|
+
const match = pattern.exec(url);
|
|
858
|
+
if (match) {
|
|
859
|
+
const project = match[2];
|
|
860
|
+
if (/^DefaultCollection$/i.test(project)) break;
|
|
861
|
+
seen.add(remoteName);
|
|
862
|
+
results.push({
|
|
863
|
+
remoteName,
|
|
864
|
+
org: match[1],
|
|
865
|
+
project,
|
|
866
|
+
hasEmbeddedSecret: httpsEmbeddedSecret.test(url)
|
|
867
|
+
});
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return results;
|
|
873
|
+
}
|
|
874
|
+
function selectRemote(candidates) {
|
|
875
|
+
if (candidates.length === 0) {
|
|
876
|
+
throw new Error("No Azure DevOps remote found. Provide --org and --project explicitly.");
|
|
877
|
+
}
|
|
878
|
+
const origin = candidates.find((c) => c.remoteName === "origin");
|
|
879
|
+
if (origin) return origin;
|
|
880
|
+
if (candidates.length === 1) return candidates[0];
|
|
881
|
+
const first = candidates[0];
|
|
882
|
+
const allSame = candidates.every((c) => c.org === first.org && c.project === first.project);
|
|
883
|
+
if (allSame) return first;
|
|
884
|
+
const names = candidates.map((c) => c.remoteName).join(", ");
|
|
885
|
+
throw new Error(
|
|
886
|
+
`Ambiguous Azure DevOps remotes (${names}). Provide --org and --project explicitly.`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
function readGitConfigContent() {
|
|
890
|
+
const gitDirEnv = process.env.GIT_DIR;
|
|
891
|
+
if (gitDirEnv) {
|
|
892
|
+
return fs.readFileSync(path.join(gitDirEnv, "config"), "utf-8");
|
|
893
|
+
}
|
|
894
|
+
let dir = process.cwd();
|
|
895
|
+
for (; ; ) {
|
|
896
|
+
const gitPath = path.join(dir, ".git");
|
|
897
|
+
try {
|
|
898
|
+
const stat = fs.statSync(gitPath);
|
|
899
|
+
if (stat.isDirectory()) {
|
|
900
|
+
return fs.readFileSync(path.join(gitPath, "config"), "utf-8");
|
|
804
901
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
902
|
+
if (stat.isFile()) {
|
|
903
|
+
const ref = fs.readFileSync(gitPath, "utf-8");
|
|
904
|
+
const m = /^gitdir:\s*(.+)$/m.exec(ref);
|
|
905
|
+
if (m) {
|
|
906
|
+
return fs.readFileSync(path.join(path.resolve(dir, m[1].trim()), "config"), "utf-8");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
const parent = path.dirname(dir);
|
|
912
|
+
if (parent === dir) break;
|
|
913
|
+
dir = parent;
|
|
914
|
+
}
|
|
915
|
+
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
916
|
+
}
|
|
917
|
+
function gitConfigToRemoteLines(configContent) {
|
|
918
|
+
const lines = [];
|
|
919
|
+
let currentRemote = null;
|
|
920
|
+
let emittedUrl = false;
|
|
921
|
+
for (const line of configContent.split("\n")) {
|
|
922
|
+
const sectionMatch = /^\[remote\s+"([^"]+)"\]/.exec(line);
|
|
923
|
+
if (sectionMatch) {
|
|
924
|
+
currentRemote = sectionMatch[1];
|
|
925
|
+
emittedUrl = false;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (/^\[/.test(line)) {
|
|
929
|
+
currentRemote = null;
|
|
930
|
+
emittedUrl = false;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (currentRemote && !emittedUrl) {
|
|
934
|
+
const urlMatch = /^\s+url\s*=\s*(.+)$/.exec(line);
|
|
935
|
+
if (urlMatch) {
|
|
936
|
+
lines.push(`${currentRemote} ${urlMatch[1].trim()} (fetch)`);
|
|
937
|
+
emittedUrl = true;
|
|
808
938
|
}
|
|
809
|
-
return { org: match[1], project };
|
|
810
939
|
}
|
|
811
940
|
}
|
|
812
|
-
return
|
|
941
|
+
return lines.join("\n");
|
|
813
942
|
}
|
|
814
943
|
function detectAzdoContext() {
|
|
815
|
-
let
|
|
944
|
+
let configContent;
|
|
816
945
|
try {
|
|
817
|
-
|
|
946
|
+
configContent = readGitConfigContent();
|
|
818
947
|
} catch {
|
|
819
948
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
820
949
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
950
|
+
const remoteLines = gitConfigToRemoteLines(configContent);
|
|
951
|
+
const candidates = parseAllAzdoRemotes(remoteLines);
|
|
952
|
+
const selected = selectRemote(candidates);
|
|
953
|
+
if (selected.hasEmbeddedSecret) {
|
|
954
|
+
noticeCredentialBearingRemote(selected.remoteName);
|
|
824
955
|
}
|
|
825
|
-
return
|
|
956
|
+
return { org: selected.org, project: selected.project };
|
|
826
957
|
}
|
|
827
958
|
function parseRepoName(url) {
|
|
828
959
|
for (const pattern of patterns) {
|
|
829
960
|
const match = pattern.exec(url);
|
|
830
961
|
if (match) {
|
|
831
|
-
if (
|
|
962
|
+
if (httpsEmbeddedSecret.test(url)) {
|
|
832
963
|
noticeCredentialBearingRemote();
|
|
833
964
|
}
|
|
834
965
|
return match[3];
|
|
@@ -886,7 +1017,7 @@ function formatResolutionError() {
|
|
|
886
1017
|
return [
|
|
887
1018
|
"Could not resolve an Azure DevOps organization. Options (in priority order):",
|
|
888
1019
|
" 1. Pass --org <name> on the command line.",
|
|
889
|
-
" 2. Run this command from a git repo
|
|
1020
|
+
" 2. Run this command from a git repo that has an Azure DevOps remote.",
|
|
890
1021
|
" 3. Run `azdo config set org <name>` once to set a persistent default."
|
|
891
1022
|
].join("\n");
|
|
892
1023
|
}
|
|
@@ -899,9 +1030,9 @@ function resolveContext(options) {
|
|
|
899
1030
|
gitContext = detectAzdoContext();
|
|
900
1031
|
} catch {
|
|
901
1032
|
}
|
|
902
|
-
const config = loadConfig();
|
|
903
1033
|
const org = resolvedOrg?.org;
|
|
904
|
-
const
|
|
1034
|
+
const scopedCfg = resolveScopedConfig(org);
|
|
1035
|
+
const project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || scopedCfg.project;
|
|
905
1036
|
if (org && project) {
|
|
906
1037
|
return { org, project };
|
|
907
1038
|
}
|
|
@@ -1354,9 +1485,10 @@ function createGetItemCommand() {
|
|
|
1354
1485
|
try {
|
|
1355
1486
|
context = resolveContext(options);
|
|
1356
1487
|
const credential = await requireAuthCredential(context.org);
|
|
1357
|
-
const
|
|
1488
|
+
const scopedCfg = resolveScopedConfig(context.org);
|
|
1489
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
|
|
1358
1490
|
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1359
|
-
const markdownEnabled = options.markdown ??
|
|
1491
|
+
const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
|
|
1360
1492
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1361
1493
|
process.stdout.write(output + "\n");
|
|
1362
1494
|
if (imageOptions.enabled) {
|
|
@@ -1836,18 +1968,47 @@ function formatConfigValue(value, unsetFallback = "") {
|
|
|
1836
1968
|
}
|
|
1837
1969
|
return Array.isArray(value) ? value.join(",") : value;
|
|
1838
1970
|
}
|
|
1971
|
+
function buildConfigListEntries(cfg) {
|
|
1972
|
+
const entries = SETTINGS.map((s) => ({
|
|
1973
|
+
scope: "default",
|
|
1974
|
+
key: s.key,
|
|
1975
|
+
value: cfg[s.key]
|
|
1976
|
+
}));
|
|
1977
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
1978
|
+
for (const [k, v] of Object.entries(scope)) {
|
|
1979
|
+
entries.push({ scope: orgName, key: k, value: v });
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return entries;
|
|
1983
|
+
}
|
|
1839
1984
|
function writeConfigList(cfg) {
|
|
1840
1985
|
const keyWidth = 10;
|
|
1841
1986
|
const valueWidth = 30;
|
|
1987
|
+
const scopeWidth = 12;
|
|
1988
|
+
process.stdout.write(
|
|
1989
|
+
`${"scope".padEnd(scopeWidth)}${"key".padEnd(keyWidth)}${"value".padEnd(valueWidth)}description
|
|
1990
|
+
`
|
|
1991
|
+
);
|
|
1842
1992
|
for (const setting of SETTINGS) {
|
|
1843
1993
|
const raw = cfg[setting.key];
|
|
1844
1994
|
const value = formatConfigValue(raw, "(not set)");
|
|
1845
1995
|
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
1846
1996
|
process.stdout.write(
|
|
1847
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1997
|
+
`${"default".padEnd(scopeWidth)}${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1848
1998
|
`
|
|
1849
1999
|
);
|
|
1850
2000
|
}
|
|
2001
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
2002
|
+
const orgScope = scope;
|
|
2003
|
+
const scopedSettings = Object.entries(orgScope);
|
|
2004
|
+
for (const [k, v] of scopedSettings) {
|
|
2005
|
+
const value = formatConfigValue(v, "(not set)");
|
|
2006
|
+
process.stdout.write(
|
|
2007
|
+
`${orgName.padEnd(scopeWidth)}${k.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}
|
|
2008
|
+
`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1851
2012
|
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
1852
2013
|
if (hasUnset) {
|
|
1853
2014
|
process.stdout.write(
|
|
@@ -1891,17 +2052,22 @@ function createConfigCommand() {
|
|
|
1891
2052
|
const config = new Command4("config");
|
|
1892
2053
|
config.description("Manage CLI settings");
|
|
1893
2054
|
const set = new Command4("set");
|
|
1894
|
-
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields)").argument("<value>", "setting value").option("--json", "output in JSON format").action((key, value, options) => {
|
|
2055
|
+
set.description("Set a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").argument("<value>", "setting value").option("--org <org>", "set value in an org-scoped configuration").option("--json", "output in JSON format").action((key, value, options) => {
|
|
1895
2056
|
try {
|
|
1896
|
-
|
|
2057
|
+
if (options.org) {
|
|
2058
|
+
setOrgScopedValue(options.org, key, value);
|
|
2059
|
+
} else {
|
|
2060
|
+
setConfigValue(key, value);
|
|
2061
|
+
}
|
|
1897
2062
|
if (options.json) {
|
|
1898
|
-
const output = { key, value };
|
|
2063
|
+
const output = { key, value, scope: options.org ?? "default" };
|
|
1899
2064
|
if (key === "fields") {
|
|
1900
2065
|
output.value = value.split(",").map((s) => s.trim());
|
|
1901
2066
|
}
|
|
1902
2067
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
1903
2068
|
} else {
|
|
1904
|
-
|
|
2069
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2070
|
+
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
1905
2071
|
`);
|
|
1906
2072
|
}
|
|
1907
2073
|
} catch (err) {
|
|
@@ -1912,12 +2078,12 @@ function createConfigCommand() {
|
|
|
1912
2078
|
}
|
|
1913
2079
|
});
|
|
1914
2080
|
const get = new Command4("get");
|
|
1915
|
-
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2081
|
+
get.description("Get a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "read from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1916
2082
|
try {
|
|
1917
|
-
const value = getConfigValue(key);
|
|
2083
|
+
const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
|
|
1918
2084
|
if (options.json) {
|
|
1919
2085
|
process.stdout.write(
|
|
1920
|
-
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
2086
|
+
JSON.stringify({ key, value: value ?? null, scope: options.org ?? "default" }) + "\n"
|
|
1921
2087
|
);
|
|
1922
2088
|
} else if (value === void 0) {
|
|
1923
2089
|
process.stdout.write(`Setting "${key}" is not configured.
|
|
@@ -1925,7 +2091,7 @@ function createConfigCommand() {
|
|
|
1925
2091
|
} else if (Array.isArray(value)) {
|
|
1926
2092
|
process.stdout.write(value.join(",") + "\n");
|
|
1927
2093
|
} else {
|
|
1928
|
-
process.stdout.write(value + "\n");
|
|
2094
|
+
process.stdout.write(String(value) + "\n");
|
|
1929
2095
|
}
|
|
1930
2096
|
} catch (err) {
|
|
1931
2097
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1938,19 +2104,25 @@ function createConfigCommand() {
|
|
|
1938
2104
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1939
2105
|
const cfg = loadConfig();
|
|
1940
2106
|
if (options.json) {
|
|
1941
|
-
|
|
2107
|
+
const entries = buildConfigListEntries(cfg);
|
|
2108
|
+
process.stdout.write(JSON.stringify(entries) + "\n");
|
|
1942
2109
|
return;
|
|
1943
2110
|
}
|
|
1944
2111
|
writeConfigList(cfg);
|
|
1945
2112
|
});
|
|
1946
2113
|
const unset = new Command4("unset");
|
|
1947
|
-
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields)").option("--json", "output in JSON format").action((key, options) => {
|
|
2114
|
+
unset.description("Remove a configuration value").argument("<key>", "setting key (org, project, fields, markdown)").option("--org <org>", "remove from an org-scoped configuration").option("--json", "output in JSON format").action((key, options) => {
|
|
1948
2115
|
try {
|
|
1949
|
-
|
|
2116
|
+
if (options.org) {
|
|
2117
|
+
unsetOrgScopedValue(options.org, key);
|
|
2118
|
+
} else {
|
|
2119
|
+
unsetConfigValue(key);
|
|
2120
|
+
}
|
|
1950
2121
|
if (options.json) {
|
|
1951
|
-
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
2122
|
+
process.stdout.write(JSON.stringify({ key, unset: true, scope: options.org ?? "default" }) + "\n");
|
|
1952
2123
|
} else {
|
|
1953
|
-
|
|
2124
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2125
|
+
process.stdout.write(`Unset "${key}"${scopeTag}
|
|
1954
2126
|
`);
|
|
1955
2127
|
}
|
|
1956
2128
|
} catch (err) {
|
|
@@ -1982,10 +2154,52 @@ function createConfigCommand() {
|
|
|
1982
2154
|
rl.close();
|
|
1983
2155
|
process.stderr.write("Configuration complete!\n");
|
|
1984
2156
|
});
|
|
2157
|
+
const orgCopy = new Command4("org-copy");
|
|
2158
|
+
orgCopy.description('Copy settings from one scope to another (use "default" as source to copy top-level settings)').argument("<from>", 'source scope name or "default"').argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2159
|
+
try {
|
|
2160
|
+
copyOrgScope(from, to, options.force ?? false);
|
|
2161
|
+
process.stdout.write(`Copied scope "${from}" to "${to}"
|
|
2162
|
+
`);
|
|
2163
|
+
} catch (err) {
|
|
2164
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2165
|
+
process.stderr.write(`Error: ${message}
|
|
2166
|
+
`);
|
|
2167
|
+
process.exit(1);
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
const orgMove = new Command4("org-move");
|
|
2171
|
+
orgMove.description("Move settings from one org scope to another").argument("<from>", "source org name").argument("<to>", "destination org name").option("--force", "overwrite existing values on collision").action((from, to, options) => {
|
|
2172
|
+
try {
|
|
2173
|
+
moveOrgScope(from, to, options.force ?? false);
|
|
2174
|
+
process.stdout.write(`Moved scope "${from}" to "${to}"
|
|
2175
|
+
`);
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2178
|
+
process.stderr.write(`Error: ${message}
|
|
2179
|
+
`);
|
|
2180
|
+
process.exit(1);
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
const orgDelete = new Command4("org-delete");
|
|
2184
|
+
orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
|
|
2185
|
+
try {
|
|
2186
|
+
deleteOrgScope(name);
|
|
2187
|
+
process.stdout.write(`Deleted scope "${name}"
|
|
2188
|
+
`);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2191
|
+
process.stderr.write(`Error: ${message}
|
|
2192
|
+
`);
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
1985
2196
|
config.addCommand(set);
|
|
1986
2197
|
config.addCommand(get);
|
|
1987
2198
|
config.addCommand(list);
|
|
1988
2199
|
config.addCommand(unset);
|
|
2200
|
+
config.addCommand(orgCopy);
|
|
2201
|
+
config.addCommand(orgMove);
|
|
2202
|
+
config.addCommand(orgDelete);
|
|
1989
2203
|
config.addCommand(wizard);
|
|
1990
2204
|
return config;
|
|
1991
2205
|
}
|
|
@@ -4411,18 +4625,18 @@ function createDownloadAttachmentCommand() {
|
|
|
4411
4625
|
}
|
|
4412
4626
|
|
|
4413
4627
|
// src/services/update-check.ts
|
|
4414
|
-
import
|
|
4415
|
-
import
|
|
4628
|
+
import fs2 from "fs";
|
|
4629
|
+
import path2 from "path";
|
|
4416
4630
|
import os from "os";
|
|
4417
4631
|
var THROTTLE_MS = 10 * 60 * 1e3;
|
|
4418
4632
|
var FETCH_TIMEOUT_MS = 1500;
|
|
4419
4633
|
var REGISTRY_URL = "https://registry.npmjs.org/azdo-cli/latest";
|
|
4420
4634
|
function getCachePath() {
|
|
4421
|
-
return
|
|
4635
|
+
return path2.join(os.homedir(), ".azdo", "update-check.json");
|
|
4422
4636
|
}
|
|
4423
4637
|
function defaultReadCache() {
|
|
4424
4638
|
try {
|
|
4425
|
-
return
|
|
4639
|
+
return fs2.readFileSync(getCachePath(), "utf-8");
|
|
4426
4640
|
} catch {
|
|
4427
4641
|
return null;
|
|
4428
4642
|
}
|
|
@@ -4430,8 +4644,8 @@ function defaultReadCache() {
|
|
|
4430
4644
|
function defaultWriteCache(data) {
|
|
4431
4645
|
try {
|
|
4432
4646
|
const cachePath = getCachePath();
|
|
4433
|
-
|
|
4434
|
-
|
|
4647
|
+
fs2.mkdirSync(path2.dirname(cachePath), { recursive: true });
|
|
4648
|
+
fs2.writeFileSync(cachePath, data);
|
|
4435
4649
|
} catch {
|
|
4436
4650
|
}
|
|
4437
4651
|
}
|