@texuli/tools 0.0.0-stage → 0.1.0-next.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -2
  3. package/dist/a2ui/from-a2ui.d.ts +35 -0
  4. package/dist/a2ui/from-a2ui.js +180 -0
  5. package/dist/a2ui/index.d.ts +11 -0
  6. package/dist/a2ui/index.js +11 -0
  7. package/dist/a2ui/props.d.ts +10 -0
  8. package/dist/a2ui/props.js +94 -0
  9. package/dist/a2ui/schema.d.ts +26 -0
  10. package/dist/a2ui/schema.js +193 -0
  11. package/dist/a2ui/to-a2ui.d.ts +30 -0
  12. package/dist/a2ui/to-a2ui.js +119 -0
  13. package/dist/a2ui/types.d.ts +71 -0
  14. package/dist/a2ui/types.js +11 -0
  15. package/dist/cli-add/add.d.ts +21 -0
  16. package/dist/cli-add/add.js +241 -0
  17. package/dist/cli-add/cli.d.ts +25 -0
  18. package/dist/cli-add/cli.js +157 -0
  19. package/dist/cli-add/index.d.ts +12 -0
  20. package/dist/cli-add/index.js +11 -0
  21. package/dist/cli-add/paths.d.ts +4 -0
  22. package/dist/cli-add/paths.js +4 -0
  23. package/dist/cli-add/provenance.d.ts +32 -0
  24. package/dist/cli-add/provenance.js +78 -0
  25. package/dist/cli-add/registry.d.ts +19 -0
  26. package/dist/cli-add/registry.js +50 -0
  27. package/dist/cli-add/types.d.ts +71 -0
  28. package/dist/cli-add/types.js +4 -0
  29. package/dist/cli-init/cli.d.ts +25 -0
  30. package/dist/cli-init/cli.js +212 -0
  31. package/dist/cli-init/config.d.ts +10 -0
  32. package/dist/cli-init/config.js +62 -0
  33. package/dist/cli-init/detect.d.ts +13 -0
  34. package/dist/cli-init/detect.js +42 -0
  35. package/dist/cli-init/files.d.ts +18 -0
  36. package/dist/cli-init/files.js +59 -0
  37. package/dist/cli-init/index.d.ts +14 -0
  38. package/dist/cli-init/index.js +13 -0
  39. package/dist/cli-init/init.d.ts +14 -0
  40. package/dist/cli-init/init.js +157 -0
  41. package/dist/cli-init/safe-path.d.ts +2 -0
  42. package/dist/cli-init/safe-path.js +26 -0
  43. package/dist/cli-init/theme-apply.d.ts +2 -0
  44. package/dist/cli-init/theme-apply.js +69 -0
  45. package/dist/cli-init/themes.d.ts +37 -0
  46. package/dist/cli-init/themes.js +83 -0
  47. package/dist/cli-init/types.d.ts +50 -0
  48. package/dist/cli-init/types.js +1 -0
  49. package/dist/cli-init/wire.d.ts +20 -0
  50. package/dist/cli-init/wire.js +90 -0
  51. package/dist/cli-update/cli.d.ts +26 -0
  52. package/dist/cli-update/cli.js +161 -0
  53. package/dist/cli-update/index.d.ts +11 -0
  54. package/dist/cli-update/index.js +10 -0
  55. package/dist/cli-update/lines.d.ts +38 -0
  56. package/dist/cli-update/lines.js +189 -0
  57. package/dist/cli-update/provenance.d.ts +14 -0
  58. package/dist/cli-update/provenance.js +51 -0
  59. package/dist/cli-update/types.d.ts +83 -0
  60. package/dist/cli-update/types.js +1 -0
  61. package/dist/cli-update/update.d.ts +12 -0
  62. package/dist/cli-update/update.js +261 -0
  63. package/dist/elements/content.d.ts +15 -0
  64. package/dist/elements/content.js +53 -0
  65. package/dist/elements/define.d.ts +49 -0
  66. package/dist/elements/define.js +281 -0
  67. package/dist/elements/index.d.ts +6 -0
  68. package/dist/elements/index.js +6 -0
  69. package/dist/golden/axe.d.ts +32 -0
  70. package/dist/golden/axe.js +22 -0
  71. package/dist/golden/catalog.d.ts +24 -0
  72. package/dist/golden/catalog.js +85 -0
  73. package/dist/golden/cli.d.ts +9 -0
  74. package/dist/golden/cli.js +45 -0
  75. package/dist/golden/guidance.d.ts +12 -0
  76. package/dist/golden/guidance.js +171 -0
  77. package/dist/golden/index.d.ts +17 -0
  78. package/dist/golden/index.js +17 -0
  79. package/dist/golden/outline.d.ts +21 -0
  80. package/dist/golden/outline.js +68 -0
  81. package/dist/golden/paint.d.ts +21 -0
  82. package/dist/golden/paint.js +103 -0
  83. package/dist/golden/report.d.ts +28 -0
  84. package/dist/golden/report.js +64 -0
  85. package/dist/golden/score.d.ts +16 -0
  86. package/dist/golden/score.js +116 -0
  87. package/dist/golden/sinks.d.ts +8 -0
  88. package/dist/golden/sinks.js +11 -0
  89. package/dist/golden/tasks.d.ts +4 -0
  90. package/dist/golden/tasks.js +686 -0
  91. package/dist/golden/types.d.ts +54 -0
  92. package/dist/golden/types.js +9 -0
  93. package/dist/index.d.ts +8 -0
  94. package/dist/index.js +8 -0
  95. package/dist/internal/paths.d.ts +56 -0
  96. package/dist/internal/paths.js +186 -0
  97. package/dist/llms/bundle.d.ts +6 -0
  98. package/dist/llms/bundle.js +47 -0
  99. package/dist/llms/cli.d.ts +24 -0
  100. package/dist/llms/cli.js +104 -0
  101. package/dist/llms/format.d.ts +35 -0
  102. package/dist/llms/format.js +166 -0
  103. package/dist/llms/index.d.ts +9 -0
  104. package/dist/llms/index.js +8 -0
  105. package/dist/llms/types.d.ts +51 -0
  106. package/dist/llms/types.js +1 -0
  107. package/dist/llms/write.d.ts +14 -0
  108. package/dist/llms/write.js +49 -0
  109. package/dist/mcp/index.d.ts +2 -0
  110. package/dist/mcp/index.js +2 -0
  111. package/dist/mcp/server.d.ts +20 -0
  112. package/dist/mcp/server.js +326 -0
  113. package/dist/mcp/stdio.d.ts +3 -0
  114. package/dist/mcp/stdio.js +34 -0
  115. package/package.json +63 -4
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ export type UpdateCliArgs = {
3
+ command: 'diff' | 'update' | null;
4
+ id: string | null;
5
+ cwd: string;
6
+ sourceRoot: string;
7
+ baseRoot: string | null;
8
+ baseVersion: string | null;
9
+ outDir: string | null;
10
+ version: string | null;
11
+ conflicts: 'refuse' | 'markers';
12
+ json: boolean;
13
+ dryRun: boolean;
14
+ prune: boolean;
15
+ help: boolean;
16
+ };
17
+ /** Parse argv after the process name: a `diff` or `update` verb, then the id. */
18
+ export declare function parseUpdateArgs(argv: readonly string[], defaults?: {
19
+ cwd?: string;
20
+ sourceRoot?: string;
21
+ }): UpdateCliArgs;
22
+ /** Run the diff/update CLI. Returns a process exit code. */
23
+ export declare function runUpdateCli(argv: readonly string[], defaults?: {
24
+ cwd?: string;
25
+ sourceRoot?: string;
26
+ }): Promise<number>;
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /// <reference types="node" />
3
+ /**
4
+ * `texuli diff <component>` and `texuli update <component>`
5
+ *
6
+ * Three-way merge of a copied component against the installed version and the
7
+ * new source. No network.
8
+ *
9
+ * pnpm exec tsx packages/tools/src/cli-update/cli.ts diff button --cwd <app> \
10
+ * --source-root <new repo> --base-root <repo at the installed version>
11
+ * … update button … [--dry-run] [--prune] [--conflicts markers] [--json]
12
+ */
13
+ import { isMainModule } from "../cli-add/cli.js";
14
+ import { diffComponent, updateComponent } from "./update.js";
15
+ const USAGE = `Usage: texuli diff <component> [options]
16
+ texuli update <component> [options]
17
+
18
+ Three-way merge of a copied component (texuli add) with a new Texuli source.
19
+
20
+ Options:
21
+ --cwd <dir> Consumer project root (default: process.cwd())
22
+ --source-root <dir> Root that holds the new source under packages/ (default: cwd)
23
+ --base-root <dir> Root that holds the source at the installed version (the merge base)
24
+ --base-version <ver> Refuse unless the installed version (from the headers) equals this
25
+ --out <dir> Component directory relative to cwd (default: src/components/texuli/<id>)
26
+ --version <semver> Version stamped in the new headers (default: 0.0.0)
27
+ --conflicts <mode> refuse (default: write nothing) or markers (write conflict markers)
28
+ --prune Delete files removed upstream that were not edited locally
29
+ --dry-run update: plan only; write nothing
30
+ --json Machine-readable result on stdout
31
+ --help Show this help
32
+ `;
33
+ /** Parse argv after the process name: a `diff` or `update` verb, then the id. */
34
+ export function parseUpdateArgs(argv, defaults) {
35
+ const cwd = defaults?.cwd ?? process.cwd();
36
+ const args = {
37
+ command: null,
38
+ id: null,
39
+ cwd,
40
+ sourceRoot: defaults?.sourceRoot ?? cwd,
41
+ baseRoot: null,
42
+ baseVersion: null,
43
+ outDir: null,
44
+ version: null,
45
+ conflicts: 'refuse',
46
+ json: false,
47
+ dryRun: false,
48
+ prune: false,
49
+ help: false,
50
+ };
51
+ for (let i = 0; i < argv.length; i += 1) {
52
+ const arg = argv[i];
53
+ if (arg === undefined)
54
+ break;
55
+ const next = () => {
56
+ const value = argv[i + 1];
57
+ if (value === undefined || value.startsWith('-'))
58
+ throw new Error(`${arg} needs a value`);
59
+ i += 1;
60
+ return value;
61
+ };
62
+ if (arg === '--help' || arg === '-h')
63
+ args.help = true;
64
+ else if (arg === '--json')
65
+ args.json = true;
66
+ else if (arg === '--dry-run')
67
+ args.dryRun = true;
68
+ else if (arg === '--prune')
69
+ args.prune = true;
70
+ else if (arg === '--cwd')
71
+ args.cwd = next();
72
+ else if (arg === '--source-root')
73
+ args.sourceRoot = next();
74
+ else if (arg === '--base-root')
75
+ args.baseRoot = next();
76
+ else if (arg === '--base-version')
77
+ args.baseVersion = next();
78
+ else if (arg === '--out')
79
+ args.outDir = next();
80
+ else if (arg === '--version')
81
+ args.version = next();
82
+ else if (arg === '--conflicts') {
83
+ const mode = next();
84
+ if (mode !== 'refuse' && mode !== 'markers') {
85
+ throw new Error(`--conflicts is refuse or markers (got ${mode})`);
86
+ }
87
+ args.conflicts = mode;
88
+ }
89
+ else if (arg.startsWith('-'))
90
+ throw new Error(`Unknown option: ${arg}`);
91
+ else if (args.command === null) {
92
+ if (arg !== 'diff' && arg !== 'update')
93
+ throw new Error(`Unknown command: ${arg}`);
94
+ args.command = arg;
95
+ }
96
+ else if (args.id === null)
97
+ args.id = arg;
98
+ else
99
+ throw new Error(`Unexpected argument: ${arg}`);
100
+ }
101
+ return args;
102
+ }
103
+ function printHuman(result, command) {
104
+ for (const error of result.errors)
105
+ console.error(error);
106
+ const from = result.installedVersion ?? '(not installed)';
107
+ console.log(`${command} ${result.id} (${result.package}) ${from} → ${result.version}: ${result.outDir}`);
108
+ for (const file of result.files) {
109
+ const note = file.conflicts > 0 ? ` (${file.conflicts} conflict(s))` : '';
110
+ console.log(` ${file.status.padEnd(16)} ${file.action.padEnd(12)} ${file.path}${note}`);
111
+ }
112
+ if (command === 'diff') {
113
+ for (const file of result.files)
114
+ if (file.diff)
115
+ process.stdout.write(file.diff);
116
+ }
117
+ }
118
+ /** Run the diff/update CLI. Returns a process exit code. */
119
+ export async function runUpdateCli(argv, defaults) {
120
+ let args;
121
+ try {
122
+ args = parseUpdateArgs(argv, defaults);
123
+ }
124
+ catch (error) {
125
+ console.error(error instanceof Error ? error.message : String(error));
126
+ console.error(USAGE);
127
+ return 1;
128
+ }
129
+ if (args.help) {
130
+ console.log(USAGE);
131
+ return 0;
132
+ }
133
+ if (!args.command || !args.id) {
134
+ console.error('A command (diff or update) and a component id are required.');
135
+ console.error(USAGE);
136
+ return 1;
137
+ }
138
+ const options = {
139
+ id: args.id,
140
+ cwd: args.cwd,
141
+ sourceRoot: args.sourceRoot,
142
+ ...(args.baseRoot ? { baseRoot: args.baseRoot } : {}),
143
+ ...(args.baseVersion ? { baseVersion: args.baseVersion } : {}),
144
+ ...(args.outDir ? { outDir: args.outDir } : {}),
145
+ ...(args.version ? { version: args.version } : {}),
146
+ conflicts: args.conflicts,
147
+ dryRun: args.dryRun,
148
+ prune: args.prune,
149
+ };
150
+ const result = args.command === 'diff' ? await diffComponent(options) : await updateComponent(options);
151
+ if (args.json)
152
+ console.log(JSON.stringify(result));
153
+ else
154
+ printHuman(result, args.command);
155
+ return result.ok ? 0 : 1;
156
+ }
157
+ if (isMainModule(import.meta.url)) {
158
+ runUpdateCli(process.argv.slice(2)).then((code) => {
159
+ process.exitCode = code;
160
+ });
161
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@texuli/tools/cli-update` — `texuli diff <component>` and `texuli update <component>`
3
+ *
4
+ * Three-way merge of a copied component (from `texuli add`) against the
5
+ * installed version and the new source. Local filesystem only (no network).
6
+ */
7
+ export { parseUpdateArgs, runUpdateCli, type UpdateCliArgs } from './cli.js';
8
+ export { type MergeLabels, type MergeResult, matchLines, merge3, splitLines, unifiedDiff, } from './lines.js';
9
+ export { type ParsedProvenance, parseProvenance } from './provenance.js';
10
+ export type { UpdateFile, UpdateFileStatus, UpdateOptions, UpdateResult, } from './types.js';
11
+ export { diffComponent, updateComponent } from './update.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@texuli/tools/cli-update` — `texuli diff <component>` and `texuli update <component>`
3
+ *
4
+ * Three-way merge of a copied component (from `texuli add`) against the
5
+ * installed version and the new source. Local filesystem only (no network).
6
+ */
7
+ export { parseUpdateArgs, runUpdateCli } from "./cli.js";
8
+ export { matchLines, merge3, splitLines, unifiedDiff, } from "./lines.js";
9
+ export { parseProvenance } from "./provenance.js";
10
+ export { diffComponent, updateComponent } from "./update.js";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Line diffing for `texuli diff` and `texuli update`: Myers' O(ND) diff,
3
+ * a diff3 three-way merge and unified diff output. Plain strings only.
4
+ */
5
+ /** Split text into lines that keep their `\n` (so `\r\n` and a missing final newline survive). */
6
+ export declare function splitLines(text: string): string[];
7
+ /**
8
+ * For each line of `a`, the index of the matching line in `b`, or -1.
9
+ * Matches increase monotonically (a longest common subsequence). When the
10
+ * files differ by more than {@link MAX_EDITS} lines, only the common prefix
11
+ * and suffix match.
12
+ */
13
+ export declare function matchLines(a: readonly string[], b: readonly string[]): Int32Array;
14
+ export interface MergeLabels {
15
+ local: string;
16
+ base: string;
17
+ upstream: string;
18
+ }
19
+ export interface MergeResult {
20
+ text: string;
21
+ /** Conflict regions written with markers (0 for a clean merge). */
22
+ conflicts: number;
23
+ }
24
+ /**
25
+ * diff3 three-way merge of `local` and `upstream`, which both started from
26
+ * `base`. A region only one side changed takes that side; a region both
27
+ * changed the same way takes it once; any other region becomes a conflict
28
+ * with `<<<<<<<`, `|||||||`, `=======` and `>>>>>>>` markers.
29
+ */
30
+ export declare function merge3(base: string, local: string, upstream: string, labels: MergeLabels): MergeResult;
31
+ /**
32
+ * A unified diff (`---`/`+++` headers and `@@` hunks) from `before` to
33
+ * `after`, or an empty string when they are equal.
34
+ */
35
+ export declare function unifiedDiff(before: string, after: string, names: {
36
+ from: string;
37
+ to: string;
38
+ }, context?: number): string;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Line diffing for `texuli diff` and `texuli update`: Myers' O(ND) diff,
3
+ * a diff3 three-way merge and unified diff output. Plain strings only.
4
+ */
5
+ /** Edit distance past which the middle of two files is treated as replaced. */
6
+ const MAX_EDITS = 4000;
7
+ /** Split text into lines that keep their `\n` (so `\r\n` and a missing final newline survive). */
8
+ export function splitLines(text) {
9
+ return text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
10
+ }
11
+ /**
12
+ * For each line of `a`, the index of the matching line in `b`, or -1.
13
+ * Matches increase monotonically (a longest common subsequence). When the
14
+ * files differ by more than {@link MAX_EDITS} lines, only the common prefix
15
+ * and suffix match.
16
+ */
17
+ export function matchLines(a, b) {
18
+ const match = new Int32Array(a.length).fill(-1);
19
+ let start = 0;
20
+ while (start < a.length && start < b.length && a[start] === b[start]) {
21
+ match[start] = start;
22
+ start += 1;
23
+ }
24
+ let endA = a.length;
25
+ let endB = b.length;
26
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
27
+ endA -= 1;
28
+ endB -= 1;
29
+ match[endA] = endB;
30
+ }
31
+ const n = endA - start;
32
+ const m = endB - start;
33
+ if (n === 0 || m === 0)
34
+ return match;
35
+ const max = Math.min(n + m, MAX_EDITS);
36
+ const offset = max + 1;
37
+ const v = new Int32Array(2 * max + 3);
38
+ const trace = [];
39
+ let found = false;
40
+ search: for (let d = 0; d <= max; d += 1) {
41
+ trace.push(v.slice());
42
+ for (let k = -d; k <= d; k += 2) {
43
+ const down = k === -d || (k !== d && (v[offset + k - 1] ?? 0) < (v[offset + k + 1] ?? 0));
44
+ let x = down ? (v[offset + k + 1] ?? 0) : (v[offset + k - 1] ?? 0) + 1;
45
+ let y = x - k;
46
+ while (x < n && y < m && a[start + x] === b[start + y]) {
47
+ x += 1;
48
+ y += 1;
49
+ }
50
+ v[offset + k] = x;
51
+ if (x >= n && y >= m) {
52
+ found = true;
53
+ break search;
54
+ }
55
+ }
56
+ }
57
+ if (!found)
58
+ return match;
59
+ let x = n;
60
+ let y = m;
61
+ for (let d = trace.length - 1; d >= 0; d -= 1) {
62
+ const prev = trace[d];
63
+ const k = x - y;
64
+ const down = k === -d || (k !== d && (prev[offset + k - 1] ?? 0) < (prev[offset + k + 1] ?? 0));
65
+ const prevK = down ? k + 1 : k - 1;
66
+ const prevX = prev[offset + prevK] ?? 0;
67
+ const prevY = prevX - prevK;
68
+ while (x > prevX && y > prevY) {
69
+ x -= 1;
70
+ y -= 1;
71
+ match[start + x] = start + y;
72
+ }
73
+ x = prevX;
74
+ y = prevY;
75
+ }
76
+ return match;
77
+ }
78
+ const sameLines = (a, b) => a.length === b.length && a.every((line, i) => line === b[i]);
79
+ const withNewline = (lines) => lines.map((line, i) => (i === lines.length - 1 && !line.endsWith('\n') ? `${line}\n` : line));
80
+ /**
81
+ * diff3 three-way merge of `local` and `upstream`, which both started from
82
+ * `base`. A region only one side changed takes that side; a region both
83
+ * changed the same way takes it once; any other region becomes a conflict
84
+ * with `<<<<<<<`, `|||||||`, `=======` and `>>>>>>>` markers.
85
+ */
86
+ export function merge3(base, local, upstream, labels) {
87
+ const o = splitLines(base);
88
+ const a = splitLines(local);
89
+ const b = splitLines(upstream);
90
+ const ma = matchLines(o, a);
91
+ const mb = matchLines(o, b);
92
+ const out = [];
93
+ let conflicts = 0;
94
+ let i = 0;
95
+ let ia = 0;
96
+ let ib = 0;
97
+ while (i < o.length || ia < a.length || ib < b.length) {
98
+ if (i < o.length && ma[i] === ia && mb[i] === ib) {
99
+ out.push(o[i]);
100
+ i += 1;
101
+ ia += 1;
102
+ ib += 1;
103
+ continue;
104
+ }
105
+ let k = i;
106
+ while (k < o.length && (ma[k] === -1 || mb[k] === -1))
107
+ k += 1;
108
+ const endA = k < o.length ? ma[k] : a.length;
109
+ const endB = k < o.length ? mb[k] : b.length;
110
+ const chunkO = o.slice(i, k);
111
+ const chunkA = a.slice(ia, endA);
112
+ const chunkB = b.slice(ib, endB);
113
+ if (sameLines(chunkA, chunkO))
114
+ out.push(...chunkB);
115
+ else if (sameLines(chunkB, chunkO) || sameLines(chunkA, chunkB))
116
+ out.push(...chunkA);
117
+ else {
118
+ conflicts += 1;
119
+ out.push(`<<<<<<< ${labels.local}\n`, ...withNewline(chunkA), `||||||| ${labels.base}\n`, ...withNewline(chunkO), '=======\n', ...withNewline(chunkB), `>>>>>>> ${labels.upstream}\n`);
120
+ }
121
+ i = k;
122
+ ia = endA;
123
+ ib = endB;
124
+ }
125
+ return { text: out.join(''), conflicts };
126
+ }
127
+ /**
128
+ * A unified diff (`---`/`+++` headers and `@@` hunks) from `before` to
129
+ * `after`, or an empty string when they are equal.
130
+ */
131
+ export function unifiedDiff(before, after, names, context = 3) {
132
+ if (before === after)
133
+ return '';
134
+ const a = splitLines(before);
135
+ const b = splitLines(after);
136
+ const match = matchLines(a, b);
137
+ const ops = [];
138
+ let j = 0;
139
+ for (let i = 0; i < a.length; i += 1) {
140
+ const target = match[i];
141
+ if (target === -1) {
142
+ ops.push({ op: '-', line: a[i] });
143
+ continue;
144
+ }
145
+ for (; j < target; j += 1)
146
+ ops.push({ op: '+', line: b[j] });
147
+ ops.push({ op: ' ', line: a[i] });
148
+ j += 1;
149
+ }
150
+ for (; j < b.length; j += 1)
151
+ ops.push({ op: '+', line: b[j] });
152
+ // Hunks: every change with `context` lines around it, overlapping ranges joined.
153
+ const ranges = [];
154
+ ops.forEach((entry, index) => {
155
+ if (entry.op === ' ')
156
+ return;
157
+ const start = Math.max(0, index - context);
158
+ const end = Math.min(ops.length, index + context + 1);
159
+ const last = ranges[ranges.length - 1];
160
+ if (last && start <= last.end)
161
+ last.end = Math.max(last.end, end);
162
+ else
163
+ ranges.push({ start, end });
164
+ });
165
+ const out = [`--- ${names.from}\n`, `+++ ${names.to}\n`];
166
+ let cursor = 0;
167
+ let oldLine = 0;
168
+ let newLine = 0;
169
+ for (const range of ranges) {
170
+ for (; cursor < range.start; cursor += 1) {
171
+ if (ops[cursor]?.op !== '+')
172
+ oldLine += 1;
173
+ if (ops[cursor]?.op !== '-')
174
+ newLine += 1;
175
+ }
176
+ const hunk = ops.slice(range.start, range.end);
177
+ const oldCount = hunk.filter((h) => h.op !== '+').length;
178
+ const newCount = hunk.filter((h) => h.op !== '-').length;
179
+ const oldStart = oldCount === 0 ? oldLine : oldLine + 1;
180
+ const newStart = newCount === 0 ? newLine : newLine + 1;
181
+ out.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n`);
182
+ for (const h of hunk) {
183
+ out.push(`${h.op}${h.line}`);
184
+ if (!h.line.endsWith('\n'))
185
+ out.push('\n\\n');
186
+ }
187
+ }
188
+ return out.join('');
189
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Read the provenance header `texuli add` stamps on copied files.
3
+ */
4
+ import type { ProvenanceMeta } from '../cli-add/provenance.js';
5
+ export interface ParsedProvenance {
6
+ meta: ProvenanceMeta;
7
+ /** The file without its header: the text the merge compares. */
8
+ body: string;
9
+ }
10
+ /**
11
+ * Parse the header at the very start of `content`, or `null` when the file
12
+ * has none (a file the consumer added, or a header they edited away).
13
+ */
14
+ export declare function parseProvenance(content: string): ParsedProvenance | null;
@@ -0,0 +1,51 @@
1
+ const BEGIN = '@texuli/add — copied styled source. Do not remove this block.';
2
+ const FIELDS = ['id', 'package', 'version', 'source'];
3
+ /** Each header form `provenanceHeader` writes: opening line, line prefix, closing line. */
4
+ const FORMS = [
5
+ { open: '/**', prefix: ' * ', close: ' */' },
6
+ { open: '<!--', prefix: ' ', close: '-->' },
7
+ { open: null, prefix: '# ', close: null },
8
+ ];
9
+ /**
10
+ * Parse the header at the very start of `content`, or `null` when the file
11
+ * has none (a file the consumer added, or a header they edited away).
12
+ */
13
+ export function parseProvenance(content) {
14
+ for (const form of FORMS) {
15
+ const expected = 1 + FIELDS.length + (form.open ? 1 : 0) + (form.close ? 1 : 0);
16
+ const lines = content.split('\n', expected);
17
+ if (lines.length < expected)
18
+ continue;
19
+ let at = 0;
20
+ if (form.open !== null && lines[at++] !== form.open)
21
+ continue;
22
+ if (lines[at++] !== `${form.prefix}${BEGIN}`)
23
+ continue;
24
+ const values = {};
25
+ let valid = true;
26
+ for (const field of FIELDS) {
27
+ const line = lines[at++] ?? '';
28
+ const head = `${form.prefix}${field}: `;
29
+ if (!line.startsWith(head)) {
30
+ valid = false;
31
+ break;
32
+ }
33
+ values[field] = line.slice(head.length);
34
+ }
35
+ if (!valid)
36
+ continue;
37
+ if (form.close !== null && lines[at++] !== form.close)
38
+ continue;
39
+ const headerLength = lines.slice(0, at).join('\n').length + 1;
40
+ return {
41
+ meta: {
42
+ id: values.id ?? '',
43
+ package: values.package ?? '',
44
+ version: values.version ?? '',
45
+ source: values.source ?? '',
46
+ },
47
+ body: content.slice(headerLength),
48
+ };
49
+ }
50
+ return null;
51
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Types for `texuli diff` and `texuli update`: three-way merge of copied
3
+ * components against the installed version.
4
+ */
5
+ import type { AddableComponent } from '../cli-add/types.js';
6
+ /** What happens to one file of the component. */
7
+ export type UpdateFileStatus =
8
+ /** Local copy already equals the new source. */
9
+ 'unchanged'
10
+ /** Not edited locally: replaced by the new source. */
11
+ | 'fast-forward'
12
+ /** Edited locally, unchanged upstream: the local edit is kept. */
13
+ | 'kept-local'
14
+ /** Edited on both sides without overlap: merged. */
15
+ | 'merged'
16
+ /** Edited on both sides in the same region. */
17
+ | 'conflict'
18
+ /** New upstream file. */
19
+ | 'added'
20
+ /** Removed upstream and not edited locally (deleted only with `prune`). */
21
+ | 'removed'
22
+ /** Removed upstream but edited locally: kept. */
23
+ | 'removed-modified'
24
+ /** Deleted locally, still upstream: stays deleted. */
25
+ | 'deleted-locally'
26
+ /** Local and new source differ but no base was given, so they cannot merge. */
27
+ | 'needs-base'
28
+ /** A local file without a provenance header: never touched. */
29
+ | 'untracked';
30
+ export type UpdateFile = {
31
+ /** Path relative to the component directory (`/` separators). */
32
+ path: string;
33
+ status: UpdateFileStatus;
34
+ action: 'write' | 'would-write' | 'delete' | 'would-delete' | 'none';
35
+ /** Unified diff from the local file to its result (empty when unchanged). */
36
+ diff: string;
37
+ /** Conflict regions in the result (written only with `conflicts: 'markers'`). */
38
+ conflicts: number;
39
+ };
40
+ export type UpdateResult = {
41
+ ok: boolean;
42
+ id: string;
43
+ package: string;
44
+ /** The component directory in the consumer project. */
45
+ outDir: string;
46
+ /** Version in the local provenance headers, or null when none is installed. */
47
+ installedVersion: string | null;
48
+ /** Version stamped by the update. */
49
+ version: string;
50
+ files: UpdateFile[];
51
+ /** Total conflict regions. */
52
+ conflicts: number;
53
+ errors: string[];
54
+ /** True when nothing was written (`diff`, or `update --dry-run`). */
55
+ dryRun: boolean;
56
+ };
57
+ export type UpdateOptions = {
58
+ /** Component id (as given to `texuli add`). */
59
+ id: string;
60
+ /** Consumer project root. */
61
+ cwd: string;
62
+ /** Monorepo (or fixture) root with the **new** source under `packages/`. */
63
+ sourceRoot: string;
64
+ /**
65
+ * Monorepo root with the source at the **installed** version, the merge
66
+ * base. Without it only files that need no merge can update.
67
+ */
68
+ baseRoot?: string;
69
+ /** When set, refuse unless the installed version equals it. */
70
+ baseVersion?: string;
71
+ /** Component directory relative to `cwd` (default `src/components/texuli/<id>`). */
72
+ outDir?: string;
73
+ /** Version stamped into the new headers (default `0.0.0`). */
74
+ version?: string;
75
+ /** Plan only; write nothing. */
76
+ dryRun?: boolean;
77
+ /** Delete files removed upstream that were not edited locally. */
78
+ prune?: boolean;
79
+ /** `refuse` (default): any conflict writes nothing. `markers`: write conflict markers. */
80
+ conflicts?: 'refuse' | 'markers';
81
+ /** Override the built-in registry (tests). */
82
+ registry?: readonly AddableComponent[];
83
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { UpdateOptions, UpdateResult } from './types.js';
2
+ /**
3
+ * Show what `texuli update` would do: each file's status and a unified diff
4
+ * from the local file to its result. Writes nothing.
5
+ */
6
+ export declare function diffComponent(options: UpdateOptions): Promise<UpdateResult>;
7
+ /**
8
+ * Update a copied component to the new source with a three-way merge.
9
+ * Writes nothing when any file conflicts (unless `conflicts: 'markers'`) or
10
+ * needs a base that was not given.
11
+ */
12
+ export declare function updateComponent(options: UpdateOptions): Promise<UpdateResult>;