html-collab 0.1.0 → 0.1.1
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 +19 -7
- package/dist/cli.js +113 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,7 +46,8 @@ revision can be done by a human or an agent.
|
|
|
46
46
|
- Suggested edits can be replacements, inserts, or deletions. Toggle between
|
|
47
47
|
the tracked-change markup and a clean preview.
|
|
48
48
|
- Autosave back to the same local file when a Chromium browser asks for
|
|
49
|
-
permission.
|
|
49
|
+
permission. Safari and Firefox keep changes in the tab until you export or
|
|
50
|
+
save another way.
|
|
50
51
|
|
|
51
52
|
Send your edited file to anyone else with `html-collab merge` to combine
|
|
52
53
|
copies, and `html-collab extract` to pull out a brief.
|
|
@@ -61,6 +62,10 @@ Requires Node.js 18+ and npm. No clone or install step is needed:
|
|
|
61
62
|
npx html-collab wrap report.html --out report.review.html
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
The command writes a self-contained review file and prints a short summary.
|
|
66
|
+
Small source files grow by roughly 90 KB because the review UI is embedded
|
|
67
|
+
inside the HTML.
|
|
68
|
+
|
|
64
69
|
Open `report.review.html` in your browser, mark it up, then export a brief:
|
|
65
70
|
|
|
66
71
|
```sh
|
|
@@ -78,7 +83,7 @@ you want another round.
|
|
|
78
83
|
|
|
79
84
|
```sh
|
|
80
85
|
npx html-collab wrap report.html --out report.review.html
|
|
81
|
-
# Open report.review.html in
|
|
86
|
+
# Open report.review.html in your browser. Mark it up. Click Brief.
|
|
82
87
|
npx html-collab extract report.review.html --format markdown --out brief.md
|
|
83
88
|
```
|
|
84
89
|
|
|
@@ -108,6 +113,8 @@ review file when Autosave asks where to save.
|
|
|
108
113
|
## CLI reference
|
|
109
114
|
|
|
110
115
|
```
|
|
116
|
+
html-collab --help
|
|
117
|
+
html-collab --version
|
|
111
118
|
html-collab wrap <input.html> --out <output.review.html>
|
|
112
119
|
html-collab unwrap <input.review.html> [--apply-edits] --out <output.html>
|
|
113
120
|
html-collab merge <a.review.html> <b.review.html> [more...] --out <merged.review.html>
|
|
@@ -116,12 +123,15 @@ html-collab skill --out html-collab.SKILL.md
|
|
|
116
123
|
```
|
|
117
124
|
|
|
118
125
|
Use `npx html-collab ...` if you have not installed the CLI globally.
|
|
119
|
-
The
|
|
120
|
-
|
|
126
|
+
The browser Brief button exports markdown. For JSON, plain text, or an
|
|
127
|
+
agent-oriented plan, use `html-collab extract --format <json|text|agent>`.
|
|
128
|
+
The `skill` command writes an optional agent workflow file you can drop into
|
|
129
|
+
a local skills directory or paste into an agent prompt.
|
|
121
130
|
|
|
122
|
-
`unwrap --apply-edits`
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
`unwrap --apply-edits` only applies suggested edits that have been accepted
|
|
132
|
+
in the browser. Open, rejected, and deleted edits are skipped and reported in
|
|
133
|
+
the command summary. Ambiguous edits fail instead of guessing, so the output
|
|
134
|
+
is never silently wrong.
|
|
125
135
|
|
|
126
136
|
## How it works
|
|
127
137
|
|
|
@@ -134,6 +144,8 @@ panel shows.
|
|
|
134
144
|
|
|
135
145
|
For mission, principles, and workflows, see the
|
|
136
146
|
[project notes](https://github.com/glendigity/html-collab/blob/main/docs/README.md).
|
|
147
|
+
For a fresh external sanity check, use the
|
|
148
|
+
[independent user test prompt](https://github.com/glendigity/html-collab/blob/main/USER_TEST_PROMPT.md).
|
|
137
149
|
|
|
138
150
|
## Status
|
|
139
151
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { realpathSync } from "node:fs";
|
|
5
|
-
import { basename as basename2 } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { basename as basename2, dirname as dirname3, join as join2 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/commands/extract.ts
|
|
@@ -2987,6 +2987,7 @@ function renderReviewShell(source, state) {
|
|
|
2987
2987
|
<meta charset="utf-8">
|
|
2988
2988
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2989
2989
|
<meta name="generator" content="html-collab">
|
|
2990
|
+
<link rel="icon" href="data:,">
|
|
2990
2991
|
<link rel="author" href="${PROJECT_URL}">
|
|
2991
2992
|
<title>${escapeHtml(state.title ?? "Reviewable HTML")}</title>
|
|
2992
2993
|
<style>
|
|
@@ -3845,6 +3846,12 @@ async function mergeFiles(inputPaths, outputPath) {
|
|
|
3845
3846
|
const result = mergeReviewStates(states);
|
|
3846
3847
|
const mergedHtml = createReviewHtmlFromParts(source, result.state);
|
|
3847
3848
|
await writeFile2(outputPath, mergedHtml, "utf8");
|
|
3849
|
+
return {
|
|
3850
|
+
addedOps: result.addedOps,
|
|
3851
|
+
addedActors: result.addedActors,
|
|
3852
|
+
totalOps: result.state.ops.length,
|
|
3853
|
+
reviewBytes: Buffer.byteLength(mergedHtml, "utf8")
|
|
3854
|
+
};
|
|
3848
3855
|
}
|
|
3849
3856
|
|
|
3850
3857
|
// src/commands/skill.ts
|
|
@@ -3890,7 +3897,8 @@ import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises
|
|
|
3890
3897
|
|
|
3891
3898
|
// src/format/apply-edits.ts
|
|
3892
3899
|
function applyAcceptedEdits(sourceHtml, state) {
|
|
3893
|
-
const
|
|
3900
|
+
const reduced = reduceReviewState(state);
|
|
3901
|
+
const accepted = reduced.edits.filter((edit) => edit.status === "accepted");
|
|
3894
3902
|
const planned = accepted.map((edit) => planEdit(sourceHtml, edit));
|
|
3895
3903
|
planned.sort((left, right) => right.start - left.start);
|
|
3896
3904
|
let html = sourceHtml;
|
|
@@ -3899,7 +3907,13 @@ function applyAcceptedEdits(sourceHtml, state) {
|
|
|
3899
3907
|
}
|
|
3900
3908
|
return {
|
|
3901
3909
|
html,
|
|
3902
|
-
appliedEdits: planned.length
|
|
3910
|
+
appliedEdits: planned.length,
|
|
3911
|
+
totalSuggestedEdits: reduced.edits.length,
|
|
3912
|
+
acceptedEdits: accepted.length,
|
|
3913
|
+
openEdits: reduced.edits.filter((edit) => edit.status === "open").length,
|
|
3914
|
+
rejectedEdits: reduced.edits.filter((edit) => edit.status === "rejected").length,
|
|
3915
|
+
deletedEdits: reduced.edits.filter((edit) => edit.status === "deleted").length,
|
|
3916
|
+
skippedEdits: reduced.edits.length - accepted.length
|
|
3903
3917
|
};
|
|
3904
3918
|
}
|
|
3905
3919
|
function planEdit(sourceHtml, edit) {
|
|
@@ -3947,12 +3961,21 @@ async function unwrapFile(inputPath, outputPath, options = {}) {
|
|
|
3947
3961
|
const sourceBytes = unwrapReviewHtml(reviewHtml);
|
|
3948
3962
|
if (!options.applyAcceptedEdits) {
|
|
3949
3963
|
await writeFile4(outputPath, sourceBytes);
|
|
3950
|
-
return {};
|
|
3964
|
+
return { sourceBytes: sourceBytes.byteLength };
|
|
3951
3965
|
}
|
|
3952
3966
|
const state = extractReviewState(reviewHtml);
|
|
3953
3967
|
const result = applyAcceptedEdits(sourceBytes.toString("utf8"), state);
|
|
3954
3968
|
await writeFile4(outputPath, result.html, "utf8");
|
|
3955
|
-
return {
|
|
3969
|
+
return {
|
|
3970
|
+
sourceBytes: Buffer.byteLength(result.html, "utf8"),
|
|
3971
|
+
appliedEdits: result.appliedEdits,
|
|
3972
|
+
totalSuggestedEdits: result.totalSuggestedEdits,
|
|
3973
|
+
acceptedEdits: result.acceptedEdits,
|
|
3974
|
+
openEdits: result.openEdits,
|
|
3975
|
+
rejectedEdits: result.rejectedEdits,
|
|
3976
|
+
deletedEdits: result.deletedEdits,
|
|
3977
|
+
skippedEdits: result.skippedEdits
|
|
3978
|
+
};
|
|
3956
3979
|
}
|
|
3957
3980
|
|
|
3958
3981
|
// src/commands/wrap.ts
|
|
@@ -3961,18 +3984,36 @@ async function wrapFile(inputPath, outputPath) {
|
|
|
3961
3984
|
const sourceBytes = await readFile5(inputPath);
|
|
3962
3985
|
const reviewHtml = createReviewHtml(sourceBytes, { sourcePath: inputPath });
|
|
3963
3986
|
await writeFile5(outputPath, reviewHtml, "utf8");
|
|
3987
|
+
return {
|
|
3988
|
+
sourceBytes: sourceBytes.byteLength,
|
|
3989
|
+
reviewBytes: Buffer.byteLength(reviewHtml, "utf8")
|
|
3990
|
+
};
|
|
3964
3991
|
}
|
|
3965
3992
|
|
|
3966
3993
|
// src/cli.ts
|
|
3967
3994
|
async function main(argv = process.argv) {
|
|
3968
3995
|
try {
|
|
3969
|
-
const
|
|
3996
|
+
const args = argv.slice(2);
|
|
3997
|
+
const commandName = basename2(argv[1] ?? "html-collab");
|
|
3998
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
3999
|
+
process.stdout.write(usage(commandName) + `
|
|
4000
|
+
`);
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
4004
|
+
process.stdout.write(packageVersion() + `
|
|
4005
|
+
`);
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
const command = parseArgs(args);
|
|
3970
4009
|
if (command.command === "wrap") {
|
|
3971
|
-
await wrapFile(command.inputPath, command.outputPath);
|
|
4010
|
+
const result2 = await wrapFile(command.inputPath, command.outputPath);
|
|
4011
|
+
writeStatus(`Wrote ${command.outputPath} (${formatBytes(result2.reviewBytes)} review file from ${formatBytes(result2.sourceBytes)} source).`);
|
|
3972
4012
|
return;
|
|
3973
4013
|
}
|
|
3974
4014
|
if (command.command === "merge") {
|
|
3975
|
-
await mergeFiles(command.inputPaths, command.outputPath);
|
|
4015
|
+
const result2 = await mergeFiles(command.inputPaths, command.outputPath);
|
|
4016
|
+
writeStatus(`Merged ${command.inputPaths.length} files into ${command.outputPath} (${formatBytes(result2.reviewBytes)}, ${result2.totalOps} ${plural(result2.totalOps, "operation")}, ${result2.addedOps} new).`);
|
|
3976
4017
|
return;
|
|
3977
4018
|
}
|
|
3978
4019
|
if (command.command === "extract") {
|
|
@@ -3982,6 +4023,8 @@ async function main(argv = process.argv) {
|
|
|
3982
4023
|
});
|
|
3983
4024
|
if (!command.outputPath) {
|
|
3984
4025
|
process.stdout.write(output);
|
|
4026
|
+
} else {
|
|
4027
|
+
writeStatus(`Wrote ${command.outputPath} (${command.format}, ${formatBytes(Buffer.byteLength(output, "utf8"))}).`);
|
|
3985
4028
|
}
|
|
3986
4029
|
return;
|
|
3987
4030
|
}
|
|
@@ -3989,14 +4032,18 @@ async function main(argv = process.argv) {
|
|
|
3989
4032
|
const output = await skillFile(command.outputPath);
|
|
3990
4033
|
if (!command.outputPath) {
|
|
3991
4034
|
process.stdout.write(output);
|
|
4035
|
+
} else {
|
|
4036
|
+
writeStatus(`Wrote ${command.outputPath} (${formatBytes(Buffer.byteLength(output, "utf8"))}).`);
|
|
3992
4037
|
}
|
|
3993
4038
|
return;
|
|
3994
4039
|
}
|
|
3995
4040
|
const result = await unwrapFile(command.inputPath, command.outputPath, {
|
|
3996
4041
|
applyAcceptedEdits: command.applyAcceptedEdits
|
|
3997
4042
|
});
|
|
3998
|
-
if (command.applyAcceptedEdits
|
|
3999
|
-
|
|
4043
|
+
if (command.applyAcceptedEdits) {
|
|
4044
|
+
writeStatus(applyEditsStatus(command.outputPath, result));
|
|
4045
|
+
} else {
|
|
4046
|
+
writeStatus(`Wrote ${command.outputPath} (${formatBytes(result.sourceBytes)}, original source HTML).`);
|
|
4000
4047
|
}
|
|
4001
4048
|
} catch (error) {
|
|
4002
4049
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -4132,6 +4179,8 @@ function isExtractFormat(value) {
|
|
|
4132
4179
|
}
|
|
4133
4180
|
function usage(commandName) {
|
|
4134
4181
|
return `Usage:
|
|
4182
|
+
${commandName} --help
|
|
4183
|
+
${commandName} --version
|
|
4135
4184
|
${commandName} wrap report.html --out report.review.html
|
|
4136
4185
|
${commandName} merge glen.review.html maya.review.html --out merged.review.html
|
|
4137
4186
|
${commandName} extract merged.review.html --format markdown --out review-brief.md
|
|
@@ -4141,6 +4190,59 @@ function usage(commandName) {
|
|
|
4141
4190
|
${commandName} unwrap report.review.html --out report.final.html
|
|
4142
4191
|
${commandName} unwrap report.review.html --apply-edits --out report.final.html`;
|
|
4143
4192
|
}
|
|
4193
|
+
function writeStatus(message) {
|
|
4194
|
+
console.error(message);
|
|
4195
|
+
}
|
|
4196
|
+
function applyEditsStatus(outputPath, result) {
|
|
4197
|
+
const applied = result.appliedEdits ?? 0;
|
|
4198
|
+
const skipped = result.skippedEdits ?? 0;
|
|
4199
|
+
const skippedStatus = skipped > 0 ? `; skipped ${formatSkippedEdits(result)}` : "";
|
|
4200
|
+
const output = `Wrote ${outputPath} (${formatBytes(result.sourceBytes)}).`;
|
|
4201
|
+
if (applied === 0) {
|
|
4202
|
+
return `Applied 0 accepted edits${skippedStatus}. ${output}`;
|
|
4203
|
+
}
|
|
4204
|
+
return `Applied ${applied} accepted ${plural(applied, "edit")}${skippedStatus}. ${output}`;
|
|
4205
|
+
}
|
|
4206
|
+
function formatSkippedEdits(result) {
|
|
4207
|
+
const parts = [
|
|
4208
|
+
countLabel(result.openEdits ?? 0, "open edit"),
|
|
4209
|
+
countLabel(result.rejectedEdits ?? 0, "rejected edit"),
|
|
4210
|
+
countLabel(result.deletedEdits ?? 0, "deleted edit")
|
|
4211
|
+
].filter(Boolean);
|
|
4212
|
+
return parts.length > 0 ? parts.join(", ") : "0 edits";
|
|
4213
|
+
}
|
|
4214
|
+
function countLabel(count, label) {
|
|
4215
|
+
return count > 0 ? `${count} ${plural(count, label)}` : undefined;
|
|
4216
|
+
}
|
|
4217
|
+
function plural(count, singular) {
|
|
4218
|
+
return count === 1 ? singular : `${singular}s`;
|
|
4219
|
+
}
|
|
4220
|
+
function formatBytes(bytes) {
|
|
4221
|
+
if (bytes < 1024) {
|
|
4222
|
+
return `${bytes} B`;
|
|
4223
|
+
}
|
|
4224
|
+
const kib = bytes / 1024;
|
|
4225
|
+
if (kib < 1024) {
|
|
4226
|
+
return `${kib.toFixed(kib >= 10 ? 0 : 1)} KB`;
|
|
4227
|
+
}
|
|
4228
|
+
const mib = kib / 1024;
|
|
4229
|
+
return `${mib.toFixed(mib >= 10 ? 0 : 1)} MB`;
|
|
4230
|
+
}
|
|
4231
|
+
function packageVersion() {
|
|
4232
|
+
let current = dirname3(fileURLToPath2(import.meta.url));
|
|
4233
|
+
while (true) {
|
|
4234
|
+
const candidate = join2(current, "package.json");
|
|
4235
|
+
if (existsSync(candidate)) {
|
|
4236
|
+
const packageJson = JSON.parse(readFileSync(candidate, "utf8"));
|
|
4237
|
+
return packageJson.version ?? "0.0.0";
|
|
4238
|
+
}
|
|
4239
|
+
const parent = dirname3(current);
|
|
4240
|
+
if (parent === current) {
|
|
4241
|
+
return "0.0.0";
|
|
4242
|
+
}
|
|
4243
|
+
current = parent;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4144
4246
|
if (isDirectRun()) {
|
|
4145
4247
|
await main(process.argv);
|
|
4146
4248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "html-collab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Turn any single-file HTML report into a portable, reviewable document. Comments, suggested edits, and deterministic merge — all stored inside the HTML file itself.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|