@ansible-flow/node 0.85.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/flowMerge.d.ts +35 -0
- package/dist/flowMerge.js +74 -0
- package/dist/flowMerge.js.map +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.
|
|
4
4
|
|
|
5
|
-
Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.
|
|
5
|
+
Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.86.0
|
|
6
6
|
and released with it. Modules are imported by their path in the source tree, for example
|
|
7
7
|
`@ansible-flow/node/ai/agents`.
|
|
8
8
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FlowMerge, type MergeSide } from '@ansible-flow/core/flow/merge';
|
|
2
|
+
/** A merge waiting for choices: the three versions, the file, and what was chosen so far. */
|
|
3
|
+
export interface MergeState {
|
|
4
|
+
/** The file's path in the repository, as git names it. */
|
|
5
|
+
file: string;
|
|
6
|
+
base: string;
|
|
7
|
+
ours: string;
|
|
8
|
+
theirs: string;
|
|
9
|
+
choices: Record<string, MergeSide>;
|
|
10
|
+
at: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const MERGE_FOLDER = "afd-merge";
|
|
13
|
+
/** Where the merge of `file` waits, in the repository's git folder. */
|
|
14
|
+
export declare function mergeStatePath(gitDir: string, file: string): string;
|
|
15
|
+
export declare function readMergeState(gitDir: string, file: string): MergeState | undefined;
|
|
16
|
+
export declare function writeMergeState(gitDir: string, state: MergeState): void;
|
|
17
|
+
export declare function removeMergeState(gitDir: string, file: string): void;
|
|
18
|
+
export type Merged =
|
|
19
|
+
/** Not three flows this can read: the caller merges the text instead. */
|
|
20
|
+
{
|
|
21
|
+
kind: 'not-flows';
|
|
22
|
+
why: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'merged';
|
|
25
|
+
text: string;
|
|
26
|
+
merge: FlowMerge;
|
|
27
|
+
/** Errors in the merged flow that neither side had: a person looks before it is taken. */
|
|
28
|
+
broken: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Merges three versions of a flow file's text, with `choices` made so far. */
|
|
31
|
+
export declare function mergeFlowTexts(base: string, ours: string, theirs: string, choices?: Record<string, MergeSide>): Merged;
|
|
32
|
+
export declare const MERGE_ATTRIBUTES: string[];
|
|
33
|
+
export declare const MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
|
|
34
|
+
/** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */
|
|
35
|
+
export declare function withMergeAttributes(text: string): string | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/node/flowMerge.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseFlow } from "@ansible-flow/core/flow/document";
|
|
5
|
+
import { mergeFlows } from "@ansible-flow/core/flow/merge";
|
|
6
|
+
import { writeMergedFlow } from "@ansible-flow/core/flow/mergeText";
|
|
7
|
+
import { validateFlow } from "@ansible-flow/core/flow/validate";
|
|
8
|
+
var MERGE_FOLDER = "afd-merge";
|
|
9
|
+
function mergeStatePath(gitDir, file) {
|
|
10
|
+
return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll("\\", "/"))}.json`);
|
|
11
|
+
}
|
|
12
|
+
function readMergeState(gitDir, file) {
|
|
13
|
+
const path = mergeStatePath(gitDir, file);
|
|
14
|
+
if (!existsSync(path)) return void 0;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeMergeState(gitDir, state) {
|
|
22
|
+
const path = mergeStatePath(gitDir, state.file);
|
|
23
|
+
mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });
|
|
24
|
+
writeFileSync(path, `${JSON.stringify(state, null, 1)}
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
function removeMergeState(gitDir, file) {
|
|
28
|
+
rmSync(mergeStatePath(gitDir, file), { force: true });
|
|
29
|
+
}
|
|
30
|
+
var errorsOf = (doc) => new Set(
|
|
31
|
+
validateFlow(doc).findings.filter((f) => f.severity === "error").map((f) => f.message)
|
|
32
|
+
);
|
|
33
|
+
function mergeFlowTexts(base, ours, theirs, choices = {}) {
|
|
34
|
+
const docs = [base, ours, theirs].map((t2) => parseFlow(t2));
|
|
35
|
+
const names = ["the common version", "ours", "theirs"];
|
|
36
|
+
for (const [i, d] of docs.entries())
|
|
37
|
+
if (!d.doc) return { kind: "not-flows", why: `${names[i]} is not a flow this can read` };
|
|
38
|
+
const [b, o, t] = docs.map((d) => d.doc);
|
|
39
|
+
const merge = mergeFlows(b, o, t, choices);
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = writeMergedFlow(ours, merge.doc);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "not-flows",
|
|
46
|
+
why: `the merged flow could not be written: ${e.message}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const had = /* @__PURE__ */ new Set([...errorsOf(o), ...errorsOf(t)]);
|
|
50
|
+
const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));
|
|
51
|
+
return { kind: "merged", text, merge, broken };
|
|
52
|
+
}
|
|
53
|
+
var MERGE_ATTRIBUTES = ["*.flow.yaml merge=ansible-flow", "*.flow.yml merge=ansible-flow"];
|
|
54
|
+
var MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
|
|
55
|
+
function withMergeAttributes(text) {
|
|
56
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, " ")));
|
|
57
|
+
const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));
|
|
58
|
+
if (!missing.length) return void 0;
|
|
59
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
60
|
+
const head = text === "" || text.endsWith("\n") ? text : `${text}${eol}`;
|
|
61
|
+
return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
MERGE_ATTRIBUTES,
|
|
65
|
+
MERGE_DRIVER,
|
|
66
|
+
MERGE_FOLDER,
|
|
67
|
+
mergeFlowTexts,
|
|
68
|
+
mergeStatePath,
|
|
69
|
+
readMergeState,
|
|
70
|
+
removeMergeState,
|
|
71
|
+
withMergeAttributes,
|
|
72
|
+
writeMergeState
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=flowMerge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/node/flowMerge.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-283: merging a flow file the way git asks a merge driver to, and the merge left for a person\n// to finish. The driver writes the merged flow over \"ours\"; when the two sides changed the same\n// place, it writes the flow with \"ours\" for each such place, keeps the three versions beside the\n// repository's git folder (never in the work tree), and tells git the file is not merged. The\n// designer reads that state, lets the person choose, and writes the file again.\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { parseFlow } from '../core/flow/document';\nimport { type FlowMerge, type MergeSide, mergeFlows } from '../core/flow/merge';\nimport { writeMergedFlow } from '../core/flow/mergeText';\nimport type { FlowDocument } from '../core/flow/types';\nimport { validateFlow } from '../core/flow/validate';\n\n/** A merge waiting for choices: the three versions, the file, and what was chosen so far. */\nexport interface MergeState {\n /** The file's path in the repository, as git names it. */\n file: string;\n base: string;\n ours: string;\n theirs: string;\n choices: Record<string, MergeSide>;\n at: string;\n}\n\nexport const MERGE_FOLDER = 'afd-merge';\n\n/** Where the merge of `file` waits, in the repository's git folder. */\nexport function mergeStatePath(gitDir: string, file: string): string {\n return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll('\\\\', '/'))}.json`);\n}\n\nexport function readMergeState(gitDir: string, file: string): MergeState | undefined {\n const path = mergeStatePath(gitDir, file);\n if (!existsSync(path)) return undefined;\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as MergeState;\n } catch {\n return undefined;\n }\n}\n\nexport function writeMergeState(gitDir: string, state: MergeState): void {\n const path = mergeStatePath(gitDir, state.file);\n mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });\n writeFileSync(path, `${JSON.stringify(state, null, 1)}\\n`);\n}\n\nexport function removeMergeState(gitDir: string, file: string): void {\n rmSync(mergeStatePath(gitDir, file), { force: true });\n}\n\nexport type Merged =\n /** Not three flows this can read: the caller merges the text instead. */\n | { kind: 'not-flows'; why: string }\n | {\n kind: 'merged';\n text: string;\n merge: FlowMerge;\n /** Errors in the merged flow that neither side had: a person looks before it is taken. */\n broken: string[];\n };\n\nconst errorsOf = (doc: FlowDocument) =>\n new Set(\n validateFlow(doc)\n .findings.filter((f) => f.severity === 'error')\n .map((f) => f.message),\n );\n\n/** Merges three versions of a flow file's text, with `choices` made so far. */\nexport function mergeFlowTexts(\n base: string,\n ours: string,\n theirs: string,\n choices: Record<string, MergeSide> = {},\n): Merged {\n const docs = [base, ours, theirs].map((t) => parseFlow(t));\n const names = ['the common version', 'ours', 'theirs'];\n for (const [i, d] of docs.entries())\n if (!d.doc) return { kind: 'not-flows', why: `${names[i]} is not a flow this can read` };\n const [b, o, t] = docs.map((d) => d.doc as FlowDocument) as [\n FlowDocument,\n FlowDocument,\n FlowDocument,\n ];\n const merge = mergeFlows(b, o, t, choices);\n let text: string;\n try {\n text = writeMergedFlow(ours, merge.doc);\n } catch (e) {\n return {\n kind: 'not-flows',\n why: `the merged flow could not be written: ${(e as Error).message}`,\n };\n }\n const had = new Set([...errorsOf(o), ...errorsOf(t)]);\n const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));\n return { kind: 'merged', text, merge, broken };\n}\n\n// --- setting a repository up (Set Up Flow Merging in Repository)\n\nexport const MERGE_ATTRIBUTES = ['*.flow.yaml merge=ansible-flow', '*.flow.yml merge=ansible-flow'];\nexport const MERGE_DRIVER = 'node .afd/afd-merge.js %O %A %B %P';\n\n/** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */\nexport function withMergeAttributes(text: string): string | undefined {\n const have = new Set(text.split(/\\r?\\n/).map((l) => l.trim().replace(/\\s+/g, ' ')));\n const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));\n if (!missing.length) return undefined;\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const head = text === '' || text.endsWith('\\n') ? text : `${text}${eol}`;\n return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["t"]
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ansible-flow/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.86.0",
|
|
4
4
|
"description": "Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/ChistokhinSV/ansible-flow-designer/blob/main/docs/packages.md",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=22.13"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ansible-flow/core": "0.
|
|
29
|
+
"@ansible-flow/core": "0.86.0",
|
|
30
30
|
"elkjs": "0.12.0",
|
|
31
31
|
"jsonc-parser": "3.3.1",
|
|
32
32
|
"yaml": "2.9.1"
|