geneseed 4.9.4 → 4.14.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 +106 -0
- package/README.md +16 -2
- package/SHIPPED.md +1 -1
- package/docs/README.md +1 -1
- package/docs/concepts/collaboration.md +1 -1
- package/docs/concepts/enforcement-table.md +1 -1
- package/docs/concepts/foreman-mode.md +1 -1
- package/docs/concepts/kits.md +91 -0
- package/docs/concepts/loop-branch.md +1 -1
- package/docs/concepts/loop-bricks.md +1 -1
- package/docs/concepts/loop-contracts.md +1 -1
- package/docs/concepts/loop-gates.md +1 -1
- package/docs/concepts/loop-notes.md +1 -1
- package/docs/concepts/loop-risk.md +1 -1
- package/docs/concepts/loop-templates.md +1 -1
- package/docs/concepts/loops.md +1 -1
- package/docs/concepts/lsp.md +1 -1
- package/docs/concepts/skills.md +1 -1
- package/docs/extending.md +107 -4
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/run-anywhere.md +2 -2
- package/docs/guides/upgrade.md +2 -2
- package/docs/reference/glossary.md +1 -0
- package/docs/reference/troubleshoot-install.md +1 -1
- package/docs/web-ui.md +9 -0
- package/harness.config.json +1 -1
- package/js/build/args.mjs +27 -6
- package/js/build/driver.mjs +29 -8
- package/js/build/emit-claude.mjs +3 -1
- package/js/build/generate.mjs +10 -5
- package/js/build/render.mjs +94 -11
- package/js/build/source.mjs +117 -4
- package/js/cli-table.json +15 -0
- package/js/hosts/installs.mjs +35 -2
- package/js/hosts/link.mjs +88 -1
- package/js/hosts/native.mjs +14 -4
- package/js/inspect/checks-authoring.mjs +6 -3
- package/js/inspect/checks-build.mjs +15 -3
- package/js/inspect/checks-counts.mjs +153 -6
- package/js/inspect/checks-repo.mjs +95 -20
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/diff.mjs +56 -8
- package/js/inspect/inventory.mjs +2 -0
- package/js/inspect/status.mjs +44 -3
- package/js/inspect/validate.mjs +3 -3
- package/js/lib/prompt.mjs +19 -2
- package/js/maintain/setup.mjs +111 -23
- package/js/maintain/update.mjs +3 -3
- package/js/web/actions.mjs +38 -6
- package/js/web/api.mjs +76 -17
- package/js/web/catalog.mjs +17 -2
- package/js/web/handler.mjs +1 -0
- package/js/web/routes.mjs +2 -1
- package/js/web/server.mjs +11 -5
- package/js/web/state.mjs +38 -3
- package/package.json +1 -1
- package/registry.json +91 -0
- package/src/AGENT.md.tmpl +7 -7
- package/src/agents/_self-improvement.md +11 -0
- package/src/agents/_template.md +1 -8
- package/src/agents/advocate.md +1 -8
- package/src/agents/architect.md +1 -8
- package/src/agents/developer.md +1 -8
- package/src/agents/devops-engineer.md +37 -0
- package/src/agents/docker-expert.md +37 -0
- package/src/agents/docs.md +1 -8
- package/src/agents/empiricist.md +1 -8
- package/src/agents/explorer.md +1 -8
- package/src/agents/framer.md +1 -8
- package/src/agents/historian.md +1 -8
- package/src/agents/kubernetes-specialist.md +37 -0
- package/src/agents/memory-curator.md +1 -8
- package/src/agents/operator.md +1 -8
- package/src/agents/performance.md +1 -8
- package/src/agents/pragmatist.md +1 -8
- package/src/agents/react-engineer.md +38 -0
- package/src/agents/react-reviewer.md +42 -0
- package/src/agents/researcher.md +5 -10
- package/src/agents/reviewer.md +3 -8
- package/src/agents/security.md +1 -8
- package/src/agents/skeptic.md +1 -8
- package/src/agents/spring-boot-engineer.md +40 -0
- package/src/agents/spring-reviewer.md +41 -0
- package/src/agents/spring-security.md +42 -0
- package/src/agents/steward.md +1 -8
- package/src/agents/tester.md +1 -8
- package/src/agents/user-advocate.md +1 -8
- package/src/agents/visionary.md +1 -8
- package/src/kits/react/rules.md +13 -0
- package/src/kits/react.json +18 -0
- package/src/kits/spring-boot/rules.md +10 -0
- package/src/kits/spring-boot.json +23 -0
- package/src/licenses/everything-claude-code.txt +21 -0
- package/src/licenses/mattpocock-skills.txt +21 -0
- package/src/licenses/piomin-spring-boot.txt +190 -0
- package/src/licenses/superpowers.txt +21 -0
- package/src/licenses/vibe-wise.txt +21 -0
- package/src/skills/_self-improvement.md +22 -5
- package/src/skills/brainstorm/builder-check.md +2 -0
- package/src/skills/brainstorm.md +11 -2
- package/src/skills/codebase-design.md +10 -2
- package/src/skills/daydream/VENDOR.md +7 -7
- package/src/skills/debug.md +6 -2
- package/src/skills/develop.md +6 -2
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/git-history.md +2 -0
- package/src/skills/handoff.md +3 -1
- package/src/skills/learn-mode.md +3 -2
- package/src/skills/parallel-agents.md +5 -1
- package/src/skills/pipeline.md +2 -0
- package/src/skills/plan.md +9 -2
- package/src/skills/prototype.md +6 -2
- package/src/skills/react/a11y.md +33 -0
- package/src/skills/react/vite.md +35 -0
- package/src/skills/react/webpack.md +40 -0
- package/src/skills/react-testing.md +68 -0
- package/src/skills/react-view-transitions/AGENTS.md +646 -221
- package/src/skills/react-view-transitions/SKILL.md +28 -16
- package/src/skills/react-view-transitions/VENDOR.md +10 -8
- package/src/skills/react-view-transitions/references/css-recipes.md +72 -2
- package/src/skills/react-view-transitions/references/implementation.md +14 -26
- package/src/skills/react-view-transitions/references/nextjs.md +94 -37
- package/src/skills/react-view-transitions/references/patterns.md +60 -48
- package/src/skills/react-view-transitions/references/troubleshooting.md +47 -0
- package/src/skills/react.md +63 -0
- package/src/skills/research.md +6 -2
- package/src/skills/review-response.md +2 -0
- package/src/skills/rule.md +6 -2
- package/src/skills/ship.md +8 -4
- package/src/skills/skill-forge.md +11 -5
- package/src/skills/spring-boot/cloud.md +103 -0
- package/src/skills/spring-boot/java-modern.md +71 -0
- package/src/skills/spring-boot/logging.md +89 -0
- package/src/skills/spring-boot/security.md +112 -0
- package/src/skills/spring-boot/web.md +112 -0
- package/src/skills/spring-boot.md +57 -0
- package/src/skills/spring-data-jpa/patterns.md +68 -0
- package/src/skills/spring-data-jpa.md +81 -0
- package/src/skills/spring-testing.md +93 -0
- package/src/skills/wayfinder.md +8 -2
- package/src/skills/wiki.md +18 -5
- package/src/upstreams.json +66 -0
- package/themes/_TEMPLATE.json +13 -0
- package/themes/biker.json +13 -0
- package/themes/commentator.json +13 -0
- package/themes/cyberpunk.json +13 -0
- package/themes/gamer.json +13 -0
- package/themes/imperial.json +13 -0
- package/themes/joker.json +13 -0
- package/themes/marvin.json +13 -0
- package/themes/mean.json +13 -0
- package/themes/military.json +13 -0
- package/themes/neutral.json +13 -0
- package/themes/pirate.json +13 -0
- package/themes/sports.json +13 -0
- package/themes/verstappen.json +13 -0
- package/themes/wizard.json +13 -0
- package/web/dist/assets/Activity-44rbWUQI.js +1 -0
- package/web/dist/assets/ActivityDetail-Dk9k893a.js +1 -0
- package/web/dist/assets/Dashboard-DCoUN60W.js +2 -0
- package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-C_je2lRG.js} +3 -3
- package/web/dist/assets/FilterInput-CZREvrjX.js +1 -0
- package/web/dist/assets/GreenhouseView-BXAOIznW.js +2 -0
- package/web/dist/assets/Installs-DAXF403D.js +3 -0
- package/web/dist/assets/JournalView-DZ184wKB.js +1 -0
- package/web/dist/assets/KitApply-pjhvGha0.js +1 -0
- package/web/dist/assets/Kits-BCCKjSIY.js +1 -0
- package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BgBUpRkE.js} +1 -1
- package/web/dist/assets/Library-CW0Zl6RF.js +1 -0
- package/web/dist/assets/Loops-BvFOzjEw.js +2 -0
- package/web/dist/assets/{Markdown-iHvBrpRN.js → Markdown-zaXXXHz9.js} +13 -13
- package/web/dist/assets/OperatorHudView-Bs4unPNi.js +2 -0
- package/web/dist/assets/Personal-C23XaT-a.js +1 -0
- package/web/dist/assets/Tabs-b2XFu4OS.js +1 -0
- package/web/dist/assets/activity-Jw8XcPiP.js +1 -0
- package/web/dist/assets/api-BfpZmUMI.js +1 -0
- package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-BERkKhx_.js} +1 -1
- package/web/dist/assets/index-C9R_bRw_.js +2 -0
- package/web/dist/assets/{index-BCJz4_8C.css → index-jYlWihN-.css} +1 -1
- package/web/dist/assets/{react-BOF6yL40.js → react-DlYQp10C.js} +1 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/dist/assets/sections-ChwaobfW.js +1 -0
- package/web/dist/assets/usePoll-bDFN-Hr6.js +1 -0
- package/web/dist/index.html +6 -5
- package/web/dist/assets/Activity-C8sZVIUU.js +0 -1
- package/web/dist/assets/ActivityDetail-Cc6vWUS4.js +0 -1
- package/web/dist/assets/Dashboard-C5ATa3ow.js +0 -2
- package/web/dist/assets/FilterInput-zLFXoSt6.js +0 -1
- package/web/dist/assets/GreenhouseView-zk2Uzc3A.js +0 -2
- package/web/dist/assets/Installs-CB_rvbBP.js +0 -3
- package/web/dist/assets/JournalView-Bfj27XHC.js +0 -1
- package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
- package/web/dist/assets/Loops-DaPk-ckL.js +0 -2
- package/web/dist/assets/OperatorHudView-CyiGhiHk.js +0 -2
- package/web/dist/assets/Personal-Bwd27CmT.js +0 -1
- package/web/dist/assets/Tabs-BOD7Kaz1.js +0 -1
- package/web/dist/assets/activity-DvPxoS4Y.js +0 -1
- package/web/dist/assets/api-CiXlkbe0.js +0 -1
- package/web/dist/assets/index-DQXG_mqo.js +0 -2
- package/web/dist/assets/sections-BET3c1WT.js +0 -1
- package/web/dist/assets/usePoll-NSTzLOF4.js +0 -1
package/js/maintain/setup.mjs
CHANGED
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
* a cell could not have varied the answers at all.
|
|
42
42
|
*/
|
|
43
43
|
import { spawnSync } from 'node:child_process';
|
|
44
|
-
import { existsSync } from 'node:fs';
|
|
44
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
45
|
+
import os from 'node:os';
|
|
45
46
|
import path from 'node:path';
|
|
46
47
|
|
|
47
48
|
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
48
49
|
import { playLine } from '../ui/anim.mjs';
|
|
49
|
-
import { discoverNames, GATE_SWITCHES, PACK_ORDER } from '../build/source.mjs';
|
|
50
|
+
import { discoverNames, GATE_SWITCHES, PACK_ORDER, loadKits } from '../build/source.mjs';
|
|
50
51
|
import { exportImprovements } from '../inspect/diff.mjs';
|
|
51
52
|
import { cmdDoctor } from '../inspect/doctor.mjs';
|
|
52
53
|
import { setupBuildArgs } from '../build/generate.mjs';
|
|
@@ -227,6 +228,25 @@ function askDoctrines(deployed = null) {
|
|
|
227
228
|
known === null || known.includes(name) ? 'yes' : 'no') === 'yes');
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The Kits questions (spec A5): one yes/no per shipped kit, each defaulting to whether the install
|
|
233
|
+
* has it on (`deployed`, its `Active kits:` marker), so holding Enter keeps the installed set. A
|
|
234
|
+
* single pick could not hold two kits at once, which is why the menu became per kit when the second
|
|
235
|
+
* kit shipped. Asked ONLY when this checkout ships a kit, so a kit-less wizard reads exactly the
|
|
236
|
+
* stdin it always did (and has no kit to carry: `kitsOfDir` drops an unshipped one).
|
|
237
|
+
* Returns the kit list, or null for none (`setupBuildArgs` omits `--kits`).
|
|
238
|
+
*/
|
|
239
|
+
export function askKits(deployed = null, kits = null) {
|
|
240
|
+
try { kits ??= loadKits(); } catch (e) {
|
|
241
|
+
process.stderr.write(`warning: kit question skipped — ${e.message}\n`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const on = (deployed ?? []).filter((k) => kits.has(k));
|
|
245
|
+
const picked = [...kits].filter(([k, m]) => askChoice(`Kit ${k} — ${m.description ?? k}`,
|
|
246
|
+
[['yes', `install the ${k} kit`], ['no', `leave ${k} out`]], on.includes(k) ? 'yes' : 'no') === 'yes');
|
|
247
|
+
return picked.length ? picked.map(([k]) => k) : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
230
250
|
/**
|
|
231
251
|
* `_harness_setup._collect_setup_lines` — the six questions, the plan, the confirm.
|
|
232
252
|
*
|
|
@@ -260,13 +280,15 @@ function targetGates(emit, out) {
|
|
|
260
280
|
}
|
|
261
281
|
}
|
|
262
282
|
|
|
263
|
-
export function collectSetupLines() {
|
|
283
|
+
export function collectSetupLines(kitMap = null) {
|
|
264
284
|
printOut('Geneseed setup — answer a few questions; nothing is written until you confirm.\n');
|
|
265
285
|
const inst = installedDefaults();
|
|
266
286
|
const theme = askChoice('Theme', themeOptions(), inst.theme || defaultTheme());
|
|
267
287
|
const posture = askChoice('Posture', postureOptions(), inst.posture || defaultPosture());
|
|
268
288
|
const mode = askChoice('Mode', modeOptions(), inst.mode || defaultMode());
|
|
269
289
|
const doctrines = askDoctrines(inst.doctrines);
|
|
290
|
+
// Kits on stay on (spec A3); `kitsOfDir` already dropped a retired one.
|
|
291
|
+
const kits = askKits(inst.kits, kitMap);
|
|
270
292
|
const emit = askChoice('Install mode', EMIT_OPTIONS, inst.emit || 'opencode-global');
|
|
271
293
|
const footprint = askChoice('Footprint', FOOTPRINT_OPTIONS,
|
|
272
294
|
inst.footprint || 'lean');
|
|
@@ -304,13 +326,13 @@ export function collectSetupLines() {
|
|
|
304
326
|
const gated = Object.values(gates).some((on) => on === false);
|
|
305
327
|
printOut(`\nAbout to run: geneseed build ${
|
|
306
328
|
setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines, allPacks,
|
|
307
|
-
excludeRules, undefined, excludeSkills, gates).join(' ')}\n`);
|
|
329
|
+
excludeRules, undefined, excludeSkills, gates, kits).join(' ')}\n`);
|
|
308
330
|
if (!confirm('Proceed?', true)) return null;
|
|
309
331
|
// The key only when there is something to carry, so a selection that excludes no skill and
|
|
310
332
|
// switches off no gate is the same object it was before either existed.
|
|
311
333
|
return {
|
|
312
334
|
theme, posture, mode, doctrines, excludeRules, ...(excludeSkills ? { excludeSkills } : {}),
|
|
313
|
-
...(gated ? { gates } : {}),
|
|
335
|
+
...(gated ? { gates } : {}), ...(kits ? { kits } : {}),
|
|
314
336
|
emit, out, root, footprint,
|
|
315
337
|
};
|
|
316
338
|
}
|
|
@@ -494,11 +516,11 @@ export function setupLines() {
|
|
|
494
516
|
}
|
|
495
517
|
const {
|
|
496
518
|
theme, emit, out, root, footprint = 'lean', posture = 'peer', mode = 'direct',
|
|
497
|
-
doctrines = null, excludeRules = null, excludeSkills = null, gates = {},
|
|
519
|
+
doctrines = null, excludeRules = null, excludeSkills = null, gates = {}, kits = null,
|
|
498
520
|
} = sel;
|
|
499
521
|
if (emit === 'opencode-global') {
|
|
500
|
-
// The build below overwrites the deployed global harness,
|
|
501
|
-
//
|
|
522
|
+
// The build below overwrites the deployed global harness, which may carry hand edits (or
|
|
523
|
+
// in-place edits from a harness older than the notebook notes). Preserve that drift first.
|
|
502
524
|
try {
|
|
503
525
|
const [ipath] = exportImprovements();
|
|
504
526
|
if (ipath) {
|
|
@@ -513,7 +535,7 @@ export function setupLines() {
|
|
|
513
535
|
// runs must carry the pack selection the plan line just previewed, or the wizard would print
|
|
514
536
|
// one command and run another.
|
|
515
537
|
const argv = setupBuildArgs(theme, emit, out, root, footprint, posture, mode, doctrines,
|
|
516
|
-
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates);
|
|
538
|
+
doctrineOptions().map(([n]) => n), excludeRules, undefined, excludeSkills, gates, kits);
|
|
517
539
|
printOut(`Running: geneseed build ${argv.join(' ')}\n`);
|
|
518
540
|
const rc = driverMain(argv);
|
|
519
541
|
if (rc !== 0) {
|
|
@@ -551,26 +573,83 @@ export function cmdSetup() {
|
|
|
551
573
|
}
|
|
552
574
|
|
|
553
575
|
/**
|
|
554
|
-
* The AI coding tools Geneseed installs into, as `[label, present, url]` rows in HOSTS
|
|
576
|
+
* The AI coding tools Geneseed installs into, as `[label, present, url, where]` rows in HOSTS
|
|
577
|
+
* order. `where` is the path found OFF the PATH (see `userInstall`), else null.
|
|
555
578
|
*
|
|
556
|
-
* PRESENT MEANS THE
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
579
|
+
* PRESENT MEANS THE TOOL ITSELF WAS FOUND — its command on PATH, or its program in a per-user
|
|
580
|
+
* install location — and never its config dir. That dir looked like a signal — a desktop-only
|
|
581
|
+
* install has one and no command — but GENESEED ITSELF CREATES IT: a `bob-global` emit writes
|
|
582
|
+
* `~/.bob` on a machine that never had Bob, and the first run of this check reported Bob present
|
|
583
|
+
* on exactly such a machine. A wrong "ok" sends the user to an install mode for a tool they
|
|
584
|
+
* cannot open; a wrong "not found" costs a line, and `toolsReport` lets them name the path.
|
|
561
585
|
*
|
|
562
586
|
* NO SPAWN, deliberately. `which` walks PATH by hand, so asking costs nothing on the CLI's
|
|
563
587
|
* child_process allow-list — a `claude --version` probe would have been a third entry there to
|
|
564
|
-
* learn nothing `which` does not already say. `lookup`
|
|
565
|
-
* rely on what this machine happens to have installed.
|
|
588
|
+
* learn nothing `which` does not already say. `lookup` and `userLookup` are the seams for the
|
|
589
|
+
* test, which cannot rely on what this machine happens to have installed.
|
|
566
590
|
*/
|
|
567
|
-
export function hostTools({ lookup = which } = {}) {
|
|
591
|
+
export function hostTools({ lookup = which, userLookup = userInstall } = {}) {
|
|
568
592
|
return [
|
|
569
593
|
['OpenCode', 'opencode', 'https://opencode.ai/docs/'],
|
|
570
594
|
['Claude Code', 'claude', 'https://code.claude.com/docs/en/setup'],
|
|
571
595
|
['IBM Bob', 'bob', 'https://bob.ibm.com/'],
|
|
572
596
|
['OpenClaude', 'openclaude', 'https://openclaude.gitlawb.com/'],
|
|
573
|
-
].map(([label, cmd, url]) =>
|
|
597
|
+
].map(([label, cmd, url]) => {
|
|
598
|
+
if (lookup(cmd)) return [label, true, url, null];
|
|
599
|
+
const where = userLookup(cmd);
|
|
600
|
+
return [label, Boolean(where), url, where || null];
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Per-host off-PATH places: a bin dir a tool's own installer uses, and a name pattern for the
|
|
606
|
+
* desktop app folder. Only Bob ships as a desktop IDE; matching `claude` against app folders
|
|
607
|
+
* would find the Claude desktop chat app, which is not Claude Code.
|
|
608
|
+
*/
|
|
609
|
+
const USER_BIN_DIRS = {
|
|
610
|
+
opencode: ['.opencode/bin'],
|
|
611
|
+
claude: ['.claude/local'],
|
|
612
|
+
bob: [],
|
|
613
|
+
openclaude: [],
|
|
614
|
+
};
|
|
615
|
+
const APP_PATTERN = { bob: /\bbob\b/i };
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Where a tool installed FOR ONE USER lives when its installer did not put it on PATH — the
|
|
619
|
+
* report of 2026-10 was IBM Bob installed under the user's own profile, which `which` never sees.
|
|
620
|
+
*
|
|
621
|
+
* Three kinds of place, each one the tool's own program, never its config dir:
|
|
622
|
+
* * a per-user bin dir: `~/.local/bin` and `~/bin` for every tool, plus the dir its own
|
|
623
|
+
* installer uses (`USER_BIN_DIRS`) — an executable file, through `which` and its PATHEXT;
|
|
624
|
+
* * on Windows, `%APPDATA%\npm` (a per-user `npm i -g`) and a desktop app folder under
|
|
625
|
+
* `%LOCALAPPDATA%\Programs` whose name matches `APP_PATTERN`;
|
|
626
|
+
* * on macOS, a matching `.app` in `/Applications` or `~/Applications`.
|
|
627
|
+
* The first hit wins. Everything a machine could differ on is a parameter, for the test.
|
|
628
|
+
*/
|
|
629
|
+
export function userInstall(cmd, {
|
|
630
|
+
home = os.homedir(), env = process.env, platform = process.platform,
|
|
631
|
+
find = which, list = listDir,
|
|
632
|
+
} = {}) {
|
|
633
|
+
const bins = ['.local/bin', 'bin', ...(USER_BIN_DIRS[cmd] ?? [])].map((d) => path.join(home, d));
|
|
634
|
+
if (platform === 'win32' && env.APPDATA) bins.push(path.join(env.APPDATA, 'npm'));
|
|
635
|
+
const hit = find(cmd, bins.join(path.delimiter));
|
|
636
|
+
if (hit) return hit;
|
|
637
|
+
const pattern = APP_PATTERN[cmd];
|
|
638
|
+
if (!pattern) return null;
|
|
639
|
+
const appDirs = platform === 'win32'
|
|
640
|
+
? (env.LOCALAPPDATA ? [path.join(env.LOCALAPPDATA, 'Programs')] : [])
|
|
641
|
+
: platform === 'darwin' ? ['/Applications', path.join(home, 'Applications')] : [];
|
|
642
|
+
for (const dir of appDirs) {
|
|
643
|
+
const name = list(dir).find((n) => pattern.test(n)
|
|
644
|
+
&& (platform !== 'darwin' || n.endsWith('.app')));
|
|
645
|
+
if (name) return path.join(dir, name);
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** `readdirSync`, with a missing or unreadable dir as no entries. */
|
|
651
|
+
function listDir(dir) {
|
|
652
|
+
try { return readdirSync(dir); } catch { return []; }
|
|
574
653
|
}
|
|
575
654
|
|
|
576
655
|
/**
|
|
@@ -578,16 +657,25 @@ export function hostTools({ lookup = which } = {}) {
|
|
|
578
657
|
* answer knowing it — and when NONE is, ask whether to stop and install one first. It never
|
|
579
658
|
* installs one: on a managed machine that is IT's call, and each tool has its own installer.
|
|
580
659
|
* A missing tool among several present is a line, not a question; nobody needs all four.
|
|
660
|
+
* When none is found, the user can name where theirs is before being asked to stop — the
|
|
661
|
+
* search covers PATH and the usual per-user places, not every folder an installer may pick.
|
|
581
662
|
* `false` is the user choosing to stop.
|
|
582
663
|
*/
|
|
583
664
|
export function toolsReport(tools = hostTools()) {
|
|
584
665
|
printOut('AI coding tools on this machine:\n');
|
|
585
|
-
for (const [label, present, url] of tools) {
|
|
586
|
-
|
|
666
|
+
for (const [label, present, url, where] of tools) {
|
|
667
|
+
if (!present) printOut(` -- ${label} not found — install: ${url}\n`);
|
|
668
|
+
else printOut(where ? ` ok ${label} (${where}, not on PATH)\n` : ` ok ${label}\n`);
|
|
587
669
|
}
|
|
588
670
|
if (tools.some(([, present]) => present)) { printOut('\n'); return true; }
|
|
589
|
-
printOut('\nNone of them
|
|
590
|
-
|
|
671
|
+
printOut('\nNone of them was found on PATH or in the usual per-user folders.\n');
|
|
672
|
+
// Quotes stripped: a path dragged into a Windows console arrives wrapped in them.
|
|
673
|
+
const given = ask('Installed somewhere else? Path to the tool (Enter to skip)')
|
|
674
|
+
.replace(/^"(.*)"$/, '$1');
|
|
675
|
+
if (given && existsSync(given)) { printOut(` ok ${given}\n\n`); return true; }
|
|
676
|
+
if (given) printOut(` -- nothing at ${given}\n`);
|
|
677
|
+
printOut('Install the one you use (links above), then run setup again — or go on and pick the '
|
|
678
|
+
+ 'plain bundle, which any AGENT.md tool can read.\n');
|
|
591
679
|
if (confirm('Continue without one?', false)) { printOut('\n'); return true; }
|
|
592
680
|
printOut('Stopped. Run setup again once a tool is installed.\n');
|
|
593
681
|
return false;
|
package/js/maintain/update.mjs
CHANGED
|
@@ -632,7 +632,7 @@ export async function installAxes(dir, { installProfile, setupBuildArgs }, notif
|
|
|
632
632
|
if (legacyProcessCarrierOfDir(String(dir), 'opencode')) return null;
|
|
633
633
|
const p = installProfile('opencode', 'global', String(dir), notify);
|
|
634
634
|
return setupBuildArgs('', 'files', null, null, p.footprint, p.posture, p.mode, p.doctrines,
|
|
635
|
-
undefined, p.excludeRules, p.trust, p.excludeSkills, p).slice(4);
|
|
635
|
+
undefined, p.excludeRules, p.trust, p.excludeSkills, p, p.kits).slice(4);
|
|
636
636
|
}
|
|
637
637
|
|
|
638
638
|
/**
|
|
@@ -796,8 +796,8 @@ export async function upgrade(ref = null, themeArg = null) {
|
|
|
796
796
|
* `_harness_lifecycle.cmd_upgrade` — the harness verb, which is `export_improvements()` and
|
|
797
797
|
* then `upgrade()`.
|
|
798
798
|
*
|
|
799
|
-
* THE EXPORT RUNS FIRST AND MUST NEVER BLOCK.
|
|
800
|
-
* harness
|
|
799
|
+
* THE EXPORT RUNS FIRST AND MUST NEVER BLOCK. A deployed harness can carry in-place edits (by
|
|
800
|
+
* hand, or by a harness older than the notebook notes) and the rebuild is about to overwrite them, so the drift has to be
|
|
801
801
|
* captured against the PRE-refresh source. Anything it throws is a warning on stderr and the
|
|
802
802
|
* upgrade continues — a failed export is a lost note, a blocked upgrade is a broken install.
|
|
803
803
|
*/
|
package/js/web/actions.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import path from 'node:path';
|
|
|
34
34
|
|
|
35
35
|
import { emitGlobalInto } from '../build/driver.mjs';
|
|
36
36
|
import {
|
|
37
|
-
ROOT, discoverNames, knownRuleIds, PACK_ORDER,
|
|
37
|
+
ROOT, discoverNames, knownRuleIds, loadKits, PACK_ORDER,
|
|
38
38
|
} from '../build/source.mjs';
|
|
39
39
|
import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
40
40
|
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
@@ -46,9 +46,10 @@ import {
|
|
|
46
46
|
opencodeConfigDir, resolvePath,
|
|
47
47
|
} from '../hosts/hosts.mjs';
|
|
48
48
|
import {
|
|
49
|
-
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir,
|
|
49
|
+
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, excludedSkillsOfDir, kitsOfDir,
|
|
50
50
|
footprintOfDir, gatesOfDir,
|
|
51
|
-
installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir,
|
|
51
|
+
installState, installTargets, legacyProcessCarrierOfDir, modeOfDir, postureOfDir, themeOfDir,
|
|
52
|
+
trustOfDir,
|
|
52
53
|
} from '../hosts/installs.mjs';
|
|
53
54
|
import {
|
|
54
55
|
isDict, mcpCommented, mcpInstallTargets, mcpLoad, mcpServerNames, mcpState,
|
|
@@ -131,6 +132,15 @@ function bodyExcludeRules(body) {
|
|
|
131
132
|
(n) => n.trim().replace(/[ \t]+/, '.'));
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* A request body's `kits` — REPLACES the install's kits (the Kits tab sends the whole set it
|
|
137
|
+
* wants): `[]`/`'none'` give `[]`, an omitted key gives `null` (keep the install's own), and an
|
|
138
|
+
* unknown name gives `null` too, which `apiInstallCmd` refuses for a body that HAS the key.
|
|
139
|
+
*/
|
|
140
|
+
function bodyKits(body) {
|
|
141
|
+
return bodyList(body, 'kits', () => [...loadKits().keys()]);
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
/**
|
|
135
145
|
* The gate switches across the trust boundary (`deletionGate`, `secretsGate`, `doneGate`), each
|
|
136
146
|
* on its own: only a real JSON boolean counts. Anything else — a string `"false"`, a number,
|
|
@@ -465,6 +475,14 @@ export function buildActionCmds(state, body) {
|
|
|
465
475
|
const [, , ...rest] = installProfile(host, scope, state.root).argv;
|
|
466
476
|
return actionCommands('build', { buildArgv: ['--theme', theme, ...rest] });
|
|
467
477
|
}
|
|
478
|
+
// Only a GLOBAL emit has a home of its own. Any other carries no folder on this route, so the
|
|
479
|
+
// generator fell back to its default `./Harness` — and jobs run with the Geneseed checkout as
|
|
480
|
+
// cwd, so a Bob "project" install landed inside the clone (reported 2026-10), where the doctor's
|
|
481
|
+
// bundle check then read it as 178 missing files. The `deploy` action takes a chosen folder.
|
|
482
|
+
if (!emit.endsWith('-global')) {
|
|
483
|
+
return { error: `${emit} installs into a folder you choose — use Deploy with a folder, `
|
|
484
|
+
+ 'not Build' };
|
|
485
|
+
}
|
|
468
486
|
return actionCommands('build', {
|
|
469
487
|
theme, emit, footprint: state.footprint, posture: state.posture, mode: state.mode,
|
|
470
488
|
trust: state.trust,
|
|
@@ -539,6 +557,8 @@ export function apiRestore(state, files) {
|
|
|
539
557
|
// re-states a rule whose hook gate the install deliberately left unwired.
|
|
540
558
|
excludeRules: excludedRulesOfDir(target),
|
|
541
559
|
excludeSkills: excludedSkillsOfDir(target),
|
|
560
|
+
// The kits: a restored AGENT.md keeps its Kits block and `Active kits:` line.
|
|
561
|
+
kits: kitsOfDir(target),
|
|
542
562
|
// The gate switches: a restored AGENT.md must keep the install's `<gate>: off` lines.
|
|
543
563
|
...gatesOfDir(target),
|
|
544
564
|
}));
|
|
@@ -607,8 +627,9 @@ export function apiInstallCmd(state, body) {
|
|
|
607
627
|
if (emit === undefined) return { error: `no install mode for ${host}:${scope}` };
|
|
608
628
|
// Every read-back below is narrowed to `host`'s OWN carrier (host-compat B1) — see the
|
|
609
629
|
// comment at `installProfile` in `js/build/generate.mjs`.
|
|
630
|
+
// The theme default is THIS install's own, not the viewed one's: the Kits tab acts on any row.
|
|
610
631
|
const { theme, footprint: fp, posture: pos, mode, trust } = bodyAxes(body, {
|
|
611
|
-
theme: state.theme, footprint: footprintOfDir(root),
|
|
632
|
+
theme: themeOfDir(root, host) || state.theme, footprint: footprintOfDir(root),
|
|
612
633
|
posture: postureOfDir(root, host) || 'peer',
|
|
613
634
|
mode: modeOfDir(root, host) || 'direct', trust: trustOfDir(root, host) || DEFAULT_PRESET });
|
|
614
635
|
// Unspecified means "keep what this install already has", exactly as theme, footprint,
|
|
@@ -626,9 +647,20 @@ export function apiInstallCmd(state, body) {
|
|
|
626
647
|
// The gate switches (the Constitution page's toggles): a strict boolean per gate in the body,
|
|
627
648
|
// else the install's own answer — so a pack Apply keeps every switch as it is.
|
|
628
649
|
const gates = bodyGates(body, root, host);
|
|
650
|
+
// The page sends `kits` deliberately, so an unknown name is refused: ignoring it would run a
|
|
651
|
+
// no-op rebuild and report success. An omitted key keeps the install's own kits.
|
|
652
|
+
let kits = null;
|
|
653
|
+
try {
|
|
654
|
+
kits = bodyKits(body);
|
|
655
|
+
if (kits === null && bget(body, 'kits') !== null) {
|
|
656
|
+
return { error: `unknown kit in ${formatRepr(bget(body, 'kits'))} — this Geneseed ships: `
|
|
657
|
+
+ [...loadKits().keys()].join(', ') };
|
|
658
|
+
}
|
|
659
|
+
} catch (e) { return { error: String(e?.message ?? e) }; }
|
|
629
660
|
const out = scope === 'global' ? null : String(root);
|
|
630
661
|
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
631
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates
|
|
662
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates,
|
|
663
|
+
kits ?? kitsOfDir(root, host));
|
|
632
664
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
633
665
|
}
|
|
634
666
|
|
|
@@ -730,6 +762,6 @@ export function apiDeployCmd(state, body) {
|
|
|
730
762
|
const gates = bodyGates(body, root, host);
|
|
731
763
|
// project-scope emit name == host name (opencode / claude / bob / openclaude)
|
|
732
764
|
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
733
|
-
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates);
|
|
765
|
+
PACK_ORDER, excludeRules, trust, excludedSkillsOfDir(root, host), gates, kitsOfDir(root, host));
|
|
734
766
|
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
735
767
|
}
|
package/js/web/api.mjs
CHANGED
|
@@ -8,31 +8,33 @@
|
|
|
8
8
|
* bind paths to these functions are `routes.mjs`. This file imports none of its route siblings,
|
|
9
9
|
* which is what keeps the old `api ↔ actions` and `api ↔ activity` cycles from coming back.
|
|
10
10
|
*/
|
|
11
|
-
import { statSync } from 'node:fs';
|
|
11
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
|
-
import { CONFIG, ROOT, THEMES, discoverNames, makeCfg } from '../build/source.mjs';
|
|
14
|
+
import { CONFIG, CREDIT_RE, ROOT, SRC, THEMES, discoverNames, loadKits, makeCfg, upstreamOf } from '../build/source.mjs';
|
|
15
15
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
16
16
|
import { sourceReleaseVersion } from '../hosts/opencode.mjs';
|
|
17
17
|
import { alwaysOnCost } from '../inspect/cost.mjs';
|
|
18
|
+
import { installProfile } from '../build/generate.mjs';
|
|
18
19
|
import { diffCollect, renderMemo } from '../inspect/diff.mjs';
|
|
19
20
|
import { excludesSnapshot } from '../inspect/excludes.mjs';
|
|
20
|
-
import {
|
|
21
|
+
import { VERSION_MARKER, resolvePath } from '../hosts/hosts.mjs';
|
|
21
22
|
import {
|
|
22
|
-
footprintOfDir, gatesOfDir, installState, installTargets,
|
|
23
|
-
|
|
23
|
+
footprintOfDir, gatesOfDir, installState, installTargets, legacyProcessCarrierOfDir, modeOfDir,
|
|
24
|
+
trustOfDir,
|
|
25
|
+
postureOfDir, readJsonMaybe, readMaybe, themeOfDir,
|
|
24
26
|
} from '../hosts/installs.mjs';
|
|
25
27
|
import { PRESETS } from '../loop/score.mjs';
|
|
26
28
|
import { loadCatalog, catalogProblems } from '../loop/catalog.mjs';
|
|
27
29
|
import { activeLoops } from '../loop/registry.mjs';
|
|
28
30
|
import { EMIT_OPTIONS, themeOptions } from '../maintain/setup.mjs';
|
|
29
|
-
import { accentFor, statusData } from '../inspect/status.mjs';
|
|
31
|
+
import { accentFor, keptKitFiles, statusData } from '../inspect/status.mjs';
|
|
30
32
|
import { isFile } from '../lib/fs.mjs';
|
|
31
33
|
import { comparePaths, normcase } from '../lib/paths.mjs';
|
|
32
34
|
import {
|
|
33
35
|
configItems, deployed, dget, memoryItems, notebookItems, wikiItems,
|
|
34
36
|
} from './catalog.mjs';
|
|
35
|
-
import { stampMinute } from './state.mjs';
|
|
37
|
+
import { stampMinute, viewCfg } from './state.mjs';
|
|
36
38
|
import { updateStatus } from './update-check.mjs';
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -106,16 +108,9 @@ export function apiDiff(state) {
|
|
|
106
108
|
/** The union across every global install, verbatim. */
|
|
107
109
|
export const apiExcludes = () => excludesSnapshot();
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
*/
|
|
113
|
-
export function viewCfg(host, scope, root) {
|
|
114
|
-
if (scope === 'project' && CLAUDE_STYLE.includes(host)) {
|
|
115
|
-
return path.join(root, HOSTS.find((h) => h.host === host).projectMarker);
|
|
116
|
-
}
|
|
117
|
-
return root;
|
|
118
|
-
}
|
|
111
|
+
// `viewCfg` moved to `state.mjs`, which needs it to pick a default view and cannot import
|
|
112
|
+
// this module back (this one imports `stampMinute` from it).
|
|
113
|
+
export { viewCfg } from './state.mjs';
|
|
119
114
|
|
|
120
115
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
121
116
|
|
|
@@ -231,6 +226,70 @@ export function apiFleet(state) {
|
|
|
231
226
|
return { installs, source_fp: sourceFp, doctor: state.doctor };
|
|
232
227
|
}
|
|
233
228
|
|
|
229
|
+
/** Source files of a kit member: `<dir>/<name>.md`, or every `.md` in a `<dir>/<name>/` folder. */
|
|
230
|
+
function memberTexts(dir, name) {
|
|
231
|
+
const one = readMaybe(path.join(SRC, dir, `${name}.md`));
|
|
232
|
+
if (one !== null && one !== undefined) return [one];
|
|
233
|
+
const d = path.join(SRC, dir, name);
|
|
234
|
+
try { return readdirSync(d).filter((f) => f.endsWith('.md')).map((f) => readMaybe(path.join(d, f)) ?? ''); } catch { return []; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Upstream credits cited by a kit's rules and member sources; an unknown key is skipped, never a 500. */
|
|
238
|
+
function kitCredits(m, rules) {
|
|
239
|
+
const texts = [rules, ...(m.skills ?? []).flatMap((n) => memberTexts('skills', n)),
|
|
240
|
+
...(m.agents ?? []).flatMap((n) => memberTexts('agents', n))];
|
|
241
|
+
const keys = new Set();
|
|
242
|
+
for (const t of texts) for (const x of t.matchAll(CREDIT_RE)) keys.add(x[1]);
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const key of [...keys].sort()) {
|
|
245
|
+
let u;
|
|
246
|
+
try { u = upstreamOf(key); } catch { u = null; }
|
|
247
|
+
if (!u) continue;
|
|
248
|
+
out.push({ key, repo: u.repo, author: u.author, license: u.license, commit: u.commit,
|
|
249
|
+
compareUrl: `https://github.com/${u.repo}/compare/${u.commit}...HEAD` });
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* EVERY INSTALL'S KITS — the Kits tab. Its own route, like `apiFleet`: it re-reads each carrier
|
|
256
|
+
* and the kit manifests, which the sidebar's `/api/installs` refetch must not pay for.
|
|
257
|
+
*
|
|
258
|
+
* `kits` per row is `installProfile(...).kits`, the reader `rebuild-all` uses, so the tab shows
|
|
259
|
+
* what a rebuild would keep. A malformed manifest answers 200 with `error` and EMPTY lists, never
|
|
260
|
+
* rows without kits (which would read as "no kit on") and never a 500. `editable:false` carries
|
|
261
|
+
* the two refusals `apiInstallCmd` makes, so the page can say why before the click.
|
|
262
|
+
*/
|
|
263
|
+
export function apiKits() {
|
|
264
|
+
let known;
|
|
265
|
+
try { known = loadKits(); } catch (e) {
|
|
266
|
+
return { kits: [], installs: [], error: String(e?.message ?? e) };
|
|
267
|
+
}
|
|
268
|
+
const kits = [...known].map(([name, m]) => {
|
|
269
|
+
const rules = readMaybe(path.join(SRC, 'kits', name, 'rules.md')) ?? '';
|
|
270
|
+
return { name, description: m.description ?? '', skills: m.skills ?? [], agents: m.agents ?? [],
|
|
271
|
+
rules, rulesWordBudget: m.rulesWordBudget ?? null, banned: m.banned ?? [],
|
|
272
|
+
credits: kitCredits(m, rules) };
|
|
273
|
+
});
|
|
274
|
+
const installs = [];
|
|
275
|
+
for (const [host, scope, root] of installTargets()) {
|
|
276
|
+
const st = installState(root, host, scope);
|
|
277
|
+
if (st === 'absent') continue;
|
|
278
|
+
try {
|
|
279
|
+
const p = installProfile(host, scope, root);
|
|
280
|
+
const reason = st === 'disabled' ? 'install is disabled — reactivate it first'
|
|
281
|
+
: legacyProcessCarrierOfDir(root, host) ? 'predates the comms pack — run geneseed setup for it first'
|
|
282
|
+
: null;
|
|
283
|
+
installs.push({ ...installRowBase(host, scope, root, st), kits: p.kits,
|
|
284
|
+
keptKitFiles: keptKitFiles(host, scope, root, p.kits), editable: reason === null, reason });
|
|
285
|
+
} catch (e) {
|
|
286
|
+
installs.push({ ...installRowBase(host, scope, root, 'unreadable'),
|
|
287
|
+
error: String(e?.message ?? e) });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { kits, installs };
|
|
291
|
+
}
|
|
292
|
+
|
|
234
293
|
/** The dashboard aggregate. */
|
|
235
294
|
export function apiOverview(state) {
|
|
236
295
|
const inv = state.inventory;
|
package/js/web/catalog.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
|
|
|
24
24
|
import { comparePaths, normcase, within } from '../lib/paths.mjs';
|
|
25
25
|
import { stripWhitespace } from '../lib/text.mjs';
|
|
26
26
|
import { readJsonc } from '../hosts/settings.mjs';
|
|
27
|
+
import { loadKits } from '../build/source.mjs';
|
|
27
28
|
|
|
28
29
|
/** A requested catalog section or item that does not exist. */
|
|
29
30
|
export class NotFound extends Error {}
|
|
@@ -466,16 +467,29 @@ function constitutionItems(inv) {
|
|
|
466
467
|
];
|
|
467
468
|
}
|
|
468
469
|
|
|
470
|
+
/** `Map<'skills/x' | 'agents/y', kit>` from the shipped manifests; a broken manifest means no badges, not a 500. */
|
|
471
|
+
function kitOwners() {
|
|
472
|
+
const owner = new Map();
|
|
473
|
+
try {
|
|
474
|
+
for (const [kit, m] of loadKits()) {
|
|
475
|
+
for (const n of m.skills ?? []) owner.set(`skill:${n}`, kit);
|
|
476
|
+
for (const n of m.agents ?? []) owner.set(`agent:${n}`, kit);
|
|
477
|
+
}
|
|
478
|
+
} catch { /* shortcut: unreadable manifests hide the badge; apiKits reports the error */ }
|
|
479
|
+
return owner;
|
|
480
|
+
}
|
|
481
|
+
|
|
469
482
|
export function apiCatalog(state, section) {
|
|
470
483
|
if (!SECTIONS.includes(section)) throw new NotFound(section);
|
|
471
484
|
const inv = state.inventory;
|
|
472
485
|
let items;
|
|
486
|
+
const owner = kitOwners();
|
|
473
487
|
if (section === 'agents') {
|
|
474
488
|
items = inv.agents.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
|
|
475
|
-
source: e.source ?? null, status: e.status ?? 'unknown' }));
|
|
489
|
+
source: e.source ?? null, status: e.status ?? 'unknown', kit: owner.get(`agent:${e.name}`) ?? null }));
|
|
476
490
|
} else if (section === 'skills') {
|
|
477
491
|
items = inv.skills.map((e) => ({ name: e.name, title: e.name, desc: e.desc,
|
|
478
|
-
source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown' }));
|
|
492
|
+
source: e.source ?? null, klass: e.klass ?? 'build', status: e.status ?? 'unknown', kit: owner.get(`skill:${e.name}`) ?? null }));
|
|
479
493
|
} else if (section === 'laws') {
|
|
480
494
|
items = constitutionItems(inv);
|
|
481
495
|
} else if (section === 'memory') {
|
|
@@ -499,6 +513,7 @@ export function apiItem(state, type, name) {
|
|
|
499
513
|
links: resolveLinks(state, e.body), source: e.source ?? null };
|
|
500
514
|
if (type === 'skill') out.klass = e.klass ?? 'build';
|
|
501
515
|
out.status = e.status ?? 'unknown';
|
|
516
|
+
out.kit = kitOwners().get(`${type}:${name}`) ?? null;
|
|
502
517
|
return out;
|
|
503
518
|
}
|
|
504
519
|
if (type === 'law') {
|
package/js/web/handler.mjs
CHANGED
|
@@ -303,6 +303,7 @@ export function makeHandler(state, jm, token, dist, holder = null) {
|
|
|
303
303
|
// Build resolves its argv off the install (`buildActionCmds`); the other actions take no
|
|
304
304
|
// axes and self-resolve the deployed theme downstream.
|
|
305
305
|
const cmds = action === 'build' ? buildActionCmds(state, body) : actionCommands(action);
|
|
306
|
+
if (cmds && Object.hasOwn(cmds, 'error')) return sendJson(res, cmds, 400, ae);
|
|
306
307
|
if (!isTruthy(cmds)) {
|
|
307
308
|
return sendJson(res, { error: `unknown action ${action}` }, 404, ae);
|
|
308
309
|
}
|
package/js/web/routes.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { percentDecode } from '../lib/text.mjs';
|
|
|
21
21
|
import { apiExcludesMutate, apiInstallToggle, apiLoopsPresetMutate, apiMcp, apiMemoryDelete, apiSelectView } from './actions.mjs';
|
|
22
22
|
import { apiActivity, apiActivityDetail, apiActivityToggle } from './activity.mjs';
|
|
23
23
|
import {
|
|
24
|
-
apiDiff, apiDoctor, apiExcludes, apiFleet, apiInstalls, apiLoops, apiLoopsActive, apiOverview,
|
|
24
|
+
apiDiff, apiDoctor, apiExcludes, apiFleet, apiInstalls, apiKits, apiLoops, apiLoopsActive, apiOverview,
|
|
25
25
|
apiRecent, apiSetup, apiThemes, apiUpdate,
|
|
26
26
|
} from './api.mjs';
|
|
27
27
|
import { NotFound, apiCatalog, apiItem } from './catalog.mjs';
|
|
@@ -151,6 +151,7 @@ export const STATE_ROUTES = {
|
|
|
151
151
|
new URL(url || '', 'http://x').searchParams.has('fresh')),
|
|
152
152
|
'/api/installs': apiInstalls,
|
|
153
153
|
'/api/fleet': apiFleet,
|
|
154
|
+
'/api/kits': apiKits,
|
|
154
155
|
'/api/update': apiUpdate,
|
|
155
156
|
'/api/usage': apiUsage,
|
|
156
157
|
'/api/gates': apiGates,
|
package/js/web/server.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { isFile, printOut } from '../lib/fs.mjs';
|
|
|
27
27
|
import { which } from '../lib/paths.mjs';
|
|
28
28
|
import { parseIntStrict } from '../lib/text.mjs';
|
|
29
29
|
import { promptLine } from '../lib/prompt.mjs';
|
|
30
|
-
import { warmDoctor, webState } from './state.mjs';
|
|
30
|
+
import { fallbackView, warmDoctor, webState } from './state.mjs';
|
|
31
31
|
import { clearDaemon, codeStamp, openUrl, readDaemon, restartDaemon, startDaemon, statusDaemon, stopDaemon, writeDaemon } from './daemon.mjs';
|
|
32
32
|
import { makeHandler } from './handler.mjs';
|
|
33
33
|
import { JobManager } from './jobs.mjs';
|
|
@@ -132,6 +132,12 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
const state = webState(theme);
|
|
135
|
+
// The daemon record and the job file stay at the DEFAULT target, captured before the view
|
|
136
|
+
// moves: `geneseed web stop`/`status` find them through `webState(theme).target`, which never
|
|
137
|
+
// moves, and a view that followed a Bob-only install into ~/.bob would hide the record.
|
|
138
|
+
const home = state.target;
|
|
139
|
+
const fallback = fallbackView(home);
|
|
140
|
+
if (fallback) state.selectView(...fallback);
|
|
135
141
|
if (!existsSync(join(state.target, GLOBAL_MANIFEST))) {
|
|
136
142
|
printOut(`[web] no deployed harness at ${state.target}.\n`);
|
|
137
143
|
printOut(' Run `geneseed setup` first — serving anyway (read-only UI).\n');
|
|
@@ -143,7 +149,7 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
143
149
|
const holder = {};
|
|
144
150
|
// The console's job list survives a reload and a restart because it is a FILE on disk,
|
|
145
151
|
// not held in process memory.
|
|
146
|
-
const jm = new JobManager(join(
|
|
152
|
+
const jm = new JobManager(join(home, '.geneseed-web-runs.json'));
|
|
147
153
|
const srv = createServer(makeHandler(state, jm, token, dist, holder));
|
|
148
154
|
holder.srv = srv;
|
|
149
155
|
return new Promise((done) => {
|
|
@@ -157,7 +163,7 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
157
163
|
const hostPort = /** @type {import('node:net').AddressInfo} */ (srv.address()).port;
|
|
158
164
|
const url = `http://127.0.0.1:${hostPort}`;
|
|
159
165
|
if (daemon) {
|
|
160
|
-
writeDaemon(
|
|
166
|
+
writeDaemon(home, {
|
|
161
167
|
pid: process.pid, port: hostPort, url,
|
|
162
168
|
token, theme: state.theme, started: Math.floor(Date.now() / 1000), code: startCode,
|
|
163
169
|
});
|
|
@@ -177,8 +183,8 @@ export async function serve({ theme = null, port = 4747, openBrowser = true,
|
|
|
177
183
|
});
|
|
178
184
|
srv.on('close', () => {
|
|
179
185
|
if (daemon) {
|
|
180
|
-
const st = readDaemon(
|
|
181
|
-
if (!st || st.pid === process.pid) clearDaemon(
|
|
186
|
+
const st = readDaemon(home);
|
|
187
|
+
if (!st || st.pid === process.pid) clearDaemon(home);
|
|
182
188
|
}
|
|
183
189
|
done(0);
|
|
184
190
|
});
|