codoc-cli 0.1.2
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/LICENSE +21 -0
- package/README.md +104 -0
- package/assets/drawio-viewer/README.md +13 -0
- package/assets/drawio-viewer/viewer-static.min.js +8640 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/dist/clients/confluence/clients/attachments-client.js +74 -0
- package/dist/clients/confluence/clients/folders-client.js +111 -0
- package/dist/clients/confluence/clients/pages-client.js +128 -0
- package/dist/clients/confluence/confluence-client.js +44 -0
- package/dist/clients/confluence/utils/confluence-url.js +25 -0
- package/dist/clients/confluence/utils/confluence-util.js +20 -0
- package/dist/commands/init.js +29 -0
- package/dist/commands/publish.js +34 -0
- package/dist/commands/pull.js +54 -0
- package/dist/commands/sync.js +27 -0
- package/dist/commands/tree.js +17 -0
- package/dist/config/codoc-config-atlassian.js +89 -0
- package/dist/config/codoc-config-raw.js +21 -0
- package/dist/config/codoc-config.js +50 -0
- package/dist/config/codoc-paths.js +9 -0
- package/dist/hooks/command_not_found.js +7 -0
- package/dist/hooks/init/check-for-update.js +68 -0
- package/dist/hooks/init/load-env.js +15 -0
- package/dist/index.js +2 -0
- package/dist/services/codoc-id.js +7 -0
- package/dist/services/confluence/attachments.js +56 -0
- package/dist/services/confluence/folders.js +42 -0
- package/dist/services/confluence/labels.js +17 -0
- package/dist/services/confluence/pages.js +28 -0
- package/dist/services/conversion/confluenceToMarkdown/diagrams/drawio-diagrams.js +77 -0
- package/dist/services/conversion/confluenceToMarkdown/diagrams/drawio-to-image.js +131 -0
- package/dist/services/conversion/confluenceToMarkdown/index.js +17 -0
- package/dist/services/conversion/confluenceToMarkdown/preprocess/macro-converters.js +68 -0
- package/dist/services/conversion/confluenceToMarkdown/preprocess/preprocess.js +113 -0
- package/dist/services/conversion/confluenceToMarkdown/turndown.js +71 -0
- package/dist/services/conversion/markdownToConfluence/conversion-state.js +18 -0
- package/dist/services/conversion/markdownToConfluence/diagrams/mermaid-diagrams.js +56 -0
- package/dist/services/conversion/markdownToConfluence/diagrams/mermaid-to-drawio.js +331 -0
- package/dist/services/conversion/markdownToConfluence/index.js +48 -0
- package/dist/services/conversion/markdownToConfluence/languages.js +66 -0
- package/dist/services/conversion/markdownToConfluence/links.js +40 -0
- package/dist/services/conversion/markdownToConfluence/raw-html/raw-html.js +86 -0
- package/dist/services/conversion/markdownToConfluence/raw-html/task-lists.js +59 -0
- package/dist/services/conversion/markdownToConfluence/render/blocks.js +171 -0
- package/dist/services/conversion/markdownToConfluence/render/inline.js +44 -0
- package/dist/services/conversion/markdownToConfluence/render/page.js +31 -0
- package/dist/services/conversion/shared/confluence-macro-builder.js +11 -0
- package/dist/services/conversion/shared/gitlab-url.js +16 -0
- package/dist/services/conversion/shared/image-attachments.js +57 -0
- package/dist/services/conversion/shared/jira.js +7 -0
- package/dist/services/conversion/shared/macro-types.js +12 -0
- package/dist/services/conversion/shared/preserved-macros.js +8 -0
- package/dist/services/conversion/shared/read-storage-format.js +29 -0
- package/dist/services/conversion/shared/regex-cache.js +14 -0
- package/dist/services/conversion/shared/xml-escaping.js +19 -0
- package/dist/services/ensure-env.js +59 -0
- package/dist/services/files-service.js +74 -0
- package/dist/services/git/default-branch.js +33 -0
- package/dist/services/init-templates/agent-md-init.js +55 -0
- package/dist/services/init-templates/doc-guide-init.js +131 -0
- package/dist/services/init-templates/env-init.js +12 -0
- package/dist/services/init-templates/yaml-init.js +56 -0
- package/dist/services/lock/lock-file.js +61 -0
- package/dist/services/log/init-printer.js +50 -0
- package/dist/services/log/logger.js +131 -0
- package/dist/services/prompt.js +32 -0
- package/dist/services/slugify.js +6 -0
- package/dist/types/codoc-types.js +1 -0
- package/dist/use-cases/init/init.js +150 -0
- package/dist/use-cases/publish/publish.js +166 -0
- package/dist/use-cases/pull/parse-page-input.js +22 -0
- package/dist/use-cases/pull/pull.js +337 -0
- package/dist/use-cases/shared/codoc-yaml.js +45 -0
- package/dist/use-cases/shared/confluence-client-registry.js +27 -0
- package/dist/use-cases/shared/env-select.js +23 -0
- package/dist/use-cases/shared/fetch-page.js +11 -0
- package/dist/use-cases/shared/list-documents.js +172 -0
- package/dist/use-cases/shared/pull-folder.js +90 -0
- package/dist/use-cases/shared/render-remote-page.js +71 -0
- package/dist/use-cases/sync/attachment-upload.js +31 -0
- package/dist/use-cases/sync/sync-actions.js +170 -0
- package/dist/use-cases/sync/sync-associate.js +41 -0
- package/dist/use-cases/sync/sync-entries.js +36 -0
- package/dist/use-cases/sync/sync-handlers.js +140 -0
- package/dist/use-cases/sync/sync-helpers.js +8 -0
- package/dist/use-cases/sync/sync.js +94 -0
- package/dist/use-cases/tree/docs-tree.js +141 -0
- package/oclif.manifest.json +255 -0
- package/package.json +98 -0
package/bin/dev.cmd
ADDED
package/bin/dev.js
ADDED
package/bin/run.cmd
ADDED
package/bin/run.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { wikiLink } from "../utils/confluence-url.js";
|
|
2
|
+
import ky from "ky";
|
|
3
|
+
//#region src/clients/confluence/clients/attachments-client.ts
|
|
4
|
+
var ConfluenceAttachmentsClient = class {
|
|
5
|
+
http;
|
|
6
|
+
constructor(http) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Recherche une pièce jointe par son nom.
|
|
11
|
+
*/
|
|
12
|
+
async find(pageId, filename) {
|
|
13
|
+
const hit = (await this.http.v1.get(`content/${pageId}/child/attachment`, { searchParams: {
|
|
14
|
+
filename,
|
|
15
|
+
expand: "version"
|
|
16
|
+
} }).json()).results?.[0];
|
|
17
|
+
if (!hit) return;
|
|
18
|
+
if (!hit._links?.download) return;
|
|
19
|
+
return {
|
|
20
|
+
id: String(hit.id),
|
|
21
|
+
filename,
|
|
22
|
+
downloadPath: hit._links.download
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Télécharge directement via _links.download (API v1).
|
|
27
|
+
*/
|
|
28
|
+
async downloadV1(downloadPath) {
|
|
29
|
+
return this.http.v1.get(wikiLink(this.http.baseUrl, downloadPath), {
|
|
30
|
+
prefixUrl: "",
|
|
31
|
+
headers: { Accept: "*/*" }
|
|
32
|
+
}).arrayBuffer();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Retourne le downloadLink signé (API v2).
|
|
36
|
+
*/
|
|
37
|
+
async getDownloadLink(attachmentId) {
|
|
38
|
+
return (await this.http.v2.get(`attachments/${attachmentId}`).json()).downloadLink;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Télécharge un attachment via un downloadLink v2 (absolu OU relatif au wiki).
|
|
42
|
+
* Le lien étant déjà pré-signé, on n'envoie pas d'en-tête d'authentification.
|
|
43
|
+
*/
|
|
44
|
+
async downloadV2(signedUrl) {
|
|
45
|
+
const url = signedUrl.startsWith("http") ? signedUrl : wikiLink(this.http.baseUrl, signedUrl);
|
|
46
|
+
return ky.get(url, { headers: { Accept: "*/*" } }).arrayBuffer();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Upload ou remplace
|
|
50
|
+
* une pièce jointe.
|
|
51
|
+
*/
|
|
52
|
+
async upload(pageId, filename, blob) {
|
|
53
|
+
const existing = await this.find(pageId, filename);
|
|
54
|
+
const form = new FormData();
|
|
55
|
+
form.append("file", blob, filename);
|
|
56
|
+
const endpoint = existing ? `content/${pageId}/child/attachment/${existing.id}/data` : `content/${pageId}/child/attachment`;
|
|
57
|
+
await this.http.v1.post(endpoint, {
|
|
58
|
+
body: form,
|
|
59
|
+
headers: { "X-Atlassian-Token": "no-check" }
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Télécharge directement
|
|
64
|
+
* le texte d'un attachment.
|
|
65
|
+
*/
|
|
66
|
+
async downloadTextV1(downloadPath) {
|
|
67
|
+
return this.http.v1.get(wikiLink(this.http.baseUrl, downloadPath), {
|
|
68
|
+
prefixUrl: "",
|
|
69
|
+
headers: { Accept: "*/*" }
|
|
70
|
+
}).text();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { ConfluenceAttachmentsClient };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { titleVariants } from "../utils/confluence-url.js";
|
|
2
|
+
//#region src/clients/confluence/clients/folders-client.ts
|
|
3
|
+
/** Extrait le curseur de pagination de `_links.next` (API v2, pagination par curseur). */
|
|
4
|
+
function extractCursor(nextLink) {
|
|
5
|
+
if (!nextLink) return void 0;
|
|
6
|
+
const query = nextLink.split("?")[1];
|
|
7
|
+
if (!query) return void 0;
|
|
8
|
+
return new URLSearchParams(query).get("cursor") ?? void 0;
|
|
9
|
+
}
|
|
10
|
+
/** Échappe une valeur pour l'insérer dans une clause CQL entre guillemets doubles. */
|
|
11
|
+
function escapeCql(value) {
|
|
12
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
13
|
+
}
|
|
14
|
+
var ConfluenceFoldersClient = class {
|
|
15
|
+
http;
|
|
16
|
+
constructor(http) {
|
|
17
|
+
this.http = http;
|
|
18
|
+
}
|
|
19
|
+
async findByTitleUnderParent(parentId, title) {
|
|
20
|
+
for (const variant of titleVariants(title)) {
|
|
21
|
+
const cql = `type = folder and parent = ${parentId} and title = "${escapeCql(variant)}"`;
|
|
22
|
+
const results = (await this.http.v1.get("search", { searchParams: {
|
|
23
|
+
cql,
|
|
24
|
+
limit: 2
|
|
25
|
+
} }).json()).results ?? [];
|
|
26
|
+
if (results.length === 1) return {
|
|
27
|
+
id: String(results[0].content.id),
|
|
28
|
+
title: results[0].content.title
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async childrenFolders(parentId) {
|
|
33
|
+
const results = [];
|
|
34
|
+
const cql = `type = folder and parent = ${parentId}`;
|
|
35
|
+
const limit = 100;
|
|
36
|
+
let start = 0;
|
|
37
|
+
for (let page = 0; page < 1e3; page++) {
|
|
38
|
+
const batch = (await this.http.v1.get("search", { searchParams: {
|
|
39
|
+
cql,
|
|
40
|
+
start,
|
|
41
|
+
limit
|
|
42
|
+
} }).json()).results ?? [];
|
|
43
|
+
results.push(...batch.map((r) => ({
|
|
44
|
+
id: String(r.content.id),
|
|
45
|
+
title: r.content.title
|
|
46
|
+
})));
|
|
47
|
+
if (batch.length < limit) break;
|
|
48
|
+
start += limit;
|
|
49
|
+
}
|
|
50
|
+
return results;
|
|
51
|
+
}
|
|
52
|
+
async exists(id) {
|
|
53
|
+
try {
|
|
54
|
+
await this.http.v2.get(`folders/${id}`);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
async get(folderId) {
|
|
60
|
+
try {
|
|
61
|
+
const data = await this.http.v2.get(`folders/${folderId}`).json();
|
|
62
|
+
return {
|
|
63
|
+
id: String(data.id),
|
|
64
|
+
title: data.title,
|
|
65
|
+
parentId: data.parentId ? String(data.parentId) : void 0
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Suit la pagination par curseur de l'API v2 - sinon les dossiers de plus de 250 enfants étaient tronqués silencieusement. */
|
|
72
|
+
async children(folderId) {
|
|
73
|
+
const results = [];
|
|
74
|
+
let cursor;
|
|
75
|
+
for (let page = 0; page < 1e3; page++) {
|
|
76
|
+
const data = await this.http.v2.get(`folders/${folderId}/direct-children`, { searchParams: cursor ? {
|
|
77
|
+
limit: 250,
|
|
78
|
+
cursor
|
|
79
|
+
} : { limit: 250 } }).json();
|
|
80
|
+
results.push(...(data.results ?? []).map((child) => ({
|
|
81
|
+
id: String(child.id),
|
|
82
|
+
title: child.title,
|
|
83
|
+
type: child.type
|
|
84
|
+
})));
|
|
85
|
+
cursor = extractCursor(data._links?.next);
|
|
86
|
+
if (!cursor) break;
|
|
87
|
+
}
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
async delete(folderId) {
|
|
91
|
+
await this.http.v2.delete(`folders/${folderId}`);
|
|
92
|
+
}
|
|
93
|
+
async create(title, spaceId, parentId) {
|
|
94
|
+
const payload = {
|
|
95
|
+
title,
|
|
96
|
+
spaceId
|
|
97
|
+
};
|
|
98
|
+
if (parentId) payload.parentId = String(parentId);
|
|
99
|
+
return this.http.v2.post("folders", { json: payload }).json();
|
|
100
|
+
}
|
|
101
|
+
async getSpaceId(spaceKey) {
|
|
102
|
+
const id = (await this.http.v2.get("spaces", { searchParams: {
|
|
103
|
+
keys: spaceKey,
|
|
104
|
+
limit: 1
|
|
105
|
+
} }).json()).results?.[0]?.id;
|
|
106
|
+
if (!id) throw new Error(`Space "${spaceKey}" introuvable.`);
|
|
107
|
+
return String(id);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
//#endregion
|
|
111
|
+
export { ConfluenceFoldersClient };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { titleVariants } from "../utils/confluence-url.js";
|
|
2
|
+
//#region src/clients/confluence/clients/pages-client.ts
|
|
3
|
+
var ConfluencePagesClient = class {
|
|
4
|
+
http;
|
|
5
|
+
configuration;
|
|
6
|
+
constructor(http, configuration) {
|
|
7
|
+
this.http = http;
|
|
8
|
+
this.configuration = configuration;
|
|
9
|
+
}
|
|
10
|
+
async exists(id) {
|
|
11
|
+
try {
|
|
12
|
+
await this.http.v1.get(`content/${id}`).json();
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async testConnection() {
|
|
19
|
+
try {
|
|
20
|
+
await this.http.v1.get("content", { searchParams: { limit: 1 } });
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async fetchPage(pageId) {
|
|
27
|
+
const data = await this.http.v1.get(`content/${pageId}`, { searchParams: { expand: "body.storage,ancestors" } }).json();
|
|
28
|
+
const storageXml = data.body?.storage?.value ?? "";
|
|
29
|
+
const parent = data.ancestors?.[data.ancestors.length - 1];
|
|
30
|
+
return {
|
|
31
|
+
id: String(data.id),
|
|
32
|
+
title: data.title,
|
|
33
|
+
storageXml,
|
|
34
|
+
parentId: parent?.id ? parent.id : void 0,
|
|
35
|
+
hasImages: /ri:attachment/i.test(storageXml) || /ri:url/i.test(storageXml)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async fetchAncestors(pageId) {
|
|
39
|
+
const data = await this.http.v1.get(`content/${pageId}`, { searchParams: { expand: "ancestors" } }).json();
|
|
40
|
+
return [...(data.ancestors ?? []).map((a) => ({
|
|
41
|
+
id: String(a.id),
|
|
42
|
+
title: a.title ?? String(a.id)
|
|
43
|
+
})), {
|
|
44
|
+
id: String(data.id),
|
|
45
|
+
title: data.title
|
|
46
|
+
}];
|
|
47
|
+
}
|
|
48
|
+
/** Suit la pagination `start`/`limit` de l'API v1 - sinon les dossiers de plus de 200 pages étaient tronqués silencieusement. */
|
|
49
|
+
async fetchChildren(pageId) {
|
|
50
|
+
const results = [];
|
|
51
|
+
const limit = 200;
|
|
52
|
+
let start = 0;
|
|
53
|
+
for (let page = 0; page < 1e3; page++) {
|
|
54
|
+
const batch = (await this.http.v1.get(`content/${pageId}/child/page`, { searchParams: {
|
|
55
|
+
start,
|
|
56
|
+
limit,
|
|
57
|
+
expand: "version"
|
|
58
|
+
} }).json()).results ?? [];
|
|
59
|
+
results.push(...batch.map((p) => ({
|
|
60
|
+
id: String(p.id),
|
|
61
|
+
title: p.title
|
|
62
|
+
})));
|
|
63
|
+
if (batch.length < limit) break;
|
|
64
|
+
start += limit;
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
async findByTitle(title) {
|
|
69
|
+
for (const variant of titleVariants(title)) {
|
|
70
|
+
const data = await this.http.v1.get("content", { searchParams: {
|
|
71
|
+
title: variant,
|
|
72
|
+
spaceKey: this.configuration.spaceKey,
|
|
73
|
+
expand: "version"
|
|
74
|
+
} }).json();
|
|
75
|
+
if (data.results?.[0]) return data.results[0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async getVersion(pageId) {
|
|
79
|
+
return this.http.v1.get(`content/${pageId}`, { searchParams: { expand: "version" } }).json();
|
|
80
|
+
}
|
|
81
|
+
async delete(pageId) {
|
|
82
|
+
await this.http.v1.delete(`content/${pageId}`);
|
|
83
|
+
}
|
|
84
|
+
/** Ajoute des labels à une page (n'écrase ni ne retire les existants - API additive). */
|
|
85
|
+
async addLabels(pageId, labels) {
|
|
86
|
+
if (!labels.length) return;
|
|
87
|
+
await this.http.v1.post(`content/${pageId}/label`, { json: labels.map((name) => ({
|
|
88
|
+
prefix: "global",
|
|
89
|
+
name
|
|
90
|
+
})) });
|
|
91
|
+
}
|
|
92
|
+
async create(title, storageContent, parentId) {
|
|
93
|
+
const effectiveParent = parentId ?? this.configuration.defaultParentPageId;
|
|
94
|
+
const payload = {
|
|
95
|
+
type: "page",
|
|
96
|
+
title,
|
|
97
|
+
space: { key: this.configuration.spaceKey },
|
|
98
|
+
body: { storage: {
|
|
99
|
+
value: storageContent,
|
|
100
|
+
representation: "storage"
|
|
101
|
+
} }
|
|
102
|
+
};
|
|
103
|
+
if (effectiveParent) payload.ancestors = [{ id: String(effectiveParent) }];
|
|
104
|
+
return this.http.v1.post("content", { json: payload }).json();
|
|
105
|
+
}
|
|
106
|
+
async update(pageId, title, storageContent, parentId) {
|
|
107
|
+
const version = await this.getVersion(pageId);
|
|
108
|
+
const effectiveParent = parentId ?? this.configuration.defaultParentPageId;
|
|
109
|
+
const payload = {
|
|
110
|
+
id: pageId,
|
|
111
|
+
type: "page",
|
|
112
|
+
title,
|
|
113
|
+
version: { number: version.version.number + 1 },
|
|
114
|
+
body: { storage: {
|
|
115
|
+
value: storageContent,
|
|
116
|
+
representation: "storage"
|
|
117
|
+
} }
|
|
118
|
+
};
|
|
119
|
+
if (effectiveParent) payload.ancestors = [{ id: String(effectiveParent) }];
|
|
120
|
+
return this.http.v1.put(`content/${pageId}`, { json: payload }).json();
|
|
121
|
+
}
|
|
122
|
+
async updateContent(pageId, storageContent) {
|
|
123
|
+
const page = await this.getVersion(pageId);
|
|
124
|
+
return this.update(pageId, page.title, storageContent);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { ConfluencePagesClient };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { wikiBase } from "./utils/confluence-url.js";
|
|
2
|
+
import { ConfluenceAttachmentsClient } from "./clients/attachments-client.js";
|
|
3
|
+
import { ConfluenceFoldersClient } from "./clients/folders-client.js";
|
|
4
|
+
import { ConfluencePagesClient } from "./clients/pages-client.js";
|
|
5
|
+
import ky from "ky";
|
|
6
|
+
//#region src/clients/confluence/confluence-client.ts
|
|
7
|
+
function createConfluenceClient(configuration) {
|
|
8
|
+
const http = createConfluenceHttp(configuration);
|
|
9
|
+
const attachments = new ConfluenceAttachmentsClient(http);
|
|
10
|
+
return {
|
|
11
|
+
pages: new ConfluencePagesClient(http, configuration),
|
|
12
|
+
folders: new ConfluenceFoldersClient(http),
|
|
13
|
+
attachments,
|
|
14
|
+
configuration,
|
|
15
|
+
foldersV2Unavailable: false
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function basicAuth(username, apiToken) {
|
|
19
|
+
return `Basic ${Buffer.from(`${username}:${apiToken}`).toString("base64")}`;
|
|
20
|
+
}
|
|
21
|
+
const CONFLUENCE_TIMEOUT_MS = 6e4;
|
|
22
|
+
function createConfluenceHttp(conf) {
|
|
23
|
+
return {
|
|
24
|
+
baseUrl: conf.baseUrl,
|
|
25
|
+
v1: ky.create({
|
|
26
|
+
prefixUrl: `${wikiBase(conf.baseUrl)}/rest/api`,
|
|
27
|
+
timeout: CONFLUENCE_TIMEOUT_MS,
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: basicAuth(conf.username, conf.apiToken),
|
|
30
|
+
Accept: "application/json"
|
|
31
|
+
}
|
|
32
|
+
}),
|
|
33
|
+
v2: ky.create({
|
|
34
|
+
prefixUrl: `${wikiBase(conf.baseUrl)}/api/v2`,
|
|
35
|
+
timeout: CONFLUENCE_TIMEOUT_MS,
|
|
36
|
+
headers: {
|
|
37
|
+
Authorization: basicAuth(conf.username, conf.apiToken),
|
|
38
|
+
Accept: "application/json"
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { createConfluenceClient, createConfluenceHttp };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/clients/confluence/utils/confluence-url.ts
|
|
2
|
+
function wikiBase(baseUrl) {
|
|
3
|
+
return `${baseUrl.replace(/\/$/, "")}/wiki`;
|
|
4
|
+
}
|
|
5
|
+
function wikiLink(baseUrl, link) {
|
|
6
|
+
if (link.startsWith("http")) return link;
|
|
7
|
+
const base = baseUrl.replace(/\/$/, "");
|
|
8
|
+
return link.startsWith("/wiki") ? `${base}${link}` : `${base}/wiki${link}`;
|
|
9
|
+
}
|
|
10
|
+
function pageUrl(baseUrl, spaceKey, pageId) {
|
|
11
|
+
return `${wikiBase(baseUrl)}/spaces/${spaceKey}/pages/${pageId}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Variantes d'un titre à essayer lors d'une recherche : Confluence peut stocker
|
|
15
|
+
* une apostrophe droite (') ou typographique (’) selon la source.
|
|
16
|
+
*/
|
|
17
|
+
function titleVariants(title) {
|
|
18
|
+
return [.../* @__PURE__ */ new Set([
|
|
19
|
+
title,
|
|
20
|
+
title.replace(/'/g, "’"),
|
|
21
|
+
title.replace(/’/g, "'")
|
|
22
|
+
])];
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { pageUrl, titleVariants, wikiBase, wikiLink };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/clients/confluence/utils/confluence-util.ts
|
|
2
|
+
/** Extrait un libellé d'erreur lisible (statut HTTP s'il existe, sinon message). */
|
|
3
|
+
async function describeError(err) {
|
|
4
|
+
const e = err;
|
|
5
|
+
if (e?.response) {
|
|
6
|
+
const status = e.response.status;
|
|
7
|
+
let body = "";
|
|
8
|
+
try {
|
|
9
|
+
body = (await e.response.text()).slice(0, 120);
|
|
10
|
+
} catch {}
|
|
11
|
+
return `HTTP ${status}${body ? ` - ${body.replace(/\s+/g, " ").trim()}` : ""}`;
|
|
12
|
+
}
|
|
13
|
+
return e?.message ?? e?.name ?? String(err);
|
|
14
|
+
}
|
|
15
|
+
/** Type-guard : vrai si le téléchargement a réussi. */
|
|
16
|
+
function isAttachmentDownload(r) {
|
|
17
|
+
return "data" in r;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { describeError, isAttachmentDownload };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { initAgentMdOnly, initCodoc } from "../use-cases/init/init.js";
|
|
2
|
+
import { printAgentMdSummary, printInitSummary } from "../services/log/init-printer.js";
|
|
3
|
+
import { Command, Flags } from "@oclif/core";
|
|
4
|
+
//#region src/commands/init.ts
|
|
5
|
+
var Init = class Init extends Command {
|
|
6
|
+
static description = "Initialise la configuration codoc : codoc.yaml, .env-codoc, guide de démarrage. Avec --agent-md, ne génère que le guide agent IA (rien d'autre).";
|
|
7
|
+
static examples = ["<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --agent-md"];
|
|
8
|
+
static flags = {
|
|
9
|
+
"agent-md": Flags.boolean({
|
|
10
|
+
default: false,
|
|
11
|
+
description: "Ne génère QUE le guide agent IA basé sur `codoc.yaml` (qui doit déjà exister et être rempli) - au choix (via --agent-target, sinon celui déjà en place, sinon demandé) dans `.github/agents/codoc-agent.md` (fichier dédié) ou injecté en bloc dans `.github/copilot-instructions.md`. N'affecte aucun autre fichier."
|
|
12
|
+
}),
|
|
13
|
+
"agent-target": Flags.string({
|
|
14
|
+
options: ["agent", "copilot"],
|
|
15
|
+
description: "Avec --agent-md : destination du guide (fichier dédié ou bloc copilot-instructions.md). Par défaut : celle déjà en place, sinon demandé."
|
|
16
|
+
})
|
|
17
|
+
};
|
|
18
|
+
async run() {
|
|
19
|
+
const { flags } = await this.parse(Init);
|
|
20
|
+
if (flags["agent-md"]) {
|
|
21
|
+
printAgentMdSummary(await initAgentMdOnly(flags["agent-target"]));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { docRelPath, created, skipped, warnings } = initCodoc();
|
|
25
|
+
printInitSummary(created, skipped, warnings, docRelPath);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { Init as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { confluenceEnvRequirements } from "../config/codoc-config-atlassian.js";
|
|
2
|
+
import { loadRawConfig } from "../config/codoc-config-raw.js";
|
|
3
|
+
import { ensureEnvVars } from "../services/ensure-env.js";
|
|
4
|
+
import { publish } from "../use-cases/publish/publish.js";
|
|
5
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
6
|
+
//#region src/commands/publish.ts
|
|
7
|
+
var Publish = class Publish extends Command {
|
|
8
|
+
static description = "Publie une doc locale (fichier ou dossier) vers Confluence, avec ajout optionnel à codoc.yaml. Toute information non fournie en flag (ni déjà présente dans codoc.yaml) est demandée en console.";
|
|
9
|
+
static args = { path: Args.string({
|
|
10
|
+
description: "Chemin local relatif de la doc .md ou du dossier à publier",
|
|
11
|
+
required: false
|
|
12
|
+
}) };
|
|
13
|
+
static flags = {
|
|
14
|
+
env: Flags.string({ description: "Environnement Confluence cible (clé). Par défaut : celui de la config existante, sinon auto/prompt." }),
|
|
15
|
+
"in-config": Flags.boolean({
|
|
16
|
+
allowNo: true,
|
|
17
|
+
description: "Ajoute (ou met à jour) l'entrée codoc.yaml de cette doc. --no-in-config publie sans toucher ni à codoc.yaml ni à codoc.lock (publication ponctuelle, non suivie par `codoc sync`). Non fourni : demande en fin de commande."
|
|
18
|
+
}),
|
|
19
|
+
"parent-page-id": Flags.string({ description: "parentPageId Confluence cible. Par défaut : celui de la config existante ou defaultParentPageId de l’env." }),
|
|
20
|
+
title: Flags.string({ description: "Titre de la page Confluence (fichier unique uniquement). Par défaut : celui de la config existante, sinon le H1 du fichier." })
|
|
21
|
+
};
|
|
22
|
+
async run() {
|
|
23
|
+
const { args, flags } = await this.parse(Publish);
|
|
24
|
+
await ensureEnvVars(confluenceEnvRequirements(loadRawConfig().atlassian?.environments));
|
|
25
|
+
await publish(args.path ?? "", {
|
|
26
|
+
env: flags.env,
|
|
27
|
+
inConfig: flags["in-config"],
|
|
28
|
+
parentPageId: flags["parent-page-id"],
|
|
29
|
+
title: flags.title
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
34
|
+
export { Publish as default };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { confluenceEnvRequirements } from "../config/codoc-config-atlassian.js";
|
|
2
|
+
import { loadRawConfig } from "../config/codoc-config-raw.js";
|
|
3
|
+
import { ensureEnvVars } from "../services/ensure-env.js";
|
|
4
|
+
import { pull } from "../use-cases/pull/pull.js";
|
|
5
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
6
|
+
//#region src/commands/pull.ts
|
|
7
|
+
var Pull = class Pull extends Command {
|
|
8
|
+
static description = "Importe une page Confluence en local, avec ajout optionnel à codoc.yaml. Toute information non fournie en flag (ni déjà présente dans codoc.yaml) est demandée en console.";
|
|
9
|
+
static args = { page: Args.string({
|
|
10
|
+
description: "URL ou ID de la page Confluence à importer",
|
|
11
|
+
required: false
|
|
12
|
+
}) };
|
|
13
|
+
static flags = {
|
|
14
|
+
env: Flags.string({ description: "Environnement Confluence où chercher la page. Par défaut : déduit de l’URL, sinon auto/prompt." }),
|
|
15
|
+
"as-folder": Flags.boolean({
|
|
16
|
+
allowNo: true,
|
|
17
|
+
description: "Si la page a des sous-pages/sous-dossiers, importe tout le dossier (--as-folder) ou seulement la page (--no-as-folder). Sans sous-élément, ignoré. Non fourni : demande le cas échéant."
|
|
18
|
+
}),
|
|
19
|
+
"keep-existing": Flags.boolean({
|
|
20
|
+
allowNo: true,
|
|
21
|
+
description: "Si un import précédent est détecté (même ID/titre/config), le remplace (--keep-existing, valeurs existantes comme défauts) ou crée un import séparé (--no-keep-existing). Non fourni : demande le cas échéant."
|
|
22
|
+
}),
|
|
23
|
+
"in-config": Flags.boolean({
|
|
24
|
+
allowNo: true,
|
|
25
|
+
description: "Ajoute (ou met à jour) l'entrée codoc.yaml de cette doc. --no-in-config importe sans toucher ni à codoc.yaml ni à codoc.lock (import ponctuel, non suivi par `codoc sync`). Non fourni : demande en fin de commande."
|
|
26
|
+
}),
|
|
27
|
+
"local-path": Flags.string({ description: "Chemin local du fichier .md (page unique) ou du dossier de destination (import dossier)." }),
|
|
28
|
+
title: Flags.string({ description: "Titre de la page Confluence (page unique). Par défaut : celui de la config existante, sinon le titre Confluence." }),
|
|
29
|
+
"parent-page-id": Flags.string({ description: "parentPageId Confluence. Par défaut : celui de la config existante, sinon le parent réel de la page." }),
|
|
30
|
+
"maintained-in": Flags.string({
|
|
31
|
+
options: ["code", "confluence"],
|
|
32
|
+
description: "code → le .md local fait foi ; confluence → la page Confluence fait foi. Par défaut : confluence."
|
|
33
|
+
}),
|
|
34
|
+
"images-dir": Flags.string({ description: "Dossier local pour les images (\"\" pour désactiver). Par défaut : doc/img." })
|
|
35
|
+
};
|
|
36
|
+
async run() {
|
|
37
|
+
const { args, flags } = await this.parse(Pull);
|
|
38
|
+
await ensureEnvVars(confluenceEnvRequirements(loadRawConfig().atlassian?.environments));
|
|
39
|
+
await pull({
|
|
40
|
+
page: args.page,
|
|
41
|
+
env: flags.env,
|
|
42
|
+
asFolder: flags["as-folder"],
|
|
43
|
+
keepExisting: flags["keep-existing"],
|
|
44
|
+
inConfig: flags["in-config"],
|
|
45
|
+
localPath: flags["local-path"],
|
|
46
|
+
title: flags.title,
|
|
47
|
+
parentPageId: flags["parent-page-id"],
|
|
48
|
+
maintainedIn: flags["maintained-in"],
|
|
49
|
+
imagesDir: flags["images-dir"]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Pull as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { confluenceEnvRequirements } from "../config/codoc-config-atlassian.js";
|
|
2
|
+
import { loadRawConfig } from "../config/codoc-config-raw.js";
|
|
3
|
+
import { ensureEnvVars } from "../services/ensure-env.js";
|
|
4
|
+
import { sync } from "../use-cases/sync/sync.js";
|
|
5
|
+
import { Command, Flags } from "@oclif/core";
|
|
6
|
+
//#region src/commands/sync.ts
|
|
7
|
+
var Sync = class Sync extends Command {
|
|
8
|
+
static description = "Synchronise chaque doc locale et Confluence";
|
|
9
|
+
static flags = {
|
|
10
|
+
env: Flags.string({ description: "Ne synchronise que cet environnement Confluence (clé). Par défaut : tous." }),
|
|
11
|
+
confirm: Flags.boolean({
|
|
12
|
+
char: "f",
|
|
13
|
+
allowNo: true,
|
|
14
|
+
description: "Répond automatiquement aux suppressions de pages et adoptions de page en conflit de titre : --confirm accepte tout (CI), --no-confirm refuse tout (rien de risqué ne se fait). Non fourni : demande à chaque cas, comme en local."
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
async run() {
|
|
18
|
+
const { flags } = await this.parse(Sync);
|
|
19
|
+
await ensureEnvVars(confluenceEnvRequirements(loadRawConfig().atlassian?.environments));
|
|
20
|
+
await sync({
|
|
21
|
+
env: flags.env,
|
|
22
|
+
confirm: flags.confirm
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { Sync as default };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { confluenceEnvRequirements } from "../config/codoc-config-atlassian.js";
|
|
2
|
+
import { loadRawConfig } from "../config/codoc-config-raw.js";
|
|
3
|
+
import { ensureEnvVars } from "../services/ensure-env.js";
|
|
4
|
+
import { printDocsTree } from "../use-cases/tree/docs-tree.js";
|
|
5
|
+
import { Command, Flags } from "@oclif/core";
|
|
6
|
+
//#region src/commands/tree.ts
|
|
7
|
+
var Tree = class Tree extends Command {
|
|
8
|
+
static description = "Affiche l’arborescence des documents";
|
|
9
|
+
static flags = { env: Flags.string({ description: "N’affiche que cet environnement Confluence (clé). Par défaut : tous." }) };
|
|
10
|
+
async run() {
|
|
11
|
+
const { flags } = await this.parse(Tree);
|
|
12
|
+
await ensureEnvVars(confluenceEnvRequirements(loadRawConfig().atlassian?.environments));
|
|
13
|
+
await printDocsTree({ env: flags.env });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { Tree as default };
|