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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
//#region src/services/log/logger.ts
|
|
3
|
+
const raw = (text) => console.log(text);
|
|
4
|
+
const warnRaw = (text) => console.warn(text);
|
|
5
|
+
const ansi = {
|
|
6
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
7
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
8
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
9
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
10
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
11
|
+
link: (text, url) => url ? `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\` : text
|
|
12
|
+
};
|
|
13
|
+
const INDENT = [
|
|
14
|
+
"",
|
|
15
|
+
" ",
|
|
16
|
+
" ",
|
|
17
|
+
" ",
|
|
18
|
+
" "
|
|
19
|
+
];
|
|
20
|
+
function info(level, text) {
|
|
21
|
+
raw(`${INDENT[level]}ℹ️ ${text}`);
|
|
22
|
+
}
|
|
23
|
+
function success(level, text) {
|
|
24
|
+
raw(`${INDENT[level]}✅ ${text}`);
|
|
25
|
+
}
|
|
26
|
+
function warning(level, text) {
|
|
27
|
+
warnRaw(`${INDENT[level]}⚠️ ${text}`);
|
|
28
|
+
}
|
|
29
|
+
function error(level, text) {
|
|
30
|
+
warnRaw(`${INDENT[level]}❌ ${text}`);
|
|
31
|
+
}
|
|
32
|
+
function step(level, text) {
|
|
33
|
+
raw(`${INDENT[level]}${text}…`);
|
|
34
|
+
}
|
|
35
|
+
const log = {
|
|
36
|
+
info0: (text) => info(0, text),
|
|
37
|
+
info1: (text) => info(1, text),
|
|
38
|
+
info2: (text) => info(2, text),
|
|
39
|
+
info3: (text) => info(3, text),
|
|
40
|
+
info4: (text) => info(4, text),
|
|
41
|
+
success0: (text) => success(0, text),
|
|
42
|
+
success1: (text) => success(1, text),
|
|
43
|
+
success2: (text) => success(2, text),
|
|
44
|
+
success3: (text) => success(3, text),
|
|
45
|
+
success4: (text) => success(4, text),
|
|
46
|
+
warning0: (text) => warning(0, text),
|
|
47
|
+
warning1: (text) => warning(1, text),
|
|
48
|
+
warning2: (text) => warning(2, text),
|
|
49
|
+
warning3: (text) => warning(3, text),
|
|
50
|
+
warning4: (text) => warning(4, text),
|
|
51
|
+
error0: (text) => error(0, text),
|
|
52
|
+
error1: (text) => error(1, text),
|
|
53
|
+
error2: (text) => error(2, text),
|
|
54
|
+
error3: (text) => error(3, text),
|
|
55
|
+
error4: (text) => error(4, text),
|
|
56
|
+
step0: (text) => step(0, text),
|
|
57
|
+
step1: (text) => step(1, text),
|
|
58
|
+
step2: (text) => step(2, text),
|
|
59
|
+
step3: (text) => step(3, text),
|
|
60
|
+
step4: (text) => step(4, text),
|
|
61
|
+
blank() {
|
|
62
|
+
raw("");
|
|
63
|
+
},
|
|
64
|
+
raw(text) {
|
|
65
|
+
raw(text);
|
|
66
|
+
},
|
|
67
|
+
startProcess(text) {
|
|
68
|
+
raw(`\n${text}\n`);
|
|
69
|
+
},
|
|
70
|
+
endProcess(text) {
|
|
71
|
+
raw(`\n${text}`);
|
|
72
|
+
},
|
|
73
|
+
field(label, value) {
|
|
74
|
+
raw(`${INDENT[1]}${label} : ${value}`);
|
|
75
|
+
},
|
|
76
|
+
itemPublished(p) {
|
|
77
|
+
raw(`${INDENT[2]}${p.isUpdate ? "[UPDATED]" : "[CREATED]"} [${p.env}] ${p.title}…`);
|
|
78
|
+
},
|
|
79
|
+
itemPulled(p) {
|
|
80
|
+
const tail = p.dest ? ` → ${p.dest}` : "";
|
|
81
|
+
raw(`${INDENT[2]}[PULL] [${p.env}] ${p.title}${tail}…`);
|
|
82
|
+
},
|
|
83
|
+
itemFolder(p) {
|
|
84
|
+
raw(`${INDENT[2]}[FOLDER] [${p.env}] ${p.title}…`);
|
|
85
|
+
},
|
|
86
|
+
itemDeleted(p) {
|
|
87
|
+
raw(`${INDENT[2]}[DELETED] [${p.env}] ${p.title}`);
|
|
88
|
+
},
|
|
89
|
+
itemKept(text) {
|
|
90
|
+
raw(`${INDENT[2]}[UNCHANGED] ${text}`);
|
|
91
|
+
},
|
|
92
|
+
itemCleaned(text) {
|
|
93
|
+
raw(`${INDENT[2]}[REMOVED] ${text}`);
|
|
94
|
+
},
|
|
95
|
+
progress(i, total, text) {
|
|
96
|
+
raw(`${INDENT[3]}[${i}/${total}] ${text}`);
|
|
97
|
+
},
|
|
98
|
+
async checklist(message, items, options = {}) {
|
|
99
|
+
return checkbox({
|
|
100
|
+
choices: items.map((item) => ({
|
|
101
|
+
name: item,
|
|
102
|
+
value: item,
|
|
103
|
+
checked: true
|
|
104
|
+
})),
|
|
105
|
+
loop: false,
|
|
106
|
+
message,
|
|
107
|
+
pageSize: options.pageSize ?? 30,
|
|
108
|
+
required: options.required ?? false
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
finalBanner(title, fields = {}) {
|
|
112
|
+
const INNER_WIDTH = 54;
|
|
113
|
+
const bar = "═".repeat(INNER_WIDTH);
|
|
114
|
+
const titleLine = ` ${title}`.padEnd(INNER_WIDTH, " ");
|
|
115
|
+
raw("");
|
|
116
|
+
raw(`╔${bar}╗`);
|
|
117
|
+
raw(`║${titleLine}║`);
|
|
118
|
+
raw(`╚${bar}╝`);
|
|
119
|
+
const entries = Object.entries(fields).filter(([, v]) => v !== void 0);
|
|
120
|
+
if (entries.length) {
|
|
121
|
+
raw("");
|
|
122
|
+
for (const [k, v] of entries) {
|
|
123
|
+
const indented = String(v).replace(/\n/g, `\n ${" ".repeat(k.length + 3)}`);
|
|
124
|
+
raw(` ${k} : ${indented}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
raw("");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { ansi, log };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import readline from "readline/promises";
|
|
2
|
+
import { stdin, stdout } from "process";
|
|
3
|
+
//#region src/services/prompt.ts
|
|
4
|
+
function createRl() {
|
|
5
|
+
return readline.createInterface({
|
|
6
|
+
input: stdin,
|
|
7
|
+
output: stdout
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async function ask(rl, question) {
|
|
11
|
+
return (await rl.question(` ${question} `)).trim();
|
|
12
|
+
}
|
|
13
|
+
async function askWithDefault(rl, question, defaultValue) {
|
|
14
|
+
return await ask(rl, `${question} [${defaultValue}]`) || defaultValue;
|
|
15
|
+
}
|
|
16
|
+
async function confirm(rl, question, defaultForce = false) {
|
|
17
|
+
const answer = (await ask(rl, `${question} ${defaultForce ? "[O/n]" : "[o/n]"}`)).toLowerCase();
|
|
18
|
+
return defaultForce ? answer !== "n" : answer === "o";
|
|
19
|
+
}
|
|
20
|
+
/** Résolution uniforme d'une valeur : flag CLI → valeur codoc.yaml → prompt (avec `promptDefault`
|
|
21
|
+
* comme valeur pré-remplie affichée, ce qui ne dispense pas de la question). */
|
|
22
|
+
async function resolveValue(rl, opts) {
|
|
23
|
+
if (opts.flag !== void 0) return opts.flag;
|
|
24
|
+
if (opts.config !== void 0) return opts.config;
|
|
25
|
+
return opts.promptDefault !== void 0 ? askWithDefault(rl, opts.prompt, opts.promptDefault) : ask(rl, opts.prompt);
|
|
26
|
+
}
|
|
27
|
+
/** Résolution uniforme d'une décision oui/non : flag CLI → prompt. */
|
|
28
|
+
async function resolveConfirm(rl, opts) {
|
|
29
|
+
return opts.flag ?? await confirm(rl, opts.question, opts.default);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { ask, askWithDefault, confirm, createRl, resolveConfirm, resolveValue };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ensureDir, fileExists, readFile, removePath, writeFile } from "../../services/files-service.js";
|
|
2
|
+
import { CODOC_ENV_FILE, CONFIG_PATH, PROJECT_ROOT } from "../../config/codoc-paths.js";
|
|
3
|
+
import { getCodocConfig } from "../../config/codoc-config.js";
|
|
4
|
+
import { generateRandomCodocId } from "../../services/codoc-id.js";
|
|
5
|
+
import { agentMdContent } from "../../services/init-templates/agent-md-init.js";
|
|
6
|
+
import { docguideContent } from "../../services/init-templates/doc-guide-init.js";
|
|
7
|
+
import { envTemplate } from "../../services/init-templates/env-init.js";
|
|
8
|
+
import { yamlTemplate } from "../../services/init-templates/yaml-init.js";
|
|
9
|
+
import { askWithDefault, createRl } from "../../services/prompt.js";
|
|
10
|
+
import crypto from "node:crypto";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
//#region src/use-cases/init/init.ts
|
|
13
|
+
const AGENT_MD_PATH = path.join(PROJECT_ROOT, ".github", "agents", "codoc-agent.md");
|
|
14
|
+
const AGENT_MD_LABEL = ".github/agents/codoc-agent.md";
|
|
15
|
+
const LEGACY_AGENT_MD_PATH = path.join(PROJECT_ROOT, ".github", "codoc-agent.md");
|
|
16
|
+
const COPILOT_INSTRUCTIONS_PATH = path.join(PROJECT_ROOT, ".github", "copilot-instructions.md");
|
|
17
|
+
const COPILOT_INSTRUCTIONS_LABEL = ".github/copilot-instructions.md";
|
|
18
|
+
const COPILOT_BLOCK_START = "<!-- codoc-agent:start -->";
|
|
19
|
+
const COPILOT_BLOCK_END = "<!-- codoc-agent:end -->";
|
|
20
|
+
function escapeRegex(s) {
|
|
21
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
|
+
}
|
|
23
|
+
function randomId() {
|
|
24
|
+
return crypto.randomBytes(3).toString("hex");
|
|
25
|
+
}
|
|
26
|
+
function initCodoc() {
|
|
27
|
+
const created = [];
|
|
28
|
+
const skipped = [];
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const id = randomId();
|
|
31
|
+
const docDir = path.join(PROJECT_ROOT, "doc");
|
|
32
|
+
const docFilename = `codoc-guide-${id}.md`;
|
|
33
|
+
const docAbsPath = path.join(docDir, docFilename);
|
|
34
|
+
const docRelPath = `doc/${docFilename}`;
|
|
35
|
+
ensureDir(docDir);
|
|
36
|
+
writeFile(docAbsPath, docguideContent(id));
|
|
37
|
+
created.push(docRelPath);
|
|
38
|
+
writeIfAbsent(CONFIG_PATH, "codoc.yaml", () => yamlTemplate(docRelPath, generateRandomCodocId()), created, skipped);
|
|
39
|
+
writeIfAbsent(CODOC_ENV_FILE, ".env-codoc", envTemplate, created, skipped);
|
|
40
|
+
ensureGitignoreEntries([".env-codoc"], created, skipped);
|
|
41
|
+
return {
|
|
42
|
+
docRelPath,
|
|
43
|
+
created,
|
|
44
|
+
skipped,
|
|
45
|
+
warnings
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** `codoc init --agent-md` : ne génère que le guide agent IA, rien d'autre. Demande la destination si aucune n'existe déjà. */
|
|
49
|
+
async function initAgentMdOnly(agentTarget) {
|
|
50
|
+
const created = [];
|
|
51
|
+
const warnings = [];
|
|
52
|
+
generateAgentMd(await resolveAgentMdTarget(agentTarget), created, warnings);
|
|
53
|
+
return {
|
|
54
|
+
created,
|
|
55
|
+
warnings
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Flag → fichier déjà en place (fichier dédié ou bloc dans copilot-instructions.md) → prompt. */
|
|
59
|
+
async function resolveAgentMdTarget(agentTarget) {
|
|
60
|
+
if (agentTarget === "copilot") return {
|
|
61
|
+
path: COPILOT_INSTRUCTIONS_PATH,
|
|
62
|
+
label: COPILOT_INSTRUCTIONS_LABEL
|
|
63
|
+
};
|
|
64
|
+
if (agentTarget === "agent") return {
|
|
65
|
+
path: AGENT_MD_PATH,
|
|
66
|
+
label: AGENT_MD_LABEL
|
|
67
|
+
};
|
|
68
|
+
if (fileExists(AGENT_MD_PATH)) return {
|
|
69
|
+
path: AGENT_MD_PATH,
|
|
70
|
+
label: AGENT_MD_LABEL
|
|
71
|
+
};
|
|
72
|
+
if (fileExists(COPILOT_INSTRUCTIONS_PATH)) return {
|
|
73
|
+
path: COPILOT_INSTRUCTIONS_PATH,
|
|
74
|
+
label: COPILOT_INSTRUCTIONS_LABEL
|
|
75
|
+
};
|
|
76
|
+
const rl = createRl();
|
|
77
|
+
try {
|
|
78
|
+
return (await askWithDefault(rl, "Générer le guide dans un fichier agent dédié ou dans le contexte Copilot ? (agent/copilot) :", "agent")).trim().toLowerCase().startsWith("copi") ? {
|
|
79
|
+
path: COPILOT_INSTRUCTIONS_PATH,
|
|
80
|
+
label: COPILOT_INSTRUCTIONS_LABEL
|
|
81
|
+
} : {
|
|
82
|
+
path: AGENT_MD_PATH,
|
|
83
|
+
label: AGENT_MD_LABEL
|
|
84
|
+
};
|
|
85
|
+
} finally {
|
|
86
|
+
rl.close();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Génère le guide agent IA à la destination choisie, migre l'ancien emplacement `.github/codoc-agent.md` si besoin. */
|
|
90
|
+
function generateAgentMd(target, created, warnings) {
|
|
91
|
+
try {
|
|
92
|
+
const config = getCodocConfig();
|
|
93
|
+
const content = agentMdContent(config);
|
|
94
|
+
const hadLegacy = fileExists(LEGACY_AGENT_MD_PATH);
|
|
95
|
+
const status = target.path === COPILOT_INSTRUCTIONS_PATH ? writeAgentMdBlock(COPILOT_INSTRUCTIONS_PATH, content) : writeDedicatedAgentMd(AGENT_MD_PATH, content);
|
|
96
|
+
if (hadLegacy && target.path !== LEGACY_AGENT_MD_PATH) removePath(LEGACY_AGENT_MD_PATH);
|
|
97
|
+
created.push(`${target.label} (${hadLegacy ? "déplacé depuis .github/codoc-agent.md" : status})`);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
warnings.push(`${target.label} : impossible de générer - ${err.message.split("\n")[0]}. Remplis codoc.yaml puis relance \`codoc init --agent-md\`.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Fichier dédié et entièrement possédé par codoc : contenu remplacé intégralement. */
|
|
103
|
+
function writeDedicatedAgentMd(absPath, content) {
|
|
104
|
+
const existed = fileExists(absPath);
|
|
105
|
+
writeFile(absPath, content);
|
|
106
|
+
return existed ? "regénéré" : "créé";
|
|
107
|
+
}
|
|
108
|
+
/** copilot-instructions.md est partagé avec d'autres instructions : injection idempotente par bloc balisé. */
|
|
109
|
+
function writeAgentMdBlock(absPath, content) {
|
|
110
|
+
const block = `${COPILOT_BLOCK_START}\n${content.trim()}\n${COPILOT_BLOCK_END}`;
|
|
111
|
+
if (!fileExists(absPath)) {
|
|
112
|
+
ensureDir(path.dirname(absPath));
|
|
113
|
+
writeFile(absPath, `${block}\n`);
|
|
114
|
+
return "créé";
|
|
115
|
+
}
|
|
116
|
+
const existing = readFile(absPath);
|
|
117
|
+
const blockRe = new RegExp(`${escapeRegex(COPILOT_BLOCK_START)}[\\s\\S]*?${escapeRegex(COPILOT_BLOCK_END)}`);
|
|
118
|
+
if (blockRe.test(existing)) {
|
|
119
|
+
writeFile(absPath, existing.replace(blockRe, block));
|
|
120
|
+
return "regénéré";
|
|
121
|
+
}
|
|
122
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
123
|
+
writeFile(absPath, `${existing}${separator}${block}\n`);
|
|
124
|
+
return "créé";
|
|
125
|
+
}
|
|
126
|
+
function writeIfAbsent(absPath, label, contentFn, created, skipped) {
|
|
127
|
+
if (fileExists(absPath)) skipped.push(label);
|
|
128
|
+
else {
|
|
129
|
+
writeFile(absPath, contentFn());
|
|
130
|
+
created.push(label);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Garantit que chaque entrée listée est présente dans `.gitignore` (idempotent, crée le fichier si absent). */
|
|
134
|
+
function ensureGitignoreEntries(entries, created, skipped) {
|
|
135
|
+
const gitignorePath = path.join(PROJECT_ROOT, ".gitignore");
|
|
136
|
+
const label = ".gitignore";
|
|
137
|
+
const existingLines = fileExists(gitignorePath) ? readFile(gitignorePath).split(/\r?\n/).map((l) => l.trim()) : [];
|
|
138
|
+
const missing = entries.filter((e) => !existingLines.includes(e));
|
|
139
|
+
if (!missing.length) {
|
|
140
|
+
skipped.push(`${label} (toutes les entrées codoc déjà présentes)`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const existing = fileExists(gitignorePath) ? readFile(gitignorePath) : "";
|
|
144
|
+
const block = `${!existing || existing.endsWith("\n") ? "" : "\n"}\n# Ajouté par codoc init\n${missing.join("\n")}\n`;
|
|
145
|
+
writeFile(gitignorePath, existing + block);
|
|
146
|
+
const action = fileExists(gitignorePath) && existing ? "mis à jour" : "créé";
|
|
147
|
+
created.push(`${label} (${action}, ajouté : ${missing.join(", ")})`);
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
export { initAgentMdOnly, initCodoc };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { fileExists, isDirectory, readFile, toPosixPath } from "../../services/files-service.js";
|
|
2
|
+
import { PROJECT_ROOT } from "../../config/codoc-paths.js";
|
|
3
|
+
import { getCodocConfig } from "../../config/codoc-config.js";
|
|
4
|
+
import { generateRandomCodocId } from "../../services/codoc-id.js";
|
|
5
|
+
import { ask, createRl, resolveConfirm, resolveValue } from "../../services/prompt.js";
|
|
6
|
+
import { log } from "../../services/log/logger.js";
|
|
7
|
+
import { loadPublishState, savePublishState } from "../../services/lock/lock-file.js";
|
|
8
|
+
import { getDefaultBranch } from "../../services/git/default-branch.js";
|
|
9
|
+
import { expandDocEntry, extractH1 } from "../shared/list-documents.js";
|
|
10
|
+
import { EnvRegistry } from "../shared/confluence-client-registry.js";
|
|
11
|
+
import { appendToDocsConfig, buildYamlEntry, removeDocsConfigEntries } from "../shared/codoc-yaml.js";
|
|
12
|
+
import { pickEnvInteractive, selectEnvs } from "../shared/env-select.js";
|
|
13
|
+
import { cleanDocPath } from "../sync/sync-entries.js";
|
|
14
|
+
import { syncMatched, syncNew } from "../sync/sync-handlers.js";
|
|
15
|
+
import { parsePageInput } from "../pull/parse-page-input.js";
|
|
16
|
+
import path from "path";
|
|
17
|
+
//#region src/use-cases/publish/publish.ts
|
|
18
|
+
/** Environnement cible : flag → env de la config existante (si remplacement) → auto/prompt. */
|
|
19
|
+
async function resolveEnv(rl, config, flagEnv, existingDoc) {
|
|
20
|
+
const key = flagEnv || existingDoc?.env;
|
|
21
|
+
return key ? selectEnvs(config, key)[0] : pickEnvInteractive(rl, config);
|
|
22
|
+
}
|
|
23
|
+
/** Lit la cible (fichier ou dossier), valide, et résout chaque information (flag → config → prompt). */
|
|
24
|
+
async function collectPublishInput(rl, config, inputPath, flags) {
|
|
25
|
+
const raw = (inputPath || await ask(rl, "Chemin local de la doc (.md) ou du dossier :")).trim();
|
|
26
|
+
if (!raw) throw new Error("Chemin local requis.");
|
|
27
|
+
const relPath = toPosixPath(raw).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
28
|
+
const abs = path.resolve(PROJECT_ROOT, relPath);
|
|
29
|
+
if (!fileExists(abs)) throw new Error(`Introuvable : ${relPath}`);
|
|
30
|
+
const isDir = isDirectory(abs);
|
|
31
|
+
if (!isDir && !relPath.endsWith(".md")) throw new Error("Le fichier doit être un .md (ou indique un dossier).");
|
|
32
|
+
const kind = isDir ? "folder" : "page";
|
|
33
|
+
const yamlPath = isDir ? `${relPath}/**` : relPath;
|
|
34
|
+
const existingDoc = config.docs.find((d) => cleanDocPath(d.path) === relPath);
|
|
35
|
+
if (existingDoc) log.warning1(`Une entrée codoc.yaml existe déjà pour "${existingDoc.path}".`);
|
|
36
|
+
const codocId = existingDoc?.codocId ?? generateRandomCodocId();
|
|
37
|
+
const env = await resolveEnv(rl, config, flags.env, existingDoc);
|
|
38
|
+
const rawParentPageId = await resolveValue(rl, {
|
|
39
|
+
flag: flags.parentPageId,
|
|
40
|
+
config: existingDoc?.confluence.parentPageId,
|
|
41
|
+
prompt: "URL ou ID de la page Confluence parente :",
|
|
42
|
+
promptDefault: env.defaultParentPageId ?? ""
|
|
43
|
+
});
|
|
44
|
+
const parentPageId = rawParentPageId ? parsePageInput(rawParentPageId).pageId : rawParentPageId;
|
|
45
|
+
let title;
|
|
46
|
+
if (!isDir) {
|
|
47
|
+
const h1 = extractH1(readFile(abs));
|
|
48
|
+
title = await resolveValue(rl, {
|
|
49
|
+
flag: flags.title,
|
|
50
|
+
config: existingDoc?.confluence.title,
|
|
51
|
+
prompt: "Titre de la page Confluence :",
|
|
52
|
+
promptDefault: h1 ?? path.basename(abs, ".md")
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const syntheticDoc = {
|
|
56
|
+
codocId,
|
|
57
|
+
path: yamlPath,
|
|
58
|
+
maintainedIn: "code",
|
|
59
|
+
env: env.key,
|
|
60
|
+
generateSummary: existingDoc?.generateSummary ?? true,
|
|
61
|
+
confluence: {
|
|
62
|
+
title,
|
|
63
|
+
parentPageId: parentPageId || void 0
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
env,
|
|
68
|
+
codocId,
|
|
69
|
+
existingDoc,
|
|
70
|
+
relPath,
|
|
71
|
+
yamlPath,
|
|
72
|
+
abs,
|
|
73
|
+
isDir,
|
|
74
|
+
parentPageId,
|
|
75
|
+
title,
|
|
76
|
+
entry: {
|
|
77
|
+
codocId,
|
|
78
|
+
kind,
|
|
79
|
+
path: yamlPath,
|
|
80
|
+
localPath: abs,
|
|
81
|
+
relPath,
|
|
82
|
+
maintainedIn: "code",
|
|
83
|
+
env,
|
|
84
|
+
parentPageId: parentPageId || void 0,
|
|
85
|
+
title
|
|
86
|
+
},
|
|
87
|
+
syntheticDoc
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Écrit/met à jour l'entrée yaml correspondant à la publication. */
|
|
91
|
+
async function persistYamlEntry(input) {
|
|
92
|
+
const yamlEntry = buildYamlEntry({
|
|
93
|
+
localPath: input.yamlPath,
|
|
94
|
+
title: input.title,
|
|
95
|
+
parentPageId: input.parentPageId,
|
|
96
|
+
maintainedIn: "code",
|
|
97
|
+
env: input.env.key,
|
|
98
|
+
codocId: input.codocId
|
|
99
|
+
});
|
|
100
|
+
if (input.existingDoc) {
|
|
101
|
+
await removeDocsConfigEntries([input.existingDoc.path]);
|
|
102
|
+
await appendToDocsConfig(yamlEntry);
|
|
103
|
+
log.success1("[UPDATED] codoc.yaml : entrée mise à jour");
|
|
104
|
+
} else {
|
|
105
|
+
await appendToDocsConfig(yamlEntry);
|
|
106
|
+
log.success1("[CREATED] codoc.yaml : entrée ajoutée");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Intégration dans codoc.yaml : flag `--in-config` → prompt unique en fin de commande, que la doc
|
|
110
|
+
* soit déjà suivie (mise à jour) ou nouvelle (ajout). Sur refus, la publication a quand même eu lieu -
|
|
111
|
+
* ni l'entrée yaml ni le lock ne sont touchés (publication ponctuelle et "utilitaire", volontairement
|
|
112
|
+
* hors suivi : aucune trace ne subsiste pour comparer un futur republish par ID). Retourne la décision
|
|
113
|
+
* pour que l'appelant sache s'il doit aussi persister le lock. */
|
|
114
|
+
async function maybePersistYamlEntry(rl, input, inConfigFlag) {
|
|
115
|
+
const question = input.existingDoc ? "Mettre à jour la config codoc.yaml existante pour cette doc ?" : "Ajouter cette doc à codoc.yaml (pour qu'elle soit synchronisée par `codoc sync`) ?";
|
|
116
|
+
const addToConfig = await resolveConfirm(rl, {
|
|
117
|
+
flag: inConfigFlag,
|
|
118
|
+
question,
|
|
119
|
+
default: true
|
|
120
|
+
});
|
|
121
|
+
if (addToConfig) await persistYamlEntry(input);
|
|
122
|
+
else log.warning1("[SKIPPED] codoc.yaml et codoc.lock non modifiés - publication ponctuelle, hors suivi (pas de comparaison future par ID).");
|
|
123
|
+
return addToConfig;
|
|
124
|
+
}
|
|
125
|
+
async function publish(inputPath, flags = {}) {
|
|
126
|
+
const config = getCodocConfig();
|
|
127
|
+
const rl = createRl();
|
|
128
|
+
try {
|
|
129
|
+
log.startProcess("Publication local → Confluence");
|
|
130
|
+
const input = await collectPublishInput(rl, config, inputPath, flags);
|
|
131
|
+
const targets = await expandDocEntry(input.syntheticDoc, input.env);
|
|
132
|
+
if (!targets.length) throw new Error(`Aucun fichier .md à publier sous "${input.relPath}".`);
|
|
133
|
+
const state = loadPublishState();
|
|
134
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
135
|
+
const ctx = {
|
|
136
|
+
config,
|
|
137
|
+
generatedAt: now,
|
|
138
|
+
registry: new EnvRegistry(),
|
|
139
|
+
envByKey: new Map(config.atlassian.environments.map((e) => [e.key, e])),
|
|
140
|
+
now,
|
|
141
|
+
confirm: () => Promise.resolve(true),
|
|
142
|
+
codeTargetsByCodocId: /* @__PURE__ */ new Map([[input.codocId, targets]]),
|
|
143
|
+
defaultBranch: await getDefaultBranch()
|
|
144
|
+
};
|
|
145
|
+
log.blank();
|
|
146
|
+
log.step1("Publication");
|
|
147
|
+
const lockHit = state.pages[input.codocId];
|
|
148
|
+
const published = lockHit ? await syncMatched(ctx, input.entry, lockHit) : await syncNew(ctx, input.entry);
|
|
149
|
+
if (!published) throw new Error(`Aucun fichier .md à publier sous "${input.relPath}".`);
|
|
150
|
+
if (await maybePersistYamlEntry(rl, input, flags.inConfig)) {
|
|
151
|
+
state.pages[input.codocId] = published;
|
|
152
|
+
state.lastPublished = state.lastPublished || now;
|
|
153
|
+
savePublishState(state);
|
|
154
|
+
log.success1("[UPDATED] codoc.lock mis à jour");
|
|
155
|
+
}
|
|
156
|
+
log.finalBanner("Publication terminée", {
|
|
157
|
+
"Doc locale": input.yamlPath,
|
|
158
|
+
Environnement: input.env.key,
|
|
159
|
+
...input.isDir ? { "Pages publiées": String(targets.filter((t) => !t.isFolder).length) } : { Page: input.title }
|
|
160
|
+
});
|
|
161
|
+
} finally {
|
|
162
|
+
rl.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { publish };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/use-cases/pull/parse-page-input.ts
|
|
2
|
+
function parsePageInput(rawInput) {
|
|
3
|
+
rawInput = rawInput.trim();
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(rawInput);
|
|
6
|
+
const match = url.pathname.match(/\/(pages|folder)\/(\d+)/);
|
|
7
|
+
if (!match) throw new Error("Aucun ID de page trouvé dans l'URL.");
|
|
8
|
+
return match[1] === "folder" ? {
|
|
9
|
+
pageId: match[2],
|
|
10
|
+
domain: url.hostname,
|
|
11
|
+
isFolder: true
|
|
12
|
+
} : {
|
|
13
|
+
pageId: match[2],
|
|
14
|
+
domain: url.hostname
|
|
15
|
+
};
|
|
16
|
+
} catch {
|
|
17
|
+
if (/^\d+$/.test(rawInput)) return { pageId: rawInput };
|
|
18
|
+
throw new Error(`Entrée invalide : "${rawInput}"\n Attendu : un ID numérique ou une URL Confluence complète.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { parsePageInput };
|