azdo-cli 0.5.0-025-multi-org-support.431 → 0.5.0-025-multi-org-support.447
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
|
}
|
|
@@ -768,15 +808,18 @@ async function status() {
|
|
|
768
808
|
import { execSync } from "child_process";
|
|
769
809
|
|
|
770
810
|
// 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
811
|
var warned = false;
|
|
773
|
-
function
|
|
812
|
+
function buildWarning(remoteName) {
|
|
813
|
+
return `azdo: warning: ${remoteName} includes embedded credentials; consider removing them with 'git remote set-url ${remoteName} <clean-url>'
|
|
814
|
+
`;
|
|
815
|
+
}
|
|
816
|
+
function noticeCredentialBearingRemote(remoteName = "origin") {
|
|
774
817
|
if (warned) {
|
|
775
818
|
return;
|
|
776
819
|
}
|
|
777
820
|
warned = true;
|
|
778
821
|
try {
|
|
779
|
-
process.stderr.write(
|
|
822
|
+
process.stderr.write(buildWarning(remoteName));
|
|
780
823
|
} catch {
|
|
781
824
|
}
|
|
782
825
|
}
|
|
@@ -794,41 +837,75 @@ var patterns = [
|
|
|
794
837
|
// SSH (legacy): {org}@vs-ssh.visualstudio.com:v3/{org}/{project}/{repo}[.git]
|
|
795
838
|
/^[^@]+@vs-ssh\.visualstudio\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/
|
|
796
839
|
];
|
|
797
|
-
var
|
|
798
|
-
function
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
840
|
+
var httpsEmbeddedSecret = /^https?:\/\/[^:@/]+:[^@/]+@/;
|
|
841
|
+
function parseAllAzdoRemotes(output) {
|
|
842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
843
|
+
const results = [];
|
|
844
|
+
for (const line of output.split("\n")) {
|
|
845
|
+
const trimmed = line.trim();
|
|
846
|
+
if (!trimmed) continue;
|
|
847
|
+
const tabIdx = trimmed.indexOf(" ");
|
|
848
|
+
if (tabIdx === -1) continue;
|
|
849
|
+
const remoteName = trimmed.slice(0, tabIdx);
|
|
850
|
+
if (seen.has(remoteName)) continue;
|
|
851
|
+
const afterTab = trimmed.slice(tabIdx + 1);
|
|
852
|
+
const urlEnd = afterTab.lastIndexOf(" (");
|
|
853
|
+
const url = urlEnd !== -1 ? afterTab.slice(0, urlEnd) : afterTab;
|
|
854
|
+
for (const pattern of patterns) {
|
|
855
|
+
const match = pattern.exec(url);
|
|
856
|
+
if (match) {
|
|
857
|
+
const project = match[2];
|
|
858
|
+
if (/^DefaultCollection$/i.test(project)) break;
|
|
859
|
+
seen.add(remoteName);
|
|
860
|
+
results.push({
|
|
861
|
+
remoteName,
|
|
862
|
+
org: match[1],
|
|
863
|
+
project,
|
|
864
|
+
hasEmbeddedSecret: httpsEmbeddedSecret.test(url)
|
|
865
|
+
});
|
|
866
|
+
break;
|
|
808
867
|
}
|
|
809
|
-
return { org: match[1], project };
|
|
810
868
|
}
|
|
811
869
|
}
|
|
812
|
-
return
|
|
870
|
+
return results;
|
|
871
|
+
}
|
|
872
|
+
function selectRemote(candidates) {
|
|
873
|
+
if (candidates.length === 0) {
|
|
874
|
+
throw new Error("No Azure DevOps remote found. Provide --org and --project explicitly.");
|
|
875
|
+
}
|
|
876
|
+
const origin = candidates.find((c) => c.remoteName === "origin");
|
|
877
|
+
if (origin) return origin;
|
|
878
|
+
if (candidates.length === 1) return candidates[0];
|
|
879
|
+
const first = candidates[0];
|
|
880
|
+
const allSame = candidates.every((c) => c.org === first.org && c.project === first.project);
|
|
881
|
+
if (allSame) return first;
|
|
882
|
+
const names = candidates.map((c) => c.remoteName).join(", ");
|
|
883
|
+
throw new Error(
|
|
884
|
+
`Ambiguous Azure DevOps remotes (${names}). Provide --org and --project explicitly.`
|
|
885
|
+
);
|
|
813
886
|
}
|
|
814
887
|
function detectAzdoContext() {
|
|
815
|
-
let
|
|
888
|
+
let remoteListOutput;
|
|
816
889
|
try {
|
|
817
|
-
|
|
890
|
+
remoteListOutput = execSync("git remote -v", {
|
|
891
|
+
encoding: "utf-8",
|
|
892
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
893
|
+
}).trim();
|
|
818
894
|
} catch {
|
|
819
895
|
throw new Error("Not in a git repository. Provide --org and --project explicitly.");
|
|
820
896
|
}
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
897
|
+
const candidates = parseAllAzdoRemotes(remoteListOutput);
|
|
898
|
+
const selected = selectRemote(candidates);
|
|
899
|
+
if (selected.hasEmbeddedSecret) {
|
|
900
|
+
noticeCredentialBearingRemote(selected.remoteName);
|
|
824
901
|
}
|
|
825
|
-
return
|
|
902
|
+
return { org: selected.org, project: selected.project };
|
|
826
903
|
}
|
|
827
904
|
function parseRepoName(url) {
|
|
828
905
|
for (const pattern of patterns) {
|
|
829
906
|
const match = pattern.exec(url);
|
|
830
907
|
if (match) {
|
|
831
|
-
if (
|
|
908
|
+
if (httpsEmbeddedSecret.test(url)) {
|
|
832
909
|
noticeCredentialBearingRemote();
|
|
833
910
|
}
|
|
834
911
|
return match[3];
|
|
@@ -886,7 +963,7 @@ function formatResolutionError() {
|
|
|
886
963
|
return [
|
|
887
964
|
"Could not resolve an Azure DevOps organization. Options (in priority order):",
|
|
888
965
|
" 1. Pass --org <name> on the command line.",
|
|
889
|
-
" 2. Run this command from a git repo
|
|
966
|
+
" 2. Run this command from a git repo that has an Azure DevOps remote.",
|
|
890
967
|
" 3. Run `azdo config set org <name>` once to set a persistent default."
|
|
891
968
|
].join("\n");
|
|
892
969
|
}
|
|
@@ -899,9 +976,9 @@ function resolveContext(options) {
|
|
|
899
976
|
gitContext = detectAzdoContext();
|
|
900
977
|
} catch {
|
|
901
978
|
}
|
|
902
|
-
const config = loadConfig();
|
|
903
979
|
const org = resolvedOrg?.org;
|
|
904
|
-
const
|
|
980
|
+
const scopedCfg = resolveScopedConfig(org);
|
|
981
|
+
const project = options.project || (gitContext?.project && gitContext.project.length > 0 ? gitContext.project : void 0) || scopedCfg.project;
|
|
905
982
|
if (org && project) {
|
|
906
983
|
return { org, project };
|
|
907
984
|
}
|
|
@@ -1354,9 +1431,10 @@ function createGetItemCommand() {
|
|
|
1354
1431
|
try {
|
|
1355
1432
|
context = resolveContext(options);
|
|
1356
1433
|
const credential = await requireAuthCredential(context.org);
|
|
1357
|
-
const
|
|
1434
|
+
const scopedCfg = resolveScopedConfig(context.org);
|
|
1435
|
+
const fieldsList = options.fields === void 0 ? parseRequestedFields(scopedCfg.fields) : parseRequestedFields(options.fields);
|
|
1358
1436
|
const workItem = await getWorkItem(context, id, credential, fieldsList);
|
|
1359
|
-
const markdownEnabled = options.markdown ??
|
|
1437
|
+
const markdownEnabled = options.markdown ?? scopedCfg.markdown ?? false;
|
|
1360
1438
|
const output = formatWorkItem(workItem, options.short ?? false, markdownEnabled);
|
|
1361
1439
|
process.stdout.write(output + "\n");
|
|
1362
1440
|
if (imageOptions.enabled) {
|
|
@@ -1836,18 +1914,47 @@ function formatConfigValue(value, unsetFallback = "") {
|
|
|
1836
1914
|
}
|
|
1837
1915
|
return Array.isArray(value) ? value.join(",") : value;
|
|
1838
1916
|
}
|
|
1917
|
+
function buildConfigListEntries(cfg) {
|
|
1918
|
+
const entries = SETTINGS.map((s) => ({
|
|
1919
|
+
scope: "default",
|
|
1920
|
+
key: s.key,
|
|
1921
|
+
value: cfg[s.key]
|
|
1922
|
+
}));
|
|
1923
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
1924
|
+
for (const [k, v] of Object.entries(scope)) {
|
|
1925
|
+
entries.push({ scope: orgName, key: k, value: v });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return entries;
|
|
1929
|
+
}
|
|
1839
1930
|
function writeConfigList(cfg) {
|
|
1840
1931
|
const keyWidth = 10;
|
|
1841
1932
|
const valueWidth = 30;
|
|
1933
|
+
const scopeWidth = 12;
|
|
1934
|
+
process.stdout.write(
|
|
1935
|
+
`${"scope".padEnd(scopeWidth)}${"key".padEnd(keyWidth)}${"value".padEnd(valueWidth)}description
|
|
1936
|
+
`
|
|
1937
|
+
);
|
|
1842
1938
|
for (const setting of SETTINGS) {
|
|
1843
1939
|
const raw = cfg[setting.key];
|
|
1844
1940
|
const value = formatConfigValue(raw, "(not set)");
|
|
1845
1941
|
const marker = raw === void 0 && setting.required ? " *" : "";
|
|
1846
1942
|
process.stdout.write(
|
|
1847
|
-
`${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1943
|
+
`${"default".padEnd(scopeWidth)}${setting.key.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}${setting.description}${marker}
|
|
1848
1944
|
`
|
|
1849
1945
|
);
|
|
1850
1946
|
}
|
|
1947
|
+
for (const [orgName, scope] of Object.entries(cfg.organizations ?? {})) {
|
|
1948
|
+
const orgScope = scope;
|
|
1949
|
+
const scopedSettings = Object.entries(orgScope);
|
|
1950
|
+
for (const [k, v] of scopedSettings) {
|
|
1951
|
+
const value = formatConfigValue(v, "(not set)");
|
|
1952
|
+
process.stdout.write(
|
|
1953
|
+
`${orgName.padEnd(scopeWidth)}${k.padEnd(keyWidth)}${String(value).padEnd(valueWidth)}
|
|
1954
|
+
`
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1851
1958
|
const hasUnset = SETTINGS.some((s) => s.required && cfg[s.key] === void 0);
|
|
1852
1959
|
if (hasUnset) {
|
|
1853
1960
|
process.stdout.write(
|
|
@@ -1891,17 +1998,22 @@ function createConfigCommand() {
|
|
|
1891
1998
|
const config = new Command4("config");
|
|
1892
1999
|
config.description("Manage CLI settings");
|
|
1893
2000
|
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) => {
|
|
2001
|
+
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
2002
|
try {
|
|
1896
|
-
|
|
2003
|
+
if (options.org) {
|
|
2004
|
+
setOrgScopedValue(options.org, key, value);
|
|
2005
|
+
} else {
|
|
2006
|
+
setConfigValue(key, value);
|
|
2007
|
+
}
|
|
1897
2008
|
if (options.json) {
|
|
1898
|
-
const output = { key, value };
|
|
2009
|
+
const output = { key, value, scope: options.org ?? "default" };
|
|
1899
2010
|
if (key === "fields") {
|
|
1900
2011
|
output.value = value.split(",").map((s) => s.trim());
|
|
1901
2012
|
}
|
|
1902
2013
|
process.stdout.write(JSON.stringify(output) + "\n");
|
|
1903
2014
|
} else {
|
|
1904
|
-
|
|
2015
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2016
|
+
process.stdout.write(`Set "${key}" to "${value}"${scopeTag}
|
|
1905
2017
|
`);
|
|
1906
2018
|
}
|
|
1907
2019
|
} catch (err) {
|
|
@@ -1912,12 +2024,12 @@ function createConfigCommand() {
|
|
|
1912
2024
|
}
|
|
1913
2025
|
});
|
|
1914
2026
|
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) => {
|
|
2027
|
+
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
2028
|
try {
|
|
1917
|
-
const value = getConfigValue(key);
|
|
2029
|
+
const value = options.org ? getOrgScopedValue(options.org, key) : getConfigValue(key);
|
|
1918
2030
|
if (options.json) {
|
|
1919
2031
|
process.stdout.write(
|
|
1920
|
-
JSON.stringify({ key, value: value ?? null }) + "\n"
|
|
2032
|
+
JSON.stringify({ key, value: value ?? null, scope: options.org ?? "default" }) + "\n"
|
|
1921
2033
|
);
|
|
1922
2034
|
} else if (value === void 0) {
|
|
1923
2035
|
process.stdout.write(`Setting "${key}" is not configured.
|
|
@@ -1925,7 +2037,7 @@ function createConfigCommand() {
|
|
|
1925
2037
|
} else if (Array.isArray(value)) {
|
|
1926
2038
|
process.stdout.write(value.join(",") + "\n");
|
|
1927
2039
|
} else {
|
|
1928
|
-
process.stdout.write(value + "\n");
|
|
2040
|
+
process.stdout.write(String(value) + "\n");
|
|
1929
2041
|
}
|
|
1930
2042
|
} catch (err) {
|
|
1931
2043
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1938,19 +2050,25 @@ function createConfigCommand() {
|
|
|
1938
2050
|
list.description("List all configuration values").option("--json", "output in JSON format").action((options) => {
|
|
1939
2051
|
const cfg = loadConfig();
|
|
1940
2052
|
if (options.json) {
|
|
1941
|
-
|
|
2053
|
+
const entries = buildConfigListEntries(cfg);
|
|
2054
|
+
process.stdout.write(JSON.stringify(entries) + "\n");
|
|
1942
2055
|
return;
|
|
1943
2056
|
}
|
|
1944
2057
|
writeConfigList(cfg);
|
|
1945
2058
|
});
|
|
1946
2059
|
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) => {
|
|
2060
|
+
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
2061
|
try {
|
|
1949
|
-
|
|
2062
|
+
if (options.org) {
|
|
2063
|
+
unsetOrgScopedValue(options.org, key);
|
|
2064
|
+
} else {
|
|
2065
|
+
unsetConfigValue(key);
|
|
2066
|
+
}
|
|
1950
2067
|
if (options.json) {
|
|
1951
|
-
process.stdout.write(JSON.stringify({ key, unset: true }) + "\n");
|
|
2068
|
+
process.stdout.write(JSON.stringify({ key, unset: true, scope: options.org ?? "default" }) + "\n");
|
|
1952
2069
|
} else {
|
|
1953
|
-
|
|
2070
|
+
const scopeTag = options.org ? ` (org: ${options.org})` : "";
|
|
2071
|
+
process.stdout.write(`Unset "${key}"${scopeTag}
|
|
1954
2072
|
`);
|
|
1955
2073
|
}
|
|
1956
2074
|
} catch (err) {
|
|
@@ -1982,10 +2100,52 @@ function createConfigCommand() {
|
|
|
1982
2100
|
rl.close();
|
|
1983
2101
|
process.stderr.write("Configuration complete!\n");
|
|
1984
2102
|
});
|
|
2103
|
+
const orgCopy = new Command4("org-copy");
|
|
2104
|
+
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) => {
|
|
2105
|
+
try {
|
|
2106
|
+
copyOrgScope(from, to, options.force ?? false);
|
|
2107
|
+
process.stdout.write(`Copied scope "${from}" to "${to}"
|
|
2108
|
+
`);
|
|
2109
|
+
} catch (err) {
|
|
2110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2111
|
+
process.stderr.write(`Error: ${message}
|
|
2112
|
+
`);
|
|
2113
|
+
process.exit(1);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
const orgMove = new Command4("org-move");
|
|
2117
|
+
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) => {
|
|
2118
|
+
try {
|
|
2119
|
+
moveOrgScope(from, to, options.force ?? false);
|
|
2120
|
+
process.stdout.write(`Moved scope "${from}" to "${to}"
|
|
2121
|
+
`);
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2124
|
+
process.stderr.write(`Error: ${message}
|
|
2125
|
+
`);
|
|
2126
|
+
process.exit(1);
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
const orgDelete = new Command4("org-delete");
|
|
2130
|
+
orgDelete.description("Delete an org-scoped configuration").argument("<name>", "org name").action((name) => {
|
|
2131
|
+
try {
|
|
2132
|
+
deleteOrgScope(name);
|
|
2133
|
+
process.stdout.write(`Deleted scope "${name}"
|
|
2134
|
+
`);
|
|
2135
|
+
} catch (err) {
|
|
2136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2137
|
+
process.stderr.write(`Error: ${message}
|
|
2138
|
+
`);
|
|
2139
|
+
process.exit(1);
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
1985
2142
|
config.addCommand(set);
|
|
1986
2143
|
config.addCommand(get);
|
|
1987
2144
|
config.addCommand(list);
|
|
1988
2145
|
config.addCommand(unset);
|
|
2146
|
+
config.addCommand(orgCopy);
|
|
2147
|
+
config.addCommand(orgMove);
|
|
2148
|
+
config.addCommand(orgDelete);
|
|
1989
2149
|
config.addCommand(wizard);
|
|
1990
2150
|
return config;
|
|
1991
2151
|
}
|