humanish 0.68.0 → 0.69.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.
@@ -0,0 +1,42 @@
1
+ import { type VerifyResult } from "./run.js";
2
+ export declare const EXPORT_SCHEMA = "humanish.export-result.v1";
3
+ /** Past this the file stops being a thing you attach to an email. Declared, never silent. */
4
+ export declare const DEFAULT_EXPORT_MAX_BYTES: number;
5
+ export interface ExportResult {
6
+ schema: typeof EXPORT_SCHEMA;
7
+ ok: true;
8
+ cwd: string;
9
+ runId: string;
10
+ /** Repo-relative path of the file written. */
11
+ path: string;
12
+ bytes: number;
13
+ embeddedImages: number;
14
+ shareSafety: VerifyResult["shareSafety"];
15
+ /** True when --local-only exported a bundle that is not share_ready; the file carries a banner. */
16
+ watermarked: boolean;
17
+ warnings: string[];
18
+ }
19
+ export interface ExportFailure {
20
+ schema: typeof EXPORT_SCHEMA;
21
+ ok: false;
22
+ cwd: string;
23
+ run: string;
24
+ shareSafety?: VerifyResult["shareSafety"];
25
+ error: {
26
+ code: "HUMANISH_EXPORT_RUN_NOT_FOUND" | "HUMANISH_EXPORT_VERIFY_FAILED" | "HUMANISH_EXPORT_SHARE_SAFETY_BLOCKED" | "HUMANISH_EXPORT_NO_OBSERVER" | "HUMANISH_EXPORT_TOO_LARGE";
27
+ message: string;
28
+ };
29
+ }
30
+ export interface ExportOptions {
31
+ out?: string;
32
+ localOnly?: boolean;
33
+ maxBytes?: number;
34
+ }
35
+ export interface ExportDeps {
36
+ /** Injected in tests: a fake verify with a chosen shareSafety, no full bundle needed. */
37
+ verify?: (cwd: string, run: string) => Promise<VerifyResult>;
38
+ }
39
+ /** The banner a --local-only export carries. Plain HTML, before the app root, so it renders with JS off. */
40
+ export declare function localOnlyBanner(reasons: string[]): string;
41
+ export declare function exportRun(cwdInput: string, runInput: string, options?: ExportOptions, deps?: ExportDeps): Promise<ExportResult | ExportFailure>;
42
+ export declare function formatExportHuman(result: ExportResult | ExportFailure): string;
package/dist/export.js ADDED
@@ -0,0 +1,169 @@
1
+ // `humanish export`: one file a coworker can open (#471).
2
+ //
3
+ // A run is a directory. Sharing it meant a tunnel (`serve --expose`) or a hand-zipped bundle,
4
+ // neither of which is "send one thing". The Observer is already a single-file artifact with the
5
+ // run's data inlined; what keeps it from travelling is the screenshots it references by path.
6
+ // Export inlines those as data URIs and writes ONE .html that opens from a mail attachment.
7
+ //
8
+ // Share safety is the point, not a step: export runs verify inside the flow and refuses a bundle
9
+ // that is not share_ready. A local_only bundle (raw screenshots) exports only with an explicit
10
+ // --local-only, and the file it writes says so in a banner nothing can miss.
11
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { resolveRunPath, verifyRun } from "./run.js";
14
+ export const EXPORT_SCHEMA = "humanish.export-result.v1";
15
+ /** Past this the file stops being a thing you attach to an email. Declared, never silent. */
16
+ export const DEFAULT_EXPORT_MAX_BYTES = 25 * 1024 * 1024;
17
+ const OBSERVER_DATA_SLOT = /<script id="observer-data" type="application\/json">([\s\S]*?)<\/script>/;
18
+ const IMAGE_MIME = {
19
+ ".png": "image/png",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".webp": "image/webp",
23
+ ".gif": "image/gif"
24
+ };
25
+ function escapeJsonScript(json) {
26
+ return json
27
+ .replace(/</g, "\\u003c")
28
+ .replace(/>/g, "\\u003e")
29
+ .replace(/&/g, "\\u0026")
30
+ .replace(/\u2028/g, "\\u2028")
31
+ .replace(/\u2029/g, "\\u2029");
32
+ }
33
+ /** The banner a --local-only export carries. Plain HTML, before the app root, so it renders with JS off. */
34
+ export function localOnlyBanner(reasons) {
35
+ const why = reasons.length === 0 ? "" : ` (${reasons.join(", ")})`;
36
+ return `<div id="humanish-local-only" role="alert" style="position:sticky;top:0;z-index:2147483647;background:#7a1f1f;color:#fff;font:600 14px/1.4 system-ui,sans-serif;padding:10px 16px;text-align:center">`
37
+ + `LOCAL ONLY. This export was made from a bundle that is not share-safe${why}. Do not forward it outside the team that owns the run.`
38
+ + `</div>`;
39
+ }
40
+ async function isInside(root, candidate) {
41
+ const rel = path.relative(root, candidate);
42
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
43
+ }
44
+ export async function exportRun(cwdInput, runInput, options = {}, deps = {}) {
45
+ const cwd = path.resolve(cwdInput);
46
+ const runPaths = await resolveRunPath(cwd, runInput).catch(() => null);
47
+ if (runPaths === null) {
48
+ return { schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, error: { code: "HUMANISH_EXPORT_RUN_NOT_FOUND", message: `No run resolves from "${runInput}" under ${cwd}.` } };
49
+ }
50
+ const runRoot = runPaths.absoluteRunRoot;
51
+ const runId = path.basename(runRoot);
52
+ const verified = await (deps.verify ?? verifyRun)(cwd, runInput);
53
+ if (!verified.ok) {
54
+ return {
55
+ schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, shareSafety: verified.shareSafety,
56
+ error: { code: "HUMANISH_EXPORT_VERIFY_FAILED", message: `Run ${runId} does not verify; export refuses to package evidence that fails its own checks.` }
57
+ };
58
+ }
59
+ const shareReady = verified.shareSafety.status === "share_ready";
60
+ if (!shareReady && options.localOnly !== true) {
61
+ return {
62
+ schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, shareSafety: verified.shareSafety,
63
+ error: {
64
+ code: "HUMANISH_EXPORT_SHARE_SAFETY_BLOCKED",
65
+ message: `Run ${runId} is ${verified.shareSafety.status}, not share_ready: ${verified.shareSafety.reasons.map((r) => r.code).join(", ")}. Re-run with policies.redactScreenshots: true, or pass --local-only to export a watermarked file for people who may see raw screenshots.`
66
+ }
67
+ };
68
+ }
69
+ const observerPath = path.join(runRoot, "observer", "index.html");
70
+ let html;
71
+ try {
72
+ html = await readFile(observerPath, "utf8");
73
+ }
74
+ catch {
75
+ return { schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, shareSafety: verified.shareSafety, error: { code: "HUMANISH_EXPORT_NO_OBSERVER", message: `Run ${runId} has no observer/index.html to export.` } };
76
+ }
77
+ const slot = OBSERVER_DATA_SLOT.exec(html);
78
+ if (slot === null) {
79
+ return { schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, shareSafety: verified.shareSafety, error: { code: "HUMANISH_EXPORT_NO_OBSERVER", message: `Run ${runId}'s Observer carries no inline data slot; rebuild the run's Observer first.` } };
80
+ }
81
+ // Every string in the data that names an image file inside the run becomes a data URI. Paths are
82
+ // run-root-relative in the data; a path that resolves outside the run is left alone, never read.
83
+ const data = JSON.parse(slot[1]);
84
+ const cache = new Map();
85
+ let embedded = 0;
86
+ let imageBytes = 0;
87
+ const warnings = [];
88
+ const inline = async (value) => {
89
+ const ext = path.extname(value).toLowerCase();
90
+ const mime = IMAGE_MIME[ext];
91
+ if (mime === undefined || value.startsWith("data:") || /^[a-z]+:\/\//i.test(value))
92
+ return value;
93
+ const cached = cache.get(value);
94
+ if (cached !== undefined)
95
+ return cached ?? value;
96
+ const candidates = [path.resolve(runRoot, value), path.resolve(runRoot, "observer", value)];
97
+ for (const candidate of candidates) {
98
+ if (!(await isInside(runRoot, candidate)))
99
+ continue;
100
+ const info = await stat(candidate).catch(() => null);
101
+ if (info === null || !info.isFile())
102
+ continue;
103
+ const bytes = await readFile(candidate);
104
+ imageBytes += bytes.byteLength;
105
+ embedded += 1;
106
+ const uri = `data:${mime};base64,${bytes.toString("base64")}`;
107
+ cache.set(value, uri);
108
+ return uri;
109
+ }
110
+ cache.set(value, null);
111
+ warnings.push(`image not found inside the run, left as a path: ${value}`);
112
+ return value;
113
+ };
114
+ const walk = async (node) => {
115
+ if (typeof node === "string")
116
+ return inline(node);
117
+ if (Array.isArray(node))
118
+ return Promise.all(node.map(walk));
119
+ if (node !== null && typeof node === "object") {
120
+ const out = {};
121
+ for (const [key, value] of Object.entries(node))
122
+ out[key] = await walk(value);
123
+ return out;
124
+ }
125
+ return node;
126
+ };
127
+ const inlined = await walk(data);
128
+ let output = html.replace(OBSERVER_DATA_SLOT, () => `<script id="observer-data" type="application/json">${escapeJsonScript(JSON.stringify(inlined))}</script>`);
129
+ const watermarked = !shareReady;
130
+ if (watermarked) {
131
+ const banner = localOnlyBanner(verified.shareSafety.reasons.map((r) => r.code));
132
+ output = output.includes("<body>") ? output.replace("<body>", `<body>${banner}`) : `${banner}${output}`;
133
+ }
134
+ const bytes = Buffer.byteLength(output, "utf8");
135
+ const maxBytes = options.maxBytes ?? DEFAULT_EXPORT_MAX_BYTES;
136
+ if (bytes > maxBytes) {
137
+ return {
138
+ schema: EXPORT_SCHEMA, ok: false, cwd, run: runInput, shareSafety: verified.shareSafety,
139
+ error: { code: "HUMANISH_EXPORT_TOO_LARGE", message: `Export would be ${bytes} bytes (${embedded} images, ${imageBytes} image bytes), over the ${maxBytes}-byte cap. Raise --max-bytes deliberately, or export a run with fewer frames.` }
140
+ };
141
+ }
142
+ const outPath = path.resolve(cwd, options.out ?? path.join(".humanish", "exports", `${runId}.html`));
143
+ await mkdir(path.dirname(outPath), { recursive: true });
144
+ await writeFile(outPath, output, "utf8");
145
+ return {
146
+ schema: EXPORT_SCHEMA,
147
+ ok: true,
148
+ cwd,
149
+ runId,
150
+ path: path.relative(cwd, outPath),
151
+ bytes,
152
+ embeddedImages: embedded,
153
+ shareSafety: verified.shareSafety,
154
+ watermarked,
155
+ warnings
156
+ };
157
+ }
158
+ export function formatExportHuman(result) {
159
+ if (!result.ok)
160
+ return `${result.error.code}: ${result.error.message}\n`;
161
+ const lines = [
162
+ `humanish export ${result.runId}`,
163
+ `file: ${result.path} (${(result.bytes / 1024).toFixed(0)} KB, ${result.embeddedImages} image(s) embedded)`,
164
+ `share safety: ${result.shareSafety.status}${result.watermarked ? " — WATERMARKED LOCAL ONLY" : ""}`,
165
+ ...result.warnings.map((warning) => `warning: ${warning}`)
166
+ ];
167
+ return `${lines.join("\n")}\n`;
168
+ }
169
+ //# sourceMappingURL=export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export.js","sourceRoot":"","sources":["../src/export.ts"],"names":[],"mappings":"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"}
package/dist/program.js CHANGED
@@ -11,6 +11,7 @@ import { redactText } from "./redaction.js";
11
11
  import { draftFeedback, listFeedback, renderIssueMarkdown, renderIssueUrl, verifyFeedback } from "./feedback.js";
12
12
  import { runInit } from "./init.js";
13
13
  import { computeStats, formatStatsHuman } from "./stats.js";
14
+ import { DEFAULT_EXPORT_MAX_BYTES, exportRun, formatExportHuman } from "./export.js";
14
15
  import { buildPayload, disabledByEnvironment, inHumanishCheckout, durationBucket, readTelemetryState, sendTelemetry, telemetryStatePath, deriveStudyFacts, TELEMETRY_NOTICE, writeTelemetryState } from "./telemetry.js";
15
16
  import { inspectLabManifest, listLabManifests, resolveLabManifest } from "./labs.js";
16
17
  import { runLabPreflight } from "./lab-preflight.js";
@@ -352,6 +353,7 @@ export function createProgram(io = {}) {
352
353
  registerReviewCommand(program, cliIo);
353
354
  registerRunsCommand(program, cliIo);
354
355
  registerStatsCommand(program, cliIo);
356
+ registerExportCommand(program, cliIo);
355
357
  registerCommsCommands(program, cliIo);
356
358
  registerReclaimCommand(program, cliIo);
357
359
  registerWatchCommand(program, cliIo);
@@ -893,6 +895,28 @@ function registerReviewCommand(parent, io) {
893
895
  io.setExitCode("ok" in result && result.ok === false ? 2 : 0);
894
896
  });
895
897
  }
