geneseed 3.1.0 → 3.1.2
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 +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +68 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +94 -69
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +34 -20
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
package/js/web/actions.mjs
CHANGED
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* of magnitude:
|
|
8
8
|
*
|
|
9
9
|
* * `_memory_drop_index` — "check whether P5's `learn` ported it". It did not; nothing in
|
|
10
|
-
* `js/` referenced it. It lands in `js/hooks.mjs`, beside the other memory-store
|
|
10
|
+
* `js/` referenced it. It lands in `js/hosts/hooks.mjs`, beside the other memory-store
|
|
11
11
|
* primitives, because that is where its Python siblings' twins already live.
|
|
12
12
|
* * `_mcp_load` — "already in js/ (P6c)". What crossed in P6c is `readJsonc` and a
|
|
13
13
|
* five-line no-host reader inside `js/web/api.mjs` for `wiki.jsonc`. The host fork and
|
|
14
|
-
* the other twelve MCP functions are 168 lines of new `js/mcp.mjs`.
|
|
14
|
+
* the other twelve MCP functions are 168 lines of new `js/hosts/mcp.mjs`.
|
|
15
15
|
* * `settings.mjs`'s atomic write — a DIFFERENT Python function from `_mcp_save`, with a
|
|
16
|
-
* different temp suffix and no parent-directory creation. See `js/mcp.mjs`.
|
|
16
|
+
* different temp suffix and no parent-directory creation. See `js/hosts/mcp.mjs`.
|
|
17
17
|
* * `api_install_toggle` — "~25 new; `_move_tree`, `_prune_empty_ancestors` and the stash
|
|
18
|
-
* ARE in `js/uninstall.mjs` (P5h)". `DISABLED_STASH`, `installKind` and `installState`
|
|
18
|
+
* ARE in `js/maintain/uninstall.mjs` (P5h)". `DISABLED_STASH`, `installKind` and `installState`
|
|
19
19
|
* are. `_move_tree`, `_prune_empty_ancestors`, `_install_move_list`, `_install_relive`,
|
|
20
20
|
* `_install_readd_entry`, `_install_deactivate`, `_install_reactivate`,
|
|
21
21
|
* `_claude_deactivate` and `_claude_reactivate` are not — 293 lines of unported
|
|
@@ -57,15 +57,13 @@
|
|
|
57
57
|
*
|
|
58
58
|
* `web/dist` therefore does NOT need rebuilding, and it was not rebuilt.
|
|
59
59
|
*
|
|
60
|
-
* WHAT GATES IT, since no cell can. The value never reaches a response, so
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* That is the absolute-assertion-per-side discipline a tolerant comparison owes, applied to
|
|
68
|
-
* a value the byte gate cannot reach.
|
|
60
|
+
* WHAT GATES IT, since no cell can. The value never reaches a response, so a request-level
|
|
61
|
+
* corpus is structurally blind to it — exactly the shape a function-level gate answers and a
|
|
62
|
+
* cell cannot. That gate is `tests/unit/web_jobs.test.mjs`: it asserts the argv HEAD
|
|
63
|
+
* absolutely — `process.execPath` plus `bin/build-driver.mjs`, and no interpreter name
|
|
64
|
+
* anywhere in it — and pins the TAIL, every argument after the head, against a frozen
|
|
65
|
+
* literal. The tail is where a real bug would live, and asserting it absolutely rather than
|
|
66
|
+
* by comparison is what let it outlive the implementation it was once compared against.
|
|
69
67
|
*
|
|
70
68
|
* THE CODE IS P6g's, THE DECISION IS THIS PHASE's. `api_install_cmd`, `api_deploy_cmd` and
|
|
71
69
|
* `api_restore` all sit behind `/api/actions/<x>`, which needs the `JobManager` to route at
|
|
@@ -86,16 +84,17 @@
|
|
|
86
84
|
* behaviour a 501 produces.
|
|
87
85
|
*
|
|
88
86
|
* ---------------------------------------------------------------------------------------
|
|
89
|
-
* `api_install_toggle`
|
|
90
|
-
*
|
|
91
|
-
* The endpoint is 27 lines over three engine calls
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* file-move fixture
|
|
97
|
-
*
|
|
98
|
-
*
|
|
87
|
+
* `api_install_toggle` CROSSED IN P6i, AND THE ENGINE CAME WITH IT.
|
|
88
|
+
*
|
|
89
|
+
* The endpoint is 27 lines over three engine calls, and the reason it waited is worth
|
|
90
|
+
* keeping: two of the three pull in 293 lines of all-or-nothing tree moves with rollback, a
|
|
91
|
+
* stash layout, an `instructions`-entry unmerge, a re-emit-while-disabled guard and a host
|
|
92
|
+
* fork. None of that is web code, and porting it inside a web phase would have put the
|
|
93
|
+
* largest engine block behind the weakest available gate — a request script rather than a
|
|
94
|
+
* file-move fixture. So it landed where it belongs: `installDeactivate` and
|
|
95
|
+
* `installReactivate` live in `js/maintain/uninstall.mjs` beside `installUninstall`, the
|
|
96
|
+
* reversible siblings of the same owned-file walk, and `apiInstallToggle` below is the thin
|
|
97
|
+
* endpoint over them.
|
|
99
98
|
*/
|
|
100
99
|
import {
|
|
101
100
|
accessSync, constants, copyFileSync, mkdirSync, mkdtempSync, rmSync, statSync, unlinkSync,
|
|
@@ -103,29 +102,31 @@ import {
|
|
|
103
102
|
import os from 'node:os';
|
|
104
103
|
import path from 'node:path';
|
|
105
104
|
|
|
106
|
-
import { emitGlobalInto } from '../../bin/
|
|
107
|
-
import {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
import {
|
|
111
|
-
import {
|
|
105
|
+
import { emitGlobalInto } from '../../bin/build-driver.mjs';
|
|
106
|
+
import {
|
|
107
|
+
ROOT, discoverNames, knownRuleIds, PACK_ORDER,
|
|
108
|
+
} from '../build/source.mjs';
|
|
109
|
+
import { withStdoutSwallowed } from '../inspect/diff.mjs';
|
|
110
|
+
import { excludeAdd, excludeRemove } from '../inspect/excludes.mjs';
|
|
111
|
+
import { setupBuildArgs } from '../build/generate.mjs';
|
|
112
|
+
import { frontmatter, memoryDropIndex } from '../hosts/hooks.mjs';
|
|
112
113
|
import {
|
|
113
114
|
HOSTS, bobConfigDir, claudeConfigDir, copilotConfigDir, expanduser, opencodeConfigDir,
|
|
114
|
-
|
|
115
|
-
} from '../hosts.mjs';
|
|
115
|
+
resolvePath,
|
|
116
|
+
} from '../hosts/hosts.mjs';
|
|
116
117
|
import {
|
|
117
|
-
EMIT_HOST_SCOPE, doctrinesForBuild, footprintOfDir, installState,
|
|
118
|
-
postureOfDir, readMaybe,
|
|
119
|
-
} from '../installs.mjs';
|
|
118
|
+
EMIT_HOST_SCOPE, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, installState,
|
|
119
|
+
installTargets, modeOfDir, postureOfDir, readMaybe,
|
|
120
|
+
} from '../hosts/installs.mjs';
|
|
120
121
|
import {
|
|
121
122
|
MCP_PRESETS, isDict, mcpApply, mcpCommented, mcpInstallTargets, mcpKnownNames, mcpLoad,
|
|
122
123
|
mcpMeta, mcpPresetBlock, mcpSave, mcpSetEnabled, mcpState,
|
|
123
|
-
} from '../mcp.mjs';
|
|
124
|
-
import {
|
|
125
|
-
|
|
126
|
-
} from '../lib/
|
|
127
|
-
import { installDeactivate, installReactivate, installUninstall } from '../uninstall.mjs';
|
|
128
|
-
import {
|
|
124
|
+
} from '../hosts/mcp.mjs';
|
|
125
|
+
import { readText, writeText } from '../lib/fs.mjs';
|
|
126
|
+
import { parseJson, formatRepr, formatValue, isTruthy } from '../lib/json.mjs';
|
|
127
|
+
import { WHITESPACE, codePointLength, stripWhitespace } from '../lib/text.mjs';
|
|
128
|
+
import { installDeactivate, installReactivate, installUninstall } from '../maintain/uninstall.mjs';
|
|
129
|
+
import { splitLines } from '../lib/udiff.mjs';
|
|
129
130
|
import {
|
|
130
131
|
NotFound, deployed, emitChoices, fingerprint, themeChoices, viewCfg, within,
|
|
131
132
|
} from './api.mjs';
|
|
@@ -138,7 +139,7 @@ const bget = (body, key, dflt = null) => (isDict(body) && Object.hasOwn(body, ke
|
|
|
138
139
|
? body[key] : dflt);
|
|
139
140
|
|
|
140
141
|
/** `str(x or "")` — Python's truthiness, then Python's `str`. */
|
|
141
|
-
const strOr = (v) => (
|
|
142
|
+
const strOr = (v) => (isTruthy(v) ? formatValue(v) : '');
|
|
142
143
|
|
|
143
144
|
/**
|
|
144
145
|
* A request body's `doctrines` -> a normalised pack list, or `null` for "said nothing usable".
|
|
@@ -170,8 +171,34 @@ function bodyDoctrines(body) {
|
|
|
170
171
|
return PACK_ORDER.filter((p) => names.includes(p));
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* The SECOND doctrine axis across the same trust boundary — which individual rules to drop.
|
|
176
|
+
*
|
|
177
|
+
* Same shape and same reasons as `bodyDoctrines` above: a list closed member by member
|
|
178
|
+
* against what this checkout actually ships (`knownRuleIds`, the one enumerator the CLI flag
|
|
179
|
+
* uses too), re-ordered canonically because the `Excluded rules:` line it produces is a
|
|
180
|
+
* marker a later reader parses back out, and `null` on any rejection so the caller decides
|
|
181
|
+
* what "unspecified" means.
|
|
182
|
+
*
|
|
183
|
+
* Accepts `process 7` or `process.7` — the console sends the dotted address, and a curl by
|
|
184
|
+
* hand naturally writes the spelling the marker shows. `none`/`[]` is the deliberate empty
|
|
185
|
+
* selection: "exclude nothing", which is a real answer and NOT the same as not saying.
|
|
186
|
+
*/
|
|
187
|
+
function bodyExcludeRules(body) {
|
|
188
|
+
const raw = bget(body, 'excludeRules');
|
|
189
|
+
if (Array.isArray(raw) && !raw.length) return [];
|
|
190
|
+
const names = Array.isArray(raw) ? raw
|
|
191
|
+
: (typeof raw === 'string' ? raw.split(',').map((s) => s.trim()).filter(Boolean) : null);
|
|
192
|
+
if (names === null || !names.length) return null;
|
|
193
|
+
if (names.length === 1 && names[0] === 'none') return [];
|
|
194
|
+
const known = knownRuleIds();
|
|
195
|
+
const ids = names.map((n) => (typeof n === 'string' ? n.trim().replace(/[ \t]+/, '.') : n));
|
|
196
|
+
if (!ids.every((n) => typeof n === 'string' && known.includes(n))) return null;
|
|
197
|
+
return known.filter((id) => ids.includes(id));
|
|
198
|
+
}
|
|
199
|
+
|
|
173
200
|
/** `str.split()` with no argument — runs of PYTHON whitespace, no empties. */
|
|
174
|
-
const
|
|
201
|
+
const splitWords = (s) => s.split(new RegExp(`[${WHITESPACE}]+`)).filter(Boolean);
|
|
175
202
|
|
|
176
203
|
/** `datetime.date.today().isoformat()` — LOCAL, as the reference's naive `date.today()` is. */
|
|
177
204
|
export function todayIso(d = new Date()) {
|
|
@@ -213,7 +240,7 @@ const rulesPath = (state) => path.join(state.target, RULES_FILE);
|
|
|
213
240
|
* `_web_catalog.RULE_HEAD_RE` — `## R<n> — Title`, anchored at column 0 so a rule body's
|
|
214
241
|
* fenced code and the stub's indented format example never parse as rules.
|
|
215
242
|
*
|
|
216
|
-
* The whitespace classes are `
|
|
243
|
+
* The whitespace classes are `WHITESPACE`, not `\s`: P6d measured the two apart and this
|
|
217
244
|
* pattern runs over a HAND-EDITED file, which is where a non-breaking space actually turns
|
|
218
245
|
* up. `\S` is its complement for the same reason.
|
|
219
246
|
*
|
|
@@ -225,26 +252,26 @@ const rulesPath = (state) => path.join(state.target, RULES_FILE);
|
|
|
225
252
|
* and the web editor cannot produce (it writes `R<n>` itself from `max(ids) + 1`).
|
|
226
253
|
*/
|
|
227
254
|
export const RULE_HEAD_RE = new RegExp(
|
|
228
|
-
`^##[${
|
|
229
|
-
+ `([^${
|
|
255
|
+
`^##[${WHITESPACE}]+R(\\d+)[${WHITESPACE}]*[—–-]+[${WHITESPACE}]*`
|
|
256
|
+
+ `([^${WHITESPACE}].*?)[${WHITESPACE}]*$`,
|
|
230
257
|
);
|
|
231
258
|
|
|
232
259
|
/** `_web_catalog.RULES_BUDGET` — advisory; nothing blocks past it. */
|
|
233
260
|
export const RULES_BUDGET = { max_rules: 15, max_tokens: 1500 };
|
|
234
261
|
|
|
235
|
-
const META_RE = new RegExp(`^\\((.+)\\)[${
|
|
262
|
+
const META_RE = new RegExp(`^\\((.+)\\)[${WHITESPACE}]*$`);
|
|
236
263
|
|
|
237
264
|
/** `_web_catalog._parse_rule_meta` — `{}` when the line is body rather than metadata. */
|
|
238
265
|
function parseRuleMeta(line) {
|
|
239
|
-
const m = META_RE.exec(
|
|
266
|
+
const m = META_RE.exec(stripWhitespace(line));
|
|
240
267
|
if (!m) return {};
|
|
241
268
|
const meta = {};
|
|
242
269
|
for (const part of m[1].split('|')) {
|
|
243
270
|
const at = part.indexOf(':');
|
|
244
271
|
if (at < 0) continue; // `partition` with no separator — not a pair
|
|
245
|
-
const key =
|
|
272
|
+
const key = stripWhitespace(part.slice(0, at)).toLowerCase().replaceAll(' ', '_');
|
|
246
273
|
if (['scope', 'source', 'trial_until'].includes(key)) {
|
|
247
|
-
meta[key] =
|
|
274
|
+
meta[key] = stripWhitespace(part.slice(at + 1));
|
|
248
275
|
}
|
|
249
276
|
}
|
|
250
277
|
return meta;
|
|
@@ -258,7 +285,7 @@ function parseRuleMeta(line) {
|
|
|
258
285
|
* formatting, hand-written sections — untouched.
|
|
259
286
|
*/
|
|
260
287
|
export function parseRules(text) {
|
|
261
|
-
const lines =
|
|
288
|
+
const lines = splitLines(text);
|
|
262
289
|
const rules = [];
|
|
263
290
|
const warnings = [];
|
|
264
291
|
const seen = new Set();
|
|
@@ -273,7 +300,7 @@ export function parseRules(text) {
|
|
|
273
300
|
if (seen.has(rid)) warnings.push(`duplicate rule id R${rid}`);
|
|
274
301
|
seen.add(rid);
|
|
275
302
|
let k = idx + 1;
|
|
276
|
-
while (k < end && !
|
|
303
|
+
while (k < end && !stripWhitespace(lines[k])) k += 1;
|
|
277
304
|
const meta = k < end ? parseRuleMeta(lines[k]) : {};
|
|
278
305
|
if (Object.keys(meta).length) k += 1;
|
|
279
306
|
rules.push({
|
|
@@ -282,7 +309,7 @@ export function parseRules(text) {
|
|
|
282
309
|
scope: Object.hasOwn(meta, 'scope') ? meta.scope : 'project',
|
|
283
310
|
source: Object.hasOwn(meta, 'source') ? meta.source : '',
|
|
284
311
|
trial_until: Object.hasOwn(meta, 'trial_until') ? meta.trial_until : '',
|
|
285
|
-
body:
|
|
312
|
+
body: stripWhitespace(lines.slice(k, end).join('\n')),
|
|
286
313
|
start: idx,
|
|
287
314
|
end,
|
|
288
315
|
});
|
|
@@ -310,8 +337,8 @@ export function apiRules(state) {
|
|
|
310
337
|
return { exists: true, path: p, rules: out, warnings, fingerprint: fingerprint(text),
|
|
311
338
|
// `len(text) // 4` — CODE POINTS. `String.length` counts UTF-16 units, so an emoji in a
|
|
312
339
|
// rule body would put the two token estimates two apart.
|
|
313
|
-
stats: { rules: rules.length, lines:
|
|
314
|
-
tokens: Math.floor(
|
|
340
|
+
stats: { rules: rules.length, lines: splitLines(text).length,
|
|
341
|
+
tokens: Math.floor(codePointLength(text) / 4), ...RULES_BUDGET } };
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
/** `_web_actions._rules_read`. */
|
|
@@ -325,7 +352,7 @@ function ruleBlock(rid, title, scope, source, trialUntil, body) {
|
|
|
325
352
|
const meta = [`scope: ${scope}`];
|
|
326
353
|
if (source) meta.push(`source: ${source}`);
|
|
327
354
|
if (trialUntil) meta.push(`trial until: ${trialUntil}`);
|
|
328
|
-
return `## R${rid} — ${title}\n(${meta.join(' | ')})\n${
|
|
355
|
+
return `## R${rid} — ${title}\n(${meta.join(' | ')})\n${stripWhitespace(body)}\n`;
|
|
329
356
|
}
|
|
330
357
|
|
|
331
358
|
/**
|
|
@@ -337,14 +364,14 @@ function ruleBlock(rid, title, scope, source, trialUntil, body) {
|
|
|
337
364
|
* `int()`.
|
|
338
365
|
*/
|
|
339
366
|
function ruleFields(body) {
|
|
340
|
-
const title =
|
|
341
|
-
const text =
|
|
367
|
+
const title = splitWords(strOr(bget(body, 'title'))).join(' ');
|
|
368
|
+
const text = stripWhitespace(strOr(bget(body, 'body')));
|
|
342
369
|
if (!title) throw new Error('a rule needs a title');
|
|
343
370
|
if (!text) throw new Error('a rule needs body text');
|
|
344
371
|
let scope = bget(body, 'scope');
|
|
345
372
|
if (scope !== 'user' && scope !== 'project') scope = 'project';
|
|
346
|
-
const source =
|
|
347
|
-
const trial =
|
|
373
|
+
const source = splitWords(strOr(bget(body, 'source'))).join(' ');
|
|
374
|
+
const trial = stripWhitespace(strOr(bget(body, 'trial_until')));
|
|
348
375
|
if (trial && !/^\p{Nd}{4}-\p{Nd}{2}-\p{Nd}{2}$/u.test(trial)) {
|
|
349
376
|
throw new Error('trial until must be YYYY-MM-DD');
|
|
350
377
|
}
|
|
@@ -360,7 +387,7 @@ function ruleFields(body) {
|
|
|
360
387
|
*/
|
|
361
388
|
function ruleId(v) {
|
|
362
389
|
if (typeof v === 'string') {
|
|
363
|
-
const s =
|
|
390
|
+
const s = stripWhitespace(v);
|
|
364
391
|
if (!/^[+-]?\d+$/.test(s)) throw new NotFound('rule id');
|
|
365
392
|
return Number(s);
|
|
366
393
|
}
|
|
@@ -384,7 +411,7 @@ function ruleId(v) {
|
|
|
384
411
|
export function apiRulesMutate(state, body) {
|
|
385
412
|
const op = bget(body, 'op');
|
|
386
413
|
if (op !== 'add' && op !== 'update' && op !== 'delete') {
|
|
387
|
-
throw new NotFound(`rules op ${
|
|
414
|
+
throw new NotFound(`rules op ${formatRepr(op)}`);
|
|
388
415
|
}
|
|
389
416
|
const [p, original] = rulesRead(state);
|
|
390
417
|
let text = original;
|
|
@@ -404,17 +431,17 @@ export function apiRulesMutate(state, body) {
|
|
|
404
431
|
rid = ruleId(bget(body, 'id'));
|
|
405
432
|
const target = rules.find((r) => r.id === rid);
|
|
406
433
|
if (target === undefined) throw new NotFound(`rule R${rid}`);
|
|
407
|
-
const lines =
|
|
434
|
+
const lines = splitLines(text);
|
|
408
435
|
if (op === 'update') {
|
|
409
436
|
const [title, rtext, scope, source, trial] = ruleFields(body);
|
|
410
437
|
const block = ruleBlock(rid, title, scope, source, trial, rtext);
|
|
411
438
|
lines.splice(target.start, target.end - target.start,
|
|
412
|
-
...
|
|
439
|
+
...splitLines(block.replace(/\n+$/, '')));
|
|
413
440
|
} else {
|
|
414
441
|
// delete — also swallow ONE preceding blank separator line, or repeated deletes leave
|
|
415
442
|
// a growing run of blank lines in the user's file.
|
|
416
443
|
let start = target.start;
|
|
417
|
-
if (start > 0 && !
|
|
444
|
+
if (start > 0 && !stripWhitespace(lines[start - 1])) start -= 1;
|
|
418
445
|
lines.splice(start, target.end - start);
|
|
419
446
|
}
|
|
420
447
|
next = `${lines.join('\n').replace(/\n+$/, '')}\n`;
|
|
@@ -447,14 +474,14 @@ export function apiRulesPromote(state, body) {
|
|
|
447
474
|
const res = apiRulesMutate(state, {
|
|
448
475
|
op: 'add',
|
|
449
476
|
fingerprint: bget(body, 'fingerprint', ''),
|
|
450
|
-
title:
|
|
451
|
-
body:
|
|
452
|
-
: (
|
|
477
|
+
title: isTruthy(bget(body, 'title')) ? bget(body, 'title') : fmName,
|
|
478
|
+
body: isTruthy(bget(body, 'body')) ? bget(body, 'body')
|
|
479
|
+
: (stripWhitespace(memBody) || fmDesc),
|
|
453
480
|
scope: bget(body, 'scope'),
|
|
454
481
|
source: `memory ${name}, promoted ${today}`,
|
|
455
482
|
trial_until: trial,
|
|
456
483
|
});
|
|
457
|
-
if (
|
|
484
|
+
if (isTruthy(res.ok) && isTruthy(bget(body, 'delete_memory'))) {
|
|
458
485
|
apiMemoryDelete(state, name);
|
|
459
486
|
res.deleted_memory = name;
|
|
460
487
|
}
|
|
@@ -520,7 +547,7 @@ export function apiMemoryDelete(state, name) {
|
|
|
520
547
|
*/
|
|
521
548
|
export function apiExcludesMutate(state, body) {
|
|
522
549
|
const action = bget(body, 'action');
|
|
523
|
-
const p =
|
|
550
|
+
const p = stripWhitespace(strOr(bget(body, 'path')));
|
|
524
551
|
if ((action !== 'add' && action !== 'remove') || !p) {
|
|
525
552
|
return { ok: false, path: p,
|
|
526
553
|
messages: ['body must be {action: add|remove, path: <folder>}'] };
|
|
@@ -585,7 +612,7 @@ export function mcpTargetPaths() {
|
|
|
585
612
|
|
|
586
613
|
export function apiMcpToggle(state, body) {
|
|
587
614
|
const name = strOr(bget(body, 'name'));
|
|
588
|
-
const want =
|
|
615
|
+
const want = isTruthy(bget(body, 'enabled'));
|
|
589
616
|
const pathArg = strOr(bget(body, 'path'));
|
|
590
617
|
const known = mcpTargetPaths();
|
|
591
618
|
const hit = known.get(pathArg);
|
|
@@ -680,7 +707,7 @@ export function buildOverride(state, body) {
|
|
|
680
707
|
* `.geneseed-emit`, so the EXPECTED render uses the install's own host dialect.
|
|
681
708
|
*
|
|
682
709
|
* Returns the HOST rather than a function, because `emitGlobalInto(host, …)` is how the twin
|
|
683
|
-
* of `build.HOSTS[host]["emit_global"]` is already spelt (`js/diff.mjs` calls it that way).
|
|
710
|
+
* of `build.HOSTS[host]["emit_global"]` is already spelt (`js/inspect/diff.mjs` calls it that way).
|
|
684
711
|
* An unknown or missing marker falls back to OpenCode, exactly as the reference's two
|
|
685
712
|
* `.get(..., default)` calls do.
|
|
686
713
|
*/
|
|
@@ -721,10 +748,10 @@ export function apiRestore(state, files) {
|
|
|
721
748
|
const restored = [];
|
|
722
749
|
const deleted = [];
|
|
723
750
|
const errors = [];
|
|
724
|
-
const target =
|
|
751
|
+
const target = resolvePath(state.target);
|
|
725
752
|
const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-restore-'));
|
|
726
753
|
try {
|
|
727
|
-
const expected =
|
|
754
|
+
const expected = resolvePath(path.join(tmp, 'expected'));
|
|
728
755
|
withStdoutSwallowed(() => emitGlobalInto(globalEmitHostFor(state.emit), {
|
|
729
756
|
theme: state.theme,
|
|
730
757
|
out: path.join(tmp, 'bundle'),
|
|
@@ -740,10 +767,10 @@ export function apiRestore(state, files) {
|
|
|
740
767
|
// read-side hole `diffCollect` had (and the reason the panel offered the file at all).
|
|
741
768
|
doctrines: doctrinesForBuild(target),
|
|
742
769
|
}));
|
|
743
|
-
for (const raw of (
|
|
744
|
-
const rel =
|
|
745
|
-
const dst =
|
|
746
|
-
const src =
|
|
770
|
+
for (const raw of (isTruthy(files) ? files : [])) {
|
|
771
|
+
const rel = stripWhitespace(formatValue(raw).replace(/\\/g, '/')).replace(/^\/+/, '');
|
|
772
|
+
const dst = resolvePath(path.join(target, rel));
|
|
773
|
+
const src = resolvePath(path.join(expected, rel));
|
|
747
774
|
if (!rel || !within(dst, target) || !within(src, expected)) {
|
|
748
775
|
errors.push(`${rel}: outside the deployed tree`);
|
|
749
776
|
} else if (isFile(src)) {
|
|
@@ -789,7 +816,7 @@ const EMIT_FOR = new Map([
|
|
|
789
816
|
* `{cmd: [...]}` IS A RETURN TYPE, NOT A RESPONSE. `_post_routes` hands it straight to the job
|
|
790
817
|
* runner in the same process; it never reaches the wire. That is what collapsed P6f's
|
|
791
818
|
* "argv-in-a-response-body" question — see this file's header — and why the twin's head is
|
|
792
|
-
* `process.execPath` + `bin/
|
|
819
|
+
* `process.execPath` + `bin/build-driver.mjs` rather than a Python argv it would then have to
|
|
793
820
|
* pretend to run.
|
|
794
821
|
*/
|
|
795
822
|
export function apiInstallCmd(state, body) {
|
|
@@ -825,9 +852,15 @@ export function apiInstallCmd(state, body) {
|
|
|
825
852
|
// `null` elided the flag, and the generator's config fallback then narrowed the install and
|
|
826
853
|
// took its consent gate with it. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
827
854
|
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
855
|
+
// The per-rule axis needs NO `doctrinesForBuild`-style rescue: the marker it reads is only
|
|
856
|
+
// written when something is excluded, so its absence is a statement ("nothing") rather than
|
|
857
|
+
// the silence a pre-2.3 carrier gives on packs. Unspecified therefore keeps the install's
|
|
858
|
+
// own answer, and a pre-marker install answers the empty list — which is what it has.
|
|
859
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
828
860
|
const out = scope === 'global' ? null : String(root);
|
|
829
|
-
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines
|
|
830
|
-
|
|
861
|
+
const argv = setupBuildArgs(theme || 'neutral', emit, out, out, fp, pos, mode, doctrines,
|
|
862
|
+
PACK_ORDER, excludeRules);
|
|
863
|
+
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
831
864
|
}
|
|
832
865
|
|
|
833
866
|
/**
|
|
@@ -848,7 +881,7 @@ export function apiInstallCmd(state, body) {
|
|
|
848
881
|
* and this endpoint moves and deletes whole trees, so the root is never built from raw body
|
|
849
882
|
* input. An unknown pair is a `NotFound`, which the shell answers 404.
|
|
850
883
|
*
|
|
851
|
-
* THE ENGINE IS `js/uninstall.mjs`'s, and P6i is what brought it. `installDeactivate` /
|
|
884
|
+
* THE ENGINE IS `js/maintain/uninstall.mjs`'s, and P6i is what brought it. `installDeactivate` /
|
|
852
885
|
* `installReactivate` are the reversible siblings of `installUninstall` — the same owned-file
|
|
853
886
|
* walk and ancestor prune with `move` where the reversal has `unlink` — which is why they live
|
|
854
887
|
* beside it rather than in the web tree. `remove` is `installUninstall`, which crossed in P5h
|
|
@@ -875,26 +908,26 @@ export function apiInstallToggle(state, body) {
|
|
|
875
908
|
// is validated IN THE ENGINE — an unknown value falls back to `keep`, never a surprise
|
|
876
909
|
// delete, which is why a bogus body value is passed through rather than rejected here.
|
|
877
910
|
const mem = bget(body, 'memory');
|
|
878
|
-
res = installUninstall(root, host, scope,
|
|
911
|
+
res = installUninstall(root, host, scope, isTruthy(mem) ? mem : 'keep');
|
|
879
912
|
} else {
|
|
880
913
|
// `{action!r}` — Python's repr, SINGLE-quoted. `JSON.stringify` would double-quote it
|
|
881
914
|
// inside a JSON string body and the two sides would differ on every typo.
|
|
882
|
-
res = { ok: false, error: `unknown action ${
|
|
915
|
+
res = { ok: false, error: `unknown action ${formatRepr(action)}` };
|
|
883
916
|
}
|
|
884
917
|
state.refresh();
|
|
885
918
|
return res;
|
|
886
919
|
}
|
|
887
920
|
|
|
888
921
|
export function apiDeployCmd(state, body) {
|
|
889
|
-
const host =
|
|
922
|
+
const host = stripWhitespace(strOr(bget(body, 'host')));
|
|
890
923
|
if (!HOSTS.some((h) => h.host === host)) {
|
|
891
924
|
return { error: `unknown host: ${host || '(none)'}` };
|
|
892
925
|
}
|
|
893
|
-
const raw =
|
|
926
|
+
const raw = stripWhitespace(strOr(bget(body, 'path')));
|
|
894
927
|
if (!raw) return { error: 'no folder given' };
|
|
895
928
|
let root;
|
|
896
929
|
try {
|
|
897
|
-
root =
|
|
930
|
+
root = resolvePath(expanduser(raw));
|
|
898
931
|
} catch {
|
|
899
932
|
return { error: `bad path: ${raw}` };
|
|
900
933
|
}
|
|
@@ -907,7 +940,7 @@ export function apiDeployCmd(state, body) {
|
|
|
907
940
|
const cfgdirs = new Set();
|
|
908
941
|
for (const fn of [opencodeConfigDir, claudeConfigDir, bobConfigDir, copilotConfigDir]) {
|
|
909
942
|
try {
|
|
910
|
-
cfgdirs.add(
|
|
943
|
+
cfgdirs.add(resolvePath(fn()));
|
|
911
944
|
} catch { /* the reference's bare `except: pass` */ }
|
|
912
945
|
}
|
|
913
946
|
if (cfgdirs.has(root)) {
|
|
@@ -929,7 +962,9 @@ export function apiDeployCmd(state, body) {
|
|
|
929
962
|
// existing all-four Claude install dropped it to one pack — measured: 6 hook groups became 5
|
|
930
963
|
// and `PreToolUse::Bash` went with them. `doctrinesForBuild` resolves unknown to ALL packs.
|
|
931
964
|
const doctrines = bodyDoctrines(body) ?? doctrinesForBuild(root);
|
|
965
|
+
const excludeRules = bodyExcludeRules(body) ?? excludedRulesOfDir(root);
|
|
932
966
|
// project-scope emit name == host name (opencode / claude / bob / copilot)
|
|
933
|
-
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines
|
|
934
|
-
|
|
967
|
+
const argv = setupBuildArgs(theme || 'neutral', host, root, root, fp, pos, mode, doctrines,
|
|
968
|
+
PACK_ORDER, excludeRules);
|
|
969
|
+
return { cmd: [process.execPath, path.join(ROOT, 'bin', 'build-driver.mjs'), ...argv] };
|
|
935
970
|
}
|
package/js/web/activity.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The live-activity surface — the one web endpoint that
|
|
3
3
|
* DELETES.
|
|
4
4
|
*
|
|
5
5
|
* It reads the per-session JSON files the `geneseed-activity` OpenCode plugin writes into
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* directory self-cleans), and returns what is left. Writer and reader share only that
|
|
8
8
|
* directory: no RPC, crash-isolated, and cross-tool by construction.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* BOTH VERBS CROSS. `/api/activity` answers a GET through `apiActivity` and a POST through
|
|
11
|
+
* `apiActivityToggle` at the bottom of this file; `NOT_PORTED_POST` is empty and still
|
|
12
|
+
* declared. It is also the one POST route whose result is sent at 200 whatever `ok` says,
|
|
13
|
+
* which is why `POST_ROUTES` in `js/web/routes.mjs` carries a 409 COLUMN rather than a rule —
|
|
14
|
+
* and giving this row the 409 treatment is mutation M22.
|
|
15
15
|
*
|
|
16
16
|
* THE PLAN GUESSED `tasklist` AND THE SOURCE SAYS OTHERWISE. `_win_pid_alive` shells
|
|
17
17
|
* nothing: it goes through `ctypes` to `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` and
|
|
@@ -33,16 +33,17 @@
|
|
|
33
33
|
* a third party and carry `cost` — a float. Python's `json.loads` types `1.0` a float and
|
|
34
34
|
* `json.dumps` writes it back `1.0`; `JSON.parse` gives a bare `1` that `bareInts` then
|
|
35
35
|
* renders `1`. `parseJson`'s wrapper is the only thing that survives the round trip, and
|
|
36
|
-
* `s-live`'s `"cost": 1.0` is the cell that says so. Its companion is `
|
|
36
|
+
* `s-live`'s `"cost": 1.0` is the cell that says so. Its companion is `isTruthy`: an
|
|
37
37
|
* `entry.get("cost") or 0` over a wrapper object is TRUE for a wrapped zero, where Python's
|
|
38
38
|
* `0.0 or 0` is the int `0` — so `s-older`'s `"cost": 0.0` must come back `0` and not `0.0`.
|
|
39
39
|
*/
|
|
40
40
|
import { mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
} from '../lib/
|
|
43
|
+
import { readText, writeText } from '../lib/fs.mjs';
|
|
44
|
+
import { parseJson, isTruthy } from '../lib/json.mjs';
|
|
45
|
+
import { normcase } from '../lib/paths.mjs';
|
|
46
|
+
import { parseIntStrict, stripWhitespace } from '../lib/text.mjs';
|
|
46
47
|
import { NotFound } from './api.mjs';
|
|
47
48
|
|
|
48
49
|
/**
|
|
@@ -86,7 +87,7 @@ export function activityEnabled(state) {
|
|
|
86
87
|
} catch {
|
|
87
88
|
return true;
|
|
88
89
|
}
|
|
89
|
-
return !['off', '0', 'false', 'no'].includes(
|
|
90
|
+
return !['off', '0', 'false', 'no'].includes(stripWhitespace(raw).toLowerCase());
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
/**
|
|
@@ -99,10 +100,10 @@ export function activityEnabled(state) {
|
|
|
99
100
|
* what `int({})` and `int([])` raising looks like from here. `int(1.9)` truncates and
|
|
100
101
|
* `int(True)` is 1, both of which a hand-edited snapshot can produce.
|
|
101
102
|
*/
|
|
102
|
-
function
|
|
103
|
+
function toIntOr(raw) {
|
|
103
104
|
if (raw === true) return 1;
|
|
104
105
|
if (raw === false) return 0;
|
|
105
|
-
if (typeof raw === 'string') return
|
|
106
|
+
if (typeof raw === 'string') return parseIntStrict(stripWhitespace(raw));
|
|
106
107
|
const n = (raw !== null && typeof raw === 'object' && !Array.isArray(raw))
|
|
107
108
|
? raw.valueOf() : raw;
|
|
108
109
|
if (typeof n !== 'number' || !Number.isFinite(n)) return null;
|
|
@@ -111,7 +112,7 @@ function pyIntOf(raw) {
|
|
|
111
112
|
|
|
112
113
|
/** `_pid_alive` — best-effort liveness for the writer process. See this file's header. */
|
|
113
114
|
export function pidAlive(raw) {
|
|
114
|
-
const pid =
|
|
115
|
+
const pid = toIntOr(raw);
|
|
115
116
|
if (pid === null || pid <= 0) return false;
|
|
116
117
|
try {
|
|
117
118
|
process.kill(pid, 0);
|
|
@@ -131,7 +132,7 @@ export function pidAlive(raw) {
|
|
|
131
132
|
*/
|
|
132
133
|
function normalizeEntry(entry, stem) {
|
|
133
134
|
const g = (k) => (Object.hasOwn(entry, k) ? entry[k] : null);
|
|
134
|
-
const or = (k, dflt) => (
|
|
135
|
+
const or = (k, dflt) => (isTruthy(g(k)) ? g(k) : dflt);
|
|
135
136
|
return {
|
|
136
137
|
session_id: or('session_id', stem),
|
|
137
138
|
agent: g('agent'),
|
|
@@ -164,7 +165,7 @@ function readEntry(p) {
|
|
|
164
165
|
|
|
165
166
|
/** `_is_live` — the writer's pid is alive AND the snapshot is not stale. */
|
|
166
167
|
function isLive(entry, now) {
|
|
167
|
-
const updated =
|
|
168
|
+
const updated = isTruthy(Object.hasOwn(entry, 'updated_at') ? entry.updated_at : null)
|
|
168
169
|
? entry.updated_at : 0;
|
|
169
170
|
return (now - Number(updated)) <= ACTIVITY_STALE_SECONDS && pidAlive(
|
|
170
171
|
Object.hasOwn(entry, 'pid') ? entry.pid : null);
|
|
@@ -236,7 +237,7 @@ export function apiActivityDetail(state, sid) {
|
|
|
236
237
|
let timeline = [];
|
|
237
238
|
let full = {};
|
|
238
239
|
const det = readEntry(path.join(d, `${stem}.detail.json`));
|
|
239
|
-
if (
|
|
240
|
+
if (isTruthy(det)) {
|
|
240
241
|
const tl = Object.hasOwn(det, 'timeline') ? det.timeline : null;
|
|
241
242
|
timeline = Array.isArray(tl) ? tl : [];
|
|
242
243
|
full = det;
|
|
@@ -245,14 +246,14 @@ export function apiActivityDetail(state, sid) {
|
|
|
245
246
|
// The detail file carries the uncapped lists; fall back to the snapshot's capped ones.
|
|
246
247
|
// Assigning over an existing key keeps its POSITION in both languages, which is what lets
|
|
247
248
|
// the response stay byte-comparable.
|
|
248
|
-
session.files =
|
|
249
|
-
session.todos =
|
|
249
|
+
session.files = isTruthy(fg('files')) ? fg('files') : session.files;
|
|
250
|
+
session.todos = isTruthy(fg('todos')) ? fg('todos') : session.todos;
|
|
250
251
|
// The compact transcript: an ordered list of {role, text, t} turns. Empty falls back to
|
|
251
252
|
// the title as the opening user turn, because OpenCode derives the title from the first
|
|
252
253
|
// prompt.
|
|
253
254
|
const conv = fg('conversation');
|
|
254
255
|
let conversation = Array.isArray(conv) ? conv : [];
|
|
255
|
-
if (conversation.length === 0 &&
|
|
256
|
+
if (conversation.length === 0 && isTruthy(session.title)) {
|
|
256
257
|
conversation = [{ role: 'user', text: session.title }];
|
|
257
258
|
}
|
|
258
259
|
return { session, timeline, conversation };
|
|
@@ -272,7 +273,7 @@ export function apiActivityDetail(state, sid) {
|
|
|
272
273
|
*/
|
|
273
274
|
export function apiActivityToggle(state, body) {
|
|
274
275
|
const raw = (body && Object.hasOwn(body, 'enabled')) ? body.enabled : true;
|
|
275
|
-
const enabled =
|
|
276
|
+
const enabled = isTruthy(raw);
|
|
276
277
|
const p = activityFlag(state);
|
|
277
278
|
try {
|
|
278
279
|
mkdirSync(path.dirname(p), { recursive: true });
|