block-in-file 1.0.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/- +3 -0
- package/.beads/README.md +85 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +23 -0
- package/.beads/metadata.json +4 -0
- package/.git-blame-ignore-revs +2 -0
- package/.gitattributes +3 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +40 -0
- package/README.md +122 -0
- package/block-in-file.ts +150 -0
- package/content +10 -0
- package/deno.json +14 -0
- package/deno.lock +1084 -0
- package/doc/PLAN-envsubst.md +200 -0
- package/doc/PLAN-restructure.md +114 -0
- package/package.json +44 -0
- package/src/attributes.ts +161 -0
- package/src/backup.ts +180 -0
- package/src/block-parser.ts +170 -0
- package/src/block-remover.ts +128 -0
- package/src/conflict-detection.ts +179 -0
- package/src/defaults.ts +23 -0
- package/src/envsubst.ts +59 -0
- package/src/file-processor.ts +378 -0
- package/src/index.ts +5 -0
- package/src/input.ts +69 -0
- package/src/mode-handler.ts +39 -0
- package/src/output.ts +107 -0
- package/src/plugins/.beads/.local_version +1 -0
- package/src/plugins/.beads/issues.jsonl +21 -0
- package/src/plugins/.beads/metadata.json +4 -0
- package/src/plugins/config.ts +282 -0
- package/src/plugins/diff.ts +109 -0
- package/src/plugins/io.ts +72 -0
- package/src/plugins/logger.ts +41 -0
- package/src/tags/tag-merger.ts +31 -0
- package/src/tags/tag-mode.ts +1 -0
- package/src/tags/tag.ts +36 -0
- package/src/tags/tags.ts +4 -0
- package/src/tags/types.ts +4 -0
- package/src/timestamp.ts +39 -0
- package/src/types.ts +32 -0
- package/src/validation.ts +11 -0
- package/test/additive-cli.test.ts +109 -0
- package/test/additive.test.ts +233 -0
- package/test/attributes-integration.test.ts +161 -0
- package/test/attributes.test.ts +100 -0
- package/test/backup.test.ts +386 -0
- package/test/block-in-file.test.ts +235 -0
- package/test/block-parser.test.ts +221 -0
- package/test/block-remover.test.ts +209 -0
- package/test/cli.test.ts +254 -0
- package/test/defaults.test.ts +38 -0
- package/test/envsubst-edge-cases.test.ts +116 -0
- package/test/envsubst-integration.test.ts +78 -0
- package/test/envsubst.test.ts +184 -0
- package/test/input.test.ts +86 -0
- package/test/mode.test.ts +193 -0
- package/test/output.test.ts +44 -0
- package/test/tag-merger.test.ts +176 -0
- package/test/tags.test.ts +116 -0
- package/test/timestamp-integration.test.ts +209 -0
- package/test/timestamp.test.ts +76 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { mkdtempSync, rmSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
describe("timestamp CLI integration", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = mkdtempSync(join(tmpdir(), "blockinfile-timestamp-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (tempDir) {
|
|
16
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const runBlockInFile = (args: string[], input?: string): string => {
|
|
21
|
+
const cmd = `node block-in-file.ts ${args.join(" ")}`;
|
|
22
|
+
return execSync(cmd, { encoding: "utf-8", cwd: "/home/rektide/src/block-in-file", input });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
it("should add timestamp tag to start marker with epoch-nano", () => {
|
|
26
|
+
const filePath = join(tempDir, "test.txt");
|
|
27
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-nano", filePath];
|
|
28
|
+
runBlockInFile(args, "test content\n");
|
|
29
|
+
|
|
30
|
+
const content = readFileSync(filePath, "utf-8");
|
|
31
|
+
const lines = content.split("\n");
|
|
32
|
+
|
|
33
|
+
expect(lines[0]).toMatch(/^# testblock start \[timestamp:\d+\]$/);
|
|
34
|
+
expect(lines[1]).toBe("test content");
|
|
35
|
+
expect(lines[2]).toBe("# testblock end");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should add timestamp tag to start marker with epoch-sec", () => {
|
|
39
|
+
const filePath = join(tempDir, "test.txt");
|
|
40
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-sec", filePath];
|
|
41
|
+
runBlockInFile(args, "test content\n");
|
|
42
|
+
|
|
43
|
+
const content = readFileSync(filePath, "utf-8");
|
|
44
|
+
const lines = content.split("\n");
|
|
45
|
+
|
|
46
|
+
expect(lines[0]).toMatch(/^# testblock start \[timestamp:\d+\]$/);
|
|
47
|
+
expect(lines[1]).toBe("test content");
|
|
48
|
+
expect(lines[2]).toBe("# testblock end");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should add timestamp tag to start marker with iso8601", () => {
|
|
52
|
+
const filePath = join(tempDir, "test.txt");
|
|
53
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "iso8601", filePath];
|
|
54
|
+
runBlockInFile(args, "test content\n");
|
|
55
|
+
|
|
56
|
+
const content = readFileSync(filePath, "utf-8");
|
|
57
|
+
const lines = content.split("\n");
|
|
58
|
+
|
|
59
|
+
expect(lines[0]).toMatch(
|
|
60
|
+
/^# testblock start \[timestamp:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\]$/,
|
|
61
|
+
);
|
|
62
|
+
expect(lines[1]).toBe("test content");
|
|
63
|
+
expect(lines[2]).toBe("# testblock end");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should update timestamp on block replacement", async () => {
|
|
67
|
+
const filePath = join(tempDir, "test.txt");
|
|
68
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-sec", filePath];
|
|
69
|
+
runBlockInFile(args, "original content\n");
|
|
70
|
+
|
|
71
|
+
const firstContent = readFileSync(filePath, "utf-8");
|
|
72
|
+
const firstTimestamp = firstContent.match(/\[timestamp:(\d+)\]/)?.[1];
|
|
73
|
+
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
75
|
+
|
|
76
|
+
const args2 = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-sec", filePath];
|
|
77
|
+
runBlockInFile(args2, "updated content\n");
|
|
78
|
+
|
|
79
|
+
const secondContent = readFileSync(filePath, "utf-8");
|
|
80
|
+
const secondTimestamp = secondContent.match(/\[timestamp:(\d+)\]/)?.[1];
|
|
81
|
+
|
|
82
|
+
expect(firstTimestamp).toBeDefined();
|
|
83
|
+
expect(secondTimestamp).toBeDefined();
|
|
84
|
+
expect(secondTimestamp).not.toBe(firstTimestamp);
|
|
85
|
+
expect(secondContent).toContain("updated content");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should not add timestamp to end marker", () => {
|
|
89
|
+
const filePath = join(tempDir, "test.txt");
|
|
90
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-nano", filePath];
|
|
91
|
+
runBlockInFile(args, "test content\n");
|
|
92
|
+
|
|
93
|
+
const content = readFileSync(filePath, "utf-8");
|
|
94
|
+
const lines = content.split("\n");
|
|
95
|
+
|
|
96
|
+
expect(lines[0]).toMatch(/\[timestamp:\d+\]/);
|
|
97
|
+
expect(lines[2]).toBe("# testblock end");
|
|
98
|
+
expect(lines[2]).not.toMatch(/\[timestamp:/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should preserve existing tags when merging", () => {
|
|
102
|
+
const filePath = join(tempDir, "test.txt");
|
|
103
|
+
writeFileSync(filePath, "# testblock start [anchor-bof:100]\nold content\n# testblock end\n");
|
|
104
|
+
|
|
105
|
+
const args = ["--input", "-", "--name", "testblock", "--timestamp", "epoch-sec", filePath];
|
|
106
|
+
runBlockInFile(args, "new content\n");
|
|
107
|
+
|
|
108
|
+
const content = readFileSync(filePath, "utf-8");
|
|
109
|
+
expect(content).toContain("[anchor-bof:100]");
|
|
110
|
+
expect(content).toContain("[timestamp:");
|
|
111
|
+
expect(content).toContain("new content");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should use tag-mode replace to remove existing tags", () => {
|
|
115
|
+
const filePath = join(tempDir, "test.txt");
|
|
116
|
+
writeFileSync(
|
|
117
|
+
filePath,
|
|
118
|
+
"# testblock start [anchor-bof:100] [oldtag:value]\nold content\n# testblock end\n",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const args = [
|
|
122
|
+
"--input",
|
|
123
|
+
"-",
|
|
124
|
+
"--name",
|
|
125
|
+
"testblock",
|
|
126
|
+
"--timestamp",
|
|
127
|
+
"epoch-sec",
|
|
128
|
+
"--tag-mode",
|
|
129
|
+
"replace",
|
|
130
|
+
filePath,
|
|
131
|
+
];
|
|
132
|
+
runBlockInFile(args, "new content\n");
|
|
133
|
+
|
|
134
|
+
const content = readFileSync(filePath, "utf-8");
|
|
135
|
+
expect(content).not.toContain("[anchor-bof:100]");
|
|
136
|
+
expect(content).not.toContain("[oldtag:value]");
|
|
137
|
+
expect(content).toContain("[timestamp:");
|
|
138
|
+
expect(content).toContain("new content");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should work with custom comment character", () => {
|
|
142
|
+
const filePath = join(tempDir, "test.txt");
|
|
143
|
+
const args = [
|
|
144
|
+
"--input",
|
|
145
|
+
"-",
|
|
146
|
+
"--name",
|
|
147
|
+
"testblock",
|
|
148
|
+
"--timestamp",
|
|
149
|
+
"epoch-nano",
|
|
150
|
+
"--comment",
|
|
151
|
+
"//",
|
|
152
|
+
filePath,
|
|
153
|
+
];
|
|
154
|
+
runBlockInFile(args, "test content\n");
|
|
155
|
+
|
|
156
|
+
const content = readFileSync(filePath, "utf-8");
|
|
157
|
+
const lines = content.split("\n");
|
|
158
|
+
|
|
159
|
+
expect(lines[0]).toMatch(/^\/\/ testblock start \[timestamp:\d+\]$/);
|
|
160
|
+
expect(lines[2]).toBe("// testblock end");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should work with custom markers", () => {
|
|
164
|
+
const filePath = join(tempDir, "test.txt");
|
|
165
|
+
const args = [
|
|
166
|
+
"--input",
|
|
167
|
+
"-",
|
|
168
|
+
"--name",
|
|
169
|
+
"testblock",
|
|
170
|
+
"--timestamp",
|
|
171
|
+
"epoch-sec",
|
|
172
|
+
"--marker-start",
|
|
173
|
+
"BEGIN",
|
|
174
|
+
"--marker-end",
|
|
175
|
+
"END",
|
|
176
|
+
filePath,
|
|
177
|
+
];
|
|
178
|
+
runBlockInFile(args, "test content\n");
|
|
179
|
+
|
|
180
|
+
const content = readFileSync(filePath, "utf-8");
|
|
181
|
+
const lines = content.split("\n");
|
|
182
|
+
|
|
183
|
+
expect(lines[0]).toMatch(/^# testblock BEGIN \[timestamp:\d+\]$/);
|
|
184
|
+
expect(lines[2]).toBe("# testblock END");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should add timestamp with append-newline", () => {
|
|
188
|
+
const filePath = join(tempDir, "test.txt");
|
|
189
|
+
const args = [
|
|
190
|
+
"--input",
|
|
191
|
+
"-",
|
|
192
|
+
"--name",
|
|
193
|
+
"testblock",
|
|
194
|
+
"--timestamp",
|
|
195
|
+
"epoch-nano",
|
|
196
|
+
"--append-newline",
|
|
197
|
+
filePath,
|
|
198
|
+
];
|
|
199
|
+
runBlockInFile(args, "test content\n");
|
|
200
|
+
|
|
201
|
+
const content = readFileSync(filePath, "utf-8");
|
|
202
|
+
const lines = content.split("\n");
|
|
203
|
+
|
|
204
|
+
expect(lines[0]).toMatch(/\[timestamp:\d+\]/);
|
|
205
|
+
expect(lines[1]).toBe("test content");
|
|
206
|
+
expect(lines[2]).toBe("# testblock end");
|
|
207
|
+
expect(lines[3]).toBe("");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
generateTimestampTag,
|
|
4
|
+
parseTimestampFormat,
|
|
5
|
+
type TimestampFormat,
|
|
6
|
+
} from "../src/timestamp.ts";
|
|
7
|
+
|
|
8
|
+
describe("timestamp generation", () => {
|
|
9
|
+
it.each([
|
|
10
|
+
{ format: "epoch-nano" as const, pattern: /^\[timestamp:\d+\]$/ },
|
|
11
|
+
{ format: "epoch-sec" as const, pattern: /^\[timestamp:\d+\]$/ },
|
|
12
|
+
{ format: "iso8601" as const, pattern: /^\[timestamp:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\]$/ },
|
|
13
|
+
])("should generate $format format", ({ format, pattern }) => {
|
|
14
|
+
const timestamp = generateTimestampTag(format);
|
|
15
|
+
expect(timestamp).toMatch(pattern);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should generate epoch-nano with 6 extra zeros", () => {
|
|
19
|
+
const timestamp = generateTimestampTag("epoch-nano");
|
|
20
|
+
const value = timestamp.replace(/^\[timestamp:/, "").replace(/\]$/, "");
|
|
21
|
+
const numValue = Number(value);
|
|
22
|
+
const milliseconds = numValue / 1000000;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
expect(milliseconds).toBeGreaterThan(now - 1000);
|
|
25
|
+
expect(milliseconds).toBeLessThan(now + 1000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should generate epoch-sec as seconds", () => {
|
|
29
|
+
const timestamp = generateTimestampTag("epoch-sec");
|
|
30
|
+
const value = timestamp.replace(/^\[timestamp:/, "").replace(/\]$/, "");
|
|
31
|
+
const numValue = Number(value);
|
|
32
|
+
const now = Math.floor(Date.now() / 1000);
|
|
33
|
+
expect(numValue).toBeGreaterThanOrEqual(now - 1);
|
|
34
|
+
expect(numValue).toBeLessThanOrEqual(now + 1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should generate iso8601 in proper format", () => {
|
|
38
|
+
const timestamp = generateTimestampTag("iso8601");
|
|
39
|
+
const value = timestamp.replace(/^\[timestamp:/, "").replace(/\]$/, "");
|
|
40
|
+
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
41
|
+
expect(value).toMatch(isoRegex);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("timestamp format parsing", () => {
|
|
46
|
+
it.each([
|
|
47
|
+
"epoch-nano",
|
|
48
|
+
"epoch-sec",
|
|
49
|
+
"iso8601",
|
|
50
|
+
] as TimestampFormat[])("should parse $s", (format) => {
|
|
51
|
+
const result = parseTimestampFormat(format);
|
|
52
|
+
expect(result).toBe(format);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return undefined for undefined input", () => {
|
|
56
|
+
const result = parseTimestampFormat(undefined);
|
|
57
|
+
expect(result).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
"invalid",
|
|
62
|
+
"unknown",
|
|
63
|
+
])("should throw error for invalid format: $s", (format) => {
|
|
64
|
+
expect(() => parseTimestampFormat(format)).toThrow(`Invalid timestamp format: ${format}`);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("timestamp format type", () => {
|
|
69
|
+
it("should accept valid timestamp format types", () => {
|
|
70
|
+
const formats: TimestampFormat[] = ["epoch-nano", "epoch-sec", "iso8601"];
|
|
71
|
+
formats.forEach((format) => {
|
|
72
|
+
const result = parseTimestampFormat(format);
|
|
73
|
+
expect(result).toBe(format);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ESNext", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*", "block-in-file.ts"],
|
|
15
|
+
"exclude": ["node_modules"]
|
|
16
|
+
}
|