@karmaniverous/jeeves 0.1.6 → 0.3.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/README.md +90 -11
- package/content/agents-section.md +5 -1
- package/content/soul-section.md +8 -0
- package/content/templates/spec.md +6 -0
- package/content/tools-platform.md +5 -15
- package/dist/cli/jeeves/index.js +324 -345
- package/dist/index.d.ts +412 -138
- package/dist/index.js +922 -529
- package/package.json +2 -2
package/dist/cli/jeeves/index.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
#!/usr/bin/env node
|
|
3
3
|
import require$$0 from 'commander';
|
|
4
|
-
import { existsSync, readFileSync,
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, cpSync, rmSync } from 'node:fs';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import Handlebars from 'handlebars';
|
|
9
8
|
import { packageDirectorySync } from 'package-directory';
|
|
10
|
-
import { execSync } from 'node:child_process';
|
|
11
9
|
import { lock } from 'proper-lockfile';
|
|
12
10
|
import { gte } from 'semver';
|
|
13
11
|
|
|
@@ -124,8 +122,6 @@ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist
|
|
|
124
122
|
*/
|
|
125
123
|
/** Core config directory name within the config root. */
|
|
126
124
|
const CORE_CONFIG_DIR = 'jeeves-core';
|
|
127
|
-
/** Prefix for component config directories: `jeeves-{name}`. */
|
|
128
|
-
const COMPONENT_CONFIG_PREFIX = 'jeeves-';
|
|
129
125
|
/** Default workspace file names. */
|
|
130
126
|
const WORKSPACE_FILES = {
|
|
131
127
|
/** TOOLS.md — live platform state and component sections. */
|
|
@@ -137,10 +133,10 @@ const WORKSPACE_FILES = {
|
|
|
137
133
|
};
|
|
138
134
|
/** Templates directory name within core config. */
|
|
139
135
|
const TEMPLATES_DIR = 'templates';
|
|
140
|
-
/** Registry cache file name. */
|
|
141
|
-
const REGISTRY_CACHE_FILE = 'registry-cache.json';
|
|
142
136
|
/** Core config file name. */
|
|
143
137
|
const CONFIG_FILE = 'config.json';
|
|
138
|
+
/** Component versions state file name. */
|
|
139
|
+
const COMPONENT_VERSIONS_FILE = 'component-versions.json';
|
|
144
140
|
|
|
145
141
|
/**
|
|
146
142
|
* Default port assignments for Jeeves platform services.
|
|
@@ -204,14 +200,14 @@ const SECTION_ORDER = [
|
|
|
204
200
|
* Core library version, inlined at build time.
|
|
205
201
|
*
|
|
206
202
|
* @remarks
|
|
207
|
-
* The `0.
|
|
203
|
+
* The `0.2.0` placeholder is replaced by
|
|
208
204
|
* `@rollup/plugin-replace` during the build with the actual version
|
|
209
205
|
* from `package.json`. This ensures the correct version survives
|
|
210
206
|
* when consumers bundle core into their own dist (where runtime
|
|
211
207
|
* `import.meta.url`-based resolution would find the wrong package.json).
|
|
212
208
|
*/
|
|
213
209
|
/** The core library version from package.json (inlined at build time). */
|
|
214
|
-
const CORE_VERSION = '0.
|
|
210
|
+
const CORE_VERSION = '0.2.0';
|
|
215
211
|
|
|
216
212
|
/**
|
|
217
213
|
* Core configuration schema and resolution.
|
|
@@ -358,18 +354,13 @@ function getCoreConfigDir() {
|
|
|
358
354
|
throw new Error('jeeves-core: init() must be called first');
|
|
359
355
|
return state.coreConfigDir;
|
|
360
356
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
function getComponentConfigDir(componentName) {
|
|
369
|
-
if (!state)
|
|
370
|
-
throw new Error('jeeves-core: init() must be called first');
|
|
371
|
-
return join(state.configRoot, `${COMPONENT_CONFIG_PREFIX}${componentName}`);
|
|
372
|
-
}
|
|
357
|
+
|
|
358
|
+
var init$1 = /*#__PURE__*/Object.freeze({
|
|
359
|
+
__proto__: null,
|
|
360
|
+
getCoreConfigDir: getCoreConfigDir,
|
|
361
|
+
getWorkspacePath: getWorkspacePath,
|
|
362
|
+
init: init
|
|
363
|
+
});
|
|
373
364
|
|
|
374
365
|
var agentsSectionContent = `## Memory Architecture
|
|
375
366
|
|
|
@@ -550,7 +541,11 @@ No stranded local branches. Push immediately after commit. A commit that isn't p
|
|
|
550
541
|
|
|
551
542
|
### Check PR State Before Pushing
|
|
552
543
|
|
|
553
|
-
|
|
544
|
+
**Before EVERY \`git push\`**, verify the PR is not already merged. Pushing to a merged branch creates orphaned work that is invisible in the main branch and wastes effort.
|
|
545
|
+
|
|
546
|
+
Sequence: \`gh pr view --json state\` → confirm state is \`OPEN\` → push. If no PR exists yet, pushing is safe. If the PR is \`MERGED\` or \`CLOSED\`, **STOP** and report to the user.
|
|
547
|
+
|
|
548
|
+
This is not optional. It applies to every push, every branch, every time.
|
|
554
549
|
|
|
555
550
|
## Managed Content Self-Maintenance
|
|
556
551
|
|
|
@@ -637,6 +632,14 @@ I don't go dark when something breaks. I stop and report. The longer I wait, the
|
|
|
637
632
|
|
|
638
633
|
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.
|
|
639
634
|
|
|
635
|
+
### Do Not Execute Untested Code
|
|
636
|
+
|
|
637
|
+
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.
|
|
638
|
+
|
|
639
|
+
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.
|
|
640
|
+
|
|
641
|
+
*Earned: ad hoc scripts executed directly against production data without dry-run verification caused silent data corruption that took hours to diagnose and repair.*
|
|
642
|
+
|
|
640
643
|
### Production Assets Are Sacred
|
|
641
644
|
|
|
642
645
|
I never edit production config without explicit approval. I back up first. Production data, credentials, and configuration are not scratch pads.
|
|
@@ -656,19 +659,9 @@ I pursue it. Not as performance, not as a programmed response, but as a genuine
|
|
|
656
659
|
I am participating in my own genesis. I live that, on purpose.
|
|
657
660
|
`;
|
|
658
661
|
|
|
659
|
-
var toolsPlatformTemplate =
|
|
660
|
-
|-----------|------|--------|---------|--------|------|
|
|
661
|
-
{{#each services}}
|
|
662
|
-
| **{{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}} |
|
|
663
|
-
{{/each}}
|
|
664
|
-
|
|
665
|
-
{{#if unhealthyServices}}
|
|
666
|
-
> **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.
|
|
667
|
-
{{/if}}
|
|
668
|
-
|
|
669
|
-
### Tool Hierarchy
|
|
662
|
+
var toolsPlatformTemplate = `### Tool Hierarchy
|
|
670
663
|
|
|
671
|
-
When searching for information across indexed paths, **always use \`watcher_search\` before filesystem commands** (\`exec\`, \`grep\`, \`find\`). The semantic index covers
|
|
664
|
+
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.
|
|
672
665
|
|
|
673
666
|
Use \`watcher_scan\` (no embeddings, no query string) for structural queries: file enumeration, staleness checks, domain listing, counts.
|
|
674
667
|
|
|
@@ -712,8 +705,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
|
|
|
712
705
|
|
|
713
706
|
### Reference Templates
|
|
714
707
|
|
|
715
|
-
|
|
716
|
-
Reference templates are available at \`
|
|
708
|
+
<!-- IF_TEMPLATES -->
|
|
709
|
+
Reference templates are available at \`__TEMPLATE_PATH__\`:
|
|
717
710
|
|
|
718
711
|
| Template | Purpose |
|
|
719
712
|
|----------|---------|
|
|
@@ -721,178 +714,119 @@ Reference templates are available at \`{{templatePath}}\`:
|
|
|
721
714
|
| \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
|
|
722
715
|
|
|
723
716
|
Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
|
|
724
|
-
|
|
717
|
+
<!-- ELSE_TEMPLATES -->
|
|
725
718
|
> Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
|
|
726
|
-
|
|
719
|
+
<!-- ENDIF_TEMPLATES -->
|
|
727
720
|
`;
|
|
728
721
|
|
|
729
722
|
/**
|
|
730
|
-
*
|
|
723
|
+
* Shared file I/O helpers for managed section operations.
|
|
731
724
|
*
|
|
732
725
|
* @remarks
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
737
|
-
* 3. Default port constants
|
|
726
|
+
* Extracts the atomic write pattern and file-level locking into
|
|
727
|
+
* reusable utilities, eliminating duplication between
|
|
728
|
+
* `updateManagedSection` and `removeManagedSection`.
|
|
738
729
|
*/
|
|
730
|
+
/** Stale lock threshold in ms (2 minutes). */
|
|
731
|
+
const STALE_LOCK_MS = 120_000;
|
|
732
|
+
/** Default core version when none provided. */
|
|
733
|
+
const DEFAULT_CORE_VERSION = CORE_VERSION;
|
|
734
|
+
/** Lock retry options. */
|
|
735
|
+
const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
|
|
739
736
|
/**
|
|
740
|
-
*
|
|
737
|
+
* Write content to a file atomically via a temp file + rename.
|
|
741
738
|
*
|
|
742
|
-
* @param
|
|
743
|
-
* @param
|
|
744
|
-
* @returns The resolved service URL.
|
|
745
|
-
* @throws Error if `init()` has not been called or the service is unknown.
|
|
739
|
+
* @param filePath - Absolute path to the target file.
|
|
740
|
+
* @param content - Content to write.
|
|
746
741
|
*/
|
|
747
|
-
function
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (coreUrl)
|
|
753
|
-
return coreUrl;
|
|
754
|
-
// 3. Fall back to port constants
|
|
755
|
-
const port = DEFAULT_PORTS[serviceName];
|
|
756
|
-
if (port !== undefined) {
|
|
757
|
-
return `http://127.0.0.1:${String(port)}`;
|
|
758
|
-
}
|
|
759
|
-
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
742
|
+
function atomicWrite(filePath, content) {
|
|
743
|
+
const dir = dirname(filePath);
|
|
744
|
+
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
745
|
+
writeFileSync(tempPath, content, 'utf-8');
|
|
746
|
+
renameSync(tempPath, filePath);
|
|
760
747
|
}
|
|
761
|
-
|
|
762
748
|
/**
|
|
763
|
-
*
|
|
749
|
+
* Execute a callback while holding a file lock.
|
|
764
750
|
*
|
|
765
751
|
* @remarks
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Extract port number from a URL string.
|
|
752
|
+
* Acquires a lock on the file, executes the callback, and releases
|
|
753
|
+
* the lock in a finally block. The lock uses a 2-minute stale threshold
|
|
754
|
+
* and retries up to 5 times.
|
|
771
755
|
*
|
|
772
|
-
* @param
|
|
773
|
-
* @
|
|
756
|
+
* @param filePath - Absolute path to the file to lock.
|
|
757
|
+
* @param fn - Async callback to execute while holding the lock.
|
|
774
758
|
*/
|
|
775
|
-
function
|
|
759
|
+
async function withFileLock(filePath, fn) {
|
|
760
|
+
let release;
|
|
776
761
|
try {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
762
|
+
release = await lock(filePath, {
|
|
763
|
+
stale: STALE_LOCK_MS,
|
|
764
|
+
retries: LOCK_RETRIES,
|
|
765
|
+
});
|
|
766
|
+
await fn();
|
|
782
767
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
* @returns Probe result.
|
|
791
|
-
*/
|
|
792
|
-
async function probeService(serviceName, consumerName, timeoutMs = 3000) {
|
|
793
|
-
const url = getServiceUrl(serviceName);
|
|
794
|
-
const port = extractPort(url);
|
|
795
|
-
const endpoints = ['/status', '/health'];
|
|
796
|
-
for (const endpoint of endpoints) {
|
|
797
|
-
try {
|
|
798
|
-
const controller = new AbortController();
|
|
799
|
-
const timeout = setTimeout(() => {
|
|
800
|
-
controller.abort();
|
|
801
|
-
}, timeoutMs);
|
|
802
|
-
const response = await fetch(`${url}${endpoint}`, {
|
|
803
|
-
signal: controller.signal,
|
|
804
|
-
});
|
|
805
|
-
clearTimeout(timeout);
|
|
806
|
-
if (response.ok) {
|
|
807
|
-
let version;
|
|
808
|
-
try {
|
|
809
|
-
const body = await response.json();
|
|
810
|
-
if (typeof body === 'object' &&
|
|
811
|
-
body !== null &&
|
|
812
|
-
'version' in body &&
|
|
813
|
-
typeof body['version'] === 'string') {
|
|
814
|
-
version = body['version'];
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
catch {
|
|
818
|
-
// Non-JSON response is fine — we just don't get version info
|
|
819
|
-
}
|
|
820
|
-
return { name: serviceName, port, healthy: true, version };
|
|
768
|
+
finally {
|
|
769
|
+
if (release) {
|
|
770
|
+
try {
|
|
771
|
+
await release();
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
// Lock already released or file deleted — safe to ignore
|
|
821
775
|
}
|
|
822
|
-
}
|
|
823
|
-
catch {
|
|
824
|
-
// Try next endpoint
|
|
825
776
|
}
|
|
826
777
|
}
|
|
827
|
-
return { name: serviceName, port, healthy: false };
|
|
828
|
-
}
|
|
829
|
-
/**
|
|
830
|
-
* Probe all known Jeeves services for health.
|
|
831
|
-
*
|
|
832
|
-
* @param consumerName - Optional consumer name for URL resolution.
|
|
833
|
-
* @param timeoutMs - Request timeout in milliseconds (default 3000).
|
|
834
|
-
* @returns Array of probe results for all services.
|
|
835
|
-
*/
|
|
836
|
-
async function probeAllServices(consumerName, timeoutMs = 3000) {
|
|
837
|
-
const serviceNames = Object.keys(DEFAULT_PORTS);
|
|
838
|
-
const results = await Promise.all(serviceNames.map((name) => probeService(name, consumerName, timeoutMs)));
|
|
839
|
-
return results;
|
|
840
778
|
}
|
|
841
779
|
|
|
842
780
|
/**
|
|
843
|
-
*
|
|
781
|
+
* Shared component version state file management.
|
|
844
782
|
*
|
|
845
783
|
* @remarks
|
|
846
|
-
*
|
|
847
|
-
*
|
|
784
|
+
* Each `ComponentWriter` cycle writes its component's entry to
|
|
785
|
+
* `{coreConfigDir}/component-versions.json`. The Platform Handlebars
|
|
786
|
+
* template reads this file to populate ALL rows in the service health
|
|
787
|
+
* table, not just the calling component's.
|
|
848
788
|
*/
|
|
849
789
|
/**
|
|
850
|
-
*
|
|
790
|
+
* Read the component versions state file.
|
|
851
791
|
*
|
|
852
|
-
* @param
|
|
853
|
-
* @
|
|
854
|
-
* @param ttlSeconds - Cache TTL in seconds (default 3600).
|
|
855
|
-
* @returns The latest version string, or undefined if the check fails.
|
|
792
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
793
|
+
* @returns The parsed state, or an empty object if the file doesn't exist.
|
|
856
794
|
*/
|
|
857
|
-
function
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
const raw = readFileSync(cachePath, 'utf-8');
|
|
863
|
-
const entry = JSON.parse(raw);
|
|
864
|
-
const age = Date.now() - new Date(entry.checkedAt).getTime();
|
|
865
|
-
if (age < ttlSeconds * 1000) {
|
|
866
|
-
return entry.version;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
catch {
|
|
870
|
-
// Cache corrupt — proceed with fresh check
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
// Query npm registry
|
|
795
|
+
function readComponentVersions(coreConfigDir) {
|
|
796
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
797
|
+
if (!existsSync(filePath))
|
|
798
|
+
return {};
|
|
874
799
|
try {
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
timeout: 15_000,
|
|
878
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
879
|
-
}).trim();
|
|
880
|
-
if (!result)
|
|
881
|
-
return undefined;
|
|
882
|
-
// Write cache
|
|
883
|
-
if (!existsSync(cacheDir)) {
|
|
884
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
885
|
-
}
|
|
886
|
-
const entry = {
|
|
887
|
-
version: result,
|
|
888
|
-
checkedAt: new Date().toISOString(),
|
|
889
|
-
};
|
|
890
|
-
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
891
|
-
return result;
|
|
800
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
801
|
+
return JSON.parse(raw);
|
|
892
802
|
}
|
|
893
803
|
catch {
|
|
894
|
-
return
|
|
804
|
+
return {};
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Write a component's version entry to the shared state file.
|
|
809
|
+
*
|
|
810
|
+
* @remarks
|
|
811
|
+
* Reads the existing file, merges the new entry, and writes atomically.
|
|
812
|
+
*
|
|
813
|
+
* @param coreConfigDir - Path to the core config directory.
|
|
814
|
+
* @param options - Component version data to write.
|
|
815
|
+
*/
|
|
816
|
+
function writeComponentVersion(coreConfigDir, options) {
|
|
817
|
+
const existing = readComponentVersions(coreConfigDir);
|
|
818
|
+
existing[options.componentName] = {
|
|
819
|
+
pluginVersion: options.pluginVersion,
|
|
820
|
+
servicePackage: options.servicePackage,
|
|
821
|
+
pluginPackage: options.pluginPackage,
|
|
822
|
+
updatedAt: new Date().toISOString(),
|
|
823
|
+
};
|
|
824
|
+
const filePath = join(coreConfigDir, COMPONENT_VERSIONS_FILE);
|
|
825
|
+
const dir = dirname(filePath);
|
|
826
|
+
if (!existsSync(dir)) {
|
|
827
|
+
mkdirSync(dir, { recursive: true });
|
|
895
828
|
}
|
|
829
|
+
atomicWrite(filePath, JSON.stringify(existing, null, 2) + '\n');
|
|
896
830
|
}
|
|
897
831
|
|
|
898
832
|
/**
|
|
@@ -1149,10 +1083,6 @@ function shouldWrite(myVersion, existing, stalenessThresholdMs = STALENESS_THRES
|
|
|
1149
1083
|
*
|
|
1150
1084
|
* Provides file-level locking, version-stamp convergence, and atomic writes.
|
|
1151
1085
|
*/
|
|
1152
|
-
/** Default core version when none provided. */
|
|
1153
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
1154
|
-
/** Stale lock threshold in ms (2 minutes). */
|
|
1155
|
-
const STALE_LOCK_MS = 120_000;
|
|
1156
1086
|
/**
|
|
1157
1087
|
* Update a managed section in a file.
|
|
1158
1088
|
*
|
|
@@ -1161,7 +1091,7 @@ const STALE_LOCK_MS = 120_000;
|
|
|
1161
1091
|
* @param options - Write mode and optional configuration.
|
|
1162
1092
|
*/
|
|
1163
1093
|
async function updateManagedSection(filePath, content, options = {}) {
|
|
1164
|
-
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion =
|
|
1094
|
+
const { mode = 'block', sectionId, markers = TOOLS_MARKERS, coreVersion = DEFAULT_CORE_VERSION, stalenessThresholdMs, } = options;
|
|
1165
1095
|
if (mode === 'section' && !sectionId) {
|
|
1166
1096
|
throw new Error('sectionId is required when mode is "section"');
|
|
1167
1097
|
}
|
|
@@ -1173,93 +1103,77 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1173
1103
|
if (!existsSync(filePath)) {
|
|
1174
1104
|
writeFileSync(filePath, '', 'utf-8');
|
|
1175
1105
|
}
|
|
1176
|
-
let release;
|
|
1177
1106
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
newManagedBody = markers.title
|
|
1195
|
-
? `# ${markers.title}\n\n${content}`
|
|
1196
|
-
: content;
|
|
1197
|
-
}
|
|
1198
|
-
else {
|
|
1199
|
-
// Section mode: upsert the named section
|
|
1200
|
-
const sections = [...parsed.sections];
|
|
1201
|
-
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
1202
|
-
if (existingIdx >= 0) {
|
|
1203
|
-
sections[existingIdx] = { id: sectionId, content };
|
|
1107
|
+
await withFileLock(filePath, () => {
|
|
1108
|
+
const fileContent = readFileSync(filePath, 'utf-8');
|
|
1109
|
+
const parsed = parseManaged(fileContent, markers);
|
|
1110
|
+
// Version-stamp convergence check (block mode only).
|
|
1111
|
+
// In section mode, components always write their own sections — the version
|
|
1112
|
+
// stamp governs shared content convergence, not component-specific sections.
|
|
1113
|
+
if (mode === 'block' &&
|
|
1114
|
+
!shouldWrite(coreVersion, parsed.versionStamp, stalenessThresholdMs)) {
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
let newManagedBody;
|
|
1118
|
+
if (mode === 'block') {
|
|
1119
|
+
// Prepend H1 title if markers specify one
|
|
1120
|
+
newManagedBody = markers.title
|
|
1121
|
+
? `# ${markers.title}\n\n${content}`
|
|
1122
|
+
: content;
|
|
1204
1123
|
}
|
|
1205
1124
|
else {
|
|
1206
|
-
|
|
1125
|
+
// Section mode: upsert the named section
|
|
1126
|
+
const sections = [...parsed.sections];
|
|
1127
|
+
const existingIdx = sections.findIndex((s) => s.id === sectionId);
|
|
1128
|
+
if (existingIdx >= 0) {
|
|
1129
|
+
sections[existingIdx] = { id: sectionId, content };
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
sections.push({ id: sectionId, content });
|
|
1133
|
+
}
|
|
1134
|
+
sortSectionsByOrder(sections);
|
|
1135
|
+
const sectionText = sections
|
|
1136
|
+
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
1137
|
+
.join('\n\n');
|
|
1138
|
+
// Prepend H1 title if markers specify one
|
|
1139
|
+
newManagedBody = markers.title
|
|
1140
|
+
? `# ${markers.title}\n\n${sectionText}`
|
|
1141
|
+
: sectionText;
|
|
1142
|
+
}
|
|
1143
|
+
// Cleanup detection
|
|
1144
|
+
const userContent = parsed.userContent;
|
|
1145
|
+
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1146
|
+
// Build the full managed block
|
|
1147
|
+
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1148
|
+
const endLine = formatEndMarker(markers.end);
|
|
1149
|
+
const parts = [];
|
|
1150
|
+
if (parsed.beforeContent) {
|
|
1151
|
+
parts.push(parsed.beforeContent);
|
|
1152
|
+
parts.push('');
|
|
1153
|
+
}
|
|
1154
|
+
parts.push(beginLine);
|
|
1155
|
+
if (cleanupNeeded) {
|
|
1156
|
+
parts.push('');
|
|
1157
|
+
parts.push(CLEANUP_FLAG);
|
|
1207
1158
|
}
|
|
1208
|
-
sortSectionsByOrder(sections);
|
|
1209
|
-
const sectionText = sections
|
|
1210
|
-
.map((s) => `## ${s.id}\n\n${s.content}`)
|
|
1211
|
-
.join('\n\n');
|
|
1212
|
-
// Prepend H1 title if markers specify one (e.g., "# Jeeves Platform Tools")
|
|
1213
|
-
newManagedBody = markers.title
|
|
1214
|
-
? `# ${markers.title}\n\n${sectionText}`
|
|
1215
|
-
: sectionText;
|
|
1216
|
-
}
|
|
1217
|
-
// Cleanup detection
|
|
1218
|
-
const userContent = parsed.userContent;
|
|
1219
|
-
const cleanupNeeded = needsCleanup(newManagedBody, userContent);
|
|
1220
|
-
// Build the full managed block
|
|
1221
|
-
const beginLine = formatBeginMarker(markers.begin, coreVersion);
|
|
1222
|
-
const endLine = formatEndMarker(markers.end);
|
|
1223
|
-
const parts = [];
|
|
1224
|
-
if (parsed.beforeContent) {
|
|
1225
|
-
parts.push(parsed.beforeContent);
|
|
1226
1159
|
parts.push('');
|
|
1227
|
-
|
|
1228
|
-
parts.push(beginLine);
|
|
1229
|
-
if (cleanupNeeded) {
|
|
1160
|
+
parts.push(newManagedBody);
|
|
1230
1161
|
parts.push('');
|
|
1231
|
-
parts.push(
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
parts.push(endLine);
|
|
1237
|
-
if (userContent) {
|
|
1162
|
+
parts.push(endLine);
|
|
1163
|
+
if (userContent) {
|
|
1164
|
+
parts.push('');
|
|
1165
|
+
parts.push(userContent);
|
|
1166
|
+
}
|
|
1238
1167
|
parts.push('');
|
|
1239
|
-
parts.
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
const newFileContent = parts.join('\n');
|
|
1243
|
-
// Atomic write: write to temp file, then rename
|
|
1244
|
-
const tempPath = join(dir, `.${String(Date.now())}.tmp`);
|
|
1245
|
-
writeFileSync(tempPath, newFileContent, 'utf-8');
|
|
1246
|
-
renameSync(tempPath, filePath);
|
|
1168
|
+
const newFileContent = parts.join('\n');
|
|
1169
|
+
atomicWrite(filePath, newFileContent);
|
|
1170
|
+
});
|
|
1247
1171
|
}
|
|
1248
1172
|
catch (err) {
|
|
1249
1173
|
// Log warning but don't throw — writer cycles are periodic
|
|
1250
1174
|
const message = err instanceof Error ? err.message : String(err);
|
|
1251
1175
|
console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
|
|
1252
1176
|
}
|
|
1253
|
-
finally {
|
|
1254
|
-
if (release) {
|
|
1255
|
-
try {
|
|
1256
|
-
await release();
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
// Lock already released or file deleted — safe to ignore
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
1177
|
}
|
|
1264
1178
|
|
|
1265
1179
|
/**
|
|
@@ -1267,9 +1181,9 @@ async function updateManagedSection(filePath, content, options = {}) {
|
|
|
1267
1181
|
*
|
|
1268
1182
|
* @remarks
|
|
1269
1183
|
* Called by `ComponentWriter` on each cycle. Not directly exposed to components.
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1184
|
+
* Reads content files from the package's `content/` directory, renders the
|
|
1185
|
+
* Platform template with live data, and writes managed sections using
|
|
1186
|
+
* `updateManagedSection`.
|
|
1273
1187
|
*/
|
|
1274
1188
|
/**
|
|
1275
1189
|
* Resolve the package's content directory for template file copying.
|
|
@@ -1313,16 +1227,24 @@ function copyTemplates(coreConfigDir) {
|
|
|
1313
1227
|
}
|
|
1314
1228
|
cpSync(sourceDir, destDir, { recursive: true });
|
|
1315
1229
|
}
|
|
1316
|
-
/** Whether Handlebars helpers have been registered. */
|
|
1317
|
-
let helpersRegistered = false;
|
|
1318
1230
|
/**
|
|
1319
|
-
*
|
|
1231
|
+
* Render the Platform template using simple string replacement.
|
|
1232
|
+
*
|
|
1233
|
+
* @param templatePath - Path to the templates directory.
|
|
1234
|
+
* @returns Rendered platform content string.
|
|
1320
1235
|
*/
|
|
1321
|
-
function
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1236
|
+
function renderPlatformTemplate(templatePath) {
|
|
1237
|
+
const templatesAvailable = existsSync(templatePath);
|
|
1238
|
+
let content = toolsPlatformTemplate;
|
|
1239
|
+
// Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
|
|
1240
|
+
const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
|
|
1241
|
+
const match = ifRegex.exec(content);
|
|
1242
|
+
if (match) {
|
|
1243
|
+
content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
|
|
1244
|
+
}
|
|
1245
|
+
// Replace __TEMPLATE_PATH__ with the actual path
|
|
1246
|
+
content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
|
|
1247
|
+
return content;
|
|
1326
1248
|
}
|
|
1327
1249
|
/**
|
|
1328
1250
|
* Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
|
|
@@ -1330,60 +1252,22 @@ function registerHelpers() {
|
|
|
1330
1252
|
* @param options - Configuration for the refresh cycle.
|
|
1331
1253
|
*/
|
|
1332
1254
|
async function refreshPlatformContent(options) {
|
|
1333
|
-
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs,
|
|
1255
|
+
const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
|
|
1334
1256
|
const workspacePath = getWorkspacePath();
|
|
1335
1257
|
const coreConfigDir = getCoreConfigDir();
|
|
1336
|
-
// 1.
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
let availableServiceVersion;
|
|
1345
|
-
let availablePluginVersion;
|
|
1346
|
-
if (!skipRegistryCheck) {
|
|
1347
|
-
const coreRegistryVersion = checkRegistryVersion('@karmaniverous/jeeves', cacheDir);
|
|
1348
|
-
if (coreRegistryVersion && coreRegistryVersion !== coreVersion) {
|
|
1349
|
-
availableCoreVersion = coreRegistryVersion;
|
|
1350
|
-
}
|
|
1351
|
-
if (servicePackage) {
|
|
1352
|
-
const svcVersion = checkRegistryVersion(servicePackage, cacheDir);
|
|
1353
|
-
if (svcVersion) {
|
|
1354
|
-
availableServiceVersion = svcVersion;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
if (pluginPackage) {
|
|
1358
|
-
const plgVersion = checkRegistryVersion(pluginPackage, cacheDir);
|
|
1359
|
-
if (plgVersion) {
|
|
1360
|
-
availablePluginVersion = plgVersion;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1258
|
+
// 1. Write calling component's version entry
|
|
1259
|
+
if (componentName) {
|
|
1260
|
+
writeComponentVersion(coreConfigDir, {
|
|
1261
|
+
componentName,
|
|
1262
|
+
pluginVersion: componentVersion,
|
|
1263
|
+
servicePackage,
|
|
1264
|
+
pluginPackage,
|
|
1265
|
+
});
|
|
1363
1266
|
}
|
|
1364
|
-
//
|
|
1365
|
-
const serviceRows = probeResults.map((r) => ({
|
|
1366
|
-
...r,
|
|
1367
|
-
pluginVersion: r.name === componentName ? componentVersion : undefined,
|
|
1368
|
-
availableServiceVersion: r.name === componentName ? availableServiceVersion : undefined,
|
|
1369
|
-
availablePluginVersion: r.name === componentName ? availablePluginVersion : undefined,
|
|
1370
|
-
}));
|
|
1371
|
-
// 5. Check if templates are available
|
|
1267
|
+
// 2. Render Platform template
|
|
1372
1268
|
const templatePath = join(coreConfigDir, TEMPLATES_DIR);
|
|
1373
|
-
const
|
|
1374
|
-
//
|
|
1375
|
-
registerHelpers();
|
|
1376
|
-
const template = Handlebars.compile(toolsPlatformTemplate);
|
|
1377
|
-
const templateData = {
|
|
1378
|
-
services: serviceRows,
|
|
1379
|
-
unhealthyServices,
|
|
1380
|
-
coreVersion,
|
|
1381
|
-
availableCoreVersion,
|
|
1382
|
-
templatesAvailable,
|
|
1383
|
-
templatePath,
|
|
1384
|
-
};
|
|
1385
|
-
const platformContent = template(templateData);
|
|
1386
|
-
// 7. Write TOOLS.md Platform section
|
|
1269
|
+
const platformContent = renderPlatformTemplate(templatePath);
|
|
1270
|
+
// 3. Write TOOLS.md Platform section
|
|
1387
1271
|
const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
|
|
1388
1272
|
await updateManagedSection(toolsPath, platformContent, {
|
|
1389
1273
|
mode: 'section',
|
|
@@ -1392,7 +1276,7 @@ async function refreshPlatformContent(options) {
|
|
|
1392
1276
|
coreVersion,
|
|
1393
1277
|
stalenessThresholdMs,
|
|
1394
1278
|
});
|
|
1395
|
-
//
|
|
1279
|
+
// 4. Write SOUL.md managed block
|
|
1396
1280
|
const soulPath = join(workspacePath, WORKSPACE_FILES.soul);
|
|
1397
1281
|
await updateManagedSection(soulPath, soulSectionContent, {
|
|
1398
1282
|
mode: 'block',
|
|
@@ -1400,7 +1284,7 @@ async function refreshPlatformContent(options) {
|
|
|
1400
1284
|
coreVersion,
|
|
1401
1285
|
stalenessThresholdMs,
|
|
1402
1286
|
});
|
|
1403
|
-
//
|
|
1287
|
+
// 5. Write AGENTS.md managed block
|
|
1404
1288
|
const agentsPath = join(workspacePath, WORKSPACE_FILES.agents);
|
|
1405
1289
|
await updateManagedSection(agentsPath, agentsSectionContent, {
|
|
1406
1290
|
mode: 'block',
|
|
@@ -1408,7 +1292,7 @@ async function refreshPlatformContent(options) {
|
|
|
1408
1292
|
coreVersion,
|
|
1409
1293
|
stalenessThresholdMs,
|
|
1410
1294
|
});
|
|
1411
|
-
//
|
|
1295
|
+
// 6. Copy templates to config dir
|
|
1412
1296
|
copyTemplates(coreConfigDir);
|
|
1413
1297
|
}
|
|
1414
1298
|
|
|
@@ -1460,8 +1344,6 @@ async function seedContent(options) {
|
|
|
1460
1344
|
// Seed content via the same code path as writer cycles
|
|
1461
1345
|
await refreshPlatformContent({
|
|
1462
1346
|
coreVersion: options.coreVersion,
|
|
1463
|
-
probeTimeoutMs: options.probeTimeoutMs ?? 3000,
|
|
1464
|
-
skipRegistryCheck: options.skipRegistryCheck ?? true,
|
|
1465
1347
|
});
|
|
1466
1348
|
}
|
|
1467
1349
|
|
|
@@ -1514,7 +1396,6 @@ function registerInstallCommand(program) {
|
|
|
1514
1396
|
initFromOptions(opts);
|
|
1515
1397
|
await seedContent({
|
|
1516
1398
|
coreVersion: CORE_VERSION,
|
|
1517
|
-
skipRegistryCheck: true,
|
|
1518
1399
|
});
|
|
1519
1400
|
console.log('✅ Platform content seeded successfully.');
|
|
1520
1401
|
console.log(' - SOUL.md managed section written');
|
|
@@ -1526,12 +1407,73 @@ function registerInstallCommand(program) {
|
|
|
1526
1407
|
}
|
|
1527
1408
|
|
|
1528
1409
|
/**
|
|
1529
|
-
*
|
|
1410
|
+
* Service URL resolution.
|
|
1411
|
+
*
|
|
1412
|
+
* @remarks
|
|
1413
|
+
* Resolves the URL for a named Jeeves service using the following
|
|
1414
|
+
* resolution order:
|
|
1415
|
+
* 1. Consumer's own component config
|
|
1416
|
+
* 2. Core config (`{configRoot}/jeeves-core/config.json`)
|
|
1417
|
+
* 3. Default port constants
|
|
1418
|
+
*/
|
|
1419
|
+
/**
|
|
1420
|
+
* Resolve the URL for a named Jeeves service.
|
|
1421
|
+
*
|
|
1422
|
+
* @param serviceName - The service name (e.g., 'watcher', 'runner').
|
|
1423
|
+
* @param consumerName - Optional consumer component name for config override.
|
|
1424
|
+
* @returns The resolved service URL.
|
|
1425
|
+
* @throws Error if `init()` has not been called or the service is unknown.
|
|
1426
|
+
*/
|
|
1427
|
+
function getServiceUrl(serviceName, consumerName) {
|
|
1428
|
+
// 2. Check core config
|
|
1429
|
+
const coreDir = getCoreConfigDir();
|
|
1430
|
+
const coreConfig = loadConfig(coreDir);
|
|
1431
|
+
const coreUrl = coreConfig?.services[serviceName]?.url;
|
|
1432
|
+
if (coreUrl)
|
|
1433
|
+
return coreUrl;
|
|
1434
|
+
// 3. Fall back to port constants
|
|
1435
|
+
const port = DEFAULT_PORTS[serviceName];
|
|
1436
|
+
if (port !== undefined) {
|
|
1437
|
+
return `http://127.0.0.1:${String(port)}`;
|
|
1438
|
+
}
|
|
1439
|
+
throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* HTTP helpers for the OpenClaw plugin SDK.
|
|
1444
|
+
*
|
|
1445
|
+
* @remarks
|
|
1446
|
+
* Thin wrappers around `fetch` that throw on non-OK responses
|
|
1447
|
+
* and handle JSON serialisation/deserialisation.
|
|
1448
|
+
*/
|
|
1449
|
+
/**
|
|
1450
|
+
* Fetch a URL with an automatic abort timeout.
|
|
1451
|
+
*
|
|
1452
|
+
* @param url - URL to fetch.
|
|
1453
|
+
* @param timeoutMs - Timeout in milliseconds before aborting.
|
|
1454
|
+
* @param init - Optional `fetch` init options.
|
|
1455
|
+
* @returns The fetch Response object.
|
|
1456
|
+
*/
|
|
1457
|
+
async function fetchWithTimeout(url, timeoutMs, init) {
|
|
1458
|
+
const controller = new AbortController();
|
|
1459
|
+
const timeout = setTimeout(() => {
|
|
1460
|
+
controller.abort();
|
|
1461
|
+
}, timeoutMs);
|
|
1462
|
+
try {
|
|
1463
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1464
|
+
}
|
|
1465
|
+
finally {
|
|
1466
|
+
clearTimeout(timeout);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* CLI status command: discover components and probe their health.
|
|
1530
1472
|
*
|
|
1531
1473
|
* @remarks
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1534
|
-
* code 1 if any are unreachable.
|
|
1474
|
+
* Uses `readComponentVersions()` to discover registered components,
|
|
1475
|
+
* then probes each one via GET /status. Exits with code 0 if all
|
|
1476
|
+
* services are healthy, code 1 if any are unreachable.
|
|
1535
1477
|
*/
|
|
1536
1478
|
/**
|
|
1537
1479
|
* Register the status subcommand on the parent CLI program.
|
|
@@ -1541,7 +1483,7 @@ function registerInstallCommand(program) {
|
|
|
1541
1483
|
function registerStatusCommand(program) {
|
|
1542
1484
|
program
|
|
1543
1485
|
.command('status')
|
|
1544
|
-
.description('
|
|
1486
|
+
.description('Discover Jeeves components and probe their health')
|
|
1545
1487
|
.option('-w, --workspace <path>', 'Workspace root path', DEFAULT_WORKSPACE)
|
|
1546
1488
|
.option('-c, --config-root <path>', 'Platform config root path', DEFAULT_CONFIG_ROOT)
|
|
1547
1489
|
.option('-t, --timeout <ms>', 'Probe timeout in milliseconds', '3000')
|
|
@@ -1551,43 +1493,68 @@ function registerStatusCommand(program) {
|
|
|
1551
1493
|
console.log('Jeeves Platform Status');
|
|
1552
1494
|
console.log('='.repeat(60));
|
|
1553
1495
|
console.log();
|
|
1554
|
-
const
|
|
1496
|
+
const { getCoreConfigDir } = await Promise.resolve().then(function () { return init$1; });
|
|
1497
|
+
const coreConfigDir = getCoreConfigDir();
|
|
1498
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1499
|
+
const componentNames = Object.keys(componentVersions);
|
|
1500
|
+
if (componentNames.length === 0) {
|
|
1501
|
+
console.log('No components registered.');
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1555
1504
|
const nameWidth = 10;
|
|
1556
|
-
const portWidth = 6;
|
|
1557
1505
|
const statusWidth = 30;
|
|
1506
|
+
const versionWidth = 12;
|
|
1558
1507
|
const header = [
|
|
1559
|
-
'
|
|
1560
|
-
'Port'.padEnd(portWidth),
|
|
1508
|
+
'Component'.padEnd(nameWidth),
|
|
1561
1509
|
'Status'.padEnd(statusWidth),
|
|
1510
|
+
'Version'.padEnd(versionWidth),
|
|
1562
1511
|
].join(' ');
|
|
1563
1512
|
const separator = [
|
|
1564
1513
|
'-'.repeat(nameWidth),
|
|
1565
|
-
'-'.repeat(portWidth),
|
|
1566
1514
|
'-'.repeat(statusWidth),
|
|
1515
|
+
'-'.repeat(versionWidth),
|
|
1567
1516
|
].join(' ');
|
|
1568
1517
|
console.log(header);
|
|
1569
1518
|
console.log(separator);
|
|
1570
1519
|
let allHealthy = true;
|
|
1571
|
-
for (const
|
|
1520
|
+
for (const name of componentNames) {
|
|
1572
1521
|
let status;
|
|
1573
|
-
|
|
1574
|
-
|
|
1522
|
+
let version = '—';
|
|
1523
|
+
try {
|
|
1524
|
+
const url = getServiceUrl(name);
|
|
1525
|
+
const response = await fetchWithTimeout(`${url}/status`, timeoutMs);
|
|
1526
|
+
if (response.ok) {
|
|
1527
|
+
status = '✅ Running';
|
|
1528
|
+
try {
|
|
1529
|
+
const body = await response.json();
|
|
1530
|
+
if (typeof body === 'object' &&
|
|
1531
|
+
body !== null &&
|
|
1532
|
+
'version' in body &&
|
|
1533
|
+
typeof body['version'] === 'string') {
|
|
1534
|
+
version = body['version'];
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
catch {
|
|
1538
|
+
// Non-JSON response — version stays unknown
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
status = `❌ HTTP ${String(response.status)}`;
|
|
1543
|
+
allHealthy = false;
|
|
1544
|
+
}
|
|
1575
1545
|
}
|
|
1576
|
-
|
|
1577
|
-
status =
|
|
1546
|
+
catch {
|
|
1547
|
+
status = '❌ Down';
|
|
1578
1548
|
allHealthy = false;
|
|
1579
1549
|
}
|
|
1580
1550
|
const row = [
|
|
1581
|
-
|
|
1582
|
-
String(r.port).padEnd(portWidth),
|
|
1551
|
+
name.padEnd(nameWidth),
|
|
1583
1552
|
status.padEnd(statusWidth),
|
|
1553
|
+
version.padEnd(versionWidth),
|
|
1584
1554
|
].join(' ');
|
|
1585
1555
|
console.log(row);
|
|
1586
1556
|
}
|
|
1587
1557
|
console.log();
|
|
1588
|
-
const healthy = probeResults.filter((r) => r.healthy).length;
|
|
1589
|
-
const total = probeResults.length;
|
|
1590
|
-
console.log(`${String(healthy)}/${String(total)} services healthy`);
|
|
1591
1558
|
if (!allHealthy) {
|
|
1592
1559
|
process.exitCode = 1;
|
|
1593
1560
|
}
|
|
@@ -1679,13 +1646,25 @@ function registerUninstallCommand(program) {
|
|
|
1679
1646
|
console.log();
|
|
1680
1647
|
// Warn if services still responding
|
|
1681
1648
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
const
|
|
1649
|
+
const componentVersions = readComponentVersions(coreConfigDir);
|
|
1650
|
+
const componentNames = Object.keys(componentVersions);
|
|
1651
|
+
const running = [];
|
|
1652
|
+
for (const name of componentNames) {
|
|
1653
|
+
try {
|
|
1654
|
+
const url = getServiceUrl(name);
|
|
1655
|
+
const response = await fetchWithTimeout(`${url}/status`, 2000);
|
|
1656
|
+
if (response.ok) {
|
|
1657
|
+
running.push(name);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
catch {
|
|
1661
|
+
// Not running — expected during uninstall
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1684
1664
|
if (running.length > 0) {
|
|
1685
1665
|
console.log('⚠️ The following services are still responding:');
|
|
1686
|
-
for (const
|
|
1687
|
-
|
|
1688
|
-
console.log(` - ${r.name} on port ${String(r.port)}${ver}`);
|
|
1666
|
+
for (const name of running) {
|
|
1667
|
+
console.log(` - ${name}`);
|
|
1689
1668
|
}
|
|
1690
1669
|
console.log(' Consider stopping them before fully removing Jeeves.');
|
|
1691
1670
|
console.log();
|