@fjall/util 12.2.0 → 12.3.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.
@@ -9,7 +9,7 @@
9
9
  "package.json",
10
10
  "tsconfig.json"
11
11
  ],
12
- "hash": "3fd0e9f62624556a06f527609beda6b85a29454c95eca244fda37f23e73c52de",
12
+ "hash": "6bdd5fed222c37914a9331fbb1fa99dce21a102ed9159de858b1b27ae44fc62b",
13
13
  "files": {
14
14
  "src/async/concurrency.ts": "3c7ebe0222a720f9",
15
15
  "src/async/singleton.ts": "7bcc21b0fb920561",
@@ -35,6 +35,8 @@
35
35
  "src/deploy/approvalTokenOutput.ts": "4be7b52825d88a37",
36
36
  "src/devSubstrate/entrypoint.ts": "0526b88258174aa6",
37
37
  "src/devSubstrate/params.ts": "43bf902b57eb9edd",
38
+ "src/diagnostics/controlSequences.ts": "cb1cc3f29f041371",
39
+ "src/diagnostics/index.ts": "249d04cb4dd7db35",
38
40
  "src/docker/DockerCli.build.ts": "93a74294437cc002",
39
41
  "src/docker/DockerCli.daemon.ts": "11b9c3352c957d7c",
40
42
  "src/docker/DockerCli.registry.ts": "f8dfcf1d7e413014",
@@ -59,6 +61,7 @@
59
61
  "src/docker/result.ts": "2ff0d59a3add8b06",
60
62
  "src/environments.ts": "b3d2f2493554cd0f",
61
63
  "src/errorUtils.ts": "c601f2905c74b8d0",
64
+ "src/errors/absence.ts": "390fea350ffbb4e5",
62
65
  "src/failure/deploymentFailure.ts": "5824db1d97d01838",
63
66
  "src/failure/extractErrorWindow.ts": "ca41f90946fa939c",
64
67
  "src/fsHelpers.ts": "0869d9739d523572",
@@ -107,9 +110,9 @@
107
110
  "src/securityHelpers.ts": "3f78606079c8f45e",
108
111
  "src/targets.ts": "126280f0a5762e21",
109
112
  "../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
110
- "package.json": "b23ba4269f90691a",
113
+ "package.json": "da4f5b3991378803",
111
114
  "tsconfig.json": "db759831ad95f396"
112
115
  },
