@sabaiway/agent-workflow-kit 1.17.0 → 1.19.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,75 @@
1
+ // surface.mjs — capability detection for the kit's OWN stdout (the direct CLI). Plan §4.5.
2
+ //
3
+ // Resolves how `node tools/family-registry.mjs` should render: the requested format, the effective
4
+ // render mode (json | plain | ansi), whether to emit color, the terminal width, and whether to fall
5
+ // back to ASCII glyphs. This governs ONLY the kit's direct-CLI output — the agent-mediated
6
+ // `/agent-workflow-kit status` surface always consumes `--json` and localizes itself.
7
+ //
8
+ // Pure + fully injectable (argv / env / isTTY / columns / platform are inputs), no side effects on
9
+ // import, Node >= 18. Tested in isolation against the §4.5 table.
10
+
11
+ export const FORMATS = Object.freeze(['auto', 'plain', 'ansi', 'json']);
12
+ export const FORMAT_ENV = 'AGENT_WORKFLOW_FORMAT';
13
+ export const MIN_WIDTH = 40; // below this, force plain ASCII (a box can't lay out under ~40 cols)
14
+ export const DEFAULT_WIDTH = 80;
15
+ const FORMAT_FLAG = '--format=';
16
+
17
+ // Resolve the requested format. A FLAG (--json or --format=X) beats the AGENT_WORKFLOW_FORMAT env;
18
+ // among flags the LAST on argv wins (deterministic, standard last-wins). `--json` is exact sugar for
19
+ // `--format=json`. A bare `--format` (no value), an empty value, or an unknown value → a LOUD reject
20
+ // (never a silent fallback — Hard Constraint). Absent everywhere → 'auto'.
21
+ export const resolveFormat = (argv = [], env = {}) => {
22
+ let fromFlag = null;
23
+ for (const a of argv) {
24
+ if (a === '--json') fromFlag = 'json';
25
+ else if (a === '--format') throw new Error(`[agent-workflow-kit] --format needs a value: --format=<${FORMATS.join('|')}>`);
26
+ else if (a.startsWith(FORMAT_FLAG)) fromFlag = a.slice(FORMAT_FLAG.length);
27
+ }
28
+ const requested = fromFlag ?? env[FORMAT_ENV] ?? 'auto';
29
+ if (!FORMATS.includes(requested)) {
30
+ throw new Error(`[agent-workflow-kit] invalid format "${requested}" — expected one of ${FORMATS.join(', ')}`);
31
+ }
32
+ return requested;
33
+ };
34
+
35
+ // Terminal width: stdout.columns (>0) wins, else $COLUMNS (>0), else the 80-col default. A garbage /
36
+ // zero / undefined value falls through to the next source — never NaN.
37
+ export const resolveWidth = ({ columns, env = {} } = {}) => {
38
+ const fromStdout = Number(columns);
39
+ if (Number.isFinite(fromStdout) && fromStdout > 0) return fromStdout;
40
+ const fromEnv = Number(env.COLUMNS);
41
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
42
+ return DEFAULT_WIDTH;
43
+ };
44
+
45
+ // Color is ORTHOGONAL to the render mode (the ansi renderer applies it; plain ignores it). Precedence:
46
+ // CLICOLOR_FORCE / FORCE_COLOR present → on (unless explicitly '0'/'false'); else NO_COLOR present
47
+ // (incl. empty value) → off; else follow isTTY. FORCE beats NO_COLOR.
48
+ export const resolveColor = ({ env = {}, isTTY = false } = {}) => {
49
+ if ('CLICOLOR_FORCE' in env || 'FORCE_COLOR' in env) {
50
+ const v = 'CLICOLOR_FORCE' in env ? env.CLICOLOR_FORCE : env.FORCE_COLOR;
51
+ return !(v === '0' || String(v).toLowerCase() === 'false');
52
+ }
53
+ if ('NO_COLOR' in env) return false; // any value, incl. empty, disables (the NO_COLOR spec)
54
+ return Boolean(isTTY);
55
+ };
56
+
57
+ const isUtf8Env = (env = {}) => /utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || '');
58
+
59
+ // The full resolved surface. mode: 'json' (machine envelope) | 'plain' | 'ansi'. For a non-json
60
+ // format: an explicit plain/ansi is honored; 'auto' detects a tier (plain when NOT a TTY, or TERM=dumb,
61
+ // or any CI env; else ansi). A width below MIN_WIDTH FORCES plain. ascii glyphs when narrow, or on a
62
+ // Windows TTY without a UTF-8 locale (shallow — deeper Windows/UTF-8 depth is deferred, Plan §9).
63
+ export const detectSurface = ({ argv = [], env = {}, isTTY = false, columns, platform = 'linux' } = {}) => {
64
+ const format = resolveFormat(argv, env);
65
+ const width = resolveWidth({ columns, env });
66
+ if (format === 'json') return { format, mode: 'json', width, color: false, ascii: false };
67
+
68
+ const autoTier = !isTTY || env.TERM === 'dumb' || 'CI' in env ? 'plain' : 'ansi';
69
+ const requestedMode = format === 'auto' ? autoTier : format; // 'plain' | 'ansi'
70
+ const narrow = width < MIN_WIDTH;
71
+ const mode = narrow ? 'plain' : requestedMode; // the width floor wins over an ansi request
72
+ const ascii = narrow || (platform === 'win32' && !isUtf8Env(env));
73
+ const color = resolveColor({ env, isTTY }); // orthogonal — the ansi renderer gates on mode + color
74
+ return { format, mode, width, color, ascii };
75
+ };
@@ -0,0 +1,89 @@
1
+ // view-model.mjs — transform the no-leak `--json` envelope (buildEnvelope output) into a render-ready
2
+ // ViewModel for the direct-CLI renderers. Plan §4.2 / §4.5: surface → VIEW-MODEL → renderers.
3
+ //
4
+ // One data source: the renderers never touch the raw surveys, only this VM, which is built from the
5
+ // ENVELOPE — so the renderers inherit the envelope's no-leak guarantee (internal manifestState / stamp
6
+ // filenames never reach them). This module resolves public tokens → English phrases (presentation.mjs)
7
+ // and computes the headline counts; the renderers do layout + glyphs only.
8
+ //
9
+ // Pure, no side effects, Node >= 18.
10
+
11
+ import { STATE_PHRASING, VISIBILITY_PHRASING } from './presentation.mjs';
12
+
13
+ const memberVm = (m) => {
14
+ const refresh = m.refresh ?? { behind: false, recommend: null };
15
+ return {
16
+ display: m.display,
17
+ version: m.version ?? null,
18
+ state: m.state,
19
+ // 'installed' → null (the renderer shows the version instead); every other public token → a phrase.
20
+ statePhrase: m.state in STATE_PHRASING ? STATE_PHRASING[m.state] : m.state,
21
+ notes: m.notes ?? [], // the verbatim caveats — printed as ↳ sub-lines (INV-3: no dedupe)
22
+ behind: Boolean(refresh.behind), // used ONLY for the headline count on the direct CLI (INV-3)
23
+ recommend: refresh.recommend ?? null,
24
+ };
25
+ };
26
+
27
+ const bridgeVm = (b) => ({
28
+ display: b.display,
29
+ readiness: b.readiness,
30
+ // preserve the three-state wrapper status (present | missing | unknown) — the renderer maps to a glyph.
31
+ wrappers: (b.wrappers ?? []).map((w) => ({ cmd: w.cmd, state: w.state })),
32
+ });
33
+
34
+ const visibilityVm = (v) => {
35
+ if (!v) return null;
36
+ if (v.error) return { error: v.error };
37
+ return { phrase: v.state in VISIBILITY_PHRASING ? VISIBILITY_PHRASING[v.state] : v.state };
38
+ };
39
+
40
+ const recipesVm = (r) => {
41
+ if (!r) return null;
42
+ if (r.error) return { error: r.error };
43
+ const pairs = [];
44
+ for (const [activity, slots] of Object.entries(r.activities ?? {})) {
45
+ for (const [slot, v] of Object.entries(slots)) pairs.push({ key: `${activity}.${slot}`, recipe: v.recipe });
46
+ }
47
+ return { pairs, detectError: r.detectError ?? null };
48
+ };
49
+
50
+ const attributionVm = (a) => {
51
+ if (!a) return null;
52
+ if (a.error) return { error: a.error };
53
+ // A local OVERRIDE only when local actually set the key (non-null) AND it differs from project — a
54
+ // null local means the key is absent there, so the project value stands (that is not an override).
55
+ const override = a.local != null && a.local !== a.project;
56
+ return { effective: a.effective ?? null, override };
57
+ };
58
+
59
+ const velocityVm = (v) => {
60
+ if (!v) return null;
61
+ if (v.error) return { error: v.error };
62
+ return { defaultMode: v.defaultMode ?? null, allow: { project: v.allowEntries?.project ?? 0, local: v.allowEntries?.local ?? 0 } };
63
+ };
64
+
65
+ const settingsVm = (s) =>
66
+ s ? { recipes: recipesVm(s.recipes), attribution: attributionVm(s.attribution), velocity: velocityVm(s.velocity) } : null;
67
+
68
+ const projectVm = (p) =>
69
+ p
70
+ ? {
71
+ dir: p.dir,
72
+ deployed: p.deployed,
73
+ docsAi: p.docsAi,
74
+ deployStamps: (p.deployStamps ?? []).map((st) => ({ display: st.display, version: st.version ?? null })),
75
+ visibility: visibilityVm(p.visibility),
76
+ settings: settingsVm(p.settings),
77
+ }
78
+ : null;
79
+
80
+ export const toViewModel = (envelope = {}) => {
81
+ const members = (envelope.installed ?? []).map(memberVm);
82
+ return {
83
+ deploymentHead: envelope.deploymentHead ?? null,
84
+ members,
85
+ headline: { total: members.length, behind: members.filter((m) => m.behind).length },
86
+ bridges: envelope.bridges ? envelope.bridges.map(bridgeVm) : null,
87
+ project: projectVm(envelope.project),
88
+ };
89
+ };