total-recall-brain 3.43.3 → 3.45.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/bin/total-recall.mjs +6 -4
- package/docs/ARCHITECTURE.md +10 -7
- package/docs/reference/cli-reference.md +16 -9
- package/frontend/dist/assets/index-DACh1dhx.js +5613 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +2 -2
- package/frontend/src/pages/PluginsPage.tsx +1 -1
- package/package.json +3 -3
- package/scaffold/.agent/skills/plugin/SKILL.md +7 -2
- package/scaffold/.agent/skills/plugin/evals/README.md +5 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +1 -0
- package/scaffold/.agent/skills/plugin/evals/cases/07-missing-reuse.mjs +11 -0
- package/scaffold/.agent/skills/plugin/evals/cases/08-unresolved-owner-command.mjs +12 -0
- package/scaffold/.agent/skills/plugin/evals/cases/09-missing-brick.mjs +12 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +4 -3
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +1 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +12 -2
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +59 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +8 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-gh-login.mjs +102 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +38 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/login-projections.mjs +157 -13
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-store.mjs +12 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-sync.mjs +161 -17
- package/scaffold/.agent/skills/total-recall/plugins/automations/BRICK.md +7 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/README.md +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +231 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/capability/index.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +121 -0
- package/{src/core/plugin-cron.mjs → scaffold/.agent/skills/total-recall/plugins/automations/cron.mjs} +1 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/inventory.mjs +88 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +7661 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +167 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/scheduler.mjs +246 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/DESIGN.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +57 -5
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +40 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +15 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/cli.mjs +135 -23
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package-lock.json +17 -16
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/package.json +5 -4
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +161 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/context.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/phase3-spec-support.mjs +50 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/runtime.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/lib/spec-support.mjs +51 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/_support.mjs +195 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/auth.mjs +201 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/bisect.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/cli-meta.mjs +159 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit.mjs +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +156 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/force-push.mjs +58 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/group.mjs +119 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/handoff.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/message.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/resolve.mjs +60 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/rules.mjs +81 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +66 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +132 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/why.mjs +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/card.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +59 -10
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/BRICK.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/capability/index.mjs +18 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +160 -29
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +231 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/skills/meta-harness/SKILL.md +19 -4
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/adoption.mjs +104 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/alerts.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +101 -16
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/control.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +54 -21
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch-request.mjs +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +68 -13
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/documentation.mjs +55 -11
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/executor.mjs +64 -15
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/hermes.mjs +29 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/gate-adapters/index.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/included-usage.mjs +249 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/jobs.mjs +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/learner.mjs +6 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/maintenance.mjs +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/mcp-server.mjs +228 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/pool.mjs +16 -8
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/process-manager.mjs +26 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reasoning.mjs +6 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +6 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/review.mjs +34 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/reviews.mjs +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +28 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/runtime.mjs +28 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/claude.mjs +319 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/codex.mjs +387 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/result.mjs +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/resume.mjs +373 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session/run.mjs +223 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/session-jobs.mjs +72 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/setup.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/termination.mjs +16 -12
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/threads.mjs +32 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/usage-queue.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/verify-ssss.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +597 -70
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worktree.mjs +76 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +47 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh-diagnostics.mjs +44 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +120 -113
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +15 -14
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/assets/scaffold.json +5 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +1 -1
- package/scaffold/.agent/skills/total-recall/references/operations.md +1 -1
- package/src/cli/agent.mjs +1 -1
- package/src/cli/command.mjs +41 -2
- package/src/cli/connect.mjs +2 -29
- package/src/cli/context.mjs +1 -0
- package/src/cli/dream.mjs +93 -69
- package/src/cli/harness.mjs +4 -4
- package/src/cli/help.mjs +1 -1
- package/src/cli/ingest-okf.mjs +4 -4
- package/src/cli/init.mjs +0 -25
- package/src/cli/lessons.mjs +51 -0
- package/src/cli/memory-plugin.mjs +1 -1
- package/src/cli/plugin/copies.mjs +64 -14
- package/src/cli/plugin/create.mjs +6 -7
- package/src/cli/plugin/index.mjs +11 -7
- package/src/cli/plugin/install.mjs +10 -15
- package/src/cli/plugin/remove.mjs +1 -1
- package/src/cli/plugin/search.mjs +2 -2
- package/src/cli/preflight.mjs +19 -6
- package/src/cli/proposals.mjs +1 -1
- package/src/cli/recall.mjs +66 -23
- package/src/cli/release.mjs +4 -1
- package/src/cli/remember.mjs +3 -14
- package/src/cli/server.mjs +18 -0
- package/src/cli/startup.mjs +1 -0
- package/src/cli/task.mjs +9 -2
- package/src/cli/update.mjs +10 -3
- package/src/core/access-ledger.mjs +78 -0
- package/src/core/agent-manager.mjs +2 -2
- package/src/core/automatic-brain-updates.mjs +11 -6
- package/src/core/brain-locate.mjs +15 -0
- package/src/core/brain-updates.mjs +42 -12
- package/src/core/claude-hooks.mjs +3 -1
- package/src/core/cleanup-task.mjs +15 -0
- package/src/core/cli-commands.mjs +2 -0
- package/src/core/cli-maintenance.mjs +4 -1
- package/src/core/cli-semantic-index.mjs +23 -3
- package/src/core/context-compactor.mjs +7 -2
- package/src/core/context-compiler.mjs +26 -1
- package/src/core/context-policy.mjs +7 -3
- package/src/core/daemon-control.mjs +22 -0
- package/src/core/daemon-loop.mjs +39 -62
- package/src/core/default-plugins.mjs +1 -1
- package/src/core/dream.mjs +33 -105
- package/src/core/ensure-dependencies.mjs +148 -0
- package/src/core/fast-recall.mjs +2 -2
- package/src/core/field-evaluation.mjs +23 -0
- package/src/core/gate-mode.mjs +1 -1
- package/src/core/global-brain-updates.mjs +1 -1
- package/src/core/harness-hooks.mjs +14 -2
- package/src/core/lesson-hooks.mjs +74 -0
- package/src/core/lesson-memory.mjs +24 -0
- package/src/core/lesson-sessions.mjs +52 -0
- package/src/core/lessons.mjs +96 -0
- package/src/core/mesh-brain-updates.mjs +2 -1
- package/src/core/meta-harness.mjs +2 -2
- package/src/core/optimizer.mjs +66 -165
- package/src/core/plugin-contracts/card.mjs +3 -0
- package/src/core/plugin-loader.mjs +12 -3
- package/src/core/plugin-runtime.mjs +40 -1
- package/src/core/plugin-store.mjs +35 -23
- package/src/core/post-mortem.mjs +52 -36
- package/src/core/preflight-gate.mjs +23 -2
- package/src/core/proposal-applier.mjs +31 -2
- package/src/core/repo-sync.mjs +4 -6
- package/src/core/required-plugins.mjs +26 -4
- package/src/core/rule-length.mjs +23 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/scheduler.mjs +35 -43
- package/src/core/schema.mjs +1 -2
- package/src/core/search.mjs +4 -3
- package/src/core/server-restart.mjs +128 -7
- package/src/core/server-service.mjs +148 -0
- package/src/core/session-watcher.mjs +19 -1
- package/src/core/skill-gap.mjs +16 -0
- package/src/core/ssss-kernel-bridge.mjs +33 -6
- package/src/core/startup-health.mjs +6 -1
- package/src/core/surface.mjs +68 -57
- package/src/core/task-context.mjs +9 -8
- package/src/core/task-executors.mjs +54 -106
- package/src/core/validated-write.mjs +5 -0
- package/src/core/vault-files.mjs +8 -2
- package/src/core/vault.mjs +1 -1
- package/src/core/vector-field.mjs +134 -192
- package/src/server/index.mjs +3 -1
- package/src/server/invoke-core-child.mjs +24 -0
- package/src/server/invoke-core-operations.mjs +102 -0
- package/src/server/route-manifest.json +0 -56
- package/src/server/routes/brains.mjs +0 -18
- package/src/server/routes/context.mjs +0 -41
- package/src/server/routes/invoke.mjs +26 -1
- package/src/server/routes/ssss.mjs +0 -169
- package/src/server/routes/system.mjs +0 -15
- package/src/server/routes/tasks.mjs +0 -26
- package/src/server/routes/vault.mjs +2 -23
- package/frontend/dist/assets/index-DD4qONvH.js +0 -5607
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +0 -53
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +0 -129
- package/src/core/append-log.mjs +0 -131
- package/src/core/plugin-tasks.mjs +0 -173
- package/templates/onboarding-interview.md +0 -82
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/LICENSE +0 -0
- /package/scaffold/.agent/skills/total-recall/plugins/{git-sentinel → git}/ui/DESIGN.md +0 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/** Automations control ledger. Values come only from the inventory owner. */
|
|
2
|
+
const SCREENS = [
|
|
3
|
+
{ id: 'scheduled', label: 'Scheduled' }, { id: 'nodes', label: 'Nodes' },
|
|
4
|
+
{ id: 'audit', label: 'Audit' }, { id: 'run', label: 'Run' },
|
|
5
|
+
];
|
|
6
|
+
const escape = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
7
|
+
const words = (value) => String(value ?? 'unknown').replaceAll('_', ' ').replaceAll('-', ' ');
|
|
8
|
+
const date = (value) => value && Number.isFinite(Date.parse(value)) ? new Date(value).toLocaleString() : '— not recorded';
|
|
9
|
+
const stateClass = (value) => ['error', 'failed', 'offline', 'unreachable', 'unauthorized'].includes(value) ? 'bad' : 'neutral';
|
|
10
|
+
|
|
11
|
+
/** Decode the host wrapper without turning command failures into inventory. */
|
|
12
|
+
export function decodeInventory(result) {
|
|
13
|
+
if (result?.success === false || (typeof result?.exitCode === 'number' && result.exitCode !== 0) || (typeof result?.code === 'number' && result.code !== 0)) {
|
|
14
|
+
throw new Error(result.error || 'Inventory command failed');
|
|
15
|
+
}
|
|
16
|
+
let data = result?.output !== undefined ? result.output : result;
|
|
17
|
+
if (typeof data === 'string') {
|
|
18
|
+
try { data = JSON.parse(data); } catch { throw new Error('Inventory returned invalid JSON'); }
|
|
19
|
+
}
|
|
20
|
+
if (!data || !Array.isArray(data.nodes) || data.nodes.some((node) => !node || !Array.isArray(node.automations))) {
|
|
21
|
+
throw new Error('Inventory response is incomplete or unsupported');
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AutomationsCard extends HTMLElement {
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.attachShadow({ mode: 'open' });
|
|
30
|
+
this.screen = 'scheduled';
|
|
31
|
+
this.filters = { node: '', source: '', trigger: '', state: '' };
|
|
32
|
+
this.inventory = null;
|
|
33
|
+
this.phase = 'unchecked';
|
|
34
|
+
this.error = '';
|
|
35
|
+
this.feedback = '';
|
|
36
|
+
this.selected = '';
|
|
37
|
+
this.request = 0;
|
|
38
|
+
this.attemptedAt = null;
|
|
39
|
+
this.pendingControl = null;
|
|
40
|
+
this.boundClick = (event) => this.click(event);
|
|
41
|
+
this.boundChange = (event) => this.change(event);
|
|
42
|
+
this.boundKey = (event) => this.key(event);
|
|
43
|
+
this.boundScreen = (event) => {
|
|
44
|
+
if (event.detail?.pluginId === this.host?.pluginId) this.selectScreen(event.detail.screen);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get host() { return this._host; }
|
|
49
|
+
set host(value) {
|
|
50
|
+
this._host = value;
|
|
51
|
+
if (this.isConnected && value?.run) void this.refresh();
|
|
52
|
+
}
|
|
53
|
+
get screens() { return SCREENS; }
|
|
54
|
+
|
|
55
|
+
connectedCallback() {
|
|
56
|
+
this.shadowRoot.addEventListener('click', this.boundClick);
|
|
57
|
+
this.shadowRoot.addEventListener('change', this.boundChange);
|
|
58
|
+
this.shadowRoot.addEventListener('keydown', this.boundKey);
|
|
59
|
+
window.addEventListener('plugin-show-screen', this.boundScreen);
|
|
60
|
+
this.render();
|
|
61
|
+
void this.refresh();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
disconnectedCallback() {
|
|
65
|
+
this.request++;
|
|
66
|
+
this.controller?.abort();
|
|
67
|
+
this.shadowRoot.removeEventListener('click', this.boundClick);
|
|
68
|
+
this.shadowRoot.removeEventListener('change', this.boundChange);
|
|
69
|
+
this.shadowRoot.removeEventListener('keydown', this.boundKey);
|
|
70
|
+
window.removeEventListener('plugin-show-screen', this.boundScreen);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async invoke(command, args = []) {
|
|
74
|
+
if (this.host?.run) return this.host.run(command, args);
|
|
75
|
+
const response = await fetch('/api/plugins/automations/run', {
|
|
76
|
+
method: 'POST', credentials: 'same-origin', signal: this.controller?.signal,
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({ subcommand: command, args }),
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) throw new Error(`Automation request failed (${response.status})`);
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async refresh() {
|
|
85
|
+
const request = ++this.request;
|
|
86
|
+
this.controller?.abort();
|
|
87
|
+
this.controller = new AbortController();
|
|
88
|
+
this.phase = 'loading'; this.inventory = null; this.error = ''; this.feedback = ''; this.pendingControl = null;
|
|
89
|
+
this.attemptedAt = new Date().toISOString();
|
|
90
|
+
this.render();
|
|
91
|
+
try {
|
|
92
|
+
const data = decodeInventory(await this.invoke('list'));
|
|
93
|
+
if (request !== this.request || !this.isConnected) return;
|
|
94
|
+
this.inventory = data; this.phase = 'ready';
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (request !== this.request || !this.isConnected) return;
|
|
97
|
+
this.inventory = null; this.phase = 'error';
|
|
98
|
+
this.error = error.message || 'Automation inventory unavailable';
|
|
99
|
+
}
|
|
100
|
+
this.render();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
records() {
|
|
104
|
+
return (this.inventory?.nodes || []).flatMap((node) => node.automations.map((item) => ({
|
|
105
|
+
...item, _node: node, _key: `${node.node_id || node.ip || node.hostname}:${item.id || `${item.plugin_id}:${item.command}`}`,
|
|
106
|
+
})));
|
|
107
|
+
}
|
|
108
|
+
filteredRecords() {
|
|
109
|
+
return this.records().filter((item) =>
|
|
110
|
+
(!this.filters.node || this.nodeKey(item._node) === this.filters.node) &&
|
|
111
|
+
(!this.filters.source || item.source === this.filters.source) &&
|
|
112
|
+
(!this.filters.trigger || item.trigger?.kind === this.filters.trigger) &&
|
|
113
|
+
(!this.filters.state || item.state === this.filters.state));
|
|
114
|
+
}
|
|
115
|
+
nodeKey(node) { return String(node.node_id || node.ip || node.hostname || ''); }
|
|
116
|
+
selectedRecord() {
|
|
117
|
+
const records = this.filteredRecords();
|
|
118
|
+
return records.find((item) => item._key === this.selected) || records[0];
|
|
119
|
+
}
|
|
120
|
+
selectScreen(screen, focus = false) {
|
|
121
|
+
if (!SCREENS.some((item) => item.id === screen)) return;
|
|
122
|
+
this.screen = screen; this.feedback = ''; this.pendingControl = null; this.render();
|
|
123
|
+
this.host?.screenChanged?.(screen);
|
|
124
|
+
if (focus) this.shadowRoot.querySelector(`[data-screen="${screen}"]`)?.focus();
|
|
125
|
+
}
|
|
126
|
+
key(event) {
|
|
127
|
+
const tab = event.target.closest('[data-screen]');
|
|
128
|
+
if (!tab) return;
|
|
129
|
+
const index = SCREENS.findIndex((item) => item.id === tab.dataset.screen);
|
|
130
|
+
const next = event.key === 'ArrowRight' ? (index + 1) % SCREENS.length
|
|
131
|
+
: event.key === 'ArrowLeft' ? (index + SCREENS.length - 1) % SCREENS.length
|
|
132
|
+
: event.key === 'Home' ? 0 : event.key === 'End' ? SCREENS.length - 1 : null;
|
|
133
|
+
if (next === null) return;
|
|
134
|
+
event.preventDefault(); this.selectScreen(SCREENS[next].id, true);
|
|
135
|
+
}
|
|
136
|
+
change(event) {
|
|
137
|
+
const filter = event.target.dataset.filter;
|
|
138
|
+
if (!(filter in this.filters)) return;
|
|
139
|
+
this.filters[filter] = event.target.value; this.selected = ''; this.pendingControl = null; this.render();
|
|
140
|
+
this.shadowRoot.querySelector(`[data-filter="${filter}"]`)?.focus();
|
|
141
|
+
}
|
|
142
|
+
click(event) {
|
|
143
|
+
const button = event.target.closest('button');
|
|
144
|
+
if (!button || button.disabled) return;
|
|
145
|
+
if (button.dataset.screen) this.selectScreen(button.dataset.screen, true);
|
|
146
|
+
else if (button.dataset.record) {
|
|
147
|
+
this.selected = button.dataset.record; this.pendingControl = null; this.render();
|
|
148
|
+
const selected = [...this.shadowRoot.querySelectorAll('[data-record]')].find((item) => item.dataset.record === this.selected);
|
|
149
|
+
selected?.focus();
|
|
150
|
+
} else if (button.dataset.action === 'refresh') void this.refresh();
|
|
151
|
+
else if (button.dataset.action === 'copy') void this.copyOutput();
|
|
152
|
+
else if (button.dataset.action === 'confirm-control') void this.executeControl(this.pendingControl?.index, true);
|
|
153
|
+
else if (button.dataset.action === 'dismiss-control') { this.pendingControl = null; this.render(); }
|
|
154
|
+
else if (button.dataset.control) void this.executeControl(Number(button.dataset.control));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async copyOutput() {
|
|
158
|
+
if (!this.inventory) return;
|
|
159
|
+
try {
|
|
160
|
+
if (!navigator.clipboard?.writeText) throw new Error('Clipboard is unavailable');
|
|
161
|
+
await navigator.clipboard.writeText(JSON.stringify(this.inventory, null, 2));
|
|
162
|
+
if (this.isConnected) this.feedback = 'Inventory copied';
|
|
163
|
+
} catch (error) { if (this.isConnected) this.feedback = `Copy failed: ${error.message}`; }
|
|
164
|
+
if (this.isConnected) this.render();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async executeControl(index, confirmed = false) {
|
|
168
|
+
const item = this.selectedRecord();
|
|
169
|
+
const action = item?.actions?.[index];
|
|
170
|
+
if (!action || typeof action.command !== 'string' || !Array.isArray(action.args) || !action.args.every((arg) => typeof arg === 'string')) return;
|
|
171
|
+
if (Array.isArray(this.host?.operations) && !this.host.operations.includes(action.command)) {
|
|
172
|
+
this.feedback = 'Action unavailable for this access level'; this.render(); return;
|
|
173
|
+
}
|
|
174
|
+
if (!['read', 'write', 'destructive', 'send'].includes(action.effect)) {
|
|
175
|
+
this.feedback = 'Action unavailable: source did not declare its effect'; this.render(); return;
|
|
176
|
+
}
|
|
177
|
+
if (action.effect !== 'read') {
|
|
178
|
+
if (!confirmed) { this.pendingControl = { key: item._key, index }; this.render(); return; }
|
|
179
|
+
if (this.pendingControl?.key !== item._key || this.pendingControl.index !== index) return;
|
|
180
|
+
}
|
|
181
|
+
this.pendingControl = null;
|
|
182
|
+
this.feedback = 'Submitting source action…'; this.render();
|
|
183
|
+
const request = this.request;
|
|
184
|
+
try {
|
|
185
|
+
const result = await this.invoke(action.command, action.args);
|
|
186
|
+
if (request !== this.request || !this.isConnected) return;
|
|
187
|
+
if (result?.success === false || result?.code > 0 || result?.exitCode > 0) throw new Error(result.error || 'Source rejected the action');
|
|
188
|
+
await this.refresh();
|
|
189
|
+
if (this.isConnected) this.feedback = 'Source action completed; inventory refreshed';
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (request !== this.request || !this.isConnected) return;
|
|
192
|
+
this.feedback = `Action failed: ${error.message}`;
|
|
193
|
+
}
|
|
194
|
+
if (this.isConnected) this.render();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
renderFilters() {
|
|
198
|
+
const records = this.records();
|
|
199
|
+
const options = (values, selected) => '<option value="">All</option>' + [...new Set(values.filter(Boolean))].sort().map((value) => `<option value="${escape(value)}" ${selected === value ? 'selected' : ''}>${escape(words(value))}</option>`).join('');
|
|
200
|
+
return `<div class="filters">
|
|
201
|
+
<label>Node<select data-filter="node">${'<option value="">All</option>' + (this.inventory?.nodes || []).map((node) => `<option value="${escape(this.nodeKey(node))}" ${this.filters.node === this.nodeKey(node) ? 'selected' : ''}>${escape(node.hostname || node.ip || 'Unnamed node')}</option>`).join('')}</select></label>
|
|
202
|
+
<label>Source<select data-filter="source">${options(records.map((item) => item.source), this.filters.source)}</select></label>
|
|
203
|
+
<label>Trigger<select data-filter="trigger">${options(records.map((item) => item.trigger?.kind), this.filters.trigger)}</select></label>
|
|
204
|
+
<label>State<select data-filter="state">${options(records.map((item) => item.state), this.filters.state)}</select></label>
|
|
205
|
+
</div>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
renderDetails(item, controls = false) {
|
|
209
|
+
if (!item) return '<section class="detail"><h3>No automation selected</h3><p>Select a record after discovery.</p></section>';
|
|
210
|
+
const knownBool = (value, yes, no) => value === true ? yes : value === false ? no : '— not reported';
|
|
211
|
+
const run = item.last_run;
|
|
212
|
+
return `<section class="detail"><p class="eyebrow">${escape(words(item.source || 'source not reported'))}</p><h3>${escape(item.name || item.intent || item.id || item.command)}</h3>
|
|
213
|
+
<dl>
|
|
214
|
+
<dt>Trigger</dt><dd>${escape(item.trigger?.summary || item.schedule || '— not reported')} <span class="subtle">${escape(item.trigger?.kind || '')}</span></dd>
|
|
215
|
+
<dt>Enabled</dt><dd>${escape(knownBool(item.enabled, 'Enabled', 'Disabled'))}</dd>
|
|
216
|
+
<dt>Runner</dt><dd>${escape(knownBool(item.runner_available, 'Available', 'Unavailable'))}</dd>
|
|
217
|
+
<dt>Node</dt><dd>${escape(item._node.hostname || item._node.ip || '— not reported')}</dd>
|
|
218
|
+
<dt>Placement</dt><dd>${escape(words(item.placement || 'not reported'))}</dd>
|
|
219
|
+
<dt>Origin</dt><dd class="mono">${escape(item.origin || item.id || '— not reported')}</dd>
|
|
220
|
+
<dt>Last handled slot</dt><dd>${escape(item.last_handled_slot || '— not recorded')}</dd>
|
|
221
|
+
<dt>Observed run</dt><dd>${run ? `${escape(words(run.state))} · ${escape(date(run.at))}${run.exit_code != null ? ` · exit ${escape(run.exit_code)}` : ''}` : '— no run outcome recorded'}</dd>
|
|
222
|
+
</dl>
|
|
223
|
+
${controls ? this.renderControls(item) : '<p class="note">A handled slot is scheduling evidence. It does not establish a successful run.</p>'}
|
|
224
|
+
</section>`;
|
|
225
|
+
}
|
|
226
|
+
renderControls(item) {
|
|
227
|
+
const actions = Array.isArray(item.actions) ? item.actions : [];
|
|
228
|
+
const pending = this.pendingControl?.key === item._key ? actions[this.pendingControl.index] : null;
|
|
229
|
+
return `<div class="source-actions">${actions.length ? actions.map((action, index) => `<button data-control="${index}" ${this.feedback === 'Submitting source action…' ? 'disabled' : ''}>${escape(action.label || words(action.command))}<span class="effect">${escape(action.effect || 'effect not declared')}</span></button>`).join('') : '<p>No executable controls were advertised by this source.</p>'}</div>${pending ? `<section class="confirmation" role="group" aria-label="Confirm source action"><p>${escape(pending.label || words(pending.command))}: <strong>${escape(item.name || item.id)}</strong>?</p><div class="source-actions"><button data-action="confirm-control">Confirm ${escape(pending.label || words(pending.command))}</button><button data-action="dismiss-control">Keep unchanged</button></div></section>` : ''}<p class="note">Actions are delegated to the source owner. Available controls depend on its capabilities and your access.</p>`;
|
|
230
|
+
}
|
|
231
|
+
renderScheduled() {
|
|
232
|
+
const records = this.filteredRecords();
|
|
233
|
+
return `${this.renderFilters()}<div class="ledger"><section class="index"><h3>Automation index <span class="count">${records.length}</span></h3>${records.length ? `<ul>${records.map((item) => `<li><button data-record="${escape(item._key)}" aria-pressed="${this.selectedRecord()?._key === item._key}"><span><strong>${escape(item.name || item.intent || item.id || item.command)}</strong><small>${escape(item._node.hostname || item._node.ip)} · ${escape(words(item.source || 'source not reported'))}</small></span><span class="state ${stateClass(item.state)}">${escape(words(item.state))}</span></button></li>`).join('')}</ul>` : '<p>No records match these filters.</p>'}</section>${this.renderDetails(this.selectedRecord())}</div>`;
|
|
234
|
+
}
|
|
235
|
+
renderNodes() {
|
|
236
|
+
const nodes = this.inventory?.nodes || [];
|
|
237
|
+
return `<div class="node-grid">${nodes.map((node) => `<section class="node"><header><h3>${escape(node.hostname || node.ip || 'Unnamed node')}</h3><span class="state ${stateClass(node.status)}">${escape(words(node.status))}</span></header><p class="mono">${escape(node.ip || node.node_id || '— address not reported')}</p>${node.error ? `<p class="bad">${escape(node.error)}</p>` : ''}${Array.isArray(node.coverage) && node.coverage.length ? `<ul class="coverage">${node.coverage.map((coverage) => `<li><div><strong>${escape(words(coverage.source))}</strong><span class="state ${stateClass(coverage.status)}">${escape(words(coverage.status))}</span></div>${coverage.error ? `<p>${escape(coverage.error)}</p>` : ''}<small>${escape(date(coverage.checked_at))}</small></li>`).join('')}</ul>` : '<p>— source coverage was not reported. This node cannot establish complete discovery.</p>'}</section>`).join('') || '<p>No mesh nodes were reported.</p>'}</div>`;
|
|
238
|
+
}
|
|
239
|
+
renderAudit() {
|
|
240
|
+
const records = this.filteredRecords();
|
|
241
|
+
return `${this.renderFilters()}<p class="note">Observed outcomes only. Declarations and handled slots do not prove execution.</p><ul class="audit">${records.map((item) => `<li><div><strong>${escape(item.name || item.id || item.command)}</strong><small>${escape(item._node.hostname || item._node.ip)} · ${escape(words(item.source))}</small></div><div><span class="state ${stateClass(item.last_run?.state)}">${escape(item.last_run ? words(item.last_run.state) : 'No observed run')}</span><small>${escape(date(item.last_run?.at))}</small>${item.last_run?.exit_code != null ? `<small>Exit ${escape(item.last_run.exit_code)}</small>` : ''}</div></li>`).join('') || '<li>No records match these filters.</li>'}</ul>`;
|
|
242
|
+
}
|
|
243
|
+
renderRun() {
|
|
244
|
+
const records = this.filteredRecords();
|
|
245
|
+
return `${this.renderFilters()}<div class="ledger"><section class="index"><h3>Choose a source-owned action</h3><p>Only advertised controls are executable.</p><ul>${records.map((item) => `<li><button data-record="${escape(item._key)}" aria-pressed="${this.selectedRecord()?._key === item._key}"><span><strong>${escape(item.name || item.id || item.command)}</strong><small>${escape(item._node.hostname || item._node.ip)} · ${(item.actions || []).length} advertised controls</small></span></button></li>`).join('')}</ul></section>${this.renderDetails(this.selectedRecord(), true)}</div>`;
|
|
246
|
+
}
|
|
247
|
+
render() {
|
|
248
|
+
const focus = this.shadowRoot.activeElement;
|
|
249
|
+
const focusedAction = focus?.dataset.action;
|
|
250
|
+
const status = this.phase === 'loading' ? 'Checking sources' : this.phase === 'error' ? 'Inventory unavailable'
|
|
251
|
+
: this.phase === 'ready' ? (this.inventory.complete === true ? 'Discovery complete' : 'Partial discovery — inspect source coverage') : 'Not checked';
|
|
252
|
+
const content = this.phase !== 'ready' ? `<div class="empty"><h3>${escape(status)}</h3><p>${escape(this.phase === 'error' ? this.error : this.phase === 'loading' ? 'Reading automation sources and node availability…' : 'Refresh to inspect automation sources.')}</p></div>`
|
|
253
|
+
: this.screen === 'nodes' ? this.renderNodes() : this.screen === 'audit' ? this.renderAudit() : this.screen === 'run' ? this.renderRun() : this.renderScheduled();
|
|
254
|
+
this.shadowRoot.innerHTML = `
|
|
255
|
+
<style>
|
|
256
|
+
:host { --face-bg:#181713; --face-surface:#24221c; --face-text:#f4efe5; --face-muted:#c5bca9; --face-border:#726652; --face-accent:#f2bd63; --face-error:#ffb5a9; --face-focus:#b8d9ff; --automation-bg:var(--bg-card,var(--face-bg)); --automation-surface:var(--bg-elevated,var(--face-surface)); --automation-text:var(--text-primary,var(--face-text)); --automation-muted:var(--text-secondary,var(--face-muted)); --automation-border:var(--border,var(--face-border)); --automation-accent:var(--accent,var(--face-accent)); --automation-error:var(--face-error); --automation-focus:var(--face-focus); display:block; color:var(--automation-text); font:14px/1.45 system-ui,sans-serif; min-width:0; color-scheme:dark; }
|
|
257
|
+
*,*::before,*::after { box-sizing:border-box; } button,select { font:inherit; color:inherit; min-height:44px; min-width:44px; } button { cursor:pointer; } button:disabled { cursor:wait; opacity:.6; } button:focus-visible,select:focus-visible { outline:3px solid var(--automation-focus); outline-offset:3px; } .workspace { border:1px solid var(--automation-border); border-radius:12px; background:var(--automation-bg); overflow:hidden; } .masthead { padding:20px; display:flex; gap:16px; align-items:center; flex-wrap:wrap; } .brand { display:flex; gap:12px; align-items:center; flex:1; min-width:0; } svg { width:40px; height:40px; color:var(--automation-accent); flex:none; } h2,h3,p { margin:0; } h2 { font-size:20px; letter-spacing:-.03em; } h3 { font-size:15px; } .eyebrow { text-transform:uppercase; letter-spacing:.11em; font-size:10px; color:var(--automation-muted); margin-bottom:4px; } .subtle,small,.note { color:var(--automation-muted); } small { display:block; font-size:11px; margin-top:3px; overflow-wrap:anywhere; } .refresh { border:1px solid var(--automation-accent); background:var(--automation-accent); color:var(--automation-bg); border-radius:7px; padding:8px 14px; font-weight:700; } .status-line { padding:12px 20px; background:var(--automation-surface); border-block:1px solid var(--automation-border); display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap; } .status-line strong { font-size:12px; font-weight:600; } .tabs { display:flex; padding:8px 12px 0; gap:4px; border-bottom:1px solid var(--automation-border); } .tabs button { flex:1; background:transparent; border:0; border-bottom:3px solid transparent; padding:10px 4px; color:var(--automation-muted); } .tabs button[aria-selected=true] { border-color:var(--automation-accent); color:var(--automation-text); font-weight:700; } .body { padding:16px; min-height:200px; animation:reveal .16s ease-out; } .filters { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; margin-bottom:16px; } label { font-size:11px; color:var(--automation-muted); display:grid; gap:4px; } select { width:100%; max-width:100%; background:var(--automation-surface); border:1px solid var(--automation-border); border-radius:5px; padding:6px; } .ledger { display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); gap:16px; } .index,.detail,.node { min-width:0; } .index h3 { margin-bottom:8px; } ul { list-style:none; padding:0; margin:0; } .index ul { max-height:380px; overflow:auto; } .index li+li { margin-top:6px; } .index button { width:100%; display:flex; justify-content:space-between; gap:12px; align-items:center; text-align:left; padding:12px; background:transparent; border:1px solid var(--automation-border); border-radius:7px; } .index button[aria-pressed=true] { border-color:var(--automation-accent); background:var(--automation-surface); } strong { overflow-wrap:anywhere; } .state { display:inline-block; text-transform:none; font:11px/1.4 ui-monospace,monospace; border:1px solid var(--automation-border); border-radius:4px; padding:3px 6px; overflow-wrap:anywhere; } .bad { color:var(--automation-error); } .detail { border-left:2px solid var(--automation-accent); padding:0 0 0 16px; } .detail h3 { font-size:19px; margin-bottom:12px; overflow-wrap:anywhere; } dl { display:grid; grid-template-columns:minmax(80px,.7fr) minmax(0,1fr); gap:9px 12px; margin:0; } dt { color:var(--automation-muted); font-size:11px; } dd { margin:0; overflow-wrap:anywhere; font-size:12px; } .mono { font-family:ui-monospace,monospace; overflow-wrap:anywhere; font-size:11px; } .note { font-size:11px; margin-top:16px; } .count { font:11px ui-monospace,monospace; margin-left:4px; color:var(--automation-muted); } .node-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr)); gap:16px; } .node { border:1px solid var(--automation-border); border-radius:8px; padding:14px; } .node header { display:flex; justify-content:space-between; align-items:start; gap:12px; margin-bottom:4px; } .coverage { margin-top:12px; } .coverage li { border-top:1px solid var(--automation-border); padding:10px 0; } .coverage li>div { display:flex; justify-content:space-between; align-items:center; gap:12px; } .coverage strong { font-size:12px; } .coverage p { font-size:12px; margin-top:6px; } .audit li { display:flex; justify-content:space-between; gap:16px; padding:12px 0; border-bottom:1px solid var(--automation-border); } .audit li>div { min-width:0; } .audit li>div:last-child { text-align:right; } .audit { margin-top:12px; } .source-actions { display:flex; flex-wrap:wrap; gap:8px; margin-top:16px; } .source-actions button { border:1px solid var(--automation-accent); border-radius:5px; background:var(--automation-surface); padding:8px 12px; } .effect { display:block; font:10px ui-monospace,monospace; color:var(--automation-muted); } .footer { display:flex; align-items:center; justify-content:space-between; padding:8px 16px; gap:12px; border-top:1px solid var(--automation-border); } .footer button { background:transparent; border:0; color:var(--automation-muted); padding:8px; font-size:12px; } .feedback { font-size:12px; overflow-wrap:anywhere; } .empty { padding:26px 12px; } .empty p { color:var(--automation-muted); margin-top:8px; } @keyframes reveal { from { opacity:.65; } to { opacity:1; } } @media(max-width:600px) { .masthead { padding:16px; gap:10px; } .filters { grid-template-columns:repeat(2,minmax(0,1fr)); } .ledger { grid-template-columns:minmax(0,1fr); } .detail { border-left:0; border-top:2px solid var(--automation-accent); padding:16px 0 0; } .index ul { max-height:260px; } .status-line { padding:10px 16px; } .body { padding:12px; } h2 { font-size:18px; } } @media(prefers-reduced-motion:reduce) { .body { animation:none; } }
|
|
258
|
+
</style>
|
|
259
|
+
<div class="workspace"><header class="masthead"><div class="brand"><svg viewBox="0 0 40 40" fill="none" aria-hidden="true"><path d="M4 20h9m0 0 7-11h12m-19 11 7 11h12M13 20h19" stroke="currentColor" stroke-width="2"/><circle cx="5" cy="20" r="3" fill="currentColor"/><path d="M30 6h6v6h-6zM30 17h6v6h-6zM30 28h6v6h-6z" stroke="currentColor" stroke-width="2"/></svg><div><p class="eyebrow">Triggers · sources · outcomes</p><h2>Automations</h2></div></div><button class="refresh" data-action="refresh" ${this.phase === 'loading' ? 'disabled' : ''}>Refresh</button></header>
|
|
260
|
+
<div class="status-line" role="status" aria-live="polite"><strong class="${this.phase === 'error' ? 'bad' : ''}">${escape(status)}</strong><small>${this.phase === 'ready' ? 'Checked' : 'Last attempt'} ${escape(date(this.phase === 'ready' ? this.inventory.checked_at : this.attemptedAt))}</small></div>
|
|
261
|
+
<nav class="tabs" role="tablist" aria-label="Automation views">${SCREENS.map((screen) => `<button id="tab-${screen.id}" role="tab" aria-controls="panel-${screen.id}" aria-selected="${this.screen === screen.id}" tabindex="${this.screen === screen.id ? '0' : '-1'}" data-screen="${screen.id}">${screen.label}</button>`).join('')}</nav>
|
|
262
|
+
<div class="body" id="panel-${this.screen}" role="tabpanel" aria-labelledby="tab-${this.screen}" tabindex="0" aria-busy="${this.phase === 'loading'}">${content}</div>
|
|
263
|
+
<footer class="footer"><span class="feedback" role="status">${escape(this.feedback || (this.phase === 'ready' ? 'Coverage determines completeness.' : 'No current inventory.'))}</span><button data-action="copy" ${this.inventory ? '' : 'disabled'}>Copy inventory</button></footer></div>`;
|
|
264
|
+
if (focusedAction) this.shadowRoot.querySelector(`[data-action="${focusedAction}"]`)?.focus();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Card and panel share the same inventory contract.
|
|
269
|
+
export const ChronosAutomationsCard = AutomationsCard;
|
|
270
|
+
export class AutomationsElement extends AutomationsCard {}
|
|
271
|
+
if (!customElements.get('tr-automations-card')) customElements.define('tr-automations-card', AutomationsCard);
|
|
272
|
+
if (!customElements.get('automations-card')) customElements.define('automations-card', class extends AutomationsCard {});
|
|
273
|
+
if (!customElements.get('tr-automations')) customElements.define('tr-automations', AutomationsElement);
|
|
@@ -17,7 +17,7 @@ export function findSkillDir(start = process.cwd()) {
|
|
|
17
17
|
|
|
18
18
|
export function runCore(script, args) {
|
|
19
19
|
if (!fs.existsSync(stateDir())) {
|
|
20
|
-
const error = new Error('No code-quality state in this repository (.agent/skills/code-quality). Create
|
|
20
|
+
const error = new Error('No code-quality state in this repository (.agent/skills/code-quality). Create configuration: total-recall code-quality detect --apply');
|
|
21
21
|
error.exitCode = 2;
|
|
22
22
|
throw error;
|
|
23
23
|
}
|
|
@@ -80,7 +80,7 @@ function loadConfig() {
|
|
|
80
80
|
if (!existsSync(CONFIG_PATH)) {
|
|
81
81
|
fail(
|
|
82
82
|
`No config.json at ${CONFIG_PATH}\n` +
|
|
83
|
-
`Run: total-recall
|
|
83
|
+
`Run: total-recall code-quality detect --apply`
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
let cfg;
|
|
@@ -678,6 +678,51 @@ function runCheck(check) {
|
|
|
678
678
|
|
|
679
679
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
680
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Restore the repository's locked dependencies before any gate runs. Idle repositories have
|
|
683
|
+
* their node_modules removed by the scheduled disk cleanup, and a gate run against a missing
|
|
684
|
+
* tree reports import failures as if the code were broken. Installs exactly what the lockfile
|
|
685
|
+
* pins, only when the installed tree is missing or older than the lockfile.
|
|
686
|
+
*/
|
|
687
|
+
const LOCKFILES = [
|
|
688
|
+
{ file: 'package-lock.json', manager: 'npm', marker: path.join('node_modules', '.package-lock.json'), args: ['ci', '--no-audit', '--no-fund'] },
|
|
689
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm', marker: path.join('node_modules', '.modules.yaml'), args: ['install', '--frozen-lockfile'] },
|
|
690
|
+
{ file: 'yarn.lock', manager: 'yarn', marker: path.join('node_modules', '.yarn-integrity'), args: ['install', '--frozen-lockfile'] },
|
|
691
|
+
];
|
|
692
|
+
|
|
693
|
+
function dependencyState(root) {
|
|
694
|
+
if (!existsSync(path.join(root, 'package.json'))) return { action: 'none', reason: 'no package.json' };
|
|
695
|
+
const lock = LOCKFILES.find((entry) => existsSync(path.join(root, entry.file)));
|
|
696
|
+
if (!lock) return { action: 'none', reason: 'no lockfile' };
|
|
697
|
+
const marker = path.join(root, lock.marker);
|
|
698
|
+
const stale = !existsSync(marker) || statSync(marker).mtimeMs < statSync(path.join(root, lock.file)).mtimeMs;
|
|
699
|
+
return { action: stale ? 'install' : 'present', manager: lock.manager, lockfile: lock.file, args: lock.args };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function ensureDependencies() {
|
|
703
|
+
const state = dependencyState(REPO_ROOT);
|
|
704
|
+
if (config.installDependencies === false || state.action !== 'install') {
|
|
705
|
+
return { ...state, action: config.installDependencies === false && state.action === 'install' ? 'disabled' : state.action, args: undefined };
|
|
706
|
+
}
|
|
707
|
+
const started = Date.now();
|
|
708
|
+
console.error(` → dependencies: ${state.manager} ${state.args.join(' ')} (${state.lockfile})`);
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
const child = spawn(state.manager, state.args, { cwd: REPO_ROOT, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
711
|
+
currentChild = child;
|
|
712
|
+
let tail = '';
|
|
713
|
+
const keep = (chunk) => { tail = (tail + chunk).slice(-4000); };
|
|
714
|
+
child.stdout.on('data', keep);
|
|
715
|
+
child.stderr.on('data', keep);
|
|
716
|
+
const done = (code, error) => {
|
|
717
|
+
currentChild = null;
|
|
718
|
+
resolve({ action: code === 0 ? 'installed' : 'failed', manager: state.manager, lockfile: state.lockfile,
|
|
719
|
+
durationMs: Date.now() - started, ...(code === 0 ? {} : { error: error || tail.trim().split('\n').slice(-20).join('\n') }) });
|
|
720
|
+
};
|
|
721
|
+
child.on('error', (err) => done(null, err.message));
|
|
722
|
+
child.on('close', (code) => done(code));
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
681
726
|
async function main() {
|
|
682
727
|
const wantTier = TIER_ORDER[TIER];
|
|
683
728
|
if (!wantTier) fail(`Unknown --tier ${TIER} (expected: fast | full | remote)`);
|
|
@@ -709,10 +754,16 @@ async function main() {
|
|
|
709
754
|
|
|
710
755
|
const startedAt = new Date().toISOString();
|
|
711
756
|
const startedMs = Date.now();
|
|
757
|
+
const testedHead = gitHead();
|
|
712
758
|
console.error(` [code-quality] ${config.toolchain} — ${checks.length} check(s) in ${REPO_ROOT}`);
|
|
713
759
|
|
|
760
|
+
const dependencies = await ensureDependencies();
|
|
761
|
+
if (dependencies.action === 'failed') {
|
|
762
|
+
console.error(` dependency install failed (${dependencies.manager}, ${dependencies.lockfile}); no gate ran:\n${dependencies.error}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
714
765
|
const results = [];
|
|
715
|
-
for (const check of checks) {
|
|
766
|
+
for (const check of dependencies.action === 'failed' ? [] : checks) {
|
|
716
767
|
const label = check.kind === 'grep-forbid'
|
|
717
768
|
? `grep-forbid (${(check.patterns || []).length} pattern(s))`
|
|
718
769
|
: (check.cmd || []).join(' ');
|
|
@@ -739,15 +790,16 @@ async function main() {
|
|
|
739
790
|
tier: TIER,
|
|
740
791
|
// Coverage is stated explicitly so a partial run can never be mistaken
|
|
741
792
|
// for a clean full run when someone reads the report later.
|
|
742
|
-
ranChecks:
|
|
793
|
+
ranChecks: results.map((r) => r.id),
|
|
743
794
|
skippedChecks: config.checks.filter((c) => !checks.includes(c)).map((c) => ({ id: c.id, tier: c.tier || 'fast' })),
|
|
744
795
|
startedAt,
|
|
745
796
|
finishedAt: new Date().toISOString(),
|
|
746
797
|
durationMs: Date.now() - startedMs,
|
|
747
|
-
gitHead:
|
|
798
|
+
gitHead: testedHead,
|
|
748
799
|
sourceFileCount: sources.length,
|
|
800
|
+
dependencies,
|
|
749
801
|
checks: results.map(({ raw, findings: _f, ...meta }) => meta),
|
|
750
|
-
infrastructureFailure: results.some((r) => r.spawnError || r.timedOut),
|
|
802
|
+
infrastructureFailure: dependencies.action === 'failed' || results.some((r) => r.spawnError || r.timedOut),
|
|
751
803
|
totalFindings: findings.length,
|
|
752
804
|
findings
|
|
753
805
|
};
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Status, most severe first:
|
|
13
13
|
* missing no report file
|
|
14
14
|
* unreadable report file is not valid JSON or has an unknown shape
|
|
15
|
+
* foreign report belongs to a different repository root
|
|
15
16
|
* infrastructure a gate failed to spawn or timed out; results are incomplete
|
|
16
17
|
* unparsed-failure a gate exited non-zero with no parseable findings
|
|
17
18
|
* failed findings or a failing gate
|
|
@@ -21,27 +22,34 @@
|
|
|
21
22
|
* partial clean, but higher-tier gates were skipped
|
|
22
23
|
* clean every gate ran, nothing changed since
|
|
23
24
|
*/
|
|
24
|
-
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
25
|
+
import { readFileSync, existsSync, statSync, realpathSync } from 'node:fs';
|
|
25
26
|
import { execFileSync } from 'node:child_process';
|
|
26
27
|
import path from 'node:path';
|
|
27
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { stateDir } from './state.mjs';
|
|
28
30
|
|
|
29
31
|
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.vue', '.svelte']);
|
|
30
32
|
|
|
31
33
|
function git(cwd, args) {
|
|
32
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 20_000 });
|
|
34
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024, timeout: 20_000 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve the caller's working tree independently of cached report metadata.
|
|
38
|
+
function repositoryRoot(cwd) {
|
|
39
|
+
try { return realpathSync(git(cwd, ['rev-parse', '--show-toplevel']).trim()); }
|
|
40
|
+
catch { return realpathSync(cwd); }
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/** Returns { head, changed } or null when git cannot be read. */
|
|
36
|
-
function freshness(report) {
|
|
44
|
+
function freshness(report, repoRoot) {
|
|
37
45
|
try {
|
|
38
|
-
const head = git(
|
|
46
|
+
const head = git(repoRoot, ['rev-parse', '--verify', 'HEAD']).trim();
|
|
39
47
|
const startedMs = Date.parse(report.startedAt);
|
|
40
48
|
const changed = [];
|
|
41
|
-
for (const rel of git(
|
|
49
|
+
for (const rel of git(repoRoot, ['ls-files', '-z']).split('\0')) {
|
|
42
50
|
if (!rel || !SOURCE_EXTS.has(path.extname(rel))) continue;
|
|
43
51
|
try {
|
|
44
|
-
if (statSync(path.join(
|
|
52
|
+
if (statSync(path.join(repoRoot, rel)).mtimeMs > startedMs) changed.push(rel);
|
|
45
53
|
} catch { /* deleted since the run */ }
|
|
46
54
|
}
|
|
47
55
|
return { head, changed };
|
|
@@ -50,10 +58,13 @@ function freshness(report) {
|
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
export function buildReportView(reportFile, { now = Date.now(), inspectGit = true } = {}) {
|
|
54
|
-
const base = { reportFile, status: 'missing', reasons: [] };
|
|
61
|
+
export function buildReportView(reportFile, { now = Date.now(), inspectGit = true, cwd = process.cwd() } = {}) {
|
|
62
|
+
const base = { reportFile, status: 'missing', clean: false, reasons: [] };
|
|
55
63
|
if (!existsSync(reportFile)) {
|
|
56
|
-
|
|
64
|
+
const configured = existsSync(path.join(stateDir(cwd), 'config.json'));
|
|
65
|
+
return { ...base, reasons: [configured
|
|
66
|
+
? 'No report file. Run code-quality-check, then reload.'
|
|
67
|
+
: 'No code-quality configuration. Run total-recall code-quality detect --apply, then run code-quality-check.'] };
|
|
57
68
|
}
|
|
58
69
|
let report;
|
|
59
70
|
try {
|
|
@@ -67,6 +78,19 @@ export function buildReportView(reportFile, { now = Date.now(), inspectGit = tru
|
|
|
67
78
|
return { ...base, status: 'unreadable', reasons: ['Report does not have the code-quality schema (checks, ranChecks, startedAt).'] };
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
if (typeof report.repoRoot !== 'string' || !path.isAbsolute(report.repoRoot)) {
|
|
82
|
+
return { ...base, status: 'unreadable', reasons: ['Report has no absolute repository root. Run code-quality-check in this repository.'] };
|
|
83
|
+
}
|
|
84
|
+
let repoRoot;
|
|
85
|
+
try {
|
|
86
|
+
repoRoot = repositoryRoot(cwd);
|
|
87
|
+
if (realpathSync(report.repoRoot) !== repoRoot) {
|
|
88
|
+
return { ...base, status: 'foreign', reasons: ['Report belongs to another repository. Run code-quality-check in this repository.'] };
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
return { ...base, status: 'unreadable', reasons: ['Cannot verify report repository root. Run code-quality-check in this repository.'] };
|
|
92
|
+
}
|
|
93
|
+
|
|
70
94
|
const findings = Array.isArray(report.findings) ? report.findings : [];
|
|
71
95
|
const hard = findings.filter((f) => f.severity !== 'warning' && f.severity !== 'info');
|
|
72
96
|
const skipped = Array.isArray(report.skippedChecks) ? report.skippedChecks : [];
|
|
@@ -87,8 +111,9 @@ export function buildReportView(reportFile, { now = Date.now(), inspectGit = tru
|
|
|
87
111
|
(c) => !c.ok && !c.spawnError && !c.timedOut && !findings.some((f) => f.check === c.id)
|
|
88
112
|
);
|
|
89
113
|
const zeroScope = report.checks.filter((c) => c.scanned === 0);
|
|
90
|
-
const fresh = inspectGit ? freshness(report) : null;
|
|
91
|
-
const
|
|
114
|
+
const fresh = inspectGit ? freshness(report, repoRoot) : null;
|
|
115
|
+
const revisionKnown = typeof report.gitHead === 'string' && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(report.gitHead);
|
|
116
|
+
const headMoved = Boolean(fresh && revisionKnown && fresh.head !== report.gitHead);
|
|
92
117
|
|
|
93
118
|
const reasons = [];
|
|
94
119
|
let status = 'clean';
|
|
@@ -102,7 +127,8 @@ export function buildReportView(reportFile, { now = Date.now(), inspectGit = tru
|
|
|
102
127
|
}
|
|
103
128
|
if (report.ranChecks.length === 0) set('zero-scope', 'No gates ran');
|
|
104
129
|
if (zeroScope.length) set('zero-scope', `${zeroScope.map((c) => c.id).join(', ')}: scanned 0 files`);
|
|
105
|
-
if (
|
|
130
|
+
if (!revisionKnown) set('unknown-freshness', 'Report has no verified revision. Run code-quality-check in this repository.');
|
|
131
|
+
if (fresh === null) set('unknown-freshness', 'git freshness could not be verified; cannot tell whether this report matches the tree');
|
|
106
132
|
else if (fresh && (fresh.changed.length || headMoved)) {
|
|
107
133
|
set('stale', headMoved
|
|
108
134
|
? `HEAD moved from ${String(report.gitHead).slice(0, 8)} to ${fresh.head.slice(0, 8)}`
|
|
@@ -116,7 +142,7 @@ export function buildReportView(reportFile, { now = Date.now(), inspectGit = tru
|
|
|
116
142
|
reasons,
|
|
117
143
|
clean: status === 'clean',
|
|
118
144
|
toolchain: report.toolchain,
|
|
119
|
-
repoRoot
|
|
145
|
+
repoRoot,
|
|
120
146
|
tier: report.tier,
|
|
121
147
|
gitHead: report.gitHead ?? null,
|
|
122
148
|
currentHead: fresh?.head ?? null,
|
|
@@ -172,7 +198,7 @@ export async function printCachedReport(args, reportFile) {
|
|
|
172
198
|
}
|
|
173
199
|
|
|
174
200
|
export function defaultReportFile() {
|
|
175
|
-
return path.
|
|
201
|
+
return path.join(stateDir(), 'reports', 'latest.json');
|
|
176
202
|
}
|
|
177
203
|
|
|
178
204
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -17,12 +17,11 @@
|
|
|
17
17
|
* report.mjs --review-diagnostics [--json] cached gate and finding details
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { readFileSync, existsSync
|
|
21
|
-
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
22
21
|
import path from 'node:path';
|
|
23
22
|
import { fileURLToPath } from 'node:url';
|
|
24
23
|
import { stateDir } from './state.mjs';
|
|
25
|
-
import { printCachedReport, wantsStructuredReport } from './report-data.mjs';
|
|
24
|
+
import { buildReportView, printCachedReport, wantsStructuredReport } from './report-data.mjs';
|
|
26
25
|
|
|
27
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
27
|
const __dirname = path.dirname(__filename);
|
|
@@ -39,39 +38,15 @@ if (wantsStructuredReport(process.argv.slice(2))) {
|
|
|
39
38
|
process.exit(await printCachedReport(process.argv.slice(2), REPORT_JSON));
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.
|
|
45
|
-
|
|
46
|
-
process.exit(0);
|
|
41
|
+
const view = buildReportView(REPORT_JSON);
|
|
42
|
+
if (['missing', 'unreadable', 'foreign'].includes(view.status)) {
|
|
43
|
+
console.error(`Code Quality report: ${view.status}\n${view.reasons.join('\n')}`);
|
|
44
|
+
process.exit(1);
|
|
47
45
|
}
|
|
46
|
+
process.exitCode = view.clean ? 0 : 1;
|
|
48
47
|
|
|
49
48
|
const report = JSON.parse(readFileSync(REPORT_JSON, 'utf8'));
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
// ─── Staleness: which files changed after the run started? ────────────────────
|
|
53
|
-
|
|
54
|
-
function changedSinceRun() {
|
|
55
|
-
let files = [];
|
|
56
|
-
try {
|
|
57
|
-
const out = execFileSync('git', ['ls-files', '-z'], {
|
|
58
|
-
cwd: report.repoRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 20_000
|
|
59
|
-
});
|
|
60
|
-
const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.vue', '.svelte']);
|
|
61
|
-
files = out.split('\0').filter((f) => f && exts.has(path.extname(f)));
|
|
62
|
-
} catch {
|
|
63
|
-
return null; // cannot determine — say so rather than implying freshness
|
|
64
|
-
}
|
|
65
|
-
const changed = [];
|
|
66
|
-
for (const rel of files) {
|
|
67
|
-
try {
|
|
68
|
-
if (statSync(path.join(report.repoRoot, rel)).mtimeMs > startedMs) changed.push(rel);
|
|
69
|
-
} catch { /* deleted since */ }
|
|
70
|
-
}
|
|
71
|
-
return changed;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const changed = changedSinceRun();
|
|
49
|
+
const changed = view.changedSinceRun;
|
|
75
50
|
const ageMin = Math.round((Date.now() - Date.parse(report.finishedAt)) / 60000);
|
|
76
51
|
|
|
77
52
|
// ─── Header ───────────────────────────────────────────────────────────────────
|
|
@@ -114,11 +89,11 @@ if (opaque.length) {
|
|
|
114
89
|
}
|
|
115
90
|
}
|
|
116
91
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
92
|
+
console.log(` Report status: ${view.status}`);
|
|
93
|
+
for (const reason of view.reasons) console.log(` ${reason}`);
|
|
94
|
+
if (changed !== null && changed.length === 0 && view.currentHead === view.gitHead) {
|
|
120
95
|
console.log(` FRESH — no tracked source file has changed since this run started.`);
|
|
121
|
-
} else {
|
|
96
|
+
} else if (changed?.length) {
|
|
122
97
|
console.log(` STALE for ${changed.length} file(s) edited after the run started:`);
|
|
123
98
|
for (const f of changed.slice(0, 8)) console.log(` ${f}`);
|
|
124
99
|
if (changed.length > 8) console.log(` … and ${changed.length - 8} more`);
|
|
@@ -146,15 +121,15 @@ const staleMark = (file) => (changed?.includes(file) ? ' ' : '');
|
|
|
146
121
|
if (mode === 'raw') {
|
|
147
122
|
if (!existsSync(REPORT_TXT)) { console.error('No raw output stored.'); process.exit(1); }
|
|
148
123
|
const raw = readFileSync(REPORT_TXT, 'utf8');
|
|
149
|
-
if (!arg) { console.log(raw.slice(0, 20000)); process.exit(0); }
|
|
124
|
+
if (!arg) { console.log(raw.slice(0, 20000)); process.exit(view.clean ? 0 : 1); }
|
|
150
125
|
const section = raw.split(/^# /m).find((s) => s.startsWith(arg));
|
|
151
126
|
console.log(section ? `# ${section}`.slice(0, 40000) : `No raw section for check "${arg}".`);
|
|
152
|
-
process.exit(section ? 0 : 1);
|
|
127
|
+
process.exit(section && view.clean ? 0 : 1);
|
|
153
128
|
}
|
|
154
129
|
|
|
155
130
|
if (findings.length === 0) {
|
|
156
131
|
console.log(' No findings.');
|
|
157
|
-
process.exit(0);
|
|
132
|
+
process.exit(view.clean ? 0 : 1);
|
|
158
133
|
}
|
|
159
134
|
|
|
160
135
|
switch (mode) {
|