113
- "outputHash": "3fe4db92b87db80c477aee925dc178caae99fab66d37ae0e1d644d9aa6c4f0d6",
114
- "outputFileCount": 197
116
+ "outputHash": "5d7079e0f38bcce1da6411693f11df940a142f138d4bfa6481553998cfaa1aab",
117
+ "outputFileCount": 203
115
118
  }
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 98 files minified at 2026-08-15T22:07:25.133Z
1
+ 101 files minified at 2026-08-16T01:19:29.355Z
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Strip terminal control sequences from a warning message before it is
3
+ * masked: ESC-introduced ANSI sequences, C1-CSI-introduced sequences, and
4
+ * every remaining C0/DEL/C1 control character except \t, \n and \r.
5
+ * Strip BEFORE mask (F-R12): a control character inside a credential-shaped
6
+ * value splits the mask regex anchors, so masking must see stripped text.
7
+ */
8
+ export declare function stripControlSequences(value: string): string;
@@ -0,0 +1 @@
1
+ var C=Object.defineProperty;var u=(e,E)=>C(e,"name",{value:E,configurable:!0});const c=/\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\(B/g,n=/\u009b[0-?]*[ -/]*[@-~]/g,r=/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;function x(e){return e.replace(c,"").replace(n,"").replace(r,"")}u(x,"stripControlSequences");export{x as stripControlSequences};
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Diagnostics spine — the one guaranteed lane for severity=warning
3
+ * diagnostics. Producers call {@link emitWarning}; the surface that owns the
4
+ * terminal end of the lane binds a {@link DiagnosticsSink} via
5
+ * {@link configureDiagnosticsSink}; screen-scoped consumers tap
6
+ * {@link subscribeWarnings} and replay via {@link consumeWarnings}; and
7
+ * {@link drainUnconsumed} is the end-of-session backstop that keeps a
8
+ * warning no surface ever consumed visible rather than silent.
9
+ *
10
+ * `consumed` means exactly "this event reached a user-visible surface":
11
+ * a rendering sink emitted it ({@link DiagnosticsSink.rendersToUser}), a
12
+ * scope-matched live subscriber received it, or a consuming read replayed
13
+ * it onto a screen. Nothing else sets the flag, so
14
+ * {@link drainUnconsumed} is a true complement — everything it returns
15
+ * was never shown anywhere, and everything shown is never reprinted.
16
+ *
17
+ * Masking is owned structurally: emitWarning strips control sequences THEN
18
+ * masks (F-R12 mechanised — no call-site ordering to get wrong), over the
19
+ * message AND every string value inside plain-object/array metadata. The
20
+ * buffer and every subscriber receive the same post-mask event object; the
21
+ * subscription taps after the mask, never before. Sinks declaring
22
+ * masksInternally receive the post-strip PRE-mask message and raw metadata
23
+ * because their own render path masks the whole tree (the agent
24
+ * renderEvent maskTree, the MCP emitter) — one mask per path, decided by
25
+ * the sink's declaration. Producers whose payloads are masked at
26
+ * construction by documented contract (e.g. StepOutput.warnings)
27
+ * double-mask idempotently — a benign overlap, named here so nobody
28
+ * deletes the construction masks: they also protect persistence surfaces
29
+ * this spine never sees.
30
+ *
31
+ * Scope binding is explicit (per-package scope constants), never ambient.
32
+ * AsyncLocalStorage was considered and rejected: ALS would give
33
+ * per-operation isolation without scope strings, but context propagation
34
+ * through Ink's React scheduler and AWS SDK callback chains is not
35
+ * async-hooks-reliable — scope binding would be silently wrong exactly
36
+ * where it matters. Explicit scope constants and subscriptions are
37
+ * inspectable.
38
+ *
39
+ * The confirm-style warn-and-wait lane (ProgressCallbacks.onWarning(message,
40
+ * proceedCallback)) is a consent flow, not a diagnostic — it must never be
41
+ * flattened onto this fire-and-forget spine.
42
+ */
43
+ export { stripControlSequences } from "./controlSequences.js";
44
+ export interface WarningEvent {
45
+ /**
46
+ * Warning text, stored and delivered post-strip+mask. Only a sink
47
+ * declaring masksInternally sees the post-strip pre-mask spelling.
48
+ */
49
+ message: string;
50
+ /**
51
+ * Per-package scope constant (diagnosticScopes.ts in each consumer) —
52
+ * free-form scope literals are forbidden in production code.
53
+ */
54
+ scope?: string;
55
+ code?: string;
56
+ /**
57
+ * Agent TOON operation spelling; the bound sink defaults it to the
58
+ * bound command path when absent. Producer-owned constant (a command
59
+ * path), never remote text — it bypasses the strip+mask, as does
60
+ * `code`.
61
+ */
62
+ operation?: string;
63
+ /**
64
+ * Spread into the agent TOON value by the agent sink. String values in
65
+ * plain objects/arrays are stripped+masked for the buffered/subscriber
66
+ * copy; non-plain objects pass through unprocessed and must therefore
67
+ * never carry sensitive text.
68
+ */
69
+ metadata?: Record<string, unknown>;
70
+ }
71
+ export interface DiagnosticsSink {
72
+ emit(event: WarningEvent): void;
73
+ /**
74
+ * true — the sink's render path masks (agent renderEvent maskTree, MCP
75
+ * emitter) and receives the post-strip PRE-mask message and raw
76
+ * metadata so credentials are masked exactly once. false — the sink
77
+ * renders verbatim and receives the post-mask event.
78
+ */
79
+ masksInternally: boolean;
80
+ /**
81
+ * true — emit() puts the event on a user-visible surface (stderr, the
82
+ * TOON stream, MCP frames), so emission marks it consumed and the
83
+ * end-of-session drain never reprints it. false — emit() renders
84
+ * nothing (the tui/mcp buffer-only defaults); the event stays
85
+ * unconsumed until a scope-matched subscriber, a consuming read, or
86
+ * the drain shows it.
87
+ */
88
+ rendersToUser: boolean;
89
+ }
90
+ /**
91
+ * Emit a warning onto the spine. Strips control sequences, masks, buffers
92
+ * the post-mask event, delivers it to the bound sink synchronously and to
93
+ * scope-matched subscribers on a microtask. The event is marked consumed
94
+ * only when a user-visible surface actually shows it: a rendering sink at
95
+ * emit, or a still-subscribed scope-matched listener at dispatch.
96
+ */
97
+ export declare function emitWarning(message: string, opts?: Omit<WarningEvent, "message">): void;
98
+ /**
99
+ * Bind the sink that owns warning rendering. Returns a restore function
100
+ * that reinstates the previously bound sink — guarded, so a stale restore
101
+ * (a surface torn down after a later rebind) never clobbers the newer
102
+ * binding.
103
+ */
104
+ export declare function configureDiagnosticsSink(sink: DiagnosticsSink): () => void;
105
+ export interface SubscribeWarningsOptions {
106
+ /**
107
+ * Deliver only events carrying exactly this scope. The declaration
108
+ * lives in the spine so delivery to a non-matching listener can never
109
+ * mark an event consumed — a foreign-scope event stays unconsumed for
110
+ * its own consumer or the drain. Omitted = every event, any scope.
111
+ */
112
+ scope?: string;
113
+ }
114
+ /**
115
+ * Subscribe to post-mask warning events (screen-scoped capture). Dispatch
116
+ * is microtask-deferred and scope-filtered by the spine. Returns
117
+ * unsubscribe.
118
+ */
119
+ export declare function subscribeWarnings(listener: (event: WarningEvent) => void, options?: SubscribeWarningsOptions): () => void;
120
+ /**
121
+ * Inspect the buffered warnings for one scope WITHOUT consuming them —
122
+ * tests and diagnostics only. A screen replaying onto a surface must use
123
+ * {@link consumeWarnings}, or the drain will reprint what it showed.
124
+ */
125
+ export declare function readWarnings(scope: string): WarningEvent[];
126
+ /**
127
+ * Read the buffered warnings for one scope onto a user-visible surface
128
+ * (replay-on-mount), marking them consumed. Events stay buffered so a
129
+ * remount replays the same set — {@link clearWarnings} at operation start
130
+ * deletes the shown ones.
131
+ */
132
+ export declare function consumeWarnings(scope: string): WarningEvent[];
133
+ /**
134
+ * Delete one scope's CONSUMED buffered warnings (call when an operation
135
+ * starts, so back-nav or re-run never re-shows a previous run's
136
+ * warnings). Never-shown events survive — they belong to the next replay
137
+ * (where showing them consumes them) or the end-of-session drain, so a
138
+ * clear can never destroy a warning nobody saw. Scoped only — the full
139
+ * clear is resetDiagnostics, and that is for tests.
140
+ */
141
+ export declare function clearWarnings(scope: string): void;
142
+ /**
143
+ * Return every buffered event no rendering sink, scope-matched
144
+ * subscriber, or consuming read ever showed, in emission order, marking
145
+ * them consumed — the end-of-session flush that keeps buffer-only modes
146
+ * honest. In rendering-sink modes every emission is consumed at emit, so
147
+ * a drain here is a no-op rather than a double-print.
148
+ */
149
+ export declare function drainUnconsumed(): WarningEvent[];
150
+ /** Tests only: drop all buffered events and subscribers, restore the default sink. */
151
+ export declare function resetDiagnostics(): void;
@@ -0,0 +1,2 @@
1
+ var b=Object.defineProperty;var r=(e,t)=>b(e,"name",{value:t,configurable:!0});import{maskSensitiveOutput as g}from"../securityHelpers.js";import{stripControlSequences as y}from"./controlSequences.js";import{stripControlSequences as _}from"./controlSequences.js";const h=50,k=Symbol("fjall.diagnostics.unscoped"),o=new Map,f=new Set;let S=0;const x={masksInternally:!1,rendersToUser:!0,emit(e){process.stderr.write(`[warn] ${e.message}
2
+ `)}};let i=x;function m(e){if(typeof e=="string")return g(y(e));if(Array.isArray(e))return e.map(m);if(e!==null&&typeof e=="object"){const t=Object.getPrototypeOf(e);if(t===Object.prototype||t===null)return Object.fromEntries(Object.entries(e).map(([n,s])=>[n,m(s)]))}return e}r(m,"maskMetadataValue");function E(e,t){const n=y(e),s={...t,...t?.metadata!==void 0&&{metadata:m(t.metadata)},message:g(n)},a={event:s,consumed:!1,seq:S++},l=s.scope??k;let c=o.get(l);c||(c=[],o.set(l,c)),c.push(a),c.length>h&&c.shift(),i.masksInternally?i.emit({...t,message:n}):i.emit(s),i.rendersToUser&&(a.consumed=!0);const d=[...f].filter(u=>u.scope===void 0||u.scope===s.scope);d.length>0&&queueMicrotask(()=>{const u=d.filter(p=>f.has(p));if(u.length!==0){a.consumed=!0;for(const{listener:p}of u)p(s)}})}r(E,"emitWarning");function C(e){const t=i;return i=e,()=>{i===e&&(i=t)}}r(C,"configureDiagnosticsSink");function U(e,t={}){const n={listener:e,scope:t.scope};return f.add(n),()=>{f.delete(n)}}r(U,"subscribeWarnings");function W(e){const t=o.get(e);return t?t.map(n=>n.event):[]}r(W,"readWarnings");function w(e){const t=o.get(e);return t?t.map(n=>(n.consumed=!0,n.event)):[]}r(w,"consumeWarnings");function P(e){const t=o.get(e);if(!t)return;const n=t.filter(s=>!s.consumed);n.length===0?o.delete(e):o.set(e,n)}r(P,"clearWarnings");function v(){const e=[];for(const t of o.values())for(const n of t)n.consumed||(n.consumed=!0,e.push(n));return e.sort((t,n)=>t.seq-n.seq).map(t=>t.event)}r(v,"drainUnconsumed");function A(){o.clear(),f.clear(),i=x,S=0}r(A,"resetDiagnostics");export{P as clearWarnings,C as configureDiagnosticsSink,w as consumeWarnings,v as drainUnconsumed,E as emitWarning,W as readWarnings,A as resetDiagnostics,_ as stripControlSequences,U as subscribeWarnings};
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared absence vocabulary. Verified absence (the thing looked for is
3
+ * known not to exist) is a different outcome from failure-to-determine;
4
+ * consumers that branch on absence probe structurally with
5
+ * {@link isAbsence} instead of sniffing per-class spellings or message
6
+ * text.
7
+ */
8
+ /**
9
+ * Discriminant literal for verified absence. Error classes spell absence
10
+ * as errorType "not_found"; wire codes derive from it via the
11
+ * PREFIX_${errorType.toUpperCase()} minting convention.
12
+ */
13
+ export declare const NOT_FOUND = "not_found";
14
+ /**
15
+ * Structural absence probe: errorType "not_found" (typed service errors)
16
+ * or statusCode 404 (API-boundary errors). Structural rather than
17
+ * instanceof — class names are presentation, not contract (dist
18
+ * minification has mangled them before), and the probe must hold across
19
+ * the cli/deploy-core package boundary.
20
+ */
21
+ export declare function isAbsence(err: unknown): boolean;
@@ -0,0 +1 @@
1
+ var o=Object.defineProperty;var n=(t,e)=>o(t,"name",{value:e,configurable:!0});const s="not_found";function u(t){if(typeof t!="object"||t===null)return!1;const e=t;return e.errorType===s||e.statusCode===404}n(u,"isAbsence");export{s as NOT_FOUND,u as isAbsence};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/util",
3
- "version": "12.2.0",
3
+ "version": "12.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -102,6 +102,14 @@
102
102
  "./secrets": {
103
103
  "types": "./dist/secrets.d.ts",
104
104
  "default": "./dist/secrets.js"
105
+ },
106
+ "./diagnostics": {
107
+ "types": "./dist/diagnostics/index.d.ts",
108
+ "default": "./dist/diagnostics/index.js"
109
+ },
110
+ "./errors/absence": {
111
+ "types": "./dist/errors/absence.d.ts",
112
+ "default": "./dist/errors/absence.js"
105
113
  }
106
114
  },
107
115
  "files": [