898
+ function registerExportCommand(parent, io) {
899
+ parent
900
+ .command("export")
901
+ .description("Write one self-contained .html of a run's Observer with screenshots inlined (#471). Verify and the share_ready gate run inside; a local_only bundle exports only with --local-only, watermarked.")
902
+ .summary("Export one run as a single shareable .html file.")
903
+ .option("--run <id>", "Run id or 'latest'.", "latest")
904
+ .option("--out <path>", "Where to write the file. Defaults to .humanish/exports/<runId>.html.")
905
+ .option("--local-only", "Export a bundle that is not share_ready, with a LOCAL ONLY banner in the file.")
906
+ .option("--max-bytes <n>", "Refuse an export larger than this.", String(DEFAULT_EXPORT_MAX_BYTES))
907
+ .option("--cwd <path>", "Target project directory.", ".")
908
+ .option("--json", JSON_OPTION_DESCRIPTION)
909
+ .action(async (options, command) => {
910
+ const maxBytes = Number.parseInt(options.maxBytes, 10);
911
+ const result = await exportRun(options.cwd, options.run, {
912
+ ...(options.out === undefined ? {} : { out: options.out }),
913
+ ...(options.localOnly === undefined ? {} : { localOnly: options.localOnly }),
914
+ ...(Number.isFinite(maxBytes) && maxBytes > 0 ? { maxBytes } : {})
915
+ });
916
+ writeResult(command, io, result, formatExportHuman);
917
+ io.setExitCode(result.ok ? 0 : 2);
918
+ });
919
+ }
896
920
  function registerStatsCommand(parent, io) {
897
921
  parent
898
922
  .command("stats")