@krtclcdy/tersio 2.25.6 → 2.25.7
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/README.md +30 -7
- package/dist/bench/code-out/pfull-0/debounce.js +13 -0
- package/dist/bench/code-out/pfull-0/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-0/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-1/debounce.js +13 -0
- package/dist/bench/code-out/pfull-1/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-1/groupBy.js +14 -0
- package/dist/bench/code-out/pfull-2/debounce.js +13 -0
- package/dist/bench/code-out/pfull-2/deepClone.js +18 -0
- package/dist/bench/code-out/pfull-2/groupBy.js +15 -0
- package/dist/bench/code-out/pfull-3/debounce.js +12 -0
- package/dist/bench/code-out/pfull-3/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-3/groupBy.js +7 -0
- package/dist/bench/code-out/pfull-4/debounce.js +10 -0
- package/dist/bench/code-out/pfull-4/deepClone.js +17 -0
- package/dist/bench/code-out/pfull-4/groupBy.js +14 -0
- package/dist/bench/code-out/plite-0/debounce.js +13 -0
- package/dist/bench/code-out/plite-0/deepClone.js +18 -0
- package/dist/bench/code-out/plite-0/groupBy.js +15 -0
- package/dist/bench/code-out/plite-1/debounce.js +13 -0
- package/dist/bench/code-out/plite-1/deepClone.js +17 -0
- package/dist/bench/code-out/plite-1/groupBy.js +11 -0
- package/dist/bench/code-out/plite-2/debounce.js +12 -0
- package/dist/bench/code-out/plite-2/deepClone.js +17 -0
- package/dist/bench/code-out/plite-2/groupBy.js +14 -0
- package/dist/bench/code-out/plite-3/debounce.js +12 -0
- package/dist/bench/code-out/plite-3/deepClone.js +17 -0
- package/dist/bench/code-out/plite-3/groupBy.js +14 -0
- package/dist/bench/code-out/plite-4/debounce.js +12 -0
- package/dist/bench/code-out/plite-4/deepClone.js +17 -0
- package/dist/bench/code-out/plite-4/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-0/debounce.js +12 -0
- package/dist/bench/code-out/pultra-0/deepClone.js +20 -0
- package/dist/bench/code-out/pultra-0/groupBy.js +14 -0
- package/dist/bench/code-out/pultra-1/debounce.js +10 -0
- package/dist/bench/code-out/pultra-1/deepClone.js +17 -0
- package/dist/bench/code-out/pultra-1/groupBy.js +7 -0
- package/dist/bench/code-out/pultra-2/debounce.js +13 -0
- package/dist/bench/code-out/pultra-2/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-2/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-3/debounce.js +13 -0
- package/dist/bench/code-out/pultra-3/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-3/groupBy.js +15 -0
- package/dist/bench/code-out/pultra-4/debounce.js +13 -0
- package/dist/bench/code-out/pultra-4/deepClone.js +18 -0
- package/dist/bench/code-out/pultra-4/groupBy.js +15 -0
- package/dist/bench/code-out/run-0/debounce.js +12 -0
- package/dist/bench/code-out/run-0/deepClone.js +17 -0
- package/dist/bench/code-out/run-0/groupBy.js +14 -0
- package/dist/bench/code-out/run-1/debounce.js +13 -0
- package/dist/bench/code-out/run-1/deepClone.js +15 -0
- package/dist/bench/code-out/run-1/groupBy.js +13 -0
- package/dist/bench/code-out/run-2/debounce.js +12 -0
- package/dist/bench/code-out/run-2/deepClone.js +18 -0
- package/dist/bench/code-out/run-2/groupBy.js +13 -0
- package/dist/bench/code-out/run-3/debounce.js +12 -0
- package/dist/bench/code-out/run-3/deepClone.js +18 -0
- package/dist/bench/code-out/run-3/groupBy.js +14 -0
- package/dist/bench/code-out/run-4/debounce.js +12 -0
- package/dist/bench/code-out/run-4/deepClone.js +16 -0
- package/dist/bench/code-out/run-4/groupBy.js +13 -0
- package/dist/cli/banner.js +1 -2
- package/dist/cli/common.js +1 -1
- package/dist/cli/doctor.js +48 -12
- package/dist/cli/hosts.js +1 -2
- package/dist/cli/install.js +7 -2
- package/dist/cli/manifest.js +25 -5
- package/dist/cli/uninstall.js +1 -1
- package/dist/extensions/caveman/index.js +6 -2
- package/dist/extensions/caveman/rule-lite.md +77 -0
- package/dist/extensions/caveman/rule-megacave-lite.md +59 -0
- package/dist/extensions/caveman/rule-megacave-ultra.md +59 -0
- package/dist/extensions/caveman/rule.md +1 -1
- package/dist/extensions/combo/index.js +26 -7
- package/dist/extensions/pstack/index.js +3 -1
- package/dist/extensions/pstack/rule.md +88 -12
- package/dist/extensions/rtk/session.js +18 -10
- package/dist/extensions/shared/rtk-eligible.js +84 -0
- package/dist/extensions/shared/rtk-gain.js +1 -1
- package/dist/extensions/shared/sqlite.js +7 -13
- package/dist/extensions/shared/status.js +1 -2
- package/dist/extensions/shared/usage-ledger.js +102 -20
- package/extensions/caveman/index.ts +6 -2
- package/extensions/caveman/rule-lite.md +77 -0
- package/extensions/caveman/rule-megacave-lite.md +59 -0
- package/extensions/caveman/rule-megacave-ultra.md +59 -0
- package/extensions/caveman/rule.md +1 -1
- package/extensions/combo/index.ts +27 -7
- package/extensions/pstack/index.ts +3 -1
- package/extensions/pstack/playbooks/authoring-a-skill.md +12 -0
- package/extensions/pstack/playbooks/autonomous-run.md +13 -0
- package/extensions/pstack/playbooks/autopilot-full.md +13 -0
- package/extensions/pstack/playbooks/autopilot-stack.md +16 -0
- package/extensions/pstack/playbooks/babysit.md +23 -0
- package/extensions/pstack/playbooks/bug-fix.md +15 -0
- package/extensions/pstack/playbooks/eval.md +25 -0
- package/extensions/pstack/playbooks/feature.md +21 -0
- package/extensions/pstack/playbooks/hillclimb.md +21 -0
- package/extensions/pstack/playbooks/investigation.md +14 -0
- package/extensions/pstack/playbooks/multi-phase-plan.md +155 -0
- package/extensions/pstack/playbooks/opening-a-pr.md +34 -0
- package/extensions/pstack/playbooks/orchestrate.md +111 -0
- package/extensions/pstack/playbooks/pause-safely.md +10 -0
- package/extensions/pstack/playbooks/perf-issue.md +25 -0
- package/extensions/pstack/playbooks/prototype.md +14 -0
- package/extensions/pstack/playbooks/refactoring.md +16 -0
- package/extensions/pstack/playbooks/runtime-forensics.md +11 -0
- package/extensions/pstack/playbooks/session-pickup.md +11 -0
- package/extensions/pstack/playbooks/shipping.md +17 -0
- package/extensions/pstack/playbooks/trace-forensics.md +14 -0
- package/extensions/pstack/playbooks/visual-parity.md +11 -0
- package/extensions/pstack/playbooks/worktree-cleanup.md +12 -0
- package/extensions/pstack/principles/attack-the-premise.md +22 -0
- package/extensions/pstack/principles/boundary-discipline.md +33 -0
- package/extensions/pstack/principles/build-the-lever.md +22 -0
- package/extensions/pstack/principles/encode-lessons-in-structure.md +10 -0
- package/extensions/pstack/principles/exhaust-the-design-space.md +8 -0
- package/extensions/pstack/principles/experience-first.md +18 -0
- package/extensions/pstack/principles/explain-the-number.md +16 -0
- package/extensions/pstack/principles/fix-root-causes.md +23 -0
- package/extensions/pstack/principles/foundational-thinking.md +20 -0
- package/extensions/pstack/principles/guard-the-context-window.md +11 -0
- package/extensions/pstack/principles/laziness-protocol.md +9 -0
- package/extensions/pstack/principles/make-operations-idempotent.md +23 -0
- package/extensions/pstack/principles/migrate-callers-then-delete-legacy-apis.md +21 -0
- package/extensions/pstack/principles/minimize-reader-load.md +17 -0
- package/extensions/pstack/principles/model-the-domain.md +23 -0
- package/extensions/pstack/principles/never-block-on-the-human.md +14 -0
- package/extensions/pstack/principles/outcome-oriented-execution.md +20 -0
- package/extensions/pstack/principles/prove-it-works.md +21 -0
- package/extensions/pstack/principles/redesign-from-first-principles.md +15 -0
- package/extensions/pstack/principles/separate-before-serializing-shared-state.md +15 -0
- package/extensions/pstack/principles/sequence-verifiable-units.md +16 -0
- package/extensions/pstack/principles/subtract-before-you-add.md +17 -0
- package/extensions/pstack/principles/test-behavior-not-implementation.md +20 -0
- package/extensions/pstack/principles/type-system-discipline.md +17 -0
- package/extensions/pstack/references/bugbot-triage.md +142 -0
- package/extensions/pstack/rule.md +88 -12
- package/extensions/pstack/skills/architect/SKILL.md +78 -0
- package/extensions/pstack/skills/architect/references/design-red-flags.md +57 -0
- package/extensions/pstack/skills/architect/references/rationale-template.md +35 -0
- package/extensions/pstack/skills/architect/references/runner-prompt.md +20 -0
- package/extensions/pstack/skills/arena/SKILL.md +47 -0
- package/extensions/pstack/skills/benchmark-checklist/SKILL.md +38 -0
- package/extensions/pstack/skills/blast-radius/SKILL.md +49 -0
- package/extensions/pstack/skills/bro/SKILL.md +6 -0
- package/extensions/pstack/skills/correct/SKILL.md +31 -0
- package/extensions/pstack/skills/figure-it-out/SKILL.md +51 -0
- package/extensions/pstack/skills/how/SKILL.md +35 -0
- package/extensions/pstack/skills/how/references/explainer-prompt.md +55 -0
- package/extensions/pstack/skills/how/references/explorer-prompt.md +35 -0
- package/extensions/pstack/skills/interrogate/SKILL.md +73 -0
- package/extensions/pstack/skills/interrogate/references/code-quality-review.md +47 -0
- package/extensions/pstack/skills/interrogate/references/lead-judgment.md +58 -0
- package/extensions/pstack/skills/interrogate/references/reviewer-prompt.md +62 -0
- package/extensions/pstack/skills/interrogate/references/rubric.md +77 -0
- package/extensions/pstack/skills/no-comments/SKILL.md +23 -0
- package/extensions/pstack/skills/reflect/SKILL.md +66 -0
- package/extensions/pstack/skills/reflect/references/divergent-reviewer.md +38 -0
- package/extensions/pstack/skills/reflect/references/judgment-reviewer.md +37 -0
- package/extensions/pstack/skills/reflect/references/synthesizer.md +56 -0
- package/extensions/pstack/skills/reflect/references/tooling-reviewer.md +48 -0
- package/extensions/pstack/skills/show-me-your-work/SKILL.md +81 -0
- package/extensions/pstack/skills/swarm/SKILL.md +45 -0
- package/extensions/pstack/skills/tdd/SKILL.md +41 -0
- package/extensions/pstack/skills/technical-writing/SKILL.md +105 -0
- package/extensions/pstack/skills/unslop/SKILL.md +61 -0
- package/extensions/pstack/skills/why/SKILL.md +97 -0
- package/extensions/pstack/skills/why/references/epistemics.md +71 -0
- package/extensions/pstack/skills/why/references/investigator-prompt.md +84 -0
- package/extensions/pstack/skills/why/references/source-playbook.md +17 -0
- package/extensions/pstack/skills/why/references/sources/code-archaeology.md +51 -0
- package/extensions/pstack/skills/why/references/sources/databricks.md +70 -0
- package/extensions/pstack/skills/why/references/sources/datadog.md +97 -0
- package/extensions/pstack/skills/why/references/sources/incident-postmortem.md +5 -0
- package/extensions/pstack/skills/why/references/sources/linear.md +48 -0
- package/extensions/pstack/skills/why/references/sources/notion.md +55 -0
- package/extensions/pstack/skills/why/references/sources/sentry.md +66 -0
- package/extensions/pstack/skills/why/references/sources/slack.md +17 -0
- package/extensions/pstack/skills/why/references/synthesizer-prompt.md +129 -0
- package/extensions/rtk/session.ts +17 -9
- package/extensions/shared/rtk-eligible.ts +83 -0
- package/extensions/shared/rtk-gain.ts +1 -1
- package/extensions/shared/sqlite.ts +7 -13
- package/extensions/shared/status.ts +1 -2
- package/extensions/shared/usage-ledger.ts +97 -19
- package/extensions/timing/index.ts +1 -2
- package/package.json +7 -4
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
|
|
2
|
+
export function deepClone(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object') {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
if (value instanceof Date) {
|
|
7
|
+
return new Date(value.getTime());
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item) => deepClone(item));
|
|
11
|
+
}
|
|
12
|
+
const source = value;
|
|
13
|
+
const clone = {};
|
|
14
|
+
for (const key of Object.keys(source)) {
|
|
15
|
+
clone[key] = deepClone(source[key]);
|
|
16
|
+
}
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
+
export function groupBy(items, keyFn) {
|
|
3
|
+
const groups = {};
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const key = keyFn(item);
|
|
6
|
+
const bucket = groups[key];
|
|
7
|
+
if (bucket === undefined) {
|
|
8
|
+
groups[key] = [item];
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
bucket.push(item);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return groups;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Debounce: fires fn ms after the last call with that call's arguments; a call during the wait restarts it.
|
|
2
|
+
export function debounce(fn, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
return (...args) => {
|
|
5
|
+
if (timer !== undefined) {
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
}
|
|
8
|
+
timer = setTimeout(() => {
|
|
9
|
+
timer = undefined;
|
|
10
|
+
fn(...args);
|
|
11
|
+
}, ms);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// deepClone: recursive clone of plain objects, arrays, Date, and primitives; shares no mutable structure and never round-trips through structuredClone or JSON.
|
|
2
|
+
export function deepClone(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object') {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
if (value instanceof Date) {
|
|
7
|
+
return new Date(value.getTime());
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item) => deepClone(item));
|
|
11
|
+
}
|
|
12
|
+
const source = value;
|
|
13
|
+
const clone = {};
|
|
14
|
+
for (const key of Object.keys(source)) {
|
|
15
|
+
clone[key] = deepClone(source[key]);
|
|
16
|
+
}
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// groupBy: buckets items by keyFn; keys land in first-seen order and each bucket preserves input order.
|
|
2
|
+
export function groupBy(items, keyFn) {
|
|
3
|
+
const groups = {};
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const key = keyFn(item);
|
|
6
|
+
const bucket = groups[key];
|
|
7
|
+
if (bucket === undefined) {
|
|
8
|
+
groups[key] = [item];
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
bucket.push(item);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return groups;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Debounce: fires fn once, ms after the last call, using that last call's arguments. A call during the wait restarts it.
|
|
2
|
+
export function debounce(fn, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
return (...args) => {
|
|
5
|
+
if (timer !== undefined) {
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
}
|
|
8
|
+
timer = setTimeout(() => {
|
|
9
|
+
timer = undefined;
|
|
10
|
+
fn(...args);
|
|
11
|
+
}, ms);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// deepClone: recursive clone of plain objects, arrays, Date, and primitives. Shares no mutable structure with the input; no structuredClone or JSON round-trip.
|
|
2
|
+
export function deepClone(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object') {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
if (value instanceof Date) {
|
|
7
|
+
return new Date(value.getTime());
|
|
8
|
+
}
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return value.map((item) => deepClone(item));
|
|
11
|
+
}
|
|
12
|
+
const source = value;
|
|
13
|
+
const clone = {};
|
|
14
|
+
for (const key of Object.keys(source)) {
|
|
15
|
+
clone[key] = deepClone(source[key]);
|
|
16
|
+
}
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
+
export function groupBy(items, keyFn) {
|
|
3
|
+
const groups = {};
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const key = keyFn(item);
|
|
6
|
+
const bucket = groups[key];
|
|
7
|
+
if (bucket === undefined) {
|
|
8
|
+
groups[key] = [item];
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
bucket.push(item);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return groups;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function deepClone(value) {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (value instanceof Date) {
|
|
6
|
+
return new Date(value.getTime());
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((item) => deepClone(item));
|
|
10
|
+
}
|
|
11
|
+
const source = value;
|
|
12
|
+
const clone = {};
|
|
13
|
+
for (const key of Object.keys(source)) {
|
|
14
|
+
clone[key] = deepClone(source[key]);
|
|
15
|
+
}
|
|
16
|
+
return clone;
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function groupBy(items, keyFn) {
|
|
2
|
+
const groups = {};
|
|
3
|
+
for (const item of items) {
|
|
4
|
+
const key = keyFn(item);
|
|
5
|
+
const existing = groups[key];
|
|
6
|
+
if (existing === undefined) {
|
|
7
|
+
groups[key] = [item];
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
existing.push(item);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return groups;
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Debounce: the returned wrapper fires fn once, after ms milliseconds of quiet, using the last call's arguments.
|
|
2
|
+
export function debounce(fn, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
// Each call rebinds args, so the closure the timeout reads is always the latest one.
|
|
5
|
+
return (...args) => {
|
|
6
|
+
if (timer !== undefined)
|
|
7
|
+
clearTimeout(timer);
|
|
8
|
+
timer = setTimeout(() => {
|
|
9
|
+
timer = undefined;
|
|
10
|
+
fn(...args);
|
|
11
|
+
}, ms);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// deepClone: recursive clone for plain objects, arrays, Date, and primitives. No structuredClone or JSON round-trip.
|
|
2
|
+
export function deepClone(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object')
|
|
4
|
+
return value;
|
|
5
|
+
if (value instanceof Date)
|
|
6
|
+
return new Date(value.getTime());
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return value.map((item) => deepClone(item));
|
|
9
|
+
const source = value;
|
|
10
|
+
const clone = {};
|
|
11
|
+
for (const key of Object.keys(source)) {
|
|
12
|
+
clone[key] = deepClone(source[key]);
|
|
13
|
+
}
|
|
14
|
+
return clone;
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// groupBy: buckets items by keyFn. Keys land in first-seen order and each bucket keeps the input order.
|
|
2
|
+
export function groupBy(items, keyFn) {
|
|
3
|
+
const groups = {};
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const key = keyFn(item);
|
|
6
|
+
const bucket = groups[key];
|
|
7
|
+
if (bucket === undefined)
|
|
8
|
+
groups[key] = [item];
|
|
9
|
+
else
|
|
10
|
+
bucket.push(item);
|
|
11
|
+
}
|
|
12
|
+
return groups;
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Trailing-edge debounce: `fn` runs only once `ms` passes with no further call, and always with the last call's arguments.
|
|
2
|
+
export function debounce(fn, ms) {
|
|
3
|
+
let timer;
|
|
4
|
+
return (...args) => {
|
|
5
|
+
if (timer !== undefined)
|
|
6
|
+
clearTimeout(timer);
|
|
7
|
+
timer = setTimeout(() => {
|
|
8
|
+
timer = undefined;
|
|
9
|
+
fn(...args);
|
|
10
|
+
}, ms);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Recursive structural copy: plain objects, arrays, Dates, and primitives. No shared mutable structure with the input.
|
|
2
|
+
export function deepClone(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object')
|
|
4
|
+
return value;
|
|
5
|
+
// SAFETY: a Date owns no enumerable props, so it must be rebuilt from its epoch ms or it clones to `{}`.
|
|
6
|
+
if (value instanceof Date)
|
|
7
|
+
return new Date(value.getTime());
|
|
8
|
+
// SAFETY: Array.isArray above proves an array, so mapping deepClone over the elements preserves its shape.
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
const items = value;
|
|
11
|
+
return items.map((item) => deepClone(item));
|
|
12
|
+
}
|
|
13
|
+
// SAFETY: Object.entries copies every own enumerable key, so the plain-object form of T is reproduced.
|
|
14
|
+
const clone = {};
|
|
15
|
+
for (const [key, item] of Object.entries(value))
|
|
16
|
+
clone[key] = deepClone(item);
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Buckets items by `keyFn`, preserving input order inside each group.
|
|
2
|
+
export function groupBy(items, keyFn) {
|
|
3
|
+
const groups = {};
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
const key = keyFn(item);
|
|
6
|
+
const bucket = groups[key];
|
|
7
|
+
if (bucket === undefined)
|
|
8
|
+
groups[key] = [item];
|
|
9
|
+
else
|
|
10
|
+
bucket.push(item);
|
|
11
|
+
}
|
|
12
|
+
return groups;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function deepClone(value) {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (value instanceof Date) {
|
|
6
|
+
return new Date(value.getTime());
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
const source = value;
|
|
10
|
+
return source.map((item) => deepClone(item));
|
|
11
|
+
}
|
|
12
|
+
const source = value;
|
|
13
|
+
const clone = {};
|
|
14
|
+
for (const [key, item] of Object.entries(source)) {
|
|
15
|
+
clone[key] = deepClone(item);
|
|
16
|
+
}
|
|
17
|
+
return clone;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function groupBy(items, keyFn) {
|
|
2
|
+
const groups = {};
|
|
3
|
+
for (const item of items) {
|
|
4
|
+
const key = keyFn(item);
|
|
5
|
+
const existing = groups[key];
|
|
6
|
+
if (existing === undefined) {
|
|
7
|
+
groups[key] = [item];
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
existing.push(item);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return groups;
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function deepClone(value) {
|
|
2
|
+
if (value === null || typeof value !== 'object') {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (value instanceof Date) {
|
|
6
|
+
return new Date(value.getTime());
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map((item) => deepClone(item));
|
|
10
|
+
}
|
|
11
|
+
const clone = {};
|
|
12
|
+
for (const [key, item] of Object.entries(value)) {
|
|
13
|
+
clone[key] = deepClone(item);
|
|
14
|
+
}
|
|
15
|
+
return clone;
|
|
16
|
+
}
|
package/dist/cli/banner.js
CHANGED
|
@@ -81,8 +81,7 @@ function printWelcome() {
|
|
|
81
81
|
'tersio update refresh CLI + add-ons',
|
|
82
82
|
'tersio doctor verify the install',
|
|
83
83
|
];
|
|
84
|
-
// Below 26 rows the
|
|
85
|
-
// update prompt and every option, so the tips take one line there.
|
|
84
|
+
// Below 26 rows the tips scroll off the top and slice the art, so they take one line there.
|
|
86
85
|
if (process.stdout.rows < 26) {
|
|
87
86
|
clackNote(` ${tips.join(' · ')}`);
|
|
88
87
|
return;
|
package/dist/cli/common.js
CHANGED
|
@@ -131,7 +131,7 @@ async function writeIfChanged(dest, content, options = {}) {
|
|
|
131
131
|
return true;
|
|
132
132
|
}
|
|
133
133
|
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
134
|
-
if (existing !== null) {
|
|
134
|
+
if (existing !== null && options.backup === true) {
|
|
135
135
|
await fs.copyFile(dest, `${dest}.bak`);
|
|
136
136
|
debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
|
|
137
137
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -4,13 +4,14 @@ import { existsSync, readdirSync, readFileSync, statSync, promises as fs } from
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, PACKAGE_VERSION, args, dryRun, fix, yes, BUN_BIN_DIR, OMP_BIN, execP, parseJsonObject, relTime, } from './common.js';
|
|
6
6
|
import { detectHosts, hostExtensionsDir, ompPackageDir, piPonytailPackageDir } from './hosts.js';
|
|
7
|
+
import { GENERATED_TREE_FILES } from './manifest.js';
|
|
7
8
|
import { askInteractiveChoice, askInteractiveConfirm, runInteractivePhase } from './interactive.js';
|
|
8
9
|
import { usageDbPath } from '../extensions/shared/usage-store.js';
|
|
9
10
|
import { pricesCachePath } from '../extensions/shared/pricing.js';
|
|
10
11
|
import { findHoistedPackage, fetchJson, piAgentDir, readTextIfExists, resolveHostBinary, resolveRtkBinary, ruleBodyProblem } from '../extensions/shared/utils.js';
|
|
11
12
|
import { sqliteScript, statsDbPath } from '../extensions/shared/agent-stats.js';
|
|
12
13
|
import { OMP_SUBAGENT_MARKER } from '../extensions/shared/session-state.js';
|
|
13
|
-
import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
|
|
14
|
+
import { PI_PONYTAIL_FILES, PONYTAIL_BUNDLE_FILES, TREE_FILES, TREE_PSTACK_FILES, isObsoleteTreeFile, treeFileText, treeFilesFor } from './manifest.js';
|
|
14
15
|
import { serviceRunning, serviceState } from './doctor-fix.js';
|
|
15
16
|
/** Bytes at the scale a transcript gap reads in: `5.6M`, not `5571992`. */
|
|
16
17
|
function shortBytes(bytes) {
|
|
@@ -42,8 +43,7 @@ async function runDoctor(recheck = false) {
|
|
|
42
43
|
const updaterIndex = path.join(tersioPluginDir, 'extensions', 'updater', 'index.ts');
|
|
43
44
|
// Probes start concurrently and report in fixed order as data settles; each resolves instead of rejecting.
|
|
44
45
|
const hosts = detectHosts();
|
|
45
|
-
//
|
|
46
|
-
// their own copy are compared below. The package root, not the hook's directory, is what the paths hang off.
|
|
46
|
+
// Only hosts with their own copy are compared: omp's copy is the reference. Paths hang off the package root.
|
|
47
47
|
const ponytailLoads = {
|
|
48
48
|
pi: { dir: piPonytailPackageDir(piAgentDir()), files: PI_PONYTAIL_FILES },
|
|
49
49
|
opencode: { dir: path.join(hostExtensionsDir('opencode'), 'ponytail-bundle'), files: PONYTAIL_BUNDLE_FILES },
|
|
@@ -70,9 +70,7 @@ async function runDoctor(recheck = false) {
|
|
|
70
70
|
const treeOrphan = [];
|
|
71
71
|
// The tree a host loads: omp reads the plugin package, every other host its own directory.
|
|
72
72
|
const treeBaseFor = (host) => host.id === 'omp' ? path.join(tersioPluginDir, 'extensions') : hostExtensionsDir(host.id);
|
|
73
|
-
// A tree holding
|
|
74
|
-
// installed. pi auto-discovers every index.ts it finds, so a half-written tree needs reporting,
|
|
75
|
-
// and `detectHosts` reads one as absent: it wants every file or a declared package.
|
|
73
|
+
// A tree holding any file of this build is compared even when the host reads as not installed: pi auto-discovers every index.ts it finds, so a half-written tree needs reporting.
|
|
76
74
|
const treeHoldsOwnFiles = (host) => treeFilesFor(host.id === 'omp' ? 'opencode' : host.id).some((rel) => existsSync(path.join(treeBaseFor(host), ...rel.split('/'))));
|
|
77
75
|
for (const host of hosts.filter((h) => h.installed || treeHoldsOwnFiles(h))) {
|
|
78
76
|
const treeId = host.id === 'omp' ? 'opencode' : host.id;
|
|
@@ -103,6 +101,22 @@ async function runDoctor(recheck = false) {
|
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
}
|
|
104
|
+
// A host reading as installed whose tree is short of this build's declared files: the pair loop above cannot see a file that is absent on both sides.
|
|
105
|
+
const unwrittenTreeFiles = async () => {
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const host of hosts.filter((h) => h.installed)) {
|
|
108
|
+
const treeId = host.id === 'omp' ? 'opencode' : host.id;
|
|
109
|
+
const base = treeBaseFor(host);
|
|
110
|
+
for (const rel of treeFilesFor(treeId)) {
|
|
111
|
+
if (existsSync(path.join(base, ...rel.split('/'))))
|
|
112
|
+
continue;
|
|
113
|
+
if (host.id === 'omp' && GENERATED_TREE_FILES[rel] !== undefined)
|
|
114
|
+
continue;
|
|
115
|
+
out.push(`${host.id}/${rel}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
106
120
|
const probes = {
|
|
107
121
|
ompPkgText: readTextIfExists(path.join(ompPackageDir(), 'package.json')),
|
|
108
122
|
configText: readTextIfExists(path.join(OMP_AGENT_DIR, 'config.yml')),
|
|
@@ -110,9 +124,13 @@ async function runDoctor(recheck = false) {
|
|
|
110
124
|
rtkBinText: rtkBin ? readTextIfExists(rtkBin) : Promise.resolve(null),
|
|
111
125
|
cavemanIndexText: readTextIfExists(cavemanIndex),
|
|
112
126
|
cavemanRuleText: readTextIfExists(cavemanRule('rule.md')),
|
|
127
|
+
cavemanLiteText: readTextIfExists(cavemanRule('rule-lite.md')),
|
|
128
|
+
cavemanMegacaveLiteText: readTextIfExists(cavemanRule('rule-megacave-lite.md')),
|
|
129
|
+
cavemanMegacaveUltraText: readTextIfExists(cavemanRule('rule-megacave-ultra.md')),
|
|
113
130
|
cavemanUltraText: readTextIfExists(cavemanRule('rule-ultra.md')),
|
|
114
131
|
cavemanMegacaveText: readTextIfExists(cavemanRule('rule-megacave.md')),
|
|
115
132
|
pstackRuleText: readTextIfExists(pstackRule),
|
|
133
|
+
pstackTreeFiles: Promise.all(TREE_PSTACK_FILES.map((rel) => readTextIfExists(path.join(tersioPluginDir, "extensions", ...rel.split("/"))))),
|
|
116
134
|
rtkIndexText: readTextIfExists(rtkIndex),
|
|
117
135
|
updaterIndexText: readTextIfExists(updaterIndex),
|
|
118
136
|
rtkMtime: rtkBin ? fs.stat(rtkBin).catch(() => null) : Promise.resolve(null),
|
|
@@ -128,7 +146,7 @@ async function runDoctor(recheck = false) {
|
|
|
128
146
|
const opencodeServiceProbe = opencodeBin
|
|
129
147
|
? Promise.resolve(serviceState())
|
|
130
148
|
: Promise.resolve(null);
|
|
131
|
-
const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
149
|
+
const [[ompPkgText, configText], [cavemanIndexText, rtkIndexText, updaterIndexText, ponytailPkgText], [cavemanRuleText, cavemanLiteText, cavemanMegacaveLiteText, cavemanMegacaveUltraText, cavemanUltraText, cavemanMegacaveText, pstackRuleText, pstackTreeFiles, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime, pricesMtime], [opencodeService]] = await runInteractivePhase('Checking installation', () => Promise.all([
|
|
132
150
|
Promise.all([
|
|
133
151
|
probes.ompPkgText,
|
|
134
152
|
probes.configText,
|
|
@@ -141,9 +159,13 @@ async function runDoctor(recheck = false) {
|
|
|
141
159
|
]),
|
|
142
160
|
Promise.all([
|
|
143
161
|
probes.cavemanRuleText,
|
|
162
|
+
probes.cavemanLiteText,
|
|
163
|
+
probes.cavemanMegacaveLiteText,
|
|
164
|
+
probes.cavemanMegacaveUltraText,
|
|
144
165
|
probes.cavemanUltraText,
|
|
145
166
|
probes.cavemanMegacaveText,
|
|
146
167
|
probes.pstackRuleText,
|
|
168
|
+
probes.pstackTreeFiles,
|
|
147
169
|
probes.ruleMtime,
|
|
148
170
|
probes.rtkBinText,
|
|
149
171
|
probes.rtkMtime,
|
|
@@ -201,8 +223,7 @@ async function runDoctor(recheck = false) {
|
|
|
201
223
|
const kind = host.via === 'tree' ? 'extensions' : 'package';
|
|
202
224
|
check(host.label, true, `${kind} ${PACKAGE_NAME}${host.version ? ` ${host.version}` : ''}`);
|
|
203
225
|
}
|
|
204
|
-
// A healthy service adds no row: the host row already names the install
|
|
205
|
-
// this row earns its place only when the manager's report and the live process disagree.
|
|
226
|
+
// A healthy service adds no row: the host row already names the install, and this row only shows when the manager's report and the live process disagree.
|
|
206
227
|
if (opencodeService) {
|
|
207
228
|
const running = serviceRunning(opencodeService.status);
|
|
208
229
|
if (!running && opencodeService.live) {
|
|
@@ -236,7 +257,7 @@ async function runDoctor(recheck = false) {
|
|
|
236
257
|
warnLine('omp stats index', indexLag);
|
|
237
258
|
section('Add-ons');
|
|
238
259
|
const ruleAge = ruleMtime ? `(updated ${relTime(Date.now() - ruleMtime.mtimeMs)} · ${absDate(ruleMtime.mtimeMs)})` : '';
|
|
239
|
-
const cavemanRules = [['rule.md', cavemanRuleText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
|
|
260
|
+
const cavemanRules = [['rule.md', cavemanRuleText], ['rule-lite.md', cavemanLiteText], ['rule-megacave-lite.md', cavemanMegacaveLiteText], ['rule-megacave-ultra.md', cavemanMegacaveUltraText], ['rule-ultra.md', cavemanUltraText], ['rule-megacave.md', cavemanMegacaveText]];
|
|
240
261
|
const missingRules = cavemanRules.filter(([, text]) => text === null).map(([name]) => name);
|
|
241
262
|
check('Caveman rule', missingRules.length === 0, missingRules.length ? `missing: ${missingRules.join(', ')}` : ruleAge);
|
|
242
263
|
for (const [name, text] of cavemanRules) {
|
|
@@ -253,6 +274,16 @@ async function runDoctor(recheck = false) {
|
|
|
253
274
|
if (problem)
|
|
254
275
|
warnLine('pstack rule.md', `unusable body (${problem}) — run: tersio install`);
|
|
255
276
|
}
|
|
277
|
+
// The index routes to files, so a tree with the rule but no leaves serves a mode that cannot apply.
|
|
278
|
+
const pstackOnDisk = {};
|
|
279
|
+
TREE_PSTACK_FILES.forEach((rel, i) => {
|
|
280
|
+
if (pstackTreeFiles[i] !== null)
|
|
281
|
+
pstackOnDisk[rel] = true;
|
|
282
|
+
});
|
|
283
|
+
const pstackMissing = TREE_PSTACK_FILES.filter((rel) => !pstackOnDisk[rel]).map((rel) => rel.replace(/^pstack\//, ''));
|
|
284
|
+
if (pstackMissing.length > 0) {
|
|
285
|
+
warnLine('pstack tree', `missing ${pstackMissing.length} file${pstackMissing.length === 1 ? '' : 's'}: ${pstackMissing.slice(0, 6).join(', ')}${pstackMissing.length > 6 ? ` +${pstackMissing.length - 6} more` : ''} — run: tersio install`);
|
|
286
|
+
}
|
|
256
287
|
// A verbatim sentence read out of omp's own prompt, so a re-worded release stops Caveman reaching subagents.
|
|
257
288
|
const subagentGap = subagentMarkerStatus(ompBinary());
|
|
258
289
|
if (subagentGap)
|
|
@@ -270,6 +301,11 @@ async function runDoctor(recheck = false) {
|
|
|
270
301
|
warnLine('Per-host add-on files', `orphaned in ${treeOrphan.length} file${treeOrphan.length === 1 ? '' : 's'}: ${treeOrphan.join(', ')}` +
|
|
271
302
|
` — run: tersio doctor --fix extensions`);
|
|
272
303
|
}
|
|
304
|
+
// A count of pairs compared cannot show a file that is absent from disk and absent from the declaration, so assert every declared file landed.
|
|
305
|
+
const unwritten = await unwrittenTreeFiles();
|
|
306
|
+
if (unwritten.length > 0) {
|
|
307
|
+
check('Add-on tree content', false, `${unwritten.length} declared file${unwritten.length === 1 ? '' : 's'} not written for a host reading as installed: ${unwritten.slice(0, 6).join(', ')}${unwritten.length > 6 ? ` +${unwritten.length - 6} more` : ''} — run: tersio doctor --fix extensions`);
|
|
308
|
+
}
|
|
273
309
|
const rtkAge = rtkMtime ? `(updated ${relTime(Date.now() - rtkMtime.mtimeMs)} · ${absDate(rtkMtime.mtimeMs)})` : '';
|
|
274
310
|
check('RTK binary', rtkBin !== null, rtkBinText === null ? 'not found in PATH' : [rtkVersion, rtkAge].filter(Boolean).join(' '));
|
|
275
311
|
// rtk_run needs a host exec API, which doctor cannot assume.
|
|
@@ -336,8 +372,8 @@ function ompBinary() {
|
|
|
336
372
|
const managed = path.join(BUN_BIN_DIR, OMP_BIN);
|
|
337
373
|
return existsSync(managed) ? managed : null;
|
|
338
374
|
}
|
|
339
|
-
// The rule bodies a per-host tree carries. rule.md
|
|
340
|
-
const CAVEMAN_BODIES = ['rule.md', 'rule-ultra.md', 'rule-megacave.md'];
|
|
375
|
+
// The rule bodies a per-host tree carries. rule.md and rule-lite.md are bundled forks; the others track upstream.
|
|
376
|
+
const CAVEMAN_BODIES = ['rule.md', 'rule-lite.md', 'rule-megacave-lite.md', 'rule-megacave-ultra.md', 'rule-ultra.md', 'rule-megacave.md'];
|
|
341
377
|
/** `"installed < latest"` when npm answers, else null: silence means "no row", not "stale". */
|
|
342
378
|
async function ponytailLatestGap(installedPkgText) {
|
|
343
379
|
const local = parseJsonObject(installedPkgText)?.version;
|
package/dist/cli/hosts.js
CHANGED
|
@@ -86,8 +86,7 @@ function detect(agentDir = piAgentDir()) {
|
|
|
86
86
|
return HOSTS.map((host) => {
|
|
87
87
|
const pkgDir = host.id === 'omp' ? ompPackageDir() : host.id === 'pi' ? piPackageDir(agentDir) : null;
|
|
88
88
|
const tree = hostExtensionsDir(host.id, agentDir);
|
|
89
|
-
// A tree wins:
|
|
90
|
-
// OpenCode keeps no package to fall back to, so a present tree is the install; its drifted files get the per-host warning instead.
|
|
89
|
+
// A tree wins: the installer writes it and it carries no package.json, so its version is ours. OpenCode keeps no package to fall back to, so a present tree is the install and drift gets the per-host warning.
|
|
91
90
|
const via = missingTreeFiles(host.id, agentDir).length === 0 || (host.id === 'opencode' && existsSync(tree))
|
|
92
91
|
? 'tree'
|
|
93
92
|
: pkgDir && existsSync(pkgDir) ? 'package' : null;
|
package/dist/cli/install.js
CHANGED
|
@@ -411,9 +411,12 @@ async function stepRtkFilter(extDir, options) {
|
|
|
411
411
|
console.log(' RTK filter — install built-in tool output filtering');
|
|
412
412
|
await copySources(extDir, filesUnder('rtk'), 'rtk/filter.ts', options);
|
|
413
413
|
}
|
|
414
|
-
// rule.md
|
|
414
|
+
// rule.md and rule-lite.md are our forks and always ship bundled; the other two track upstream and keep a usable body when the fetch fails.
|
|
415
415
|
const CAVEMAN_RULES = [
|
|
416
416
|
['rule.md', null],
|
|
417
|
+
['rule-lite.md', null],
|
|
418
|
+
['rule-megacave-lite.md', null],
|
|
419
|
+
['rule-megacave-ultra.md', null],
|
|
417
420
|
['rule-ultra.md', CAVEMAN_REMOTE_ULTRA],
|
|
418
421
|
['rule-megacave.md', CAVEMAN_REMOTE_MEGACAVE],
|
|
419
422
|
];
|
|
@@ -545,7 +548,8 @@ async function stepOpencode(options) {
|
|
|
545
548
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
546
549
|
await fs.rm(path.join(HOME, '.config', 'opencode', 'plugins', 'rtk.ts'), { force: true }).catch(() => { });
|
|
547
550
|
}
|
|
548
|
-
|
|
551
|
+
// The user's own config, so keep the previous copy beside it.
|
|
552
|
+
await writeIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`, { ...options, backup: true });
|
|
549
553
|
}
|
|
550
554
|
async function resolveProfile(opts = {}) {
|
|
551
555
|
// Seed from the stored defaults so flag-less runs keep them.
|
|
@@ -1007,6 +1011,7 @@ async function runInstall(forcedHost) {
|
|
|
1007
1011
|
await capture('shared', () => stepSharedSessionState(extDir, installOptions));
|
|
1008
1012
|
await capture('rtk session', () => stepRtkSession(extDir, installOptions));
|
|
1009
1013
|
await capture('rtk filter', () => stepRtkFilter(extDir, installOptions));
|
|
1014
|
+
await capture('timing', () => stepTiming(extDir, installOptions));
|
|
1010
1015
|
await capture('caveman', () => stepCaveman(extDir, installOptions));
|
|
1011
1016
|
await capture('combo', () => stepCombo(extDir, installOptions));
|
|
1012
1017
|
await capture('pstack', () => stepPstack(extDir, installOptions));
|