@floh-solutions/pharos-cli 0.24.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/README.md +97 -10
- 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 +176 -31
- 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/comment-github.d.ts +138 -0
- package/dist/commands/comment-github.d.ts.map +1 -0
- package/dist/commands/comment-github.js +427 -0
- package/dist/commands/comment-github.js.map +1 -0
- package/dist/commands/comment.d.ts.map +1 -1
- package/dist/commands/comment.js +114 -19
- package/dist/commands/comment.js.map +1 -1
- package/dist/commands/doctor.d.ts +169 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +241 -44
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/issue.d.ts +133 -0
- package/dist/commands/issue.d.ts.map +1 -1
- package/dist/commands/issue.js +292 -110
- 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/dist/half-link.d.ts +14 -4
- package/dist/half-link.d.ts.map +1 -1
- package/dist/half-link.js +14 -4
- package/dist/half-link.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +123 -1
- package/dist/output.js.map +1 -1
- package/dist/session.d.ts +51 -2
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +62 -11
- package/dist/session.js.map +1 -1
- package/dist/target.d.ts +40 -4
- package/dist/target.d.ts.map +1 -1
- package/dist/target.js +14 -3
- package/dist/target.js.map +1 -1
- package/package.json +4 -4
- package/skill/SKILL.md +167 -7
package/README.md
CHANGED
|
@@ -116,16 +116,18 @@ capability that must be impossible has to be withheld at the token.
|
|
|
116
116
|
task <id> the whole context, in one call
|
|
117
117
|
wiki list | tree | read | write | delete
|
|
118
118
|
comment list | add | edit | delete | react | unreact | reactors
|
|
119
|
+
comment hide | unhide | pin | unpin GitHub only
|
|
119
120
|
issue adopt | link | say | trail | drift | backfill | close
|
|
120
121
|
a GitHub issue and a work item, joined —
|
|
121
122
|
and kept honest
|
|
122
123
|
plan <file> an implementation plan becomes a work item tree
|
|
123
124
|
```
|
|
124
125
|
|
|
125
|
-
`comment` takes
|
|
126
|
-
page is an **absolute path** (`/Plans/Foo`)
|
|
127
|
-
|
|
128
|
-
them apart is also the only
|
|
126
|
+
`comment` takes any of three targets: a work item is its number (`210`), a wiki
|
|
127
|
+
page is an **absolute path** (`/Plans/Foo`), a GitHub issue is
|
|
128
|
+
`owner/name#45`. The leading slash is required by the wiki API itself — it
|
|
129
|
+
answers 404 without one — so the spelling that tells them apart is also the only
|
|
130
|
+
spelling that works, and the three grammars cannot collide.
|
|
129
131
|
|
|
130
132
|
Long text does not belong on a command line:
|
|
131
133
|
|
|
@@ -134,6 +136,54 @@ pharos comment add 210 --file review.md
|
|
|
134
136
|
pharos wiki write /Plans/Sprint-4 --stdin < plan.md
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
### `comment` on a GitHub issue — the surface `gh` half has and half has not
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pharos comment list contoso/widgets#45 # ids, state, and what you MAY do
|
|
143
|
+
pharos comment edit contoso/widgets#45 900 --file fixed.md
|
|
144
|
+
pharos comment delete contoso/widgets#45 900 --yes
|
|
145
|
+
pharos comment react contoso/widgets#45 900 thumbs-up
|
|
146
|
+
pharos comment hide contoso/widgets#45 900 off-topic
|
|
147
|
+
pharos comment pin contoso/widgets#45 900
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This is the one place `pharos` reaches past the join, and it is two arguments
|
|
151
|
+
rather than one. **Editing or deleting a comment can destroy the link record**,
|
|
152
|
+
which nothing outside this repo knows to protect. And **hide and pin have no
|
|
153
|
+
`gh` verb at all** — they are GraphQL mutations with no REST route, so they pass
|
|
154
|
+
the same "nothing else can do this" test the `issue` verbs pass. Posting a *new*
|
|
155
|
+
comment is still `gh issue comment` or `pharos issue say`, and `comment add` on
|
|
156
|
+
a GitHub issue is refused with both of those named.
|
|
157
|
+
|
|
158
|
+
**`list` is the load-bearing verb**, and it is not one the macOS app needed: the
|
|
159
|
+
app has a window, an agent has this. Before it there was no way to learn a
|
|
160
|
+
GitHub comment id from this CLI at all, so nothing else here was addressable. It
|
|
161
|
+
prints both ids per comment — the REST number that edit, delete and reactions
|
|
162
|
+
address, and the node id that hide and pin take, because neither is derivable
|
|
163
|
+
from the other — plus a `may` object read from GitHub's own `viewerCan…` fields.
|
|
164
|
+
For a window those grey out a menu item; for an agent they turn a write that
|
|
165
|
+
would 403 into a fact known beforehand.
|
|
166
|
+
|
|
167
|
+
**An edit cannot double or strip the `pharos:v1` trailer.** Send prose with no
|
|
168
|
+
trailer and the existing one is restored, with `markerPreserved: true` saying
|
|
169
|
+
so; send two, or one naming a different work item, or one on a comment that
|
|
170
|
+
never had one, and the edit is refused with the reason and the repair. The rule
|
|
171
|
+
lives in `gh-core`, not here, because it is a rule about the link record rather
|
|
172
|
+
than about this command — and the macOS app edits through the same package.
|
|
173
|
+
|
|
174
|
+
**Deleting the comment that carries the trailer is allowed and never silent.**
|
|
175
|
+
The `--yes` refusal names the work item about to lose its GitHub end and hands
|
|
176
|
+
over the `pharos issue link` that puts it back; `--dry-run` prints the same
|
|
177
|
+
preview and exits 0. Both read the comment first, because a preview that could
|
|
178
|
+
not say which of the two deletes this is would be a preview of the wrong thing.
|
|
179
|
+
|
|
180
|
+
Reactions are **not one vocabulary across the two platforms**. Azure DevOps
|
|
181
|
+
takes `like dislike heart hooray smile confused`; GitHub takes `thumbs-up
|
|
182
|
+
thumbs-down laugh hooray confused heart rocket eyes`. Passing one platform's
|
|
183
|
+
name to the other is refused rather than translated. The CLI spelling is words
|
|
184
|
+
because GitHub's own names for the first two are `+1` and `-1`, and `-1` as a
|
|
185
|
+
positional is read as a flag by every argument parser including this one.
|
|
186
|
+
|
|
137
187
|
### `issue` — the GitHub edge, and only the edge
|
|
138
188
|
|
|
139
189
|
```bash
|
|
@@ -144,6 +194,7 @@ pharos issue trail 4821 # …or trail contoso/widgets#45
|
|
|
144
194
|
pharos issue drift # where the two platforms disagree
|
|
145
195
|
pharos issue backfill contoso/widgets --limit 25 --yes
|
|
146
196
|
pharos issue close 4821 --text "Shipped in 1.4.0."
|
|
197
|
+
pharos issue edit contoso/widgets#45 --title "Crash on resize" --yes
|
|
147
198
|
```
|
|
148
199
|
|
|
149
200
|
Azure DevOps already has a GitHub integration and **the link it makes carries
|
|
@@ -152,12 +203,38 @@ the transition only ever fires from a commit or a PR merge, never from closing
|
|
|
152
203
|
an issue. That is the gap, and it is the whole pitch — not "there is no
|
|
153
204
|
integration".
|
|
154
205
|
|
|
155
|
-
**
|
|
206
|
+
**Eight verbs, because `gh` already exists.** Listing, viewing and commenting on
|
|
156
207
|
an issue is `gh`'s job and it is better at it — closing one *end* is `gh issue
|
|
157
208
|
close`, and closing the *pair* is `pharos issue close`; a second GitHub CLI
|
|
158
209
|
would be the drifting duplicate this repo has been bitten by three times.
|
|
159
210
|
Pharos owns what `gh` cannot see: the join.
|
|
160
211
|
|
|
212
|
+
**`edit` is the one verb that overlaps `gh` head on, and it earns that by
|
|
213
|
+
refusing.** `gh issue edit` writes the same fields and writes them well; what it
|
|
214
|
+
cannot do is notice that somebody changed the thing you are overwriting. It is
|
|
215
|
+
last-write-wins — if a colleague renamed the issue while you were composing,
|
|
216
|
+
their rename is gone and neither of you is told. Fine for a human at a terminal,
|
|
217
|
+
wrong for an agent that reads, thinks for a minute, and writes into a world that
|
|
218
|
+
moved. Want an unguarded edit? `gh issue edit` is right there.
|
|
219
|
+
|
|
220
|
+
The guard is a **value compare**, because GitHub offers nothing better:
|
|
221
|
+
`PATCH /repos/{o}/{r}/issues/{n}` answers **400** to `If-Match` (measured), and
|
|
222
|
+
`updated_at` and the ETag both move when somebody merely *comments*, so a
|
|
223
|
+
timestamp guard would refuse a good edit for a change that touched nothing. So
|
|
224
|
+
`edit` re-reads immediately before sending and refuses only if the text it is
|
|
225
|
+
about to overwrite moved. Pass `--if-title` / `--if-body` / `--if-body-file` to
|
|
226
|
+
guard against what **you** saw rather than against this command's own read; the
|
|
227
|
+
output says which, as `fields.guard.window`. It narrows the race to one round
|
|
228
|
+
trip and does not close it, and it says so.
|
|
229
|
+
|
|
230
|
+
Two things GitHub does quietly, which `edit` reports:
|
|
231
|
+
`POST …/labels` **creates** a repository label that does not exist (so an unknown
|
|
232
|
+
one is refused with near misses — `--create-label` is how you mean it), and
|
|
233
|
+
`POST …/assignees` **ignores** a login it will not assign while still answering
|
|
234
|
+
201 (so read `assignees.ignored`). Labels and assignees go through their own
|
|
235
|
+
add/remove endpoints, never the whole-issue PATCH, which carries them as whole
|
|
236
|
+
arrays and would overwrite every label on every write.
|
|
237
|
+
|
|
161
238
|
`adopt` creates the work item **and** writes both ends of the link:
|
|
162
239
|
|
|
163
240
|
| | where | |
|
|
@@ -196,11 +273,12 @@ asks **this machine** — an adoption is written to `~/.config/pharos/half-links
|
|
|
196
273
|
between the create and the comment, and removed when the comment lands — and
|
|
197
274
|
then **the board**, narrowing on `[System.HyperLinkCount]` and matching the
|
|
198
275
|
relation URLs client-side, which is the only way round Azure DevOps having no
|
|
199
|
-
query over them. The
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
276
|
+
query over them. The board is the stronger of the two — it sees a half-link
|
|
277
|
+
left on another machine — and the local record covers what it cannot: a board
|
|
278
|
+
query that is refused, a half-link older than that scan's cap, and a refusal
|
|
279
|
+
that costs no round trip. Every run reports `boardCheck`, because "the board
|
|
280
|
+
says nobody has adopted this" and "the board could not be asked" are different
|
|
281
|
+
facts and only one of them means the check happened.
|
|
204
282
|
|
|
205
283
|
`trail` is read-only and answers from **either** end — the output shape does not
|
|
206
284
|
depend on which you passed. Read `evidence`: `["hyperlink","marker"]` is a
|
|
@@ -430,6 +508,15 @@ name, so the wrong one answers **200** for a different repository. That is what
|
|
|
430
508
|
— without it every `pharos issue` verb is refused by name with the reason, and
|
|
431
509
|
the Azure DevOps verbs are untouched.
|
|
432
510
|
|
|
511
|
+
**And it runs in the other direction too: a verb whose target is a GitHub issue
|
|
512
|
+
needs no `ADO_PAT`.** `pharos comment list contoso/widgets#45` reads over `gh`'s
|
|
513
|
+
credential and touches Azure DevOps at no point, so it works on a machine that
|
|
514
|
+
has never had a PAT. The Azure DevOps configuration is resolved when something
|
|
515
|
+
actually needs Azure DevOps, and not before — which means the `kind: "config"`
|
|
516
|
+
error naming `ADO_PAT` now arrives only from a verb that would have used it.
|
|
517
|
+
`pharos issue` is the exception and honestly so: it writes to both platforms in
|
|
518
|
+
one invocation, so it needs both credentials.
|
|
519
|
+
|
|
433
520
|
**`half-links.json` sits beside it, and it is the only thing this CLI remembers
|
|
434
521
|
between runs.** An adoption is written there between the work item create and
|
|
435
522
|
the GitHub comment and removed when the comment lands, so what survives is
|
|
@@ -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"}
|