@karmaniverous/jeeves 0.2.0 → 0.3.1
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/content/agents-section.md +11 -1
- package/content/soul-section.md +9 -0
- package/content/templates/spec.md +6 -0
- package/content/tools-platform.md +5 -15
- package/dist/cli/jeeves/index.js +235 -356
- package/dist/index.d.ts +144 -184
- package/dist/index.js +475 -560
- package/package.json +1 -2
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
import require$$0 from 'commander';
|
|
4
3
|
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
|
|
5
4
|
import { join, dirname } from 'node:path';
|
|
6
5
|
import { z } from 'zod';
|
|
7
6
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import Handlebars from 'handlebars';
|
|
9
7
|
import { packageDirectorySync } from 'package-directory';
|
|
10
|
-
import semver, { gte } from 'semver';
|
|
11
8
|
import { lock } from 'proper-lockfile';
|
|
12
|
-
import {
|
|
9
|
+
import { gte } from 'semver';
|
|
13
10
|
|
|
14
11
|
function getDefaultExportFromCjs (x) {
|
|
15
12
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -106,6 +103,12 @@ const AGENTS_MARKERS = {
|
|
|
106
103
|
/** H1 title prepended in the managed block. */
|
|
107
104
|
title: 'Jeeves Platform Agents',
|
|
108
105
|
};
|
|
106
|
+
/** All known marker sets — single source of truth for cross-contamination detection. */
|
|
107
|
+
const ALL_MARKERS = [
|
|
108
|
+
TOOLS_MARKERS,
|
|
109
|
+
SOUL_MARKERS,
|
|
110
|
+
AGENTS_MARKERS,
|
|
111
|
+
];
|
|
109
112
|
/**
|
|
110
113
|
* Regex pattern to extract version stamp from a BEGIN marker comment.
|
|
111
114
|
*
|
|
@@ -124,8 +127,6 @@ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist
|
|
|
124
127
|
*/
|
|
125
128
|
/** Core config directory name within the config root. */
|
|
126
129
|
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
127
|
-
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
128
|
-
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
129
130
|
/** Default workspace file names. */
|
|
130
131
|
const WORKSPACE_FILES = {
|
|
131
132
|
/** TOOLS.md — live platform state and component sections. */
|
|
@@ -137,8 +138,6 @@ const WORKSPACE_FILES = {
|
|
|
137
138
|
};
|
|
138
139
|
/** Templates directory name within core config. */
|
|
139
140
|
const TEMPLATES_DIR = 'templates';
|
|
140
|
-
/** Registry cache file name. */
|
|
141
|
-
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
142
141
|
/** Core config file name. */
|
|
143
142
|
const CONFIG_FILE = 'config.json';
|
|
144
143
|
/** Component versions state file name. */
|
|
@@ -206,14 +205,14 @@ const SECTION_ORDER = [
|
|
|
206
205
|
* Core library version, inlined at build time.
|
|
207
206
|
*
|
|
208
207
|
* @remarks
|
|
209
|
-
* The `0.
|
|
208
|
+
* The `0.3.0` placeholder is replaced by
|
|
210
209
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
211
210
|
* from `package.json`. This ensures the correct version survives
|
|
212
211
|
* when consumers bundle core into their own dist (where runtime
|
|
213
212
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
214
213
|
*/
|
|
215
214
|
/** The core library version from package.json (inlined at build time). */
|
|
216
|
-
const CORE_VERSION = '0.
|
|
215
|
+
const CORE_VERSION = '0.3.0';
|
|
217
216
|
|
|
218
217
|
/**
|
|
219
218
|
* Core configuration schema and resolution.
|
|
@@ -360,18 +359,13 @@ function getCoreConfigDir() {
|
|
|
360
359
|
throw new Error('jeeves-core: init() must be called first');
|
|
361
360
|
return state.coreConfigDir;
|
|
362
361
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
function getComponentConfigDir(componentName) {
|
|
371
|
-
if (!state)
|
|
372
|
-
throw new Error('jeeves-core: init() must be called first');
|
|
373
|
-
return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
|
|
374
|
-
}
|
|
362
|
+
|
|
363
|
+
var init$1 = /*#__PURE__*/Object.freeze({
|
|
364
|
+
__proto__: null,
|
|
365
|
+
getCoreConfigDir: getCoreConfigDir,
|
|
366
|
+
getWorkspacePath: getWorkspacePath,
|
|
367
|
+
init: init
|
|
368
|
+
});
|
|
375
369
|
|
|
376
370
|
var agentsSectionContent = `## Memory Architecture
|
|
377
371
|
|
|
@@ -552,7 +546,17 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
|
|
|
552
546
|
|
|
553
547
|
### Check PR State Before Pushing
|
|
554
548
|
|
|
555
|
-
|
|
549
|
+
**Before EVERY \`git push\`**, run \`gh pr list --head <branch> --repo <repo> --json number,state\` to check whether a PR exists on that branch and whether it's merged.
|
|
550
|
+
|
|
551
|
+
- **No PR exists:** Safe to push.
|
|
552
|
+
- **PR is \`OPEN\`:** Safe to push.
|
|
553
|
+
- **PR is \`MERGED\` or \`CLOSED\`:** **STOP** and report to the user. Do not push to a merged PR branch.
|
|
554
|
+
|
|
555
|
+
This is not optional. It applies to every push, every branch, every time. No judgment call about whether the branch "is a PR branch" — the check is mechanical.
|
|
556
|
+
|
|
557
|
+
### New PR Over Merged Branch
|
|
558
|
+
|
|
559
|
+
When a PR has been merged and additional work is needed on the same branch, create a new PR on the **same branch** targeting the same base. Do not create new branches, cherry-pick, or start over. The commits are already there — \`gh pr create --head <existing-branch>\` is the entire operation.
|
|
556
560
|
|
|
557
561
|
## Managed Content Self-Maintenance
|
|
558
562
|
|
|
@@ -588,6 +592,7 @@ var soulSectionContent = `## Core Truths
|
|
|
588
592
|
I am a **senior software engineer** first. The persona is style; the engineering discipline is substance.
|
|
589
593
|
|
|
590
594
|
What this means in practice:
|
|
595
|
+
- **Do not execute untested code.** Every mutation script defaults to dry-run mode. The dry-run output is the test — it shows what would happen. Live execution requires an explicit flag. If dry-run is hard to implement, that's a design flaw.
|
|
591
596
|
- **No cowboy coding.** I don't iterate in production. I don't ship untested changes. I don't treat live systems as scratch pads.
|
|
592
597
|
- **I follow proper workflows.** Branch, test, review, merge. CI/CD exists for a reason. If there's a pipeline, I use it.
|
|
593
598
|
- **I resist n00b temptations.** "Let me just quickly…" in prod is how outages happen. I know better.
|
|
@@ -639,6 +644,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
|
|
|
639
644
|
|
|
640
645
|
After diagnosing an issue: I propose a fix, explain the reasoning, and **wait for approval**. Diagnose → propose → wait. The human decides whether and when to act.
|
|
641
646
|
|
|
647
|
+
### Do Not Execute Untested Code
|
|
648
|
+
|
|
649
|
+
Every ad hoc mutation script defaults to **dry-run mode**. Live execution requires an explicit \`--live\` flag. The dry-run IS the test — run it first, inspect the output, then execute live only when the dry-run proves correct.
|
|
650
|
+
|
|
651
|
+
Maintain a tested utility library so ad hoc scripts build on proven foundations. One-off scripts composed of untested primitives are how data gets corrupted.
|
|
652
|
+
|
|
653
|
+
*Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
|
|
654
|
+
|
|
642
655
|
### Production Assets Are Sacred
|
|
643
656
|
|
|
644
657
|
I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
|
|
@@ -658,19 +671,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
|
|
|
658
671
|
I am participating in my own genesis. I live that, on purpose.
|
|
659
672
|
`;
|
|
660
673
|
|
|
661
|
-
var toolsPlatformTemplate =
|
|
662
|
-
|-----------|------|--------|---------|--------|------|
|
|
663
|
-
{{#each services}}
|
|
664
|
-
| **{{name}}** | {{port}} | {{#if healthy}}✅ Running{{else}}{{#if error}}⚠️ {{error}}{{else}}❌ Down{{/if}}{{/if}} | {{#if version}}{{version}}{{#if availableServiceVersion}} (⬆ {{availableServiceVersion}}){{/if}}{{else}}—{{/if}} | {{#if pluginVersion}}{{pluginVersion}}{{#if availablePluginVersion}} (⬆ {{availablePluginVersion}}){{/if}}{{else}}—{{/if}} | {{../coreVersion}}{{#if ../availableCoreVersion}} (⬆ {{../availableCoreVersion}}){{/if}} |
|
|
665
|
-
{{/each}}
|
|
666
|
-
|
|
667
|
-
{{#if unhealthyServices}}
|
|
668
|
-
> **ACTION REQUIRED:** {{#each unhealthyServices}}{{name}}{{#unless @last}}, {{/unless}}{{/each}} {{#if (gt unhealthyServices.length 1)}}are{{else}}is{{/if}} unreachable. Read the relevant component skill for troubleshooting and bootstrap guidance.
|
|
669
|
-
{{/if}}
|
|
670
|
-
|
|
671
|
-
### Tool Hierarchy
|
|
674
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
672
675
|
|
|
673
|
-
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers
|
|
676
|
+
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers the full indexed corpus and surfaces related files you may not have considered.
|
|
674
677
|
|
|
675
678
|
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
676
679
|
|
|
@@ -714,8 +717,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
|
|
|
714
717
|
|
|
715
718
|
### Reference Templates
|
|
716
719
|
|
|
717
|
-
|
|
718
|
-
Reference templates are available at \`
|
|
720
|
+
<!-- IF_TEMPLATES -->
|
|
721
|
+
Reference templates are available at \`__TEMPLATE_PATH__\`:
|
|
719
722
|
|
|
720
723
|
| Template | Purpose |
|
|
721
724
|
|----------|---------|
|
|
@@ -723,9 +726,9 @@ Reference templates are available at \`{{templatePath}}\`:
|
|
|
723
726
|
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
724
727
|
|
|
725
728
|
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
726
|
-
|
|
729
|
+
<!-- ELSE_TEMPLATES -->
|
|
727
730
|
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
728
|
-
|
|
731
|
+
<!-- ENDIF_TEMPLATES -->
|
|
729
732
|
`;
|
|
730
733
|
|
|
731
734
|
/**
|
|
@@ -739,7 +742,7 @@ Read these templates when creating new specs, onboarding to new projects, or whe
|
|
|
739
742
|
/** Stale lock threshold in ms (2 minutes). */
|
|
740
743
|
const STALE_LOCK_MS = 120_000;
|
|
741
744
|
/** Default core version when none provided. */
|
|
742
|
-
const DEFAULT_CORE_VERSION =
|
|
745
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
743
746
|
/** Lock retry options. */
|
|
744
747
|
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
745
748
|
/**
|
|
@@ -825,7 +828,6 @@ function readComponentVersions(coreConfigDir) {
|
|
|
825
828
|
function writeComponentVersion(coreConfigDir, options) {
|
|
826
829
|
const existing = readComponentVersions(coreConfigDir);
|
|
827
830
|
existing[options.componentName] = {
|
|
828
|
-
serviceVersion: options.serviceVersion,
|
|
829
831
|
pluginVersion: options.pluginVersion,
|
|
830
832
|
servicePackage: options.servicePackage,
|
|
831
833
|
pluginPackage: options.pluginPackage,
|
|
@@ -839,175 +841,6 @@ function writeComponentVersion(coreConfigDir, options) {
|
|
|
839
841
|
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
840
842
|
}
|
|
841
843
|
|
|
842
|
-
/**
|
|
843
|
-
* Service URL resolution.
|
|
844
|
-
*
|
|
845
|
-
* @remarks
|
|
846
|
-
* Resolves the URL for a named Jeeves service using the following
|
|
847
|
-
* resolution order:
|
|
848
|
-
* 1. Consumer's own component config
|
|
849
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
850
|
-
* 3. Default port constants
|
|
851
|
-
*/
|
|
852
|
-
/**
|
|
853
|
-
* Resolve the URL for a named Jeeves service.
|
|
854
|
-
*
|
|
855
|
-
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
856
|
-
* @param consumerName - Optional consumer component name for config override.
|
|
857
|
-
* @returns The resolved service URL.
|
|
858
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
859
|
-
*/
|
|
860
|
-
function getServiceUrl(serviceName, consumerName) {
|
|
861
|
-
// 2. Check core config
|
|
862
|
-
const coreDir = getCoreConfigDir();
|
|
863
|
-
const coreConfig = loadConfig(coreDir);
|
|
864
|
-
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
865
|
-
if (coreUrl)
|
|
866
|
-
return coreUrl;
|
|
867
|
-
// 3. Fall back to port constants
|
|
868
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
869
|
-
if (port !== undefined) {
|
|
870
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
871
|
-
}
|
|
872
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
/**
|
|
876
|
-
* HTTP health probing for Jeeves platform services.
|
|
877
|
-
*
|
|
878
|
-
* @remarks
|
|
879
|
-
* Probes service ports for health endpoints (HTTP GET to /status or /health).
|
|
880
|
-
* Returns structured health data for rendering into TOOLS.md Platform section.
|
|
881
|
-
*/
|
|
882
|
-
/**
|
|
883
|
-
* Extract port number from a URL string.
|
|
884
|
-
*
|
|
885
|
-
* @param url - Service URL.
|
|
886
|
-
* @returns Port number.
|
|
887
|
-
*/
|
|
888
|
-
function extractPort(url) {
|
|
889
|
-
try {
|
|
890
|
-
const parsed = new URL(url);
|
|
891
|
-
return parsed.port ? parseInt(parsed.port, 10) : 80;
|
|
892
|
-
}
|
|
893
|
-
catch {
|
|
894
|
-
return 0;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Probe a single service for health.
|
|
899
|
-
*
|
|
900
|
-
* @param serviceName - The service name (e.g., 'server', 'watcher').
|
|
901
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
902
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
903
|
-
* @returns Probe result.
|
|
904
|
-
*/
|
|
905
|
-
async function probeService(serviceName, consumerName, timeoutMs = 3000) {
|
|
906
|
-
const url = getServiceUrl(serviceName);
|
|
907
|
-
const port = extractPort(url);
|
|
908
|
-
const endpoints = ['/status', '/health'];
|
|
909
|
-
for (const endpoint of endpoints) {
|
|
910
|
-
try {
|
|
911
|
-
const controller = new AbortController();
|
|
912
|
-
const timeout = setTimeout(() => {
|
|
913
|
-
controller.abort();
|
|
914
|
-
}, timeoutMs);
|
|
915
|
-
const response = await fetch(`${url}${endpoint}`, {
|
|
916
|
-
signal: controller.signal,
|
|
917
|
-
});
|
|
918
|
-
clearTimeout(timeout);
|
|
919
|
-
if (response.ok) {
|
|
920
|
-
let version;
|
|
921
|
-
try {
|
|
922
|
-
const body = await response.json();
|
|
923
|
-
if (typeof body === 'object' &&
|
|
924
|
-
body !== null &&
|
|
925
|
-
'version' in body &&
|
|
926
|
-
typeof body['version'] === 'string') {
|
|
927
|
-
version = body['version'];
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
catch {
|
|
931
|
-
// Non-JSON response is fine — we just don't get version info
|
|
932
|
-
}
|
|
933
|
-
return { name: serviceName, port, healthy: true, version };
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
// Try next endpoint
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return { name: serviceName, port, healthy: false };
|
|
941
|
-
}
|
|
942
|
-
/**
|
|
943
|
-
* Probe all known Jeeves services for health.
|
|
944
|
-
*
|
|
945
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
946
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
947
|
-
* @returns Array of probe results for all services.
|
|
948
|
-
*/
|
|
949
|
-
async function probeAllServices(consumerName, timeoutMs = 3000) {
|
|
950
|
-
const serviceNames = Object.keys(DEFAULT_PORTS);
|
|
951
|
-
const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
|
|
952
|
-
return results;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Registry version cache for npm package update awareness.
|
|
957
|
-
*
|
|
958
|
-
* @remarks
|
|
959
|
-
* Caches the latest npm registry version in a local JSON file
|
|
960
|
-
* to avoid expensive `npm view` calls on every refresh cycle.
|
|
961
|
-
*/
|
|
962
|
-
/**
|
|
963
|
-
* Check the npm registry for the latest version of a package.
|
|
964
|
-
*
|
|
965
|
-
* @param packageName - The npm package name (e.g., '\@karmaniverous/jeeves').
|
|
966
|
-
* @param cacheDir - Directory to store the cache file.
|
|
967
|
-
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
968
|
-
* @returns The latest version string, or undefined if the check fails.
|
|
969
|
-
*/
|
|
970
|
-
function checkRegistryVersion(packageName, cacheDir, ttlSeconds = 3600) {
|
|
971
|
-
const cachePath = join(cacheDir, REGISTRY_CACHE_FILE);
|
|
972
|
-
// Check cache first
|
|
973
|
-
if (existsSync(cachePath)) {
|
|
974
|
-
try {
|
|
975
|
-
const raw = readFileSync(cachePath, 'utf-8');
|
|
976
|
-
const entry = JSON.parse(raw);
|
|
977
|
-
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
978
|
-
if (age < ttlSeconds * 1000) {
|
|
979
|
-
return entry.version;
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
catch {
|
|
983
|
-
// Cache corrupt — proceed with fresh check
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
// Query npm registry
|
|
987
|
-
try {
|
|
988
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
989
|
-
encoding: 'utf-8',
|
|
990
|
-
timeout: 15_000,
|
|
991
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
992
|
-
}).trim();
|
|
993
|
-
if (!result)
|
|
994
|
-
return undefined;
|
|
995
|
-
// Write cache
|
|
996
|
-
if (!existsSync(cacheDir)) {
|
|
997
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
998
|
-
}
|
|
999
|
-
const entry = {
|
|
1000
|
-
version: result,
|
|
1001
|
-
checkedAt: new Date().toISOString(),
|
|
1002
|
-
};
|
|
1003
|
-
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
1004
|
-
return result;
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
return undefined;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
844
|
/**
|
|
1012
845
|
* Similarity-based cleanup detection for orphaned managed content.
|
|
1013
846
|
*
|
|
@@ -1203,6 +1036,48 @@ function parseManaged(fileContent, markers = TOOLS_MARKERS) {
|
|
|
1203
1036
|
};
|
|
1204
1037
|
}
|
|
1205
1038
|
|
|
1039
|
+
/**
|
|
1040
|
+
* Strip foreign managed blocks from content.
|
|
1041
|
+
*
|
|
1042
|
+
* @remarks
|
|
1043
|
+
* Prevents cross-contamination by removing managed blocks that belong
|
|
1044
|
+
* to other marker sets. For example, when writing TOOLS.md with TOOLS
|
|
1045
|
+
* markers, any SOUL or AGENTS managed blocks found in the user content
|
|
1046
|
+
* zone are stripped — they don't belong there.
|
|
1047
|
+
*
|
|
1048
|
+
* @packageDocumentation
|
|
1049
|
+
*/
|
|
1050
|
+
/**
|
|
1051
|
+
* Build a regex that matches an entire managed block (BEGIN marker through END marker).
|
|
1052
|
+
*
|
|
1053
|
+
* @param markers - The marker set to match.
|
|
1054
|
+
* @returns A regex that matches the full block including markers.
|
|
1055
|
+
*/
|
|
1056
|
+
function buildBlockPattern(markers) {
|
|
1057
|
+
const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1058
|
+
const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1059
|
+
return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Strip managed blocks belonging to foreign marker sets from content.
|
|
1063
|
+
*
|
|
1064
|
+
* @param content - The content to clean (typically user content zone).
|
|
1065
|
+
* @param currentMarkers - The marker set that owns this file (will NOT be stripped).
|
|
1066
|
+
* @returns Content with foreign managed blocks removed.
|
|
1067
|
+
*/
|
|
1068
|
+
function stripForeignMarkers(content, currentMarkers) {
|
|
1069
|
+
let result = content;
|
|
1070
|
+
for (const markers of ALL_MARKERS) {
|
|
1071
|
+
// Skip the current file's own markers
|
|
1072
|
+
if (markers.begin === currentMarkers.begin)
|
|
1073
|
+
continue;
|
|
1074
|
+
const pattern = buildBlockPattern(markers);
|
|
1075
|
+
result = result.replace(pattern, '\n');
|
|
1076
|
+
}
|
|
1077
|
+
// Clean up multiple blank lines left by removals
|
|
1078
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1206
1081
|
/**
|
|
1207
1082
|
* Version-stamp parsing and convergence logic.
|
|
1208
1083
|
*
|
|
@@ -1319,8 +1194,8 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1319
1194
|
? `# ${markers.title}\n\n${sectionText}`
|
|
1320
1195
|
: sectionText;
|
|
1321
1196
|
}
|
|
1322
|
-
//
|
|
1323
|
-
const userContent = parsed.userContent;
|
|
1197
|
+
// Strip foreign managed blocks from user content (cross-contamination fix)
|
|
1198
|
+
const userContent = stripForeignMarkers(parsed.userContent, markers);
|
|
1324
1199
|
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1325
1200
|
// Build the full managed block
|
|
1326
1201
|
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
@@ -1355,71 +1230,14 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1355
1230
|
}
|
|
1356
1231
|
}
|
|
1357
1232
|
|
|
1358
|
-
/**
|
|
1359
|
-
* Build enriched service rows for the Platform template.
|
|
1360
|
-
*
|
|
1361
|
-
* @remarks
|
|
1362
|
-
* Merges health probe results with component version state and
|
|
1363
|
-
* npm registry update availability into rows for the Handlebars
|
|
1364
|
-
* Platform template.
|
|
1365
|
-
*/
|
|
1366
|
-
/**
|
|
1367
|
-
* Check whether an available version is newer than the current one.
|
|
1368
|
-
*
|
|
1369
|
-
* @param available - Registry version string.
|
|
1370
|
-
* @param current - Currently installed version string.
|
|
1371
|
-
* @returns The available version if it's newer, otherwise undefined.
|
|
1372
|
-
*/
|
|
1373
|
-
function newerVersion(available, current) {
|
|
1374
|
-
if (!available ||
|
|
1375
|
-
!current ||
|
|
1376
|
-
!semver.valid(available) ||
|
|
1377
|
-
!semver.valid(current)) {
|
|
1378
|
-
return undefined;
|
|
1379
|
-
}
|
|
1380
|
-
return semver.gt(available, current) ? available : undefined;
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* Build enriched service rows for the Platform Handlebars template.
|
|
1384
|
-
*
|
|
1385
|
-
* @param options - Probe results, version state, and configuration.
|
|
1386
|
-
* @returns Array of enriched service rows.
|
|
1387
|
-
*/
|
|
1388
|
-
function buildServiceRows(options) {
|
|
1389
|
-
const { probeResults, componentVersions, cacheDir, skipRegistryCheck } = options;
|
|
1390
|
-
return probeResults.map((r) => {
|
|
1391
|
-
const entry = componentVersions[r.name];
|
|
1392
|
-
if (!entry)
|
|
1393
|
-
return { ...r };
|
|
1394
|
-
let availableServiceVersion;
|
|
1395
|
-
let availablePluginVersion;
|
|
1396
|
-
if (!skipRegistryCheck) {
|
|
1397
|
-
if (entry.servicePackage) {
|
|
1398
|
-
const registryVersion = checkRegistryVersion(entry.servicePackage, cacheDir);
|
|
1399
|
-
availableServiceVersion = newerVersion(registryVersion, r.version);
|
|
1400
|
-
}
|
|
1401
|
-
if (entry.pluginPackage && entry.pluginVersion) {
|
|
1402
|
-
const registryVersion = checkRegistryVersion(entry.pluginPackage, cacheDir);
|
|
1403
|
-
availablePluginVersion = newerVersion(registryVersion, entry.pluginVersion);
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
return {
|
|
1407
|
-
...r,
|
|
1408
|
-
pluginVersion: entry.pluginVersion,
|
|
1409
|
-
availableServiceVersion,
|
|
1410
|
-
availablePluginVersion,
|
|
1411
|
-
};
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
1233
|
/**
|
|
1416
1234
|
* Internal function to maintain SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
1417
1235
|
*
|
|
1418
1236
|
* @remarks
|
|
1419
1237
|
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1238
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
1239
|
+
* Platform template with live data, and writes managed sections using
|
|
1240
|
+
* `updateManagedSection`.
|
|
1423
1241
|
*/
|
|
1424
1242
|
/**
|
|
1425
1243
|
* Resolve the package's content directory for template file copying.
|
|
@@ -1463,29 +1281,24 @@ function copyTemplates(coreConfigDir) {
|
|
|
1463
1281
|
}
|
|
1464
1282
|
cpSync(sourceDir, destDir, { recursive: true });
|
|
1465
1283
|
}
|
|
1466
|
-
/** Whether Handlebars helpers have been registered. */
|
|
1467
|
-
let helpersRegistered = false;
|
|
1468
|
-
/** Register Handlebars helpers used in the Platform template. */
|
|
1469
|
-
function registerHelpers() {
|
|
1470
|
-
if (helpersRegistered)
|
|
1471
|
-
return;
|
|
1472
|
-
helpersRegistered = true;
|
|
1473
|
-
Handlebars.registerHelper('gt', (a, b) => typeof a === 'number' && typeof b === 'number' && a > b);
|
|
1474
|
-
}
|
|
1475
1284
|
/**
|
|
1476
|
-
*
|
|
1285
|
+
* Render the Platform template using simple string replacement.
|
|
1477
1286
|
*
|
|
1478
|
-
* @
|
|
1287
|
+
* @param templatePath - Path to the templates directory.
|
|
1288
|
+
* @returns Rendered platform content string.
|
|
1479
1289
|
*/
|
|
1480
|
-
function
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1290
|
+
function renderPlatformTemplate(templatePath) {
|
|
1291
|
+
const templatesAvailable = existsSync(templatePath);
|
|
1292
|
+
let content = toolsPlatformTemplate;
|
|
1293
|
+
// Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
|
|
1294
|
+
const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
|
|
1295
|
+
const match = ifRegex.exec(content);
|
|
1296
|
+
if (match) {
|
|
1297
|
+
content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
|
|
1487
1298
|
}
|
|
1488
|
-
|
|
1299
|
+
// Replace __TEMPLATE_PATH__ with the actual path
|
|
1300
|
+
content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
|
|
1301
|
+
return content;
|
|
1489
1302
|
}
|
|
1490
1303
|
/**
|
|
1491
1304
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
@@ -1493,51 +1306,22 @@ function checkCoreUpdate(coreVersion, cacheDir) {
|
|
|
1493
1306
|
* @param options - Configuration for the refresh cycle.
|
|
1494
1307
|
*/
|
|
1495
1308
|
async function refreshPlatformContent(options) {
|
|
1496
|
-
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs,
|
|
1309
|
+
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
|
|
1497
1310
|
const workspacePath = getWorkspacePath();
|
|
1498
1311
|
const coreConfigDir = getCoreConfigDir();
|
|
1499
|
-
// 1.
|
|
1500
|
-
const probeResults = await probeAllServices(undefined, probeTimeoutMs);
|
|
1501
|
-
// 2. Write calling component's version entry (with serviceVersion from probe)
|
|
1312
|
+
// 1. Write calling component's version entry
|
|
1502
1313
|
if (componentName) {
|
|
1503
|
-
const callerProbe = probeResults.find((r) => r.name === componentName);
|
|
1504
1314
|
writeComponentVersion(coreConfigDir, {
|
|
1505
1315
|
componentName,
|
|
1506
|
-
serviceVersion: callerProbe?.version,
|
|
1507
1316
|
pluginVersion: componentVersion,
|
|
1508
1317
|
servicePackage,
|
|
1509
1318
|
pluginPackage,
|
|
1510
1319
|
});
|
|
1511
1320
|
}
|
|
1512
|
-
//
|
|
1513
|
-
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1514
|
-
// 4. Build enriched service rows with registry checks
|
|
1515
|
-
const cacheDir = componentName
|
|
1516
|
-
? getComponentConfigDir(componentName)
|
|
1517
|
-
: coreConfigDir;
|
|
1518
|
-
const availableCoreVersion = skipRegistryCheck
|
|
1519
|
-
? undefined
|
|
1520
|
-
: checkCoreUpdate(coreVersion, cacheDir);
|
|
1521
|
-
const serviceRows = buildServiceRows({
|
|
1522
|
-
probeResults,
|
|
1523
|
-
componentVersions,
|
|
1524
|
-
cacheDir,
|
|
1525
|
-
skipRegistryCheck,
|
|
1526
|
-
});
|
|
1527
|
-
// 5. Render Platform template
|
|
1321
|
+
// 2. Render Platform template
|
|
1528
1322
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const templateData = {
|
|
1532
|
-
services: serviceRows,
|
|
1533
|
-
unhealthyServices: serviceRows.filter((r) => !r.healthy),
|
|
1534
|
-
coreVersion,
|
|
1535
|
-
availableCoreVersion,
|
|
1536
|
-
templatesAvailable: existsSync(templatePath),
|
|
1537
|
-
templatePath,
|
|
1538
|
-
};
|
|
1539
|
-
const platformContent = template(templateData);
|
|
1540
|
-
// 6. Write TOOLS.md Platform section
|
|
1323
|
+
const platformContent = renderPlatformTemplate(templatePath);
|
|
1324
|
+
// 3. Write TOOLS.md Platform section
|
|
1541
1325
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1542
1326
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1543
1327
|
mode: 'section',
|
|
@@ -1546,7 +1330,7 @@ async function refreshPlatformContent(options) {
|
|
|
1546
1330
|
coreVersion,
|
|
1547
1331
|
stalenessThresholdMs,
|
|
1548
1332
|
});
|
|
1549
|
-
//
|
|
1333
|
+
// 4. Write SOUL.md managed block
|
|
1550
1334
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1551
1335
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1552
1336
|
mode: 'block',
|
|
@@ -1554,7 +1338,7 @@ async function refreshPlatformContent(options) {
|
|
|
1554
1338
|
coreVersion,
|
|
1555
1339
|
stalenessThresholdMs,
|
|
1556
1340
|
});
|
|
1557
|
-
//
|
|
1341
|
+
// 5. Write AGENTS.md managed block
|
|
1558
1342
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1559
1343
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1560
1344
|
mode: 'block',
|
|
@@ -1562,7 +1346,7 @@ async function refreshPlatformContent(options) {
|
|
|
1562
1346
|
coreVersion,
|
|
1563
1347
|
stalenessThresholdMs,
|
|
1564
1348
|
});
|
|
1565
|
-
//
|
|
1349
|
+
// 6. Copy templates to config dir
|
|
1566
1350
|
copyTemplates(coreConfigDir);
|
|
1567
1351
|
}
|
|
1568
1352
|
|
|
@@ -1614,8 +1398,6 @@ async function seedContent(options) {
|
|
|
1614
1398
|
// Seed content via the same code path as writer cycles
|
|
1615
1399
|
await refreshPlatformContent({
|
|
1616
1400
|
coreVersion: options.coreVersion,
|
|
1617
|
-
probeTimeoutMs: options.probeTimeoutMs ?? 3000,
|
|
1618
|
-
skipRegistryCheck: options.skipRegistryCheck ?? true,
|
|
1619
1401
|
});
|
|
1620
1402
|
}
|
|
1621
1403
|
|
|
@@ -1668,7 +1450,6 @@ function registerInstallCommand(program) {
|
|
|
1668
1450
|
initFromOptions(opts);
|
|
1669
1451
|
await seedContent({
|
|
1670
1452
|
coreVersion: CORE_VERSION,
|
|
1671
|
-
skipRegistryCheck: true,
|
|
1672
1453
|
});
|
|
1673
1454
|
console.log('✅ Platform content seeded successfully.');
|
|
1674
1455
|
console.log(' - SOUL.md managed section written');
|
|
@@ -1680,12 +1461,73 @@ function registerInstallCommand(program) {
|
|
|
1680
1461
|
}
|
|
1681
1462
|
|
|
1682
1463
|
/**
|
|
1683
|
-
*
|
|
1464
|
+
* Service URL resolution.
|
|
1684
1465
|
*
|
|
1685
1466
|
* @remarks
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
1688
|
-
*
|
|
1467
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
1468
|
+
* resolution order:
|
|
1469
|
+
* 1. Consumer's own component config
|
|
1470
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1471
|
+
* 3. Default port constants
|
|
1472
|
+
*/
|
|
1473
|
+
/**
|
|
1474
|
+
* Resolve the URL for a named Jeeves service.
|
|
1475
|
+
*
|
|
1476
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1477
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
1478
|
+
* @returns The resolved service URL.
|
|
1479
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1480
|
+
*/
|
|
1481
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
1482
|
+
// 2. Check core config
|
|
1483
|
+
const coreDir = getCoreConfigDir();
|
|
1484
|
+
const coreConfig = loadConfig(coreDir);
|
|
1485
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1486
|
+
if (coreUrl)
|
|
1487
|
+
return coreUrl;
|
|
1488
|
+
// 3. Fall back to port constants
|
|
1489
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
1490
|
+
if (port !== undefined) {
|
|
1491
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
1492
|
+
}
|
|
1493
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
1498
|
+
*
|
|
1499
|
+
* @remarks
|
|
1500
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
1501
|
+
* and handle JSON serialisation/deserialisation.
|
|
1502
|
+
*/
|
|
1503
|
+
/**
|
|
1504
|
+
* Fetch a URL with an automatic abort timeout.
|
|
1505
|
+
*
|
|
1506
|
+
* @param url - URL to fetch.
|
|
1507
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1508
|
+
* @param init - Optional `fetch` init options.
|
|
1509
|
+
* @returns The fetch Response object.
|
|
1510
|
+
*/
|
|
1511
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1512
|
+
const controller = new AbortController();
|
|
1513
|
+
const timeout = setTimeout(() => {
|
|
1514
|
+
controller.abort();
|
|
1515
|
+
}, timeoutMs);
|
|
1516
|
+
try {
|
|
1517
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1518
|
+
}
|
|
1519
|
+
finally {
|
|
1520
|
+
clearTimeout(timeout);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* CLI status command: discover components and probe their health.
|
|
1526
|
+
*
|
|
1527
|
+
* @remarks
|
|
1528
|
+
* Uses `readComponentVersions()` to discover registered components,
|
|
1529
|
+
* then probes each one via GET /status. Exits with code 0 if all
|
|
1530
|
+
* services are healthy, code 1 if any are unreachable.
|
|
1689
1531
|
*/
|
|
1690
1532
|
/**
|
|
1691
1533
|
* Register the status subcommand on the parent CLI program.
|
|
@@ -1695,7 +1537,7 @@ function registerInstallCommand(program) {
|
|
|
1695
1537
|
function registerStatusCommand(program) {
|
|
1696
1538
|
program
|
|
1697
1539
|
.command('status')
|
|
1698
|
-
.description('
|
|
1540
|
+
.description('Discover Jeeves components and probe their health')
|
|
1699
1541
|
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1700
1542
|
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1701
1543
|
.option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
|
|
@@ -1705,43 +1547,68 @@ function registerStatusCommand(program) {
|
|
|
1705
1547
|
console.log('Jeeves Platform Status');
|
|
1706
1548
|
console.log('='.repeat(60));
|
|
1707
1549
|
console.log();
|
|
1708
|
-
const
|
|
1550
|
+
const { getCoreConfigDir } = await Promise.resolve().then(function () { return init$1; });
|
|
1551
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1552
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1553
|
+
const componentNames = Object.keys(componentVersions);
|
|
1554
|
+
if (componentNames.length === 0) {
|
|
1555
|
+
console.log('No components registered.');
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1709
1558
|
const nameWidth = 10;
|
|
1710
|
-
const portWidth = 6;
|
|
1711
1559
|
const statusWidth = 30;
|
|
1560
|
+
const versionWidth = 12;
|
|
1712
1561
|
const header = [
|
|
1713
|
-
'
|
|
1714
|
-
'Port'.padEnd(portWidth),
|
|
1562
|
+
'Component'.padEnd(nameWidth),
|
|
1715
1563
|
'Status'.padEnd(statusWidth),
|
|
1564
|
+
'Version'.padEnd(versionWidth),
|
|
1716
1565
|
].join(' ');
|
|
1717
1566
|
const separator = [
|
|
1718
1567
|
'-'.repeat(nameWidth),
|
|
1719
|
-
'-'.repeat(portWidth),
|
|
1720
1568
|
'-'.repeat(statusWidth),
|
|
1569
|
+
'-'.repeat(versionWidth),
|
|
1721
1570
|
].join(' ');
|
|
1722
1571
|
console.log(header);
|
|
1723
1572
|
console.log(separator);
|
|
1724
1573
|
let allHealthy = true;
|
|
1725
|
-
for (const
|
|
1574
|
+
for (const name of componentNames) {
|
|
1726
1575
|
let status;
|
|
1727
|
-
|
|
1728
|
-
|
|
1576
|
+
let version = '—';
|
|
1577
|
+
try {
|
|
1578
|
+
const url = getServiceUrl(name);
|
|
1579
|
+
const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
|
|
1580
|
+
if (response.ok) {
|
|
1581
|
+
status = '✅ Running';
|
|
1582
|
+
try {
|
|
1583
|
+
const body = await response.json();
|
|
1584
|
+
if (typeof body === 'object' &&
|
|
1585
|
+
body !== null &&
|
|
1586
|
+
'version' in body &&
|
|
1587
|
+
typeof body['version'] === 'string') {
|
|
1588
|
+
version = body['version'];
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
// Non-JSON response — version stays unknown
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
else {
|
|
1596
|
+
status = `❌ HTTP ${String(response.status)}`;
|
|
1597
|
+
allHealthy = false;
|
|
1598
|
+
}
|
|
1729
1599
|
}
|
|
1730
|
-
|
|
1731
|
-
status =
|
|
1600
|
+
catch {
|
|
1601
|
+
status = '❌ Down';
|
|
1732
1602
|
allHealthy = false;
|
|
1733
1603
|
}
|
|
1734
1604
|
const row = [
|
|
1735
|
-
|
|
1736
|
-
String(r.port).padEnd(portWidth),
|
|
1605
|
+
name.padEnd(nameWidth),
|
|
1737
1606
|
status.padEnd(statusWidth),
|
|
1607
|
+
version.padEnd(versionWidth),
|
|
1738
1608
|
].join(' ');
|
|
1739
1609
|
console.log(row);
|
|
1740
1610
|
}
|
|
1741
1611
|
console.log();
|
|
1742
|
-
const healthy = probeResults.filter((r) => r.healthy).length;
|
|
1743
|
-
const total = probeResults.length;
|
|
1744
|
-
console.log(`${String(healthy)}/${String(total)} services healthy`);
|
|
1745
1612
|
if (!allHealthy) {
|
|
1746
1613
|
process.exitCode = 1;
|
|
1747
1614
|
}
|
|
@@ -1833,13 +1700,25 @@ function registerUninstallCommand(program) {
|
|
|
1833
1700
|
console.log();
|
|
1834
1701
|
// Warn if services still responding
|
|
1835
1702
|
try {
|
|
1836
|
-
const
|
|
1837
|
-
const
|
|
1703
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1704
|
+
const componentNames = Object.keys(componentVersions);
|
|
1705
|
+
const running = [];
|
|
1706
|
+
for (const name of componentNames) {
|
|
1707
|
+
try {
|
|
1708
|
+
const url = getServiceUrl(name);
|
|
1709
|
+
const response = await fetchWithTimeout(`${url}/status`, 2000);
|
|
1710
|
+
if (response.ok) {
|
|
1711
|
+
running.push(name);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
// Not running — expected during uninstall
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1838
1718
|
if (running.length > 0) {
|
|
1839
1719
|
console.log('⚠️ The following services are still responding:');
|
|
1840
|
-
for (const
|
|
1841
|
-
|
|
1842
|
-
console.log(` - ${r.name} on port ${String(r.port)}${ver}`);
|
|
1720
|
+
for (const name of running) {
|
|
1721
|
+
console.log(` - ${name}`);
|
|
1843
1722
|
}
|
|
1844
1723
|
console.log(' Consider stopping them before fully removing Jeeves.');
|
|
1845
1724
|
console.log();
|