@floh-solutions/pharos-cli 0.25.0 → 0.26.0
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/dist/adopt-type.d.ts +58 -0
- package/dist/adopt-type.d.ts.map +1 -0
- package/dist/adopt-type.js +61 -0
- package/dist/adopt-type.js.map +1 -0
- package/dist/bridge.d.ts +156 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +157 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +47 -24
- package/dist/cli.js.map +1 -1
- package/dist/commands/bridge.d.ts +30 -0
- package/dist/commands/bridge.d.ts.map +1 -0
- package/dist/commands/bridge.js +255 -0
- package/dist/commands/bridge.js.map +1 -0
- package/dist/commands/doctor.d.ts +113 -4
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +152 -18
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/issue.d.ts +98 -0
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +6 -102
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/setup.d.ts +45 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +74 -0
- package/dist/commands/setup.js.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +8 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { GitHubIssue } from "@floh-solutions/gh-core";
|
|
2
|
+
import { type CategorySource } from "./bridge.js";
|
|
3
|
+
import type { Session } from "./session.js";
|
|
4
|
+
/**
|
|
5
|
+
* Which work item type an adopted issue becomes — **resolved, never named**
|
|
6
|
+
* (#872).
|
|
7
|
+
*
|
|
8
|
+
* ## The bug this replaces
|
|
9
|
+
*
|
|
10
|
+
* `issue.ts` carried `const DEFAULT_WORK_ITEM_TYPE = "Issue"` and used it in two
|
|
11
|
+
* places. That is correct on *this* project by accident: Basic's requirement
|
|
12
|
+
* type happens to be called `Issue`. On **Agile** the word means an
|
|
13
|
+
* *impediment* — a blocker, tracked off the backlog — and the requirement type
|
|
14
|
+
* is `User Story`. So every GitHub issue adopted onto an Agile board was filed
|
|
15
|
+
* as a blocker, silently, and the CLI shipped that way in 0.25.0.
|
|
16
|
+
*
|
|
17
|
+
* A type name is a property of the project's **process template**. It can never
|
|
18
|
+
* be written down here.
|
|
19
|
+
*
|
|
20
|
+
* ## How it is decided
|
|
21
|
+
*
|
|
22
|
+
* 1. `--type` wins outright. Somebody naming a type has looked at their own
|
|
23
|
+
* board, and this must not argue with them.
|
|
24
|
+
* 2. Otherwise the issue's **labels** choose a category — a saved mapping by
|
|
25
|
+
* label id first, then the name defaults for a repository nobody has
|
|
26
|
+
* configured. See `bridge.ts`.
|
|
27
|
+
* 3. The category is resolved against **this project's** own answer, from
|
|
28
|
+
* `wit/workitemtypecategories`. The lookups are ordered because they
|
|
29
|
+
* degrade: a feature request asks for `FeatureCategory` and settles for
|
|
30
|
+
* `RequirementCategory`, which is all Basic has.
|
|
31
|
+
* 4. If none of that resolves — an unreachable board, a project with no
|
|
32
|
+
* requirement category at all — it falls back to the old constant **and says
|
|
33
|
+
* so**, because refusing to adopt over a naming question would be worse than
|
|
34
|
+
* adopting into a type somebody can change.
|
|
35
|
+
*
|
|
36
|
+
* Every step is reported, because "this became a Bug because you mapped it" and
|
|
37
|
+
* "this became a Bug because nothing matched" are different facts and only one
|
|
38
|
+
* of them is a decision.
|
|
39
|
+
*/
|
|
40
|
+
export interface ResolvedType {
|
|
41
|
+
/** What to create. */
|
|
42
|
+
type: string;
|
|
43
|
+
/** How it was arrived at, for the report. */
|
|
44
|
+
source: CategorySource | "flag" | "fallback";
|
|
45
|
+
/** The category that was asked for, when one was. */
|
|
46
|
+
category?: string;
|
|
47
|
+
/** The label that decided it, when one did. */
|
|
48
|
+
label?: string;
|
|
49
|
+
/** Set when the project could not answer and the constant was used. */
|
|
50
|
+
note?: string;
|
|
51
|
+
}
|
|
52
|
+
/** The last-resort name. Basic's requirement type, and the historical default. */
|
|
53
|
+
export declare const FALLBACK_WORK_ITEM_TYPE = "Issue";
|
|
54
|
+
export declare function resolveWorkItemType(session: Session, issue: Pick<GitHubIssue, "repo" | "labels">, options?: {
|
|
55
|
+
type?: string | undefined;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
}): Promise<ResolvedType>;
|
|
58
|
+
//# sourceMappingURL=adopt-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adopt-type.d.ts","sourceRoot":"","sources":["../src/adopt-type.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,WAAW,YAAY;IAC3B,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,MAAM,EAAE,cAAc,GAAG,MAAM,GAAG,UAAU,CAAC;IAC7C,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,UAAU,CAAC;AAE/C,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,QAAQ,CAAC,EAC3C,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;CAAO,GACnE,OAAO,CAAC,YAAY,CAAC,CAkDvB"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { WorkItemTypesApi, typeForCategory } from "@floh-solutions/ado-core";
|
|
2
|
+
import { categoryFor, defaultBridgePath, parseBridgeConfig, repoConfig, } from "./bridge.js";
|
|
3
|
+
/** The last-resort name. Basic's requirement type, and the historical default. */
|
|
4
|
+
export const FALLBACK_WORK_ITEM_TYPE = "Issue";
|
|
5
|
+
export async function resolveWorkItemType(session, issue, options = {}) {
|
|
6
|
+
if (options.type !== undefined && options.type !== "") {
|
|
7
|
+
return { type: options.type, source: "flag" };
|
|
8
|
+
}
|
|
9
|
+
const env = options.env ?? process.env;
|
|
10
|
+
const organization = session.client.organization;
|
|
11
|
+
const project = session.client.project ?? "";
|
|
12
|
+
const config = await readBridge(env);
|
|
13
|
+
const repo = repoConfig(config, organization, project, issue.repo);
|
|
14
|
+
const choice = categoryFor(issue.labels.map((label) => ({ id: label.id ?? -1, name: label.name })), repo);
|
|
15
|
+
let catalog;
|
|
16
|
+
try {
|
|
17
|
+
catalog = await new WorkItemTypesApi(session.client.http).categories();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// The board could not be asked. Adopting into the historical default is
|
|
21
|
+
// better than refusing, and naming the reason is what stops it reading as a
|
|
22
|
+
// deliberate choice.
|
|
23
|
+
return {
|
|
24
|
+
type: FALLBACK_WORK_ITEM_TYPE,
|
|
25
|
+
source: "fallback",
|
|
26
|
+
note: `the project's work item types could not be read (${error.message}), so the `
|
|
27
|
+
+ `default "${FALLBACK_WORK_ITEM_TYPE}" was used — pass --type to be explicit`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const resolved = typeForCategory(catalog, ...choice.categories);
|
|
31
|
+
if (resolved === undefined) {
|
|
32
|
+
return {
|
|
33
|
+
type: FALLBACK_WORK_ITEM_TYPE,
|
|
34
|
+
source: "fallback",
|
|
35
|
+
...(choice.label === undefined ? {} : { label: choice.label }),
|
|
36
|
+
note: `this project has none of ${choice.categories.join(", ")}, so the default `
|
|
37
|
+
+ `"${FALLBACK_WORK_ITEM_TYPE}" was used — pass --type to be explicit`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
type: resolved,
|
|
42
|
+
source: choice.source,
|
|
43
|
+
...(choice.categories[0] === undefined ? {} : { category: choice.categories[0] }),
|
|
44
|
+
...(choice.label === undefined ? {} : { label: choice.label }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** The mapping, or an empty one. A missing or malformed file is not an error. */
|
|
48
|
+
async function readBridge(env) {
|
|
49
|
+
const path = defaultBridgePath(env);
|
|
50
|
+
try {
|
|
51
|
+
const { readFile } = await import("node:fs/promises");
|
|
52
|
+
return parseBridgeConfig(await readFile(path, "utf8"), path);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// No file yet is the ordinary state, and a malformed one must not take down
|
|
56
|
+
// `adopt` — it degrades to the name defaults, which is what an
|
|
57
|
+
// unconfigured repository gets anyway.
|
|
58
|
+
return { version: 1, scopes: {} };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=adopt-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adopt-type.js","sourceRoot":"","sources":["../src/adopt-type.ts"],"names":[],"mappings":"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"}
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `~/.config/pharos/bridge.json` — **what an issue becomes when somebody adopts
|
|
3
|
+
* it**, and the one file the CLI and the app both read (#861).
|
|
4
|
+
*
|
|
5
|
+
* ## Why it is a file rather than app state
|
|
6
|
+
*
|
|
7
|
+
* The same rule `pharos-convert` and `doctor` follow, and the one AGENTS.md
|
|
8
|
+
* keeps restating: if the app owns the mapping and the CLI guesses, then
|
|
9
|
+
* `pharos issue adopt` in a headless session files to a different work item type
|
|
10
|
+
* than the app does — and **the one that drifts is the one nobody is watching**.
|
|
11
|
+
* So the CLI owns the file, the CLI has verbs over it, and the app's settings
|
|
12
|
+
* page is a GUI over those verbs rather than a second implementation.
|
|
13
|
+
*
|
|
14
|
+
* ## Why it is keyed by scope on day one
|
|
15
|
+
*
|
|
16
|
+
* The UI only ever shows one organisation and project today. The key is there
|
|
17
|
+
* anyway because this is the single artefact multi-scope would otherwise force a
|
|
18
|
+
* rewrite of — see `docs/pharos-multi-scope-plan.md`. It costs one nesting level
|
|
19
|
+
* now and saves throwing the file away later.
|
|
20
|
+
*
|
|
21
|
+
* ## Why labels are keyed by ID
|
|
22
|
+
*
|
|
23
|
+
* **Probed on a live repository:** creating a label, renaming it and reading it
|
|
24
|
+
* back leaves `id` and `node_id` unchanged. So an id-keyed mapping survives a
|
|
25
|
+
* rename, which is the thing people actually do to labels.
|
|
26
|
+
*
|
|
27
|
+
* Two consequences worth stating rather than discovering:
|
|
28
|
+
*
|
|
29
|
+
* - **Ids are per repository.** `bug` is `11710323621` on one repo and
|
|
30
|
+
* `1593484620` on another. A mapping is therefore per repo by construction,
|
|
31
|
+
* not a global preference.
|
|
32
|
+
* - **Delete-and-recreate yields a NEW id**, so a mapping whose id no longer
|
|
33
|
+
* resolves goes visibly *broken* rather than silently re-binding to a
|
|
34
|
+
* different label that happens to share a name. That is the correct
|
|
35
|
+
* behaviour: the old label is gone, and what replaced it is somebody else's
|
|
36
|
+
* decision to confirm.
|
|
37
|
+
*
|
|
38
|
+
* `name` is stored beside the id for display and for diagnosis. It is never what
|
|
39
|
+
* a lookup matches on.
|
|
40
|
+
*/
|
|
41
|
+
export interface BridgeConfig {
|
|
42
|
+
version: 1;
|
|
43
|
+
scopes: Record<string, ScopeConfig>;
|
|
44
|
+
}
|
|
45
|
+
export interface ScopeConfig {
|
|
46
|
+
repos: Record<string, RepoBridgeConfig>;
|
|
47
|
+
digest: DigestPolicy;
|
|
48
|
+
}
|
|
49
|
+
export interface RepoBridgeConfig {
|
|
50
|
+
/** Label **id** (as a string key) → what adopting an issue with it produces. */
|
|
51
|
+
labels: Record<string, LabelMapping>;
|
|
52
|
+
/** Where an issue with no mapped label goes. */
|
|
53
|
+
defaultCategory: string;
|
|
54
|
+
}
|
|
55
|
+
export interface LabelMapping {
|
|
56
|
+
/** Display only, and deliberately not what anything matches on. */
|
|
57
|
+
name: string;
|
|
58
|
+
/** An Azure DevOps work item type CATEGORY, never a type name. */
|
|
59
|
+
category: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* When a digest is written back to the work item.
|
|
63
|
+
*
|
|
64
|
+
* **Two events, not every comment.** Adopt and close are decisions; a comment is
|
|
65
|
+
* chatter. Mirroring chatter puts noise on a client's board and re-opens the
|
|
66
|
+
* echo problem the hub already paid for — while never writing turns the work
|
|
67
|
+
* item into a dead link, which is worse. See the wave spec.
|
|
68
|
+
*/
|
|
69
|
+
export interface DigestPolicy {
|
|
70
|
+
onAdopt: boolean;
|
|
71
|
+
onClose: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare const DEFAULT_DIGEST: DigestPolicy;
|
|
74
|
+
/** Azure DevOps' category reference names, as used here. */
|
|
75
|
+
export declare const CATEGORY: {
|
|
76
|
+
readonly bug: "Microsoft.BugCategory";
|
|
77
|
+
readonly requirement: "Microsoft.RequirementCategory";
|
|
78
|
+
readonly feature: "Microsoft.FeatureCategory";
|
|
79
|
+
readonly epic: "Microsoft.EpicCategory";
|
|
80
|
+
readonly task: "Microsoft.TaskCategory";
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* What a repository nobody has configured does.
|
|
84
|
+
*
|
|
85
|
+
* **Matched by NAME, and that is the one place a name is allowed to matter.** A
|
|
86
|
+
* repo you have never opened the settings for should still adopt sensibly, and
|
|
87
|
+
* the only thing available before somebody maps anything is what the labels are
|
|
88
|
+
* called. The moment a mapping is saved it pins to the id and renames stop
|
|
89
|
+
* mattering — *id for bindings you made, name for defaults you did not*.
|
|
90
|
+
*
|
|
91
|
+
* Ordered, because the lookups degrade: a feature request wants
|
|
92
|
+
* `FeatureCategory` and settles for `RequirementCategory`, which is all Basic
|
|
93
|
+
* has.
|
|
94
|
+
*/
|
|
95
|
+
export declare const NAME_DEFAULTS: readonly {
|
|
96
|
+
readonly match: readonly string[];
|
|
97
|
+
readonly categories: readonly string[];
|
|
98
|
+
}[];
|
|
99
|
+
/** Where the file lives. `PHAROS_BRIDGE_FILE` overrides it, for tests. */
|
|
100
|
+
export declare function defaultBridgePath(env?: NodeJS.ProcessEnv): string;
|
|
101
|
+
/** `org/project`, lowercased — the scope key. */
|
|
102
|
+
export declare function scopeKey(org: string, project: string): string;
|
|
103
|
+
export declare function emptyBridgeConfig(): BridgeConfig;
|
|
104
|
+
/**
|
|
105
|
+
* Read a config, or refuse with a message naming the file.
|
|
106
|
+
*
|
|
107
|
+
* **A file it cannot parse is an answer, not a crash** — the same rule
|
|
108
|
+
* `doctor`'s repos.json handling follows. A malformed mapping means adoption
|
|
109
|
+
* falls back to the name defaults and says so, rather than taking the CLI down
|
|
110
|
+
* on an unrelated verb.
|
|
111
|
+
*/
|
|
112
|
+
export declare function parseBridgeConfig(text: string, source?: string): BridgeConfig;
|
|
113
|
+
export declare function formatBridgeConfig(config: BridgeConfig): string;
|
|
114
|
+
/** This repository's mapping in this scope, or the empty one. */
|
|
115
|
+
export declare function repoConfig(config: BridgeConfig, org: string, project: string, repo: string): RepoBridgeConfig;
|
|
116
|
+
export declare function digestPolicy(config: BridgeConfig, org: string, project: string): DigestPolicy;
|
|
117
|
+
/** One label as GitHub reports it. */
|
|
118
|
+
export interface IssueLabel {
|
|
119
|
+
id: number;
|
|
120
|
+
name: string;
|
|
121
|
+
}
|
|
122
|
+
/** How a category was arrived at — reported, because a silent default misleads. */
|
|
123
|
+
export type CategorySource = "mapping" | "name-default" | "repo-default";
|
|
124
|
+
export interface CategoryChoice {
|
|
125
|
+
/** Ordered: try each in turn, because a project may not have the first. */
|
|
126
|
+
categories: readonly string[];
|
|
127
|
+
source: CategorySource;
|
|
128
|
+
/** Which label decided it, when one did. */
|
|
129
|
+
label?: string;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Which category an issue's labels ask for.
|
|
133
|
+
*
|
|
134
|
+
* **Saved mappings win, by id.** Then the name defaults, for a repository nobody
|
|
135
|
+
* has configured. Then the repository's own default.
|
|
136
|
+
*
|
|
137
|
+
* The `source` comes back with it on purpose: "this went to a requirement
|
|
138
|
+
* because you mapped `bug`" and "this went to a requirement because nothing
|
|
139
|
+
* matched" are different facts, and a picker that showed the same preselection
|
|
140
|
+
* for both would be hiding the second one.
|
|
141
|
+
*/
|
|
142
|
+
export declare function categoryFor(labels: readonly IssueLabel[], repo: RepoBridgeConfig): CategoryChoice;
|
|
143
|
+
/**
|
|
144
|
+
* A saved mapping whose label no longer exists in the repository.
|
|
145
|
+
*
|
|
146
|
+
* Reported rather than pruned. The mapping is a decision somebody made, and a
|
|
147
|
+
* label that has gone is usually a rename they did not realise breaks it — or a
|
|
148
|
+
* delete-and-recreate, which is a genuinely different label. Either way the
|
|
149
|
+
* answer is to show it, not to guess.
|
|
150
|
+
*/
|
|
151
|
+
export declare function brokenMappings(repo: RepoBridgeConfig, live: readonly IssueLabel[]): {
|
|
152
|
+
id: string;
|
|
153
|
+
name: string;
|
|
154
|
+
category: string;
|
|
155
|
+
}[];
|
|
156
|
+
//# sourceMappingURL=bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IACxC,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACrC,gDAAgD;IAChD,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,cAAc,EAAE,YAA+C,CAAC;AAE7E,4DAA4D;AAC5D,eAAO,MAAM,QAAQ;;;;;;CAMX,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,aAAa,EAAE,SAAS;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAMjH,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAK9E;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,iBAAiB,IAAI,YAAY,CAEhD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAgB,GAAG,YAAY,CA+CpF;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAE/D;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,gBAAgB,CAQlB;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAE7F;AAED,sCAAsC;AACtC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,MAAM,EAAE,cAAc,CAAC;IACvB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,IAAI,EAAE,gBAAgB,GACrB,cAAc,CAiBhB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,SAAS,UAAU,EAAE,GAC1B;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAAE,CAKlD"}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export const DEFAULT_DIGEST = { onAdopt: true, onClose: true };
|
|
4
|
+
/** Azure DevOps' category reference names, as used here. */
|
|
5
|
+
export const CATEGORY = {
|
|
6
|
+
bug: "Microsoft.BugCategory",
|
|
7
|
+
requirement: "Microsoft.RequirementCategory",
|
|
8
|
+
feature: "Microsoft.FeatureCategory",
|
|
9
|
+
epic: "Microsoft.EpicCategory",
|
|
10
|
+
task: "Microsoft.TaskCategory",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* What a repository nobody has configured does.
|
|
14
|
+
*
|
|
15
|
+
* **Matched by NAME, and that is the one place a name is allowed to matter.** A
|
|
16
|
+
* repo you have never opened the settings for should still adopt sensibly, and
|
|
17
|
+
* the only thing available before somebody maps anything is what the labels are
|
|
18
|
+
* called. The moment a mapping is saved it pins to the id and renames stop
|
|
19
|
+
* mattering — *id for bindings you made, name for defaults you did not*.
|
|
20
|
+
*
|
|
21
|
+
* Ordered, because the lookups degrade: a feature request wants
|
|
22
|
+
* `FeatureCategory` and settles for `RequirementCategory`, which is all Basic
|
|
23
|
+
* has.
|
|
24
|
+
*/
|
|
25
|
+
export const NAME_DEFAULTS = [
|
|
26
|
+
{ match: ["bug", "defect", "regression"], categories: [CATEGORY.bug] },
|
|
27
|
+
{
|
|
28
|
+
match: ["enhancement", "feature", "feature request"],
|
|
29
|
+
categories: [CATEGORY.feature, CATEGORY.requirement],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
/** Where the file lives. `PHAROS_BRIDGE_FILE` overrides it, for tests. */
|
|
33
|
+
export function defaultBridgePath(env = process.env) {
|
|
34
|
+
const override = env.PHAROS_BRIDGE_FILE;
|
|
35
|
+
if (override !== undefined && override !== "")
|
|
36
|
+
return override;
|
|
37
|
+
const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
38
|
+
return join(base, "pharos", "bridge.json");
|
|
39
|
+
}
|
|
40
|
+
/** `org/project`, lowercased — the scope key. */
|
|
41
|
+
export function scopeKey(org, project) {
|
|
42
|
+
return `${org.trim().toLowerCase()}/${project.trim().toLowerCase()}`;
|
|
43
|
+
}
|
|
44
|
+
export function emptyBridgeConfig() {
|
|
45
|
+
return { version: 1, scopes: {} };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a config, or refuse with a message naming the file.
|
|
49
|
+
*
|
|
50
|
+
* **A file it cannot parse is an answer, not a crash** — the same rule
|
|
51
|
+
* `doctor`'s repos.json handling follows. A malformed mapping means adoption
|
|
52
|
+
* falls back to the name defaults and says so, rather than taking the CLI down
|
|
53
|
+
* on an unrelated verb.
|
|
54
|
+
*/
|
|
55
|
+
export function parseBridgeConfig(text, source = "bridge.json") {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = JSON.parse(text);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new Error(`${source} is not valid JSON: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof raw !== "object" || raw === null)
|
|
64
|
+
throw new Error(`${source} is not an object.`);
|
|
65
|
+
const record = raw;
|
|
66
|
+
if (record.version !== 1) {
|
|
67
|
+
throw new Error(`${source} has version ${String(record.version)}; this build understands 1.`);
|
|
68
|
+
}
|
|
69
|
+
const scopes = {};
|
|
70
|
+
const rawScopes = (record.scopes ?? {});
|
|
71
|
+
for (const [key, value] of Object.entries(rawScopes)) {
|
|
72
|
+
const scope = (value ?? {});
|
|
73
|
+
const repos = {};
|
|
74
|
+
for (const [repo, rawRepo] of Object.entries((scope.repos ?? {}))) {
|
|
75
|
+
const entry = (rawRepo ?? {});
|
|
76
|
+
const labels = {};
|
|
77
|
+
for (const [id, rawLabel] of Object.entries((entry.labels ?? {}))) {
|
|
78
|
+
const label = (rawLabel ?? {});
|
|
79
|
+
if (typeof label.category !== "string")
|
|
80
|
+
continue;
|
|
81
|
+
labels[id] = {
|
|
82
|
+
name: typeof label.name === "string" ? label.name : "",
|
|
83
|
+
category: label.category,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
repos[repo.toLowerCase()] = {
|
|
87
|
+
labels,
|
|
88
|
+
defaultCategory: typeof entry.defaultCategory === "string" ? entry.defaultCategory : CATEGORY.requirement,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const digest = (scope.digest ?? {});
|
|
92
|
+
scopes[key.toLowerCase()] = {
|
|
93
|
+
repos,
|
|
94
|
+
digest: {
|
|
95
|
+
onAdopt: digest.onAdopt !== false,
|
|
96
|
+
onClose: digest.onClose !== false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { version: 1, scopes };
|
|
101
|
+
}
|
|
102
|
+
export function formatBridgeConfig(config) {
|
|
103
|
+
return `${JSON.stringify(config, null, 2)}\n`;
|
|
104
|
+
}
|
|
105
|
+
/** This repository's mapping in this scope, or the empty one. */
|
|
106
|
+
export function repoConfig(config, org, project, repo) {
|
|
107
|
+
const scope = config.scopes[scopeKey(org, project)];
|
|
108
|
+
return (scope?.repos[repo.trim().toLowerCase()] ?? {
|
|
109
|
+
labels: {},
|
|
110
|
+
defaultCategory: CATEGORY.requirement,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export function digestPolicy(config, org, project) {
|
|
114
|
+
return config.scopes[scopeKey(org, project)]?.digest ?? DEFAULT_DIGEST;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Which category an issue's labels ask for.
|
|
118
|
+
*
|
|
119
|
+
* **Saved mappings win, by id.** Then the name defaults, for a repository nobody
|
|
120
|
+
* has configured. Then the repository's own default.
|
|
121
|
+
*
|
|
122
|
+
* The `source` comes back with it on purpose: "this went to a requirement
|
|
123
|
+
* because you mapped `bug`" and "this went to a requirement because nothing
|
|
124
|
+
* matched" are different facts, and a picker that showed the same preselection
|
|
125
|
+
* for both would be hiding the second one.
|
|
126
|
+
*/
|
|
127
|
+
export function categoryFor(labels, repo) {
|
|
128
|
+
for (const label of labels) {
|
|
129
|
+
const mapped = repo.labels[String(label.id)];
|
|
130
|
+
if (mapped !== undefined) {
|
|
131
|
+
return { categories: [mapped.category], source: "mapping", label: label.name };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const label of labels) {
|
|
135
|
+
const lowered = label.name.trim().toLowerCase();
|
|
136
|
+
const rule = NAME_DEFAULTS.find((candidate) => candidate.match.includes(lowered));
|
|
137
|
+
if (rule !== undefined) {
|
|
138
|
+
return { categories: rule.categories, source: "name-default", label: label.name };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { categories: [repo.defaultCategory], source: "repo-default" };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* A saved mapping whose label no longer exists in the repository.
|
|
145
|
+
*
|
|
146
|
+
* Reported rather than pruned. The mapping is a decision somebody made, and a
|
|
147
|
+
* label that has gone is usually a rename they did not realise breaks it — or a
|
|
148
|
+
* delete-and-recreate, which is a genuinely different label. Either way the
|
|
149
|
+
* answer is to show it, not to guess.
|
|
150
|
+
*/
|
|
151
|
+
export function brokenMappings(repo, live) {
|
|
152
|
+
const known = new Set(live.map((label) => String(label.id)));
|
|
153
|
+
return Object.entries(repo.labels)
|
|
154
|
+
.filter(([id]) => !known.has(id))
|
|
155
|
+
.map(([id, mapping]) => ({ id, name: mapping.name, category: mapping.category }));
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type ExitCode } from "./output.js";
|
|
|
12
12
|
* expensive kind of wrong. Anything added to the switch belongs here, and the
|
|
13
13
|
* test below fails if it does not.
|
|
14
14
|
*/
|
|
15
|
-
export declare const COMMANDS: readonly ["setup", "whoami", "query", "task", "types", "history", "iterations", "areas", "links", "fields", "create", "update", "link", "unlink", "delete", "restore", "deleted", "attach", "detach", "download", "image", "people", "wiki", "comment", "hooks", "plan", "doctor", "issue"];
|
|
15
|
+
export declare const COMMANDS: readonly ["setup", "whoami", "query", "task", "types", "history", "iterations", "areas", "links", "fields", "create", "update", "link", "unlink", "delete", "restore", "deleted", "attach", "detach", "download", "image", "people", "wiki", "comment", "hooks", "plan", "doctor", "issue", "bridge"];
|
|
16
16
|
export interface RunOptions {
|
|
17
17
|
argv: string[];
|
|
18
18
|
cwd: string;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AA+B5D,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,aAAa,CAAC;AAyBrB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,uSAMX,CAAC;AA8gBX,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,gDAAgD;IAChD,SAAS,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAChD,+EAA+E;IAC/E,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,uDAAuD;IACvD,MAAM,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;CACnC;AAED,wBAAsB,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAOhE"}
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runClassification, runFields, runHistory, runLinkTypes, } from "./comma
|
|
|
9
9
|
import { runComment } from "./commands/comment.js";
|
|
10
10
|
import { runCreate } from "./commands/create.js";
|
|
11
11
|
import { runDetach } from "./commands/detach.js";
|
|
12
|
+
import { runBridge } from "./commands/bridge.js";
|
|
12
13
|
import { runDoctor } from "./commands/doctor.js";
|
|
13
14
|
import { runDownload, runTypes, runWhoami } from "./commands/discover.js";
|
|
14
15
|
import { runImage, runWikiImage } from "./commands/image.js";
|
|
@@ -61,7 +62,7 @@ export const COMMANDS = [
|
|
|
61
62
|
"history", "iterations", "areas", "links", "fields",
|
|
62
63
|
"create", "update", "link", "unlink", "delete", "restore", "deleted",
|
|
63
64
|
"attach", "detach", "download", "image", "people",
|
|
64
|
-
"wiki", "comment", "hooks", "plan", "doctor", "issue",
|
|
65
|
+
"wiki", "comment", "hooks", "plan", "doctor", "issue", "bridge",
|
|
65
66
|
];
|
|
66
67
|
/** Where long text comes from, for the verbs that take some. */
|
|
67
68
|
const CONTENT_OPTIONS = {
|
|
@@ -735,31 +736,44 @@ async function dispatch(io, options) {
|
|
|
735
736
|
"if-body": { type: "string" },
|
|
736
737
|
"if-body-file": { type: "string" },
|
|
737
738
|
}
|
|
738
|
-
: command === "
|
|
739
|
-
? {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
739
|
+
: command === "bridge"
|
|
740
|
+
? {
|
|
741
|
+
repo: { type: "string" },
|
|
742
|
+
// Singular and NOT repeatable, unlike `issue`'s `--label`: this one
|
|
743
|
+
// names exactly one mapping to write or remove.
|
|
744
|
+
label: { type: "string" },
|
|
745
|
+
category: { type: "string" },
|
|
746
|
+
"on-adopt": { type: "boolean" },
|
|
747
|
+
"on-close": { type: "boolean" },
|
|
748
|
+
// The only way to say "no". A boolean flag cannot express false, so
|
|
749
|
+
// without this the digest policy was write-only.
|
|
750
|
+
manual: { type: "boolean" },
|
|
751
|
+
}
|
|
752
|
+
: command === "deleted"
|
|
753
|
+
? { top: { type: "string" } }
|
|
754
|
+
: command === "history"
|
|
755
|
+
? { top: { type: "string" }, all: { type: "boolean" } }
|
|
756
|
+
: command === "iterations" || command === "areas"
|
|
757
|
+
? { depth: { type: "string" } }
|
|
758
|
+
: command === "fields"
|
|
759
|
+
? { type: { type: "string" }, constrained: { type: "boolean" } }
|
|
760
|
+
: command === "hooks"
|
|
753
761
|
? {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
convert: { type: "boolean" },
|
|
758
|
-
install: { type: "string" },
|
|
759
|
-
repo: { type: "string" },
|
|
760
|
-
"gh-account": { type: "string" },
|
|
762
|
+
hub: { type: "string" },
|
|
763
|
+
user: { type: "string" },
|
|
764
|
+
password: { type: "string" },
|
|
761
765
|
}
|
|
762
|
-
:
|
|
766
|
+
: command === "setup"
|
|
767
|
+
? {
|
|
768
|
+
print: { type: "boolean" },
|
|
769
|
+
stdin: { type: "boolean" },
|
|
770
|
+
skill: { type: "boolean" },
|
|
771
|
+
convert: { type: "boolean" },
|
|
772
|
+
install: { type: "string" },
|
|
773
|
+
repo: { type: "string" },
|
|
774
|
+
"gh-account": { type: "string" },
|
|
775
|
+
}
|
|
776
|
+
: CONTENT_OPTIONS;
|
|
763
777
|
const { values, positionals } = parse(options.argv, { ...GLOBAL_OPTIONS, ...extras });
|
|
764
778
|
if (values["version"] === true)
|
|
765
779
|
return emitText(io, await version(options.cwd));
|
|
@@ -956,6 +970,15 @@ async function dispatch(io, options) {
|
|
|
956
970
|
return runComment(io, session, rest, {
|
|
957
971
|
text: await content(options, values),
|
|
958
972
|
});
|
|
973
|
+
case "bridge":
|
|
974
|
+
return runBridge(io, session, rest, {
|
|
975
|
+
repo: asString(values["repo"]),
|
|
976
|
+
label: asString(values["label"]),
|
|
977
|
+
category: asString(values["category"]),
|
|
978
|
+
onAdopt: values["on-adopt"] === undefined ? undefined : values["on-adopt"] === true,
|
|
979
|
+
onClose: values["on-close"] === undefined ? undefined : values["on-close"] === true,
|
|
980
|
+
manual: values["manual"] === true ? true : undefined,
|
|
981
|
+
});
|
|
959
982
|
case "issue":
|
|
960
983
|
return runIssue(io, session, rest, {
|
|
961
984
|
type: asString(values["type"]),
|