geneseed 3.10.0 → 3.12.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/CHANGELOG.md +96 -0
- package/DESIGN.md +1 -1
- package/README.md +27 -45
- package/SHIPPED.md +2 -2
- package/adapters/bob/README.md +1 -1
- package/adapters/claude-code/README.md +17 -12
- package/adapters/openclaude/README.md +1 -1
- package/adapters/opencode/README.md +4 -4
- package/adapters/opencode/plugins/geneseed-guard.js +157 -2
- package/bin/build-driver.mjs +2 -0
- package/bin/geneseed-cli.mjs +21 -0
- package/bin/geneseed-hook.mjs +2 -0
- package/docs/README.md +42 -0
- package/docs/_groups.json +18 -0
- package/docs/concepts/agents.md +44 -0
- package/docs/concepts/collaboration.md +62 -0
- package/docs/concepts/exclusions.md +55 -0
- package/docs/concepts/footprint.md +52 -0
- package/docs/concepts/foreman-mode.md +69 -0
- package/docs/concepts/hooks.md +101 -0
- package/docs/concepts/hosts.md +55 -0
- package/docs/concepts/loops.md +165 -0
- package/docs/concepts/lsp.md +50 -0
- package/docs/{web → concepts}/memory.md +2 -2
- package/docs/{web → concepts}/notebook.md +2 -2
- package/docs/concepts/rules.md +52 -0
- package/docs/concepts/skills.md +49 -0
- package/docs/concepts/themes.md +50 -0
- package/docs/extending.md +32 -21
- package/docs/guides/autostart.md +70 -0
- package/docs/guides/choose-your-setup.md +134 -0
- package/docs/guides/headless.md +22 -0
- package/docs/guides/install.md +161 -0
- package/docs/guides/mcp.md +195 -0
- package/docs/guides/migrate.md +41 -0
- package/docs/guides/project-context.md +87 -0
- package/docs/guides/run-anywhere.md +45 -0
- package/docs/guides/uninstall.md +51 -0
- package/docs/guides/upgrade.md +65 -0
- package/docs/guides/verify.md +65 -0
- package/docs/guides/wiki.md +69 -0
- package/docs/guides/worktree.md +47 -0
- package/docs/reference/about.md +7 -0
- package/docs/reference/cli.md +7 -0
- package/docs/reference/environment.md +99 -0
- package/docs/reference/glossary.md +37 -0
- package/docs/reference/opencode-plugins.md +133 -0
- package/docs/reference/troubleshooting.md +350 -0
- package/docs/understand/a-day.md +77 -0
- package/docs/understand/enforced-vs-asked.md +64 -0
- package/docs/understand/harness.md +36 -0
- package/docs/understand/on-your-machine.md +76 -0
- package/docs/web-ui.md +13 -2
- package/harness.config.json +1 -1
- package/js/README.md +27 -4
- package/js/build/driver.mjs +14 -8
- package/js/build/generate.mjs +22 -7
- package/js/build/render.mjs +5 -0
- package/js/build/source.mjs +11 -1
- package/js/cli-table.json +236 -1
- package/js/hosts/gitref.mjs +99 -0
- package/js/hosts/hooks.mjs +88 -1
- package/js/hosts/installs.mjs +21 -0
- package/js/hosts/mcp.mjs +1 -1
- package/js/hosts/settings.mjs +13 -1
- package/js/inspect/checks-authoring.mjs +15 -3
- package/js/inspect/diff.mjs +3 -0
- package/js/inspect/doctor.mjs +3 -2
- package/js/inspect/inventory.mjs +2 -1
- package/js/inspect/registry.mjs +20 -3
- package/js/inspect/status.mjs +8 -3
- package/js/lib/node-floor.mjs +30 -0
- package/js/loop/catalog.mjs +116 -0
- package/js/loop/cli.mjs +151 -0
- package/js/loop/graph.mjs +145 -0
- package/js/loop/score.mjs +143 -0
- package/js/loop/state.mjs +322 -0
- package/js/maintain/migrate.mjs +4 -2
- package/js/maintain/setup.mjs +109 -21
- package/js/maintain/update.mjs +4 -15
- package/js/ui/cli.mjs +2 -2
- package/js/web/actions.mjs +9 -3
- package/js/web/api.mjs +41 -2
- package/js/web/daemon.mjs +94 -4
- package/js/web/docs.mjs +149 -65
- package/js/web/handler.mjs +1 -0
- package/js/web/jobs.mjs +3 -1
- package/js/web/server.mjs +10 -5
- package/package.json +6 -4
- package/registry.json +21 -0
- package/src/AGENT.md.tmpl +6 -0
- package/src/bricks/README.md +32 -0
- package/src/bricks/_template.md +32 -0
- package/src/bricks/apply.md +17 -0
- package/src/bricks/baseline-green.md +17 -0
- package/src/bricks/bruno-test.md +16 -0
- package/src/bricks/ci-fix.md +17 -0
- package/src/bricks/deps-audit.md +14 -0
- package/src/bricks/docs-update.md +15 -0
- package/src/bricks/identify.md +20 -0
- package/src/bricks/lint.md +14 -0
- package/src/bricks/plan.md +17 -0
- package/src/bricks/reproduce.md +16 -0
- package/src/bricks/review.md +17 -0
- package/src/bricks/security-scan.md +17 -0
- package/src/bricks/test.md +14 -0
- package/src/doctrines/process.md +11 -8
- package/src/loops/bugfix.json +22 -0
- package/src/loops/feature.json +21 -0
- package/src/loops/refactor.json +20 -0
- package/src/postures/mentor.md +1 -0
- package/src/skills/brick-forge.md +46 -0
- package/src/skills/geneseed.md +1 -1
- package/src/skills/learn-mode.md +89 -0
- package/src/skills/loop.md +99 -0
- package/src/skills/pipeline.md +2 -2
- package/src/skills/quiz.md +2 -1
- package/src/skills/teach.md +2 -1
- package/themes/_TEMPLATE.json +3 -0
- package/themes/biker.json +3 -0
- package/themes/commentator.json +3 -0
- package/themes/cyberpunk.json +3 -0
- package/themes/gamer.json +3 -0
- package/themes/imperial.json +3 -0
- package/themes/joker.json +3 -0
- package/themes/marvin.json +3 -0
- package/themes/mean.json +3 -0
- package/themes/military.json +3 -0
- package/themes/neutral.json +3 -0
- package/themes/pirate.json +3 -0
- package/themes/sports.json +3 -0
- package/themes/verstappen.json +3 -0
- package/themes/wizard.json +3 -0
- package/web/dist/assets/Activity-GaaZWXTY.js +1 -0
- package/web/dist/assets/ActivityDetail-4t7d45Du.js +1 -0
- package/web/dist/assets/Dashboard-CYn7Tp16.js +2 -0
- package/web/dist/assets/Docs-BeFKLg2T.js +5 -0
- package/web/dist/assets/{GreenhouseView-D0cbCJ5m.js → GreenhouseView-C6ibc8IE.js} +1 -1
- package/web/dist/assets/Installs-DRYjYnxs.js +2 -0
- package/web/dist/assets/JournalView-1HbU1mfg.js +1 -0
- package/web/dist/assets/Laws-DFoG8JWB.js +3 -0
- package/web/dist/assets/Library-DDjgNwz_.js +1 -0
- package/web/dist/assets/Loops-D9VEoO0F.js +1 -0
- package/web/dist/assets/{Markdown-CkFyfgNf.js → Markdown-qjM2_ofS.js} +5 -5
- package/web/dist/assets/{OperatorHudView-CiAkAkQL.js → OperatorHudView-Bs7paWmh.js} +1 -1
- package/web/dist/assets/Personal-BY5n4gTw.js +1 -0
- package/web/dist/assets/Tabs-ho-EYD8i.js +1 -0
- package/web/dist/assets/api-CyMRbU2x.js +1 -0
- package/web/dist/assets/drawable-CLS9drMb.js +1 -0
- package/web/dist/assets/index-CX_hXMi5.css +1 -0
- package/web/dist/assets/index-Cjr7XCA3.js +2 -0
- package/web/dist/index.html +3 -2
- package/QUICKSTART.md +0 -50
- package/SETUP.md +0 -1148
- package/docs/web/_groups.json +0 -45
- package/docs/web/about.md +0 -6
- package/docs/web/adapters-claude-code.md +0 -8
- package/docs/web/adapters-opencode-loads.md +0 -8
- package/docs/web/adapters-opencode-spec.md +0 -8
- package/docs/web/adapters-opencode.md +0 -8
- package/docs/web/agents.md +0 -8
- package/docs/web/authoring.md +0 -11
- package/docs/web/autostart.md +0 -9
- package/docs/web/claude-hooks.md +0 -37
- package/docs/web/cli.md +0 -6
- package/docs/web/collaboration.md +0 -27
- package/docs/web/configure-wiki.md +0 -9
- package/docs/web/design.md +0 -7
- package/docs/web/env-knobs.md +0 -9
- package/docs/web/exclusions.md +0 -26
- package/docs/web/first-session.md +0 -28
- package/docs/web/footprint.md +0 -38
- package/docs/web/foreman-mode.md +0 -33
- package/docs/web/glossary.md +0 -6
- package/docs/web/harnesses.md +0 -30
- package/docs/web/headless.md +0 -9
- package/docs/web/ingest-docs.md +0 -9
- package/docs/web/install-paths.md +0 -9
- package/docs/web/install-quick.md +0 -35
- package/docs/web/lsp-overview.md +0 -37
- package/docs/web/mcp-claude-code.md +0 -10
- package/docs/web/mcp-filesystem.md +0 -9
- package/docs/web/mcp-gitlab.md +0 -9
- package/docs/web/mcp-markitdown.md +0 -9
- package/docs/web/mcp-overview.md +0 -34
- package/docs/web/mcp-trouble.md +0 -9
- package/docs/web/mcp-verify.md +0 -9
- package/docs/web/migrate.md +0 -39
- package/docs/web/model.md +0 -20
- package/docs/web/pipelines.md +0 -24
- package/docs/web/plugin-activity.md +0 -23
- package/docs/web/plugin-context.md +0 -26
- package/docs/web/plugin-guard.md +0 -24
- package/docs/web/plugin-learn.md +0 -27
- package/docs/web/plugin-notify.md +0 -24
- package/docs/web/plugin-ponytail.md +0 -23
- package/docs/web/plugin-setup.md +0 -7
- package/docs/web/plugin-workflow.md +0 -25
- package/docs/web/plugins.md +0 -17
- package/docs/web/project-context.md +0 -9
- package/docs/web/rules.md +0 -30
- package/docs/web/run-anywhere.md +0 -9
- package/docs/web/self-improve.md +0 -8
- package/docs/web/setup-choices.md +0 -64
- package/docs/web/skills.md +0 -8
- package/docs/web/themes-author.md +0 -20
- package/docs/web/themes.md +0 -9
- package/docs/web/token-footprint.md +0 -7
- package/docs/web/trouble.md +0 -38
- package/docs/web/upgrade.md +0 -9
- package/docs/web/verify.md +0 -30
- package/docs/web/wiki.md +0 -7
- package/docs/web/worktree.md +0 -10
- package/docs/wiki.md +0 -43
- package/web/dist/assets/Activity-BjVp522q.js +0 -1
- package/web/dist/assets/ActivityDetail-8ep1bspW.js +0 -1
- package/web/dist/assets/Dashboard-OkC_PCH6.js +0 -2
- package/web/dist/assets/Docs-CKPWz00o.js +0 -2
- package/web/dist/assets/Installs-BtGGdoTt.js +0 -2
- package/web/dist/assets/JournalView-Ckl4dmzR.js +0 -1
- package/web/dist/assets/Laws-CLpdRRk8.js +0 -3
- package/web/dist/assets/Library-wPPuKQrI.js +0 -1
- package/web/dist/assets/Personal-D8CnDo9v.js +0 -1
- package/web/dist/assets/Tabs-B4HNrrCs.js +0 -1
- package/web/dist/assets/drawable-BiwaOo6x.js +0 -1
- package/web/dist/assets/index-C1yil5j1.js +0 -2
- package/web/dist/assets/index-CDPDk2vj.css +0 -1
package/js/web/api.mjs
CHANGED
|
@@ -21,10 +21,12 @@ import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
|
21
21
|
import { GLOBAL_MANIFEST, HOSTS, opencodeConfigDir, resolvePath } from '../hosts/hosts.mjs';
|
|
22
22
|
import {
|
|
23
23
|
doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState, installTargets,
|
|
24
|
-
installedDefaults, modeOfDir,
|
|
24
|
+
installedDefaults, modeOfDir, trustOfDir,
|
|
25
25
|
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
26
26
|
} from '../hosts/installs.mjs';
|
|
27
27
|
import { frontmatter } from '../hosts/hooks.mjs';
|
|
28
|
+
import { DEFAULT_PRESET, PRESETS } from '../loop/score.mjs';
|
|
29
|
+
import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
|
|
28
30
|
import {
|
|
29
31
|
SKILL_CLASS, entityStatus, loadRegistry, tuiInventory,
|
|
30
32
|
} from '../inspect/inventory.mjs';
|
|
@@ -87,6 +89,7 @@ export function webState(theme = null, target = null) {
|
|
|
87
89
|
footprint: null,
|
|
88
90
|
posture: null,
|
|
89
91
|
mode: null,
|
|
92
|
+
trust: null,
|
|
90
93
|
_inv: null,
|
|
91
94
|
_doctor: null,
|
|
92
95
|
};
|
|
@@ -98,6 +101,7 @@ export function webState(theme = null, target = null) {
|
|
|
98
101
|
st.footprint = footprintOfDir(st.target); // 'full' when no marker
|
|
99
102
|
st.posture = postureOfDir(st.target) || 'peer';
|
|
100
103
|
st.mode = modeOfDir(st.target) || 'direct';
|
|
104
|
+
st.trust = trustOfDir(st.target) || DEFAULT_PRESET;
|
|
101
105
|
|
|
102
106
|
Object.defineProperty(st, 'inventory', {
|
|
103
107
|
get() {
|
|
@@ -154,6 +158,7 @@ export function webState(theme = null, target = null) {
|
|
|
154
158
|
st.footprint = footprintOfDir(st.root);
|
|
155
159
|
st.posture = postureOfDir(st.root) || 'peer';
|
|
156
160
|
st.mode = modeOfDir(st.root) || 'direct';
|
|
161
|
+
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
157
162
|
st._inv = null;
|
|
158
163
|
st._doctor = null;
|
|
159
164
|
};
|
|
@@ -165,6 +170,7 @@ export function webState(theme = null, target = null) {
|
|
|
165
170
|
st.footprint = footprintOfDir(st.root);
|
|
166
171
|
st.posture = postureOfDir(st.root) || 'peer';
|
|
167
172
|
st.mode = modeOfDir(st.root) || 'direct';
|
|
173
|
+
st.trust = trustOfDir(st.root) || DEFAULT_PRESET;
|
|
168
174
|
};
|
|
169
175
|
return st;
|
|
170
176
|
}
|
|
@@ -741,11 +747,12 @@ export function apiInstalls(state) {
|
|
|
741
747
|
footprint: footprintOfDir(root),
|
|
742
748
|
posture: postureOfDir(root),
|
|
743
749
|
mode: modeOfDir(root),
|
|
750
|
+
trust: trustOfDir(root),
|
|
744
751
|
selected: samePath(viewCfg(host, scope, root), state.target),
|
|
745
752
|
});
|
|
746
753
|
}
|
|
747
754
|
return { installs: out, postures: discoverNames('postures', 'peer'),
|
|
748
|
-
modes: discoverNames('modes', 'direct') };
|
|
755
|
+
modes: discoverNames('modes', 'direct'), trusts: Object.keys(PRESETS) };
|
|
749
756
|
}
|
|
750
757
|
|
|
751
758
|
/** Beside the deployed AGENT.md. */
|
|
@@ -819,6 +826,9 @@ export function apiOverview(state) {
|
|
|
819
826
|
install,
|
|
820
827
|
target: state.target,
|
|
821
828
|
deployed: deployed(state),
|
|
829
|
+
// True when the files on disk changed under this server since it started (an upgrade).
|
|
830
|
+
// `serve` installs the comparison; a state built without a server has none and is false.
|
|
831
|
+
daemon_stale: Boolean(state.daemonStale?.()),
|
|
822
832
|
counts: {
|
|
823
833
|
agents: inv.agents.length,
|
|
824
834
|
skills: inv.skills.length,
|
|
@@ -845,6 +855,9 @@ export function apiOverview(state) {
|
|
|
845
855
|
notebook: notebookItems(state).length,
|
|
846
856
|
wiki: wikiItems(state).length,
|
|
847
857
|
config: configItems(state).length,
|
|
858
|
+
// The rail's Loops badge: templates only, from the same catalogue `/api/loops` reads —
|
|
859
|
+
// a brick is a part, the template is what a person runs.
|
|
860
|
+
loops: loadCatalog({ projectRoot: state.root }).templates.size,
|
|
848
861
|
},
|
|
849
862
|
doctor: state.doctor,
|
|
850
863
|
diff,
|
|
@@ -930,6 +943,31 @@ export function apiRecent(state) {
|
|
|
930
943
|
return { items: items.slice(0, RECENT_LIMIT), skipped, limit: RECENT_LIMIT };
|
|
931
944
|
}
|
|
932
945
|
|
|
946
|
+
/**
|
|
947
|
+
* The Loops page: the brick and template catalogue the `geneseed loop` CLI reads, resolved from
|
|
948
|
+
* the install ROOT — for a project install that is the repo, where `<repo>/.geneseed/` lives; for
|
|
949
|
+
* a global one it is the config dir, which has no `.geneseed/` and so adds nothing. Read-only:
|
|
950
|
+
* the page draws templates and lists bricks, it never writes one.
|
|
951
|
+
*
|
|
952
|
+
* A template's graph is passed whole (`graph`) rather than flattened, because the page's ring
|
|
953
|
+
* layout needs `nodes`, `edges` and `loops` exactly as the engine reads them.
|
|
954
|
+
*/
|
|
955
|
+
export function apiLoops(state) {
|
|
956
|
+
const { bricks, templates, overridden } = loadCatalog({ projectRoot: state.root });
|
|
957
|
+
const byName = ([a], [b]) => (a < b ? -1 : 1);
|
|
958
|
+
return {
|
|
959
|
+
templates: [...templates].sort(byName).map(([name, { origin, ...graph }]) => ({
|
|
960
|
+
name, description: graph.description || '', origin, graph,
|
|
961
|
+
})),
|
|
962
|
+
bricks: [...bricks].sort(byName).map(([, b]) => ({ reason: null, ...b })),
|
|
963
|
+
overridden,
|
|
964
|
+
// A brick or template the catalogue skipped (bad frontmatter, not JSON, misnamed), and a
|
|
965
|
+
// template that fails the graph rules: the page says so — the same list `loop check`
|
|
966
|
+
// prints — or a team's broken override would just silently not be there.
|
|
967
|
+
problems: catalogProblems({ projectRoot: state.root }),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
933
971
|
/** The source's release label, or null when it cannot be read. */
|
|
934
972
|
function releaseLabel() {
|
|
935
973
|
const v = sourceReleaseVersion({ config: CONFIG });
|
|
@@ -979,4 +1017,5 @@ export const STATE_ROUTES = {
|
|
|
979
1017
|
// answers — a daemon whose POST worked and whose GET 501'd would have no way to obtain one.
|
|
980
1018
|
'/api/rules': apiRules,
|
|
981
1019
|
'/api/mcp': apiMcp,
|
|
1020
|
+
'/api/loops': apiLoops,
|
|
982
1021
|
};
|
package/js/web/daemon.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { printOut, writeText } from '../lib/fs.mjs';
|
|
|
13
13
|
import { isTruthy, jsonDumpsCompact } from '../lib/json.mjs';
|
|
14
14
|
import { webState } from './api.mjs';
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import { chmodSync, mkdirSync, openSync, readFileSync, unlinkSync } from 'node:fs';
|
|
17
18
|
import { join, resolve as pathResolve } from 'node:path';
|
|
18
19
|
|
|
@@ -20,6 +21,29 @@ const ROOT = pathResolve(import.meta.dirname, '..', '..');
|
|
|
20
21
|
|
|
21
22
|
// ---- the daemon record -----------------------------------------------------
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Which code a console serves: the package version and a hash of the `web/dist/index.html` it
|
|
26
|
+
* would load. A daemon writes the stamp it started with into its record; an npm upgrade or a
|
|
27
|
+
* `git pull` that replaces `web/dist/` changes the stamp on disk, and the daemon — still
|
|
28
|
+
* serving the old `index.html`, whose chunk hashes may no longer exist — is then STALE.
|
|
29
|
+
* Read fresh on every call: the comparison is against the files as they are NOW.
|
|
30
|
+
*/
|
|
31
|
+
export function codeStamp(root = ROOT) {
|
|
32
|
+
const read = (rel) => { try { return readFileSync(join(root, rel)); } catch { return ''; } };
|
|
33
|
+
let version = 'unknown';
|
|
34
|
+
try { version = JSON.parse(read('package.json')).version ?? version; } catch { /* unknown */ }
|
|
35
|
+
const html = createHash('sha256').update(read(join('web', 'dist', 'index.html'))).digest('hex');
|
|
36
|
+
return `${version}:${html.slice(0, 12)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A record written by an older daemon carries no `code` — and is stale by definition. */
|
|
40
|
+
export function daemonStale(record, root = ROOT) {
|
|
41
|
+
return record?.code !== codeStamp(root);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const STALE_LINE = '[web] running OLD code (started before the last upgrade) — '
|
|
45
|
+
+ 'geneseed web restart';
|
|
46
|
+
|
|
23
47
|
const statePath = (target) => join(target, '.geneseed-web.json');
|
|
24
48
|
|
|
25
49
|
export function readDaemon(target) {
|
|
@@ -243,9 +267,42 @@ export async function startDaemon(theme, port, openBrowser = true) {
|
|
|
243
267
|
return 1;
|
|
244
268
|
}
|
|
245
269
|
|
|
246
|
-
|
|
270
|
+
/**
|
|
271
|
+
* `--port` NAMES WHICH DAEMON, and the record holds only one. With no `--port` (`port` null)
|
|
272
|
+
* the verbs act on whatever the record names, as they always have. With one, they act only if
|
|
273
|
+
* the record is on that port: `stop --port 4791` once POSTed shutdown to the recorded url and
|
|
274
|
+
* stopped the console on 4747. So a record on another port is refused — no request sent, record
|
|
275
|
+
* left in place — and a server answering on the named port that is NOT the record is explained,
|
|
276
|
+
* because its token is not on record and there is nothing to guess it from.
|
|
277
|
+
*
|
|
278
|
+
* Null when the verb may go ahead; otherwise the exit code, already explained on stdout.
|
|
279
|
+
*/
|
|
280
|
+
async function portRefusal(st, port, verb) {
|
|
281
|
+
if (port === null || port === undefined) return null;
|
|
282
|
+
const recorded = isTruthy(st) && isTruthy(st.url) ? Number(st.port) : null;
|
|
283
|
+
if (recorded === port) return null;
|
|
284
|
+
if (recorded !== null) {
|
|
285
|
+
if (verb === 'status') {
|
|
286
|
+
printOut(`[web] not running on port ${port} (the recorded daemon is on port ${recorded}).\n`);
|
|
287
|
+
} else {
|
|
288
|
+
printOut(`[web] the recorded daemon is on port ${recorded}, not ${port} — not touching it.\n`);
|
|
289
|
+
printOut(` \`geneseed web ${verb} --port ${recorded}\` (or no --port) ${verb}s that one.\n`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (await probe(`http://127.0.0.1:${port}`)) {
|
|
293
|
+
printOut(`[web] a server answers on port ${port} but is not the recorded daemon, so its `
|
|
294
|
+
+ 'token is unknown — stop it from its own page (Stop) or end its process.\n');
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
// Nothing recorded and nothing on the named port: the verb's own no-daemon arm says so.
|
|
298
|
+
return recorded === null ? null : 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function stopDaemon(theme = null, port = null) {
|
|
247
302
|
const { target } = webState(theme);
|
|
248
303
|
const st = readDaemon(target);
|
|
304
|
+
const refused = await portRefusal(st, port, 'stop');
|
|
305
|
+
if (refused !== null) return refused;
|
|
249
306
|
if (!isTruthy(st) || !isTruthy(st.url)) {
|
|
250
307
|
// NO `clearDaemon` HERE — deliberately asymmetric with `liveDaemon`/`statusDaemon`,
|
|
251
308
|
// which DO delete a record naming no url. Unifying the two would delete a file this
|
|
@@ -263,11 +320,14 @@ export async function stopDaemon(theme = null) {
|
|
|
263
320
|
return 0;
|
|
264
321
|
}
|
|
265
322
|
|
|
266
|
-
export async function statusDaemon(theme = null) {
|
|
323
|
+
export async function statusDaemon(theme = null, port = null) {
|
|
267
324
|
const { target } = webState(theme);
|
|
325
|
+
const refused = await portRefusal(readDaemon(target), port, 'status');
|
|
326
|
+
if (refused !== null) return refused;
|
|
268
327
|
const st = await liveDaemon(target);
|
|
269
328
|
if (st) {
|
|
270
329
|
printOut(`[web] running on ${st.url} (theme: ${none(st.theme)}, pid ${none(st.pid)})\n`);
|
|
330
|
+
if (daemonStale(st)) printOut(`${STALE_LINE}\n`);
|
|
271
331
|
return 0;
|
|
272
332
|
}
|
|
273
333
|
// EXIT 1, which is the verb's whole contract for a script. The snapshot's `<exit>`
|
|
@@ -276,13 +336,43 @@ export async function statusDaemon(theme = null) {
|
|
|
276
336
|
return 1;
|
|
277
337
|
}
|
|
278
338
|
|
|
279
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Bounce a running console so it serves the code now on disk — `upgrade` after it rebuilt,
|
|
341
|
+
* `rebuild-all` when the running one is stale. EXCEPT inside a web job (`GENESEED_WEB_JOB`,
|
|
342
|
+
* set by `js/web/jobs.mjs`): restarting here would kill the process tracking that very job,
|
|
343
|
+
* leaving the console stuck on 'running'; the server restarts itself after an update job, and
|
|
344
|
+
* shows its stale banner otherwise. `onlyIfStale` is `rebuild-all`'s: after an `upgrade`
|
|
345
|
+
* (which bounces, then runs `rebuild-all`) the console is current, and bouncing it again
|
|
346
|
+
* would drop the open tab for nothing. Never throws — no rebuild fails over a console.
|
|
347
|
+
*/
|
|
348
|
+
export async function bounceDaemonIfRunning(theme, log, { onlyIfStale = false } = {}) {
|
|
349
|
+
if (onlyIfStale) {
|
|
350
|
+
const st = readDaemon(webState(theme).target);
|
|
351
|
+
if (!st || !daemonStale(st)) return;
|
|
352
|
+
}
|
|
353
|
+
if (process.env.GENESEED_WEB_JOB) {
|
|
354
|
+
log(onlyIfStale
|
|
355
|
+
? '[geneseed] the web console is running old code — use its Restart banner.'
|
|
356
|
+
: '[geneseed] web daemon will restart itself after this job to load the new code.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await restartDaemon(theme, null, false, true);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
log(`[geneseed] ⚠️ could not refresh the web daemon (${e && e.message ? e.message : e}) — \`geneseed web restart\` manually if it was running.`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** `port` null keeps the recorded daemon's port (4747 if none); see `portRefusal`. */
|
|
367
|
+
export async function restartDaemon(theme = null, port = null, openBrowser = true,
|
|
280
368
|
onlyIfRunning = false) {
|
|
281
369
|
const { target } = webState(theme);
|
|
282
370
|
const st = readDaemon(target);
|
|
371
|
+
const refused = await portRefusal(st, port, 'restart');
|
|
372
|
+
if (refused !== null) return refused;
|
|
283
373
|
const live = (await liveDaemon(target)) !== null;
|
|
284
374
|
if (onlyIfRunning && !live) return 0;
|
|
285
|
-
const usePort = (st && isTruthy(st.port) ? st.port : null) ||
|
|
375
|
+
const usePort = port ?? ((st && isTruthy(st.port) ? st.port : null) || 4747);
|
|
286
376
|
let open = openBrowser;
|
|
287
377
|
if (live) {
|
|
288
378
|
// A live daemon means a tab is already open on this (preserved) port and will
|
package/js/web/docs.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The web console's Docs pages, and the registry built from `docs
|
|
2
|
+
* The web console's Docs pages, and the registry built from `docs/<folder>/` (DOC_FOLDERS).
|
|
3
3
|
*
|
|
4
4
|
* `cli` docs render via `cliReference()` in `js/ui/cli.mjs`, which reads
|
|
5
5
|
* `js/cli-table.json` — the CLI's own metadata, since `harness.build_argparser()`'s parser
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* the same file, so there is exactly one description of the CLI surface, not a second
|
|
8
8
|
* transcription of it.
|
|
9
9
|
*
|
|
10
|
-
* `tests/unit/web_server.test.mjs` holds `KINDS` against a written-out list of the
|
|
10
|
+
* `tests/unit/web_server.test.mjs` holds `KINDS` against a written-out list of the six
|
|
11
11
|
* kinds this daemon dispatches on — same declaration-vs-dispatch shape as the route table,
|
|
12
|
-
* which is why it is a table here too and not
|
|
12
|
+
* which is why it is a table here too and not six `if`s.
|
|
13
13
|
*
|
|
14
14
|
* THE `?harness=` QUERY PARAM IS the Docs selector, and it is the ONLY input to these
|
|
15
15
|
* endpoints that is not the checkout itself — the one thing a test can vary. Every
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { readdirSync } from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
|
|
21
|
-
import { ROOT, THEMES } from '../build/source.mjs';
|
|
21
|
+
import { DOCS, DOC_FOLDERS, ROOT, THEMES } from '../build/source.mjs';
|
|
22
22
|
import { cliReference } from '../ui/cli.mjs';
|
|
23
23
|
import { readJsonMaybe, readMaybe } from '../hosts/installs.mjs';
|
|
24
24
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
@@ -30,8 +30,6 @@ import { statusData } from '../inspect/status.mjs';
|
|
|
30
30
|
import { originDisplay } from '../maintain/update.mjs';
|
|
31
31
|
import { NotFound, deployed, resolveLinks } from './api.mjs';
|
|
32
32
|
|
|
33
|
-
const DOC_DIR = path.join(ROOT, 'docs', 'web');
|
|
34
|
-
|
|
35
33
|
/**
|
|
36
34
|
* A docs page split into its frontmatter map and its body.
|
|
37
35
|
*
|
|
@@ -61,24 +59,45 @@ function docFrontmatter(text) {
|
|
|
61
59
|
}
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
|
-
*
|
|
62
|
+
* Every docs page source as `{ id, rel }`, `rel` relative to ROOT with forward slashes.
|
|
63
|
+
*
|
|
64
|
+
* The id is the basename because it is the router's address (`#/docs/<id>`); the folder is
|
|
65
|
+
* GitHub's business, not the console's. Two pages with one basename would leave one of them
|
|
66
|
+
* unreachable, so a duplicate throws instead of shadowing.
|
|
67
|
+
*/
|
|
68
|
+
export function docSources(docRoot = DOCS) {
|
|
69
|
+
const out = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
for (const folder of DOC_FOLDERS) {
|
|
72
|
+
let names;
|
|
73
|
+
try { names = readdirSync(path.join(docRoot, folder)); } catch { continue; }
|
|
74
|
+
for (const name of names.filter((n) => normcase(n).endsWith('.md')).sort()) {
|
|
75
|
+
const id = name.slice(0, -3);
|
|
76
|
+
if (seen.has(id)) throw new Error(`duplicate docs page id: ${id}`);
|
|
77
|
+
seen.add(id);
|
|
78
|
+
out.push({ id, rel: `docs/${folder}/${name}` });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The Docs registry, built from `docSources()` and `docs/_groups.json`.
|
|
65
86
|
*
|
|
66
87
|
* A page whose file is missing or malformed is skipped rather than crashing the server; an
|
|
67
88
|
* unreadable registry yields an empty Docs section, which the UI renders as "no pages".
|
|
68
89
|
*
|
|
69
|
-
* NOT cached at module load. The difference is invisible to a request —
|
|
90
|
+
* NOT cached at module load. The difference is invisible to a request — the docs tree cannot
|
|
70
91
|
* change under a running daemon any more than `dist/` can — and a module-level constant
|
|
71
92
|
* would freeze whatever `ROOT` was when this file was first imported.
|
|
72
93
|
*/
|
|
73
94
|
export function docGroups() {
|
|
74
|
-
const groups = readJsonMaybe(path.join(
|
|
95
|
+
const groups = readJsonMaybe(path.join(DOCS, '_groups.json'));
|
|
75
96
|
if (!Array.isArray(groups)) return [];
|
|
76
97
|
const byId = new Map();
|
|
77
98
|
for (const g of groups) byId.set(g.id, { ...g, pages: [] });
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const name of names.filter((n) => normcase(n).endsWith('.md')).sort()) {
|
|
81
|
-
const text = readMaybe(path.join(DOC_DIR, name));
|
|
99
|
+
for (const { id, rel } of docSources()) {
|
|
100
|
+
const text = readMaybe(path.join(ROOT, rel));
|
|
82
101
|
if (text === null) continue;
|
|
83
102
|
const [meta, body] = docFrontmatter(text);
|
|
84
103
|
const group = byId.get(meta.group);
|
|
@@ -86,7 +105,7 @@ export function docGroups() {
|
|
|
86
105
|
if (group === undefined) continue;
|
|
87
106
|
const order = Object.hasOwn(meta, 'order') ? meta.order : 0;
|
|
88
107
|
delete meta.order;
|
|
89
|
-
const page = { id
|
|
108
|
+
const page = { id, rel, ...meta };
|
|
90
109
|
if (body.trim()) page.body = body.replace(/\n+$/, '');
|
|
91
110
|
group.pages.push([order, page]);
|
|
92
111
|
}
|
|
@@ -103,50 +122,96 @@ export function docGroups() {
|
|
|
103
122
|
return out;
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
// ---- markdown tables as data ------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
/** One table row's cells; a `\|` inside a cell is kept as a literal bar. */
|
|
128
|
+
function tableCells(line) {
|
|
129
|
+
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/)
|
|
130
|
+
.map((c) => c.trim().replace(/\\\|/g, '|'));
|
|
131
|
+
}
|
|
132
|
+
|
|
106
133
|
/**
|
|
107
|
-
* The
|
|
108
|
-
*
|
|
134
|
+
* The body rows of the first table whose header, lower-cased, is exactly `head` — or `null`.
|
|
135
|
+
*
|
|
136
|
+
* A TABLE IS THE DATA so a page reads the same on GitHub as in the console: the map on
|
|
137
|
+
* understand page 2 and the glossary are both tables a person can read, and the console builds
|
|
138
|
+
* its richer view from the same rows rather than from a second copy that could drift. A row
|
|
139
|
+
* with the wrong number of cells is a malformed table, so the whole answer is `null`.
|
|
109
140
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
]
|
|
141
|
+
export function tableRows(body, head) {
|
|
142
|
+
const lines = body.split('\n');
|
|
143
|
+
for (let i = 0; i + 1 < lines.length; i += 1) {
|
|
144
|
+
if (!lines[i].trim().startsWith('|')) continue;
|
|
145
|
+
const h = tableCells(lines[i]).map((c) => c.toLowerCase());
|
|
146
|
+
if (h.length !== head.length || h.some((c, j) => c !== head[j])) continue;
|
|
147
|
+
const rows = [];
|
|
148
|
+
for (let j = i + 2; j < lines.length && lines[j].trim().startsWith('|'); j += 1) {
|
|
149
|
+
const row = tableCells(lines[j]);
|
|
150
|
+
if (row.length !== head.length) return null;
|
|
151
|
+
rows.push(row);
|
|
152
|
+
}
|
|
153
|
+
return rows;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const MAP_HEAD = ['piece', 'what it does for you', 'kind', 'cost', 'turn it off'];
|
|
159
|
+
export const MAP_KINDS = ['enforced', 'automatic', 'asked', 'on demand'];
|
|
160
|
+
|
|
161
|
+
/** The "what lands on your machine" table as rows, or `null` when absent or malformed. */
|
|
162
|
+
export function parseMapTable(body) {
|
|
163
|
+
const rows = tableRows(body, MAP_HEAD);
|
|
164
|
+
if (!rows || !rows.length) return null;
|
|
165
|
+
const out = rows.map(([piece, does, kind, cost, off]) => (
|
|
166
|
+
{ piece, does, kind: kind.toLowerCase(), cost, off }));
|
|
167
|
+
return out.every((r) => MAP_KINDS.includes(r.kind)) ? out : null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const GLOSSARY_HEAD = ['term', 'theme key', 'meaning', 'in dev terms'];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The glossary table: term, the theme key whose value renames it (`—` when no theme does),
|
|
174
|
+
* the meaning, and the developer analogy (`—` when there is none).
|
|
175
|
+
*/
|
|
176
|
+
export function parseGlossaryTable(body) {
|
|
177
|
+
const rows = tableRows(body, GLOSSARY_HEAD);
|
|
178
|
+
if (!rows) return [];
|
|
179
|
+
const dash = (s) => (s === '—' || s === '-' || s === '' ? null : s);
|
|
180
|
+
return rows.map(([label, key, desc, analogy]) => (
|
|
181
|
+
{ label, key: dash(key), desc, analogy: dash(analogy) }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---- links between pages ----------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
const MD_LINK_RE = /\]\((?!https?:|#|mailto:)([^)\s]+?\.md)(#[^)\s]*)?\)/g;
|
|
187
|
+
// ponytail: the canonical repo, not the install's origin; a fork's console links upstream.
|
|
188
|
+
const GITHUB_BLOB = 'https://github.com/Arylmera/Geneseed/blob/main/';
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Relative `.md` links, which is how the pages link on GitHub, rewritten for the console.
|
|
192
|
+
*
|
|
193
|
+
* A target that is a docs page becomes `#/docs/<id>` (its `#anchor` dropped — the console
|
|
194
|
+
* addresses pages, not headings); any other repo file — a maintainer doc, a README under
|
|
195
|
+
* `src/` — opens on GitHub, since the console does not serve it. Absolute URLs, in-page
|
|
196
|
+
* anchors and fenced examples are left alone.
|
|
197
|
+
*/
|
|
198
|
+
export function rewriteDocLinks(body, fromRel, idByRel) {
|
|
199
|
+
if (!body.includes('.md')) return body;
|
|
200
|
+
let inFence = false;
|
|
201
|
+
return body.split('\n').map((line) => {
|
|
202
|
+
if (line.startsWith('```')) { inFence = !inFence; return line; }
|
|
203
|
+
if (inFence) return line;
|
|
204
|
+
return line.replace(MD_LINK_RE, (m, target, anchor) => {
|
|
205
|
+
const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(fromRel), target));
|
|
206
|
+
const id = idByRel.get(resolved);
|
|
207
|
+
return id ? `](#/docs/${id})` : `](${GITHUB_BLOB}${resolved}${anchor ?? ''})`;
|
|
208
|
+
});
|
|
209
|
+
}).join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function idsByRel() {
|
|
213
|
+
return new Map(docSources().map((s) => [s.rel, s.id]));
|
|
214
|
+
}
|
|
150
215
|
|
|
151
216
|
// ---- counts substituted into concept bodies -------------------------------------------
|
|
152
217
|
|
|
@@ -239,6 +304,13 @@ export function harnessBlocksBalanced(lines) {
|
|
|
239
304
|
return !open;
|
|
240
305
|
}
|
|
241
306
|
|
|
307
|
+
/**
|
|
308
|
+
* The visible `*(OpenCode only)*` line a block opens with. It exists for GitHub, which hides
|
|
309
|
+
* the marker; the console has already filtered the block to the reader's host, so there the
|
|
310
|
+
* label only repeats what the host selector says, and it goes with the marker.
|
|
311
|
+
*/
|
|
312
|
+
const HOST_LABEL_RE = /^\s*\*\((OpenCode|Claude Code) only\)\*\s*$/;
|
|
313
|
+
|
|
242
314
|
export function stripHarnessBlocks(body, harnessName) {
|
|
243
315
|
if (!HARNESS_HINT_RE.test(body)) return body;
|
|
244
316
|
const lines = splitLines(body);
|
|
@@ -246,7 +318,10 @@ export function stripHarnessBlocks(body, harnessName) {
|
|
|
246
318
|
const out = [];
|
|
247
319
|
let keep = true;
|
|
248
320
|
let inFence = false;
|
|
321
|
+
let afterOpen = false;
|
|
249
322
|
for (const line of lines) {
|
|
323
|
+
const labelSlot = afterOpen;
|
|
324
|
+
afterOpen = false;
|
|
250
325
|
if (line.startsWith('```')) {
|
|
251
326
|
inFence = !inFence;
|
|
252
327
|
if (keep) out.push(line);
|
|
@@ -254,8 +329,9 @@ export function stripHarnessBlocks(body, harnessName) {
|
|
|
254
329
|
}
|
|
255
330
|
if (!inFence) {
|
|
256
331
|
const m = HARNESS_OPEN_RE.exec(line);
|
|
257
|
-
if (m) { keep = m[1] === harnessName; continue; }
|
|
332
|
+
if (m) { keep = m[1] === harnessName; afterOpen = true; continue; }
|
|
258
333
|
if (HARNESS_CLOSE_RE.test(line)) { keep = true; continue; }
|
|
334
|
+
if (labelSlot && HOST_LABEL_RE.test(line)) continue;
|
|
259
335
|
}
|
|
260
336
|
if (keep) out.push(line);
|
|
261
337
|
}
|
|
@@ -363,8 +439,8 @@ function readDocSource(rel) {
|
|
|
363
439
|
return readMaybe(target) ?? '';
|
|
364
440
|
}
|
|
365
441
|
|
|
366
|
-
/** The deployed theme's words beside the neutral ones. */
|
|
367
|
-
function glossary(state) {
|
|
442
|
+
/** The deployed theme's words beside the neutral ones, from `docs/reference/glossary.md`. */
|
|
443
|
+
function glossary(state, page) {
|
|
368
444
|
const load = (theme) => {
|
|
369
445
|
const doc = readJsonMaybe(path.join(THEMES, `${theme}.json`));
|
|
370
446
|
return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {};
|
|
@@ -372,10 +448,10 @@ function glossary(state) {
|
|
|
372
448
|
const neutral = load('neutral');
|
|
373
449
|
const themed = state.theme !== 'neutral' ? load(state.theme) : neutral;
|
|
374
450
|
const rows = [];
|
|
375
|
-
for (const
|
|
451
|
+
for (const { label, key, desc, analogy } of parseGlossaryTable(page.body ?? '')) {
|
|
376
452
|
if (key === null) {
|
|
377
453
|
const term = label.toLowerCase();
|
|
378
|
-
rows.push({ label, neutral: term, themed: term, desc });
|
|
454
|
+
rows.push({ label, neutral: term, themed: term, desc, analogy });
|
|
379
455
|
continue;
|
|
380
456
|
}
|
|
381
457
|
rows.push({
|
|
@@ -383,6 +459,7 @@ function glossary(state) {
|
|
|
383
459
|
neutral: String(Object.hasOwn(neutral, key) ? neutral[key] : '').trim(),
|
|
384
460
|
themed: String(Object.hasOwn(themed, key) ? themed[key] : '').trim(),
|
|
385
461
|
desc,
|
|
462
|
+
analogy,
|
|
386
463
|
});
|
|
387
464
|
}
|
|
388
465
|
return { theme: state.theme, rows };
|
|
@@ -452,7 +529,7 @@ export function apiDocsPage(state, pageId, harnessName = null) {
|
|
|
452
529
|
}
|
|
453
530
|
|
|
454
531
|
/**
|
|
455
|
-
* The
|
|
532
|
+
* The six kinds, as a TABLE and not six `if`s.
|
|
456
533
|
*
|
|
457
534
|
* `tests/unit/web_server.test.mjs` holds `Object.keys(KIND_ROUTES)` against a written-out
|
|
458
535
|
* list of the kinds `apiDocsPage` must dispatch on — this table is what the dispatcher
|
|
@@ -470,17 +547,24 @@ const KIND_ROUTES = {
|
|
|
470
547
|
body = sliced;
|
|
471
548
|
if (ok) anchor = null;
|
|
472
549
|
}
|
|
473
|
-
body = stripHarnessBlocks(body, hn);
|
|
550
|
+
body = rewriteDocLinks(stripHarnessBlocks(body, hn), page.source, idsByRel());
|
|
474
551
|
return { id: pageId, title: page.title, kind: 'markdown', body,
|
|
475
552
|
source: page.source, anchor, links: resolveLinks(state, body) };
|
|
476
553
|
},
|
|
477
554
|
concept: (state, pageId, page, hn) => {
|
|
478
|
-
const body = subCounts(state, stripHarnessBlocks(page.body ?? '', hn))
|
|
555
|
+
const body = rewriteDocLinks(subCounts(state, stripHarnessBlocks(page.body ?? '', hn)),
|
|
556
|
+
page.rel, idsByRel());
|
|
479
557
|
return { id: pageId, title: page.title, kind: 'concept', body,
|
|
480
558
|
link: page.link ?? null, links: resolveLinks(state, body) };
|
|
481
559
|
},
|
|
560
|
+
// A concept page plus the rows of its own map table, for the console's map view.
|
|
561
|
+
map: (state, pageId, page, hn) => {
|
|
562
|
+
const body = rewriteDocLinks(stripHarnessBlocks(page.body ?? '', hn), page.rel, idsByRel());
|
|
563
|
+
return { id: pageId, title: page.title, kind: 'map', body, rows: parseMapTable(body),
|
|
564
|
+
links: resolveLinks(state, body) };
|
|
565
|
+
},
|
|
482
566
|
glossary: (state, pageId, page) => ({
|
|
483
|
-
id: pageId, title: page.title, kind: 'glossary', ...glossary(state),
|
|
567
|
+
id: pageId, title: page.title, kind: 'glossary', ...glossary(state, page),
|
|
484
568
|
}),
|
|
485
569
|
about: (state, pageId, page) => ({
|
|
486
570
|
id: pageId, title: page.title, kind: 'about', ...about(state),
|
package/js/web/handler.mjs
CHANGED
|
@@ -295,6 +295,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
295
295
|
// packs — never to `harness.config.json`, which would drop the consent gate.
|
|
296
296
|
const cmds = actionCommands(action, {
|
|
297
297
|
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
298
|
+
trust: state.trust,
|
|
298
299
|
doctrines: doctrinesForBuild(state.target),
|
|
299
300
|
});
|
|
300
301
|
if (!isTruthy(cmds)) {
|
package/js/web/jobs.mjs
CHANGED
|
@@ -88,6 +88,7 @@ import path from 'node:path';
|
|
|
88
88
|
|
|
89
89
|
import { ROOT, PACK_ORDER } from '../build/source.mjs';
|
|
90
90
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
91
|
+
import { DEFAULT_PRESET } from '../loop/score.mjs';
|
|
91
92
|
import { isDict } from '../hosts/mcp.mjs';
|
|
92
93
|
import { readText, writeText, isFile } from '../lib/fs.mjs';
|
|
93
94
|
import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
|
|
@@ -402,9 +403,10 @@ export const INLINE_ACTIONS = ['restore', 'install', 'deploy'];
|
|
|
402
403
|
function actionTable({
|
|
403
404
|
theme = 'neutral', emit = 'opencode-global', footprint = 'full',
|
|
404
405
|
posture = 'peer', mode = 'direct', doctrines = [...PACK_ORDER], excludeRules = [],
|
|
406
|
+
trust = DEFAULT_PRESET,
|
|
405
407
|
} = {}) {
|
|
406
408
|
const buildArgv = setupBuildArgs(theme, emit, null, null, footprint, posture, mode,
|
|
407
|
-
doctrines, PACK_ORDER, excludeRules);
|
|
409
|
+
doctrines, PACK_ORDER, excludeRules, trust);
|
|
408
410
|
return {
|
|
409
411
|
doctor: [[NODE(), CLI(), 'doctor']],
|
|
410
412
|
build: [[NODE(), GEN(), ...buildArgv]],
|