@floh-solutions/pharos-cli 0.4.0 → 0.13.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/cli.d.ts.map +1 -1
- package/dist/cli.js +418 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/attach.d.ts +30 -0
- package/dist/commands/attach.d.ts.map +1 -0
- package/dist/commands/attach.js +55 -0
- package/dist/commands/attach.js.map +1 -0
- package/dist/commands/create.d.ts +35 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +82 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/detach.d.ts +34 -0
- package/dist/commands/detach.d.ts.map +1 -0
- package/dist/commands/detach.js +117 -0
- package/dist/commands/detach.js.map +1 -0
- package/dist/commands/discover.d.ts +66 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +128 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/hooks.d.ts +24 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +184 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/image.d.ts +44 -0
- package/dist/commands/image.d.ts.map +1 -0
- package/dist/commands/image.js +114 -0
- package/dist/commands/image.js.map +1 -0
- package/dist/commands/import.d.ts +51 -0
- package/dist/commands/import.d.ts.map +1 -0
- package/dist/commands/import.js +190 -0
- package/dist/commands/import.js.map +1 -0
- package/dist/commands/link.d.ts +40 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +150 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/people.d.ts +39 -0
- package/dist/commands/people.d.ts.map +1 -0
- package/dist/commands/people.js +64 -0
- package/dist/commands/people.js.map +1 -0
- package/dist/commands/query.d.ts +43 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +202 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/recycle.d.ts +10 -0
- package/dist/commands/recycle.d.ts.map +1 -0
- package/dist/commands/recycle.js +118 -0
- package/dist/commands/recycle.js.map +1 -0
- package/dist/commands/task.d.ts +13 -0
- package/dist/commands/task.d.ts.map +1 -1
- package/dist/commands/task.js +106 -4
- package/dist/commands/task.js.map +1 -1
- package/dist/commands/update.d.ts +52 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +181 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/wiki.d.ts.map +1 -1
- package/dist/commands/wiki.js +133 -2
- package/dist/commands/wiki.js.map +1 -1
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -2
- package/dist/output.js.map +1 -1
- package/dist/wiql-build.d.ts +92 -0
- package/dist/wiql-build.d.ts.map +1 -0
- package/dist/wiql-build.js +147 -0
- package/dist/wiql-build.js.map +1 -0
- package/package.json +2 -2
- package/skill/SKILL.md +388 -62
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { requiredSubscriptions, subscriptionStatus, } from "@floh-solutions/ado-core";
|
|
2
|
+
import { emit, usageError } from "../output.js";
|
|
3
|
+
import { approve } from "../session.js";
|
|
4
|
+
export async function runHooks(io, session, positionals, input) {
|
|
5
|
+
const verb = positionals[0] ?? "list";
|
|
6
|
+
switch (verb) {
|
|
7
|
+
case "list":
|
|
8
|
+
return list(io, session);
|
|
9
|
+
case "check":
|
|
10
|
+
return check(io, session, input);
|
|
11
|
+
case "create":
|
|
12
|
+
return create(io, session, input);
|
|
13
|
+
case "repoint":
|
|
14
|
+
return repoint(io, session, input);
|
|
15
|
+
case "delete":
|
|
16
|
+
return remove(io, session, positionals[1]);
|
|
17
|
+
default:
|
|
18
|
+
throw usageError(`Unknown hooks verb "${verb}". Try list, check, create, repoint or delete.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function list(io, session) {
|
|
22
|
+
const subscriptions = await session.client.hooks.list();
|
|
23
|
+
return emit(io, subscriptions.map(describe));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* What is set up, what is not, and what is pointing somewhere else.
|
|
27
|
+
*
|
|
28
|
+
* Read-only, so it needs no confirmation and no write budget — the point is
|
|
29
|
+
* that somebody can run it on a real organisation without consequences.
|
|
30
|
+
*/
|
|
31
|
+
async function check(io, session, input) {
|
|
32
|
+
const { statuses, hub } = await assess(session, input);
|
|
33
|
+
const missing = statuses.filter((s) => s.state === "missing");
|
|
34
|
+
const elsewhere = statuses.filter((s) => s.state === "elsewhere");
|
|
35
|
+
return emit(io, {
|
|
36
|
+
hub,
|
|
37
|
+
ok: missing.length === 0 && elsewhere.length === 0,
|
|
38
|
+
subscriptions: statuses.map(summarise),
|
|
39
|
+
// Separated because they need different verbs, and conflating them is how
|
|
40
|
+
// somebody ends up with two subscriptions delivering the same event twice.
|
|
41
|
+
missing: missing.length,
|
|
42
|
+
pointingElsewhere: elsewhere.length,
|
|
43
|
+
...(missing.length > 0 ? { hint: "pharos hooks create --hub <url> --user <u> --password <p> --yes" } : {}),
|
|
44
|
+
...(elsewhere.length > 0
|
|
45
|
+
? { repointHint: "pharos hooks repoint --hub <url> --user <u> --password <p> --yes" }
|
|
46
|
+
: {}),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function create(io, session, input) {
|
|
50
|
+
const { statuses, hub, projectId, credentials } = await assess(session, input, {
|
|
51
|
+
needCredentials: true,
|
|
52
|
+
});
|
|
53
|
+
const missing = statuses.filter((s) => s.state === "missing");
|
|
54
|
+
if (missing.length === 0) {
|
|
55
|
+
return emit(io, { applied: false, action: "nothingMissing", hub });
|
|
56
|
+
}
|
|
57
|
+
const stop = approve(io, session, {
|
|
58
|
+
what: `create ${missing.length} service hook subscription(s) pointing at ${hub}`,
|
|
59
|
+
would: {
|
|
60
|
+
action: "createSubscriptions",
|
|
61
|
+
hub,
|
|
62
|
+
subscriptions: missing.map((s) => s.required.label),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (stop !== undefined)
|
|
66
|
+
return stop;
|
|
67
|
+
const created = [];
|
|
68
|
+
for (const status of missing) {
|
|
69
|
+
const subscription = await session.client.hooks.create({
|
|
70
|
+
eventType: status.required.eventType,
|
|
71
|
+
projectId,
|
|
72
|
+
url: hub,
|
|
73
|
+
basicAuthUsername: credentials.user,
|
|
74
|
+
basicAuthPassword: credentials.password,
|
|
75
|
+
...(status.required.repositoryId === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { repositoryId: status.required.repositoryId }),
|
|
78
|
+
});
|
|
79
|
+
created.push(subscription.id);
|
|
80
|
+
}
|
|
81
|
+
return emit(io, { applied: true, action: "created", hub, count: created.length, ids: created });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Move existing subscriptions to a new URL.
|
|
85
|
+
*
|
|
86
|
+
* The verb a hostname change needs. Moving a hub means re-pointing every
|
|
87
|
+
* subscription, and there are six — which is also why doing it by hand is where
|
|
88
|
+
* one gets forgotten and realtime half-works.
|
|
89
|
+
*/
|
|
90
|
+
async function repoint(io, session, input) {
|
|
91
|
+
const { statuses, hub, credentials } = await assess(session, input, { needCredentials: true });
|
|
92
|
+
const moving = statuses.filter((s) => s.state === "elsewhere" && s.existing !== undefined);
|
|
93
|
+
if (moving.length === 0) {
|
|
94
|
+
return emit(io, { applied: false, action: "nothingToRepoint", hub });
|
|
95
|
+
}
|
|
96
|
+
const stop = approve(io, session, {
|
|
97
|
+
what: `re-point ${moving.length} subscription(s) to ${hub}`,
|
|
98
|
+
would: {
|
|
99
|
+
action: "repointSubscriptions",
|
|
100
|
+
to: hub,
|
|
101
|
+
subscriptions: moving.map((s) => ({ event: s.required.label, from: s.currentUrl })),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
if (stop !== undefined)
|
|
105
|
+
return stop;
|
|
106
|
+
const moved = [];
|
|
107
|
+
for (const status of moving) {
|
|
108
|
+
// The password is re-sent, never carried over: Azure DevOps returns it
|
|
109
|
+
// masked as `********`, so a read-modify-write would send that literal
|
|
110
|
+
// string and every delivery would 401 while the subscription still read
|
|
111
|
+
// as enabled.
|
|
112
|
+
const updated = await session.client.hooks.repoint(status.existing, hub, credentials.user, credentials.password);
|
|
113
|
+
moved.push(updated.id);
|
|
114
|
+
}
|
|
115
|
+
return emit(io, { applied: true, action: "repointed", hub, count: moved.length, ids: moved });
|
|
116
|
+
}
|
|
117
|
+
async function remove(io, session, id) {
|
|
118
|
+
if (id === undefined || id === "") {
|
|
119
|
+
throw usageError("Which subscription? `pharos hooks delete <id>` — ids come from `hooks list`.");
|
|
120
|
+
}
|
|
121
|
+
const stop = approve(io, session, {
|
|
122
|
+
what: `delete service hook subscription ${id}`,
|
|
123
|
+
would: { action: "deleteSubscription", id },
|
|
124
|
+
});
|
|
125
|
+
if (stop !== undefined)
|
|
126
|
+
return stop;
|
|
127
|
+
await session.client.hooks.remove(id);
|
|
128
|
+
return emit(io, { applied: true, action: "deleted", id });
|
|
129
|
+
}
|
|
130
|
+
// MARK: - Shared assessment
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the project GUID, the wiki's repository GUID, and the current state.
|
|
133
|
+
*
|
|
134
|
+
* The wiki repository GUID is the whole reason this is not a browser job, and
|
|
135
|
+
* it comes from the Wiki API rather than the git one — the repositories
|
|
136
|
+
* endpoint does not list wiki repos at all.
|
|
137
|
+
*/
|
|
138
|
+
async function assess(session, input, options = {}) {
|
|
139
|
+
const hub = input.hub ?? "";
|
|
140
|
+
if (hub === "") {
|
|
141
|
+
throw usageError("Which hub? Pass --hub https://your-hub.example/hooks/ado — the URL Azure DevOps should POST to.");
|
|
142
|
+
}
|
|
143
|
+
const credentials = { user: input.user ?? "", password: input.password ?? "" };
|
|
144
|
+
if (options.needCredentials === true && (credentials.user === "" || credentials.password === "")) {
|
|
145
|
+
throw usageError("A subscription needs the hub's webhook credentials — pass --user and --password. They must "
|
|
146
|
+
+ "match what the hub is configured with (PHAROS_WEBHOOK_USER / PHAROS_WEBHOOK_PASSWORD), "
|
|
147
|
+
+ "or Azure DevOps will deliver and the hub will answer 401.");
|
|
148
|
+
}
|
|
149
|
+
const wiki = await session.wikiForLinking().catch(() => undefined);
|
|
150
|
+
const wikis = await session.wiki.listWikis().catch(() => []);
|
|
151
|
+
const repositoryId = wikis.find((w) => w.name === wiki?.name)?.repositoryId;
|
|
152
|
+
const projectId = wiki?.projectId ?? wikis[0]?.projectId ?? "";
|
|
153
|
+
if (projectId === "") {
|
|
154
|
+
throw usageError("Could not resolve the project GUID — a subscription cannot be scoped without it.");
|
|
155
|
+
}
|
|
156
|
+
const existing = await session.client.hooks.list();
|
|
157
|
+
const statuses = subscriptionStatus(existing, requiredSubscriptions(repositoryId), {
|
|
158
|
+
projectId,
|
|
159
|
+
url: hub,
|
|
160
|
+
});
|
|
161
|
+
return { statuses, hub, projectId, credentials };
|
|
162
|
+
}
|
|
163
|
+
function summarise(status) {
|
|
164
|
+
return {
|
|
165
|
+
event: status.required.eventType,
|
|
166
|
+
what: status.required.label,
|
|
167
|
+
state: status.state,
|
|
168
|
+
...(status.existing === undefined ? {} : { id: status.existing.id }),
|
|
169
|
+
...(status.currentUrl === undefined ? {} : { currentUrl: status.currentUrl }),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function describe(subscription) {
|
|
173
|
+
return {
|
|
174
|
+
id: subscription.id,
|
|
175
|
+
eventType: subscription.eventType,
|
|
176
|
+
status: subscription.status,
|
|
177
|
+
url: subscription.consumerInputs?.["url"],
|
|
178
|
+
projectId: subscription.publisherInputs?.["projectId"],
|
|
179
|
+
// Present only on git.push, and its absence there means `[Any]` — every
|
|
180
|
+
// push in the project, which is the wrong default rather than a detail.
|
|
181
|
+
repository: subscription.publisherInputs?.["repository"],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/commands/hooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ExitCode, type Io } from "../output.js";
|
|
2
|
+
import type { Session } from "../session.js";
|
|
3
|
+
/**
|
|
4
|
+
* `pharos image <file>` and `pharos wiki image <file>` — put a picture INSIDE
|
|
5
|
+
* the text.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this is not `pharos attach`
|
|
8
|
+
*
|
|
9
|
+
* They look like the same verb and they are not, and the difference is the
|
|
10
|
+
* reason this exists.
|
|
11
|
+
*
|
|
12
|
+
* `attach` uploads bytes **and adds an `AttachedFile` relation**, so the file
|
|
13
|
+
* appears in the work item's Attachments list. An inline image has **no
|
|
14
|
+
* relation at all** — measured on #333: two screenshots pasted into a
|
|
15
|
+
* description gave `relations: 0` and `attachments: []`. It is an `` in
|
|
16
|
+
* the field text and nothing else, which is why the Attachments list is right
|
|
17
|
+
* to show nothing and why every piece of machinery that works from relations
|
|
18
|
+
* cannot see it.
|
|
19
|
+
*
|
|
20
|
+
* So: `attach` for a file somebody will open, `image` for a picture somebody
|
|
21
|
+
* will look at while reading. Using the wrong one is not fatal, but it puts a
|
|
22
|
+
* screenshot in the file list or a spreadsheet in the middle of a sentence.
|
|
23
|
+
*
|
|
24
|
+
* ## Two endpoints, and they are not interchangeable
|
|
25
|
+
*
|
|
26
|
+
* A **work item** image goes to `POST wit/attachments` and comes back with a
|
|
27
|
+
* GUID url, so two files called `image.png` are simply two attachments.
|
|
28
|
+
*
|
|
29
|
+
* A **wiki** image goes to `PUT wiki/wikis/{wiki}/attachments?name=`, whose body
|
|
30
|
+
* must be BASE64 (Microsoft's spec says stream; the server disagrees with a 500),
|
|
31
|
+
* and which writes a file into a git repository — so the NAME is the identity
|
|
32
|
+
* and a second `image.png` overwrites the first. The name is made unique here
|
|
33
|
+
* before it is sent.
|
|
34
|
+
*/
|
|
35
|
+
export interface ImageCommandOptions {
|
|
36
|
+
/** Alt text. Defaults to the file's own name, which is what a screenshot has. */
|
|
37
|
+
alt: string | undefined;
|
|
38
|
+
allowOversize: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** `pharos image <file>` — for a work item description or comment. */
|
|
41
|
+
export declare function runImage(io: Io, session: Session, cwd: string, positionals: readonly string[], options: ImageCommandOptions): Promise<ExitCode>;
|
|
42
|
+
/** `pharos wiki image <file>` — for a wiki page or a wiki page comment. */
|
|
43
|
+
export declare function runWikiImage(io: Io, session: Session, cwd: string, positionals: readonly string[], options: ImageCommandOptions): Promise<ExitCode>;
|
|
44
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/commands/image.ts"],"names":[],"mappings":"AAKA,OAAO,EAA8B,KAAK,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,sEAAsE;AACtE,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CA6BnB;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,QAAQ,CAAC,CAyCnB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, resolve } from "node:path";
|
|
3
|
+
import { uniqueWikiAttachmentName } from "@floh-solutions/ado-core";
|
|
4
|
+
import { emit, emitText, usageError } from "../output.js";
|
|
5
|
+
/** `pharos image <file>` — for a work item description or comment. */
|
|
6
|
+
export async function runImage(io, session, cwd, positionals, options) {
|
|
7
|
+
const { path, bytes, name } = await readImage(cwd, positionals[0], "pharos image ./shot.png");
|
|
8
|
+
const alt = options.alt ?? altTextFor(name);
|
|
9
|
+
if (session.previewOnly) {
|
|
10
|
+
return report(io, session, {
|
|
11
|
+
applied: false,
|
|
12
|
+
dryRun: true,
|
|
13
|
+
target: "workItem",
|
|
14
|
+
name,
|
|
15
|
+
bytes: bytes.length,
|
|
16
|
+
markdown: ``,
|
|
17
|
+
path,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const uploaded = await session.client.attachments.upload(name, bytes, {
|
|
21
|
+
allowOversize: options.allowOversize,
|
|
22
|
+
});
|
|
23
|
+
return report(io, session, {
|
|
24
|
+
applied: true,
|
|
25
|
+
target: "workItem",
|
|
26
|
+
name,
|
|
27
|
+
bytes: bytes.length,
|
|
28
|
+
url: uploaded.url,
|
|
29
|
+
markdown: ``,
|
|
30
|
+
path,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** `pharos wiki image <file>` — for a wiki page or a wiki page comment. */
|
|
34
|
+
export async function runWikiImage(io, session, cwd, positionals, options) {
|
|
35
|
+
const { path, bytes, name } = await readImage(cwd, positionals[0], "pharos wiki image ./diagram.png");
|
|
36
|
+
const alt = options.alt ?? altTextFor(name);
|
|
37
|
+
// **Unique before it is sent.** A wiki attachment is a file in a git repo, so
|
|
38
|
+
// the name IS the identity: a second `image.png` lands on the first and every
|
|
39
|
+
// page pointing at it silently changes picture.
|
|
40
|
+
const unique = uniqueWikiAttachmentName(name, Date.now());
|
|
41
|
+
if (session.previewOnly) {
|
|
42
|
+
return report(io, session, {
|
|
43
|
+
applied: false,
|
|
44
|
+
dryRun: true,
|
|
45
|
+
target: "wiki",
|
|
46
|
+
name: unique,
|
|
47
|
+
bytes: bytes.length,
|
|
48
|
+
markdown: ``,
|
|
49
|
+
path,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const wiki = await session.wikiIdentifier();
|
|
53
|
+
const created = await session.wiki.createAttachment(wiki, unique, bytes);
|
|
54
|
+
return report(io, session, {
|
|
55
|
+
applied: true,
|
|
56
|
+
target: "wiki",
|
|
57
|
+
wiki,
|
|
58
|
+
name: created.name,
|
|
59
|
+
bytes: bytes.length,
|
|
60
|
+
// Repo-relative, NOT an absolute url. A page referencing an absolute
|
|
61
|
+
// `wit/attachments` url renders for anyone with a session and breaks for
|
|
62
|
+
// everybody else.
|
|
63
|
+
attachmentPath: created.path,
|
|
64
|
+
markdown: ``,
|
|
65
|
+
path,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async function readImage(cwd, given, example) {
|
|
69
|
+
if (given === undefined) {
|
|
70
|
+
throw usageError(`Which file? \`${example}\`.`);
|
|
71
|
+
}
|
|
72
|
+
const path = resolve(cwd, given);
|
|
73
|
+
let bytes;
|
|
74
|
+
try {
|
|
75
|
+
bytes = await readFile(path);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
// Exit 2, not 1: a missing file is the call being wrong, and repeating it
|
|
79
|
+
// unchanged will never work.
|
|
80
|
+
throw usageError(`Cannot read "${given}": ${error instanceof Error ? error.message : error}`, {
|
|
81
|
+
path,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return { path, bytes, name: basename(path) };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Alt text worth reading, from a file name.
|
|
88
|
+
*
|
|
89
|
+
* `Screenshot 2026-08-06 at 14.02.11.png` → `Screenshot 2026-08-06 at 14.02.11`.
|
|
90
|
+
* Not brilliant, and better than `image.png` or nothing: alt text is what
|
|
91
|
+
* somebody using a screen reader gets instead of the picture, and it is what
|
|
92
|
+
* every renderer shows when the image fails to load.
|
|
93
|
+
*/
|
|
94
|
+
function altTextFor(fileName) {
|
|
95
|
+
const dot = fileName.lastIndexOf(".");
|
|
96
|
+
const stem = dot > 0 ? fileName.slice(0, dot) : fileName;
|
|
97
|
+
return stem.trim() === "" ? "image" : stem;
|
|
98
|
+
}
|
|
99
|
+
function report(io, session, result) {
|
|
100
|
+
if (session.pretty) {
|
|
101
|
+
const where = result.target === "wiki" ? "the wiki" : "this project's work item attachments";
|
|
102
|
+
emitText(io, result.dryRun === true
|
|
103
|
+
? `Would upload ${result.name} (${result.bytes} bytes) to ${where}.`
|
|
104
|
+
: `Uploaded ${result.name} (${result.bytes} bytes) to ${where}.`);
|
|
105
|
+
emitText(io, "");
|
|
106
|
+
// The markdown is the ANSWER, not a detail of it — the whole point of the
|
|
107
|
+
// verb is the line you paste into a description, a comment or a page.
|
|
108
|
+
emitText(io, result.markdown);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
emit(io, result);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/commands/image.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ExitCode, type Io } from "../output.js";
|
|
2
|
+
import type { Session } from "../session.js";
|
|
3
|
+
/**
|
|
4
|
+
* `pharos wiki import <file…>` — documents in, wiki pages out.
|
|
5
|
+
*
|
|
6
|
+
* ## What it is for
|
|
7
|
+
*
|
|
8
|
+
* The macOS app grew this in #641: pick files, name them, choose one parent, and
|
|
9
|
+
* each becomes a page. Everything about it that is a RULE rather than a window —
|
|
10
|
+
* how a file name becomes a page name, what happens when two files want the same
|
|
11
|
+
* name, what happens when the page already exists — is the same here, because
|
|
12
|
+
* getting those wrong loses somebody's work either way.
|
|
13
|
+
*
|
|
14
|
+
* ## Where the conversion comes from
|
|
15
|
+
*
|
|
16
|
+
* `.md` and `.txt` are copied VERBATIM. A markdown file is already the target
|
|
17
|
+
* format and anything done to it on the way in would be reformatting a document
|
|
18
|
+
* somebody wrote deliberately.
|
|
19
|
+
*
|
|
20
|
+
* `.docx`, `.pdf`, `.rtf`, `.html` need converting, and that converter is Swift:
|
|
21
|
+
* it lives in `PharosCore`, has thirty-odd tests behind it and was measured
|
|
22
|
+
* against 246 real `.docx` files. **This shells out to it rather than
|
|
23
|
+
* reimplementing it**, because a second converter in TypeScript would be a
|
|
24
|
+
* second set of answers to "what does this document say", and the one that
|
|
25
|
+
* drifts is the one nobody is looking at.
|
|
26
|
+
*
|
|
27
|
+
* The helper is `pharos-convert`. When it is not on PATH, `.md` and `.txt`
|
|
28
|
+
* still import and everything else is refused **by name, with the reason** —
|
|
29
|
+
* rather than half-importing, which is the failure that would be worth
|
|
30
|
+
* complaining about.
|
|
31
|
+
*
|
|
32
|
+
* ## The rules that stop it losing work
|
|
33
|
+
*
|
|
34
|
+
* - **A collision SKIPS and says so.** A wiki page write with an empty version
|
|
35
|
+
* is a create, so writing over an existing page is silent data loss.
|
|
36
|
+
* - **Names are settled against the batch as well as the wiki.** Importing
|
|
37
|
+
* `Notes.docx` and `Notes.pdf` together is two files wanting one page name;
|
|
38
|
+
* checking only the server would make that one page written twice, inside a
|
|
39
|
+
* single command, faster than anybody could see.
|
|
40
|
+
* - **An empty document is refused.** A scanned PDF carries no text at all —
|
|
41
|
+
* PDFKit hands back an empty string and no error — and an empty page would
|
|
42
|
+
* look like a successful import until somebody opened it.
|
|
43
|
+
*/
|
|
44
|
+
export interface ImportCommandOptions {
|
|
45
|
+
/** The wiki path everything lands under. Empty is the top level. */
|
|
46
|
+
under: string | undefined;
|
|
47
|
+
/** Override the page name. Only meaningful for a single file. */
|
|
48
|
+
as: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
export declare function runWikiImport(io: Io, session: Session, cwd: string, positionals: readonly string[], options: ImportCommandOptions): Promise<ExitCode>;
|
|
51
|
+
//# sourceMappingURL=import.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"AAOA,OAAO,EAA8B,KAAK,QAAQ,EAAE,KAAK,EAAE,EAAE,MAAM,cAAc,CAAC;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAI7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iEAAiE;IACjE,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;CACxB;AASD,wBAAsB,aAAa,CACjC,EAAE,EAAE,EAAE,EACN,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA6GnB"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, extname, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { AdoNotFoundError } from "@floh-solutions/ado-core";
|
|
6
|
+
import { emit, emitText, usageError } from "../output.js";
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
export async function runWikiImport(io, session, cwd, positionals, options) {
|
|
9
|
+
if (positionals.length === 0) {
|
|
10
|
+
throw usageError("Which files? `pharos wiki import ./notes.md ./report.docx --under /Guides`.");
|
|
11
|
+
}
|
|
12
|
+
if (options.as !== undefined && positionals.length > 1) {
|
|
13
|
+
throw usageError("`--as` names ONE page, so it cannot be used with several files.");
|
|
14
|
+
}
|
|
15
|
+
const wiki = await session.wikiIdentifier();
|
|
16
|
+
const parent = normalisePath(options.under ?? "");
|
|
17
|
+
// Every path already in the wiki, for the collision check. One call, and the
|
|
18
|
+
// check is repeated per file against the growing set so the batch cannot
|
|
19
|
+
// collide with itself.
|
|
20
|
+
const taken = new Set();
|
|
21
|
+
for (const path of await existingPaths(session, wiki)) {
|
|
22
|
+
taken.add(path.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
const outcomes = [];
|
|
25
|
+
for (const given of positionals) {
|
|
26
|
+
const path = resolve(cwd, given);
|
|
27
|
+
const file = basename(path);
|
|
28
|
+
let converted;
|
|
29
|
+
try {
|
|
30
|
+
converted = await convert(path, file);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
outcomes.push({ file, skipped: error instanceof Error ? error.message : String(error) });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// **A NAME SOMEBODY TYPED IS NOT RENAMED.** A name DERIVED from a file name
|
|
37
|
+
// is counted up when it is taken — that is the same thing the app's staging
|
|
38
|
+
// list does, and importing `Notes.docx` beside `Notes.pdf` should give two
|
|
39
|
+
// pages rather than one refusal. But `--as` is an instruction, and quietly
|
|
40
|
+
// turning it into "… 2" writes a page nobody asked for under a name nobody
|
|
41
|
+
// chose. So an explicit name is allowed to collide, and collisions skip.
|
|
42
|
+
const explicit = options.as !== undefined;
|
|
43
|
+
const leaf = sanitise(options.as ?? converted.name);
|
|
44
|
+
const target = joinPath(parent, explicit ? leaf : freeName(leaf, taken, parent));
|
|
45
|
+
if (taken.has(target.toLowerCase())) {
|
|
46
|
+
outcomes.push({
|
|
47
|
+
file,
|
|
48
|
+
page: target,
|
|
49
|
+
skipped: `There is already a page at ${target}. It was left alone.`,
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (session.previewOnly) {
|
|
54
|
+
outcomes.push({ file, page: target, created: false });
|
|
55
|
+
taken.add(target.toLowerCase());
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
// ANCESTORS ARE NOT CREATED FOR YOU — writing `/A/B` when `/A` does not
|
|
59
|
+
// exist is refused by Azure DevOps. Made once per missing ancestor, empty,
|
|
60
|
+
// which is what the website does when you add a page under a folder.
|
|
61
|
+
for (const ancestor of ancestorsOf(target)) {
|
|
62
|
+
if (taken.has(ancestor.toLowerCase()))
|
|
63
|
+
continue;
|
|
64
|
+
try {
|
|
65
|
+
await session.wiki.writePage(wiki, ancestor, "");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Already there, or refused for a reason the child's own write will
|
|
69
|
+
// report properly. Either way it is not this loop's business.
|
|
70
|
+
}
|
|
71
|
+
taken.add(ancestor.toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
await session.wiki.writePage(wiki, target, converted.markdown);
|
|
74
|
+
taken.add(target.toLowerCase());
|
|
75
|
+
outcomes.push({ file, page: target, created: true });
|
|
76
|
+
}
|
|
77
|
+
const created = outcomes.filter((outcome) => outcome.created === true).length;
|
|
78
|
+
const skipped = outcomes.filter((outcome) => outcome.skipped !== undefined).length;
|
|
79
|
+
if (session.pretty) {
|
|
80
|
+
for (const outcome of outcomes) {
|
|
81
|
+
emitText(io, outcome.skipped !== undefined
|
|
82
|
+
? `skipped ${outcome.file} — ${outcome.skipped}`
|
|
83
|
+
: `${outcome.created === true ? "created" : "would create"} ${outcome.page}`);
|
|
84
|
+
}
|
|
85
|
+
emitText(io, "");
|
|
86
|
+
emitText(io, `${created} created, ${skipped} skipped.`);
|
|
87
|
+
return skipped > 0 && created === 0 ? 3 : 0;
|
|
88
|
+
}
|
|
89
|
+
emit(io, {
|
|
90
|
+
applied: !session.previewOnly,
|
|
91
|
+
dryRun: session.previewOnly ? true : undefined,
|
|
92
|
+
wiki,
|
|
93
|
+
under: parent === "" ? "/" : parent,
|
|
94
|
+
created,
|
|
95
|
+
skipped,
|
|
96
|
+
pages: outcomes,
|
|
97
|
+
});
|
|
98
|
+
// Exit 3 — a guard here refused — only when NOTHING landed. A partial batch
|
|
99
|
+
// did real work and reporting it as a failure would invite a blind retry that
|
|
100
|
+
// collides with the pages it just made.
|
|
101
|
+
return skipped > 0 && created === 0 ? 3 : 0;
|
|
102
|
+
}
|
|
103
|
+
/** A document, as markdown. */
|
|
104
|
+
async function convert(path, file) {
|
|
105
|
+
const extension = extname(file).toLowerCase();
|
|
106
|
+
const stem = file.slice(0, file.length - extension.length) || file;
|
|
107
|
+
if (extension === ".md" || extension === ".markdown" || extension === ".txt") {
|
|
108
|
+
const markdown = await readFile(path, "utf8");
|
|
109
|
+
if (markdown.trim() === "") {
|
|
110
|
+
throw new Error(`${file} has no text in it, so there is nothing to make a page from.`);
|
|
111
|
+
}
|
|
112
|
+
return { name: stem, markdown };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const { stdout } = await run("pharos-convert", [path], { maxBuffer: 32 * 1024 * 1024 });
|
|
116
|
+
const parsed = JSON.parse(stdout);
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const failure = error;
|
|
121
|
+
if (failure.code === "ENOENT") {
|
|
122
|
+
throw new Error(`${file} needs \`pharos-convert\`, which is not on PATH. `
|
|
123
|
+
+ "Markdown and text import without it; Word documents and PDFs need it because the "
|
|
124
|
+
+ "converter is shared with the macOS app rather than written twice.");
|
|
125
|
+
}
|
|
126
|
+
// The helper's own sentence, forwarded verbatim — it already says what is
|
|
127
|
+
// wrong and what to do, including the scanned-PDF case.
|
|
128
|
+
const said = (failure.stderr ?? "").trim();
|
|
129
|
+
throw new Error(said === "" ? `${file} could not be converted.` : said);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** Every page path already in the wiki. */
|
|
133
|
+
async function existingPaths(session, wiki) {
|
|
134
|
+
try {
|
|
135
|
+
const ids = await session.wiki.pageIdsByPath(wiki);
|
|
136
|
+
return [...ids.keys()];
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof AdoNotFoundError)
|
|
140
|
+
return [];
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Leading slash, no trailing one. The API is inconsistent about both. */
|
|
145
|
+
function normalisePath(path) {
|
|
146
|
+
let trimmed = path.trim();
|
|
147
|
+
while (trimmed.endsWith("/"))
|
|
148
|
+
trimmed = trimmed.slice(0, -1);
|
|
149
|
+
if (trimmed === "")
|
|
150
|
+
return "";
|
|
151
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
152
|
+
}
|
|
153
|
+
function joinPath(parent, leaf) {
|
|
154
|
+
const head = normalisePath(parent);
|
|
155
|
+
const tail = leaf.replace(/^\/+|\/+$/g, "");
|
|
156
|
+
return head === "" ? `/${tail}` : `${head}/${tail}`;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* A page name a wiki path can hold.
|
|
160
|
+
*
|
|
161
|
+
* **The slash is the one that matters**: a path is slash-separated, so a file
|
|
162
|
+
* called `Q1/Q2 review` would otherwise create a page called `Q2 review` nested
|
|
163
|
+
* under a brand-new `Q1`, in a place nobody chose and with `--under` silently
|
|
164
|
+
* overruled.
|
|
165
|
+
*/
|
|
166
|
+
function sanitise(name) {
|
|
167
|
+
const cleaned = name
|
|
168
|
+
.replace(/[/\\:*?"<>|#\t\n\r]+/g, "-")
|
|
169
|
+
.replace(/^[\s\-.]+|[\s\-.]+$/g, "");
|
|
170
|
+
return cleaned === "" ? "Untitled" : cleaned;
|
|
171
|
+
}
|
|
172
|
+
/** A name not already taken, counting from 2 because the unnumbered one is first. */
|
|
173
|
+
function freeName(name, taken, parent) {
|
|
174
|
+
if (!taken.has(joinPath(parent, name).toLowerCase()))
|
|
175
|
+
return name;
|
|
176
|
+
let suffix = 2;
|
|
177
|
+
while (taken.has(joinPath(parent, `${name} ${suffix}`).toLowerCase()))
|
|
178
|
+
suffix += 1;
|
|
179
|
+
return `${name} ${suffix}`;
|
|
180
|
+
}
|
|
181
|
+
/** `/A/B/C` → `["/A", "/A/B"]`, outermost first. */
|
|
182
|
+
function ancestorsOf(path) {
|
|
183
|
+
const parts = normalisePath(path).split("/").filter((part) => part !== "");
|
|
184
|
+
const out = [];
|
|
185
|
+
for (let index = 1; index < parts.length; index += 1) {
|
|
186
|
+
out.push(`/${parts.slice(0, index).join("/")}`);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=import.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/commands/import.ts"],"names":[],"mappings":"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"}
|