@evref-bl/pharo-agent 0.11.0 → 0.12.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/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +6 -2
- package/README.md +18 -2
- package/package.json +2 -2
- package/plugin.json +1 -1
- package/skills/pharo-setup/SKILL.md +1 -1
- package/src/cli.js +8 -1
- package/src/gateway.js +5 -2
- package/src/help.js +6 -5
- package/src/images.js +25 -42
- package/src/init.js +4 -2
- package/src/live.js +65 -46
- package/src/mcpServer.js +4 -3
- package/src/project.js +19 -27
- package/src/runtime.js +5 -7
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
@@ -6,6 +6,20 @@ Claude Code, Codex and OpenCode.
|
|
|
6
6
|
|
|
7
7
|
## Setup
|
|
8
8
|
|
|
9
|
+
In Claude Code, install it as a plugin:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
/plugin marketplace add Evref-BL/pharo-agent-plugin
|
|
13
|
+
/plugin install pharo-agent@evref-bl
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Then ask the agent to enable Pharo tooling in a project. The plugin carries a
|
|
17
|
+
setup skill that runs the command below for you, and declares the `image` MCP
|
|
18
|
+
server, so the running image's tools reach the chat without a server
|
|
19
|
+
configured by hand. [Hosts](docs/hosts.md) covers enabling it per project
|
|
20
|
+
rather than everywhere.
|
|
21
|
+
|
|
22
|
+
Any host can run the CLI instead, which is the whole of the setup either way.
|
|
9
23
|
In the project directory:
|
|
10
24
|
|
|
11
25
|
```sh
|
|
@@ -35,8 +49,10 @@ the file ownership rules and [Images](docs/images.md) for image-side MCP updates
|
|
|
35
49
|
|
|
36
50
|
Node 22.12 or later.
|
|
37
51
|
|
|
38
|
-
|
|
39
|
-
[fuse-t](https://www.fuse-t.org/)
|
|
52
|
+
Images and their Pharo tools run anywhere Node does. The source projection is
|
|
53
|
+
the exception: it needs macOS with [fuse-t](https://www.fuse-t.org/), whose
|
|
54
|
+
daemon is downloaded for you. Everywhere else an image starts with its tools
|
|
55
|
+
and says that the projection is not available, rather than failing.
|
|
40
56
|
|
|
41
57
|
## What the agent gets
|
|
42
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evref-bl/pharo-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Terminal-first Pharo integration for coding agents.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@evref-bl/pharo-mcp": "3.3.1",
|
|
36
|
-
"@evref-bl/plexus": "0.
|
|
36
|
+
"@evref-bl/plexus": "0.12.1",
|
|
37
37
|
"@modelcontextprotocol/sdk": "1.32.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
package/plugin.json
CHANGED
|
@@ -8,7 +8,7 @@ Run this only when the user asks to enable or refresh Pharo tooling in this proj
|
|
|
8
8
|
From the project root, run the released CLI that matches this plugin:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npx --yes @evref-bl/pharo-agent@0.
|
|
11
|
+
npx --yes @evref-bl/pharo-agent@0.12.1 init --host claude
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Name the host this chat runs in: `--host claude` in Claude Code,
|
package/src/cli.js
CHANGED
|
@@ -29,11 +29,18 @@ function parseInit(args) {
|
|
|
29
29
|
return { host, mountRoot };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The template is required, and the loop below refuses the call without one,
|
|
34
|
+
* which the dynamic key assignment hides from the typechecker.
|
|
35
|
+
*
|
|
36
|
+
* @returns {{ id: string, template: string, category?: string, profile?: string }}
|
|
37
|
+
*/
|
|
32
38
|
function parseImageAdd(args) {
|
|
33
39
|
const [id, ...rest] = args;
|
|
34
40
|
if (!id || id.startsWith("--")) {
|
|
35
41
|
throw new Error("image add requires an image id");
|
|
36
42
|
}
|
|
43
|
+
/** @type {{ id: string, template?: string, category?: string, profile?: string }} */
|
|
37
44
|
const parsed = { id };
|
|
38
45
|
const options = {
|
|
39
46
|
"--template": "template",
|
|
@@ -56,7 +63,7 @@ function parseImageAdd(args) {
|
|
|
56
63
|
if (!parsed.template) {
|
|
57
64
|
throw new Error("image add requires --template <name>");
|
|
58
65
|
}
|
|
59
|
-
return parsed;
|
|
66
|
+
return { ...parsed, template: parsed.template };
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
// `image start` and `image stop` name their subject, like `image use` and
|
package/src/gateway.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { PlexusGateway } from "@evref-bl/plexus/gateway";
|
|
3
|
+
import { loadProjectState } from "@evref-bl/plexus";
|
|
3
4
|
import {
|
|
4
|
-
readJson,
|
|
5
5
|
selectRunningImage,
|
|
6
6
|
unappliedPharoMcpPin,
|
|
7
7
|
} from "./runtime.js";
|
|
@@ -15,7 +15,10 @@ export const { version } = createRequire(import.meta.url)("../package.json");
|
|
|
15
15
|
// is nothing to register here and nothing that can go stale between the call
|
|
16
16
|
// that started the image and this one.
|
|
17
17
|
export function imageGateway(projectRoot, running) {
|
|
18
|
-
const state =
|
|
18
|
+
const state = loadProjectState(running.statePath);
|
|
19
|
+
if (!state) {
|
|
20
|
+
throw new Error(`PLexus has no state at ${running.statePath}.`);
|
|
21
|
+
}
|
|
19
22
|
const { image, mcpServerId } = selectRunningImage(state, running.imageId);
|
|
20
23
|
if (image.id !== running.imageId || mcpServerId !== running.mcpServerId) {
|
|
21
24
|
throw new Error(`The PLexus image ${running.imageId} changed; run pharo-agent image start again.`);
|
package/src/help.js
CHANGED
|
@@ -40,15 +40,16 @@ const commands = {
|
|
|
40
40
|
},
|
|
41
41
|
"image start": {
|
|
42
42
|
usage: "image start [<id>]",
|
|
43
|
-
description: "Create if needed and start an image with its tools
|
|
44
|
-
+ "
|
|
45
|
-
+ "
|
|
43
|
+
description: "Create if needed and start an image with its Pharo tools. Omit the id to use the\n"
|
|
44
|
+
+ "project's default image. Images already running stay running. A source projection is\n"
|
|
45
|
+
+ "mounted on macOS with fuse-t; elsewhere the image starts without one and says so.\n"
|
|
46
|
+
+ "Pharo versions outside the MCP support range start without tools or a projection.",
|
|
46
47
|
example: "image start dev",
|
|
47
48
|
},
|
|
48
49
|
"image stop": {
|
|
49
50
|
usage: "image stop [<id>] [--all]",
|
|
50
|
-
description: "Stop the named image and
|
|
51
|
-
+ "no id is given. Use --all to stop every image this project started.",
|
|
51
|
+
description: "Stop the named image and any source projection it has, or the project's default\n"
|
|
52
|
+
+ "image when no id is given. Use --all to stop every image this project started.",
|
|
52
53
|
example: "image stop dev",
|
|
53
54
|
},
|
|
54
55
|
mcp: {
|
package/src/images.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
projectIdentity,
|
|
4
|
-
readProjectConfig,
|
|
5
|
-
writeProjectConfig,
|
|
6
|
-
} from "./project.js";
|
|
1
|
+
import { declareProjectImage, removeProjectImage } from "@evref-bl/plexus";
|
|
2
|
+
import { configuredImages } from "./project.js";
|
|
7
3
|
import { defaultImageId, readConfig, readRunningImages, writeConfig } from "./runtime.js";
|
|
8
4
|
|
|
9
5
|
export const defaultLoadScript = "pharo/load-mcp.st";
|
|
@@ -16,11 +12,6 @@ function assertNotRunning(projectRoot, imageId, action) {
|
|
|
16
12
|
}
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function derivedImageName(projectRoot, imageId) {
|
|
20
|
-
const { name } = projectIdentity(projectRoot);
|
|
21
|
-
return `${name}-{workspaceId}-${imageId}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
export function listImages(projectRoot) {
|
|
25
16
|
const configured = configuredImages(projectRoot);
|
|
26
17
|
const running = readRunningImages(projectRoot);
|
|
@@ -51,40 +42,37 @@ export function listImages(projectRoot) {
|
|
|
51
42
|
};
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* PLexus owns `plexus.json`, so PLexus writes it. This package chooses what
|
|
47
|
+
* to declare — the load script that puts the pinned MCP in the image — and
|
|
48
|
+
* leaves the schema, the name template, the defaults and the validation to
|
|
49
|
+
* the package that reads them back.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ id: string, template: string, category?: string, profile?: string }} request
|
|
52
|
+
*/
|
|
55
53
|
export function addImage(projectRoot, request) {
|
|
56
54
|
const { id, template, category, profile } = request;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
const declaredBefore = configuredImages(projectRoot).length;
|
|
56
|
+
const config = declareProjectImage(projectRoot, {
|
|
57
|
+
id,
|
|
58
|
+
templateName: template,
|
|
59
|
+
...(category ? { templateCategory: category } : {}),
|
|
60
|
+
...(profile ? { profileId: profile } : {}),
|
|
61
|
+
loadScript: defaultLoadScript,
|
|
62
|
+
});
|
|
63
|
+
const declared = config.images.find((image) => image.id === id);
|
|
64
|
+
if (!declared) {
|
|
65
|
+
throw new Error(`PLexus declared image ${id} but did not record it.`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
const image = {
|
|
68
|
-
id,
|
|
69
|
-
imageName,
|
|
70
|
-
mcp: { loadScript: defaultLoadScript, loadPolicy: "ifMissing" },
|
|
71
|
-
create: {
|
|
72
|
-
kind: "template",
|
|
73
|
-
...(profile ? { profileId: profile } : {}),
|
|
74
|
-
templateName: template,
|
|
75
|
-
...(category ? { templateCategory: category } : {}),
|
|
76
|
-
cleanupPolicy: "workspace_cleanup_only",
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
writeProjectConfig(projectRoot, { ...config, images: [...images, image] });
|
|
80
68
|
const pluginConfig = readConfig(projectRoot);
|
|
81
|
-
const isDefault = pluginConfig.defaultImage === undefined &&
|
|
69
|
+
const isDefault = pluginConfig.defaultImage === undefined && declaredBefore === 0;
|
|
82
70
|
if (isDefault) {
|
|
83
71
|
writeConfig(projectRoot, { ...pluginConfig, defaultImage: id });
|
|
84
72
|
}
|
|
85
73
|
return {
|
|
86
74
|
id,
|
|
87
|
-
imageName,
|
|
75
|
+
imageName: declared.imageName,
|
|
88
76
|
isDefault,
|
|
89
77
|
templateName: template,
|
|
90
78
|
...(category ? { templateCategory: category } : {}),
|
|
@@ -100,16 +88,11 @@ export function useImage(projectRoot, id) {
|
|
|
100
88
|
}
|
|
101
89
|
|
|
102
90
|
export function removeImage(projectRoot, id) {
|
|
103
|
-
|
|
104
|
-
const images = Array.isArray(config.images) ? config.images : [];
|
|
105
|
-
if (!images.some((image) => image.id === id)) {
|
|
91
|
+
if (!configuredImages(projectRoot).some((image) => image.id === id)) {
|
|
106
92
|
throw new Error(`Image ${id} is not configured.`);
|
|
107
93
|
}
|
|
108
94
|
assertNotRunning(projectRoot, id, "removing it");
|
|
109
|
-
|
|
110
|
-
...config,
|
|
111
|
-
images: images.filter((image) => image.id !== id),
|
|
112
|
-
});
|
|
95
|
+
removeProjectImage(projectRoot, id);
|
|
113
96
|
const pluginConfig = readConfig(projectRoot);
|
|
114
97
|
if (pluginConfig.defaultImage === id) {
|
|
115
98
|
const { defaultImage: _removed, ...rest } = pluginConfig;
|
package/src/init.js
CHANGED
|
@@ -137,8 +137,10 @@ starts without MCP tools or a source projection, saying so.
|
|
|
137
137
|
use <id>\` changes the default, and \`pharo-agent image remove <id>\` drops a
|
|
138
138
|
declaration. Stop an image before removing its declaration.
|
|
139
139
|
|
|
140
|
-
Use \`pharo-agent image start\` before live-image work
|
|
141
|
-
under \`.pharo-agent/mounts
|
|
140
|
+
Use \`pharo-agent image start\` before live-image work. On macOS with fuse-t
|
|
141
|
+
installed the image's source is also projected under \`.pharo-agent/mounts\`
|
|
142
|
+
and can be edited as files; everywhere else the Pharo tools are the way in,
|
|
143
|
+
and \`image start\` says so.
|
|
142
144
|
|
|
143
145
|
With the pharo-agent plugin enabled for this project, the image's tools are MCP
|
|
144
146
|
tools: start an image with \`image_start\` and call the rest by name. Without it,
|
package/src/live.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
stopProjectImage,
|
|
10
10
|
} from "@evref-bl/plexus";
|
|
11
11
|
import { callImageTool } from "./gateway.js";
|
|
12
|
-
import { readProjectConfig
|
|
12
|
+
import { readProjectConfig } from "./project.js";
|
|
13
13
|
import {
|
|
14
14
|
readConfig,
|
|
15
15
|
readRunningImages,
|
|
@@ -24,6 +24,48 @@ import {
|
|
|
24
24
|
|
|
25
25
|
export const pifsRelease = "v0.9.2";
|
|
26
26
|
|
|
27
|
+
// fuse-t ships as a macOS installer package, so it lands in /usr/local
|
|
28
|
+
// whatever Homebrew's prefix is. The unversioned symlink is what the daemon
|
|
29
|
+
// links against.
|
|
30
|
+
export const fuseLibraryPath = "/usr/local/lib/libfuse-t.dylib";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Whether a source projection can be mounted here.
|
|
34
|
+
*
|
|
35
|
+
* PIFS needs macOS and fuse-t, which is a kernel-adjacent install the user
|
|
36
|
+
* has to approve. PLexus and the Pharo MCP run anywhere, so until this was
|
|
37
|
+
* checked, `image start` mounted unconditionally and then stopped the image
|
|
38
|
+
* when the mount failed -- every image on Linux and Windows, and on any Mac
|
|
39
|
+
* without fuse-t. The image and its tools were never the part that could not
|
|
40
|
+
* work.
|
|
41
|
+
*
|
|
42
|
+
* @param {{ platform?: string, exists?: (path: string) => boolean }} [host]
|
|
43
|
+
*/
|
|
44
|
+
export function projectionAvailability(host = {}) {
|
|
45
|
+
const platform = host.platform ?? process.platform;
|
|
46
|
+
const exists = host.exists ?? fs.existsSync;
|
|
47
|
+
if (platform !== "darwin") {
|
|
48
|
+
return {
|
|
49
|
+
available: false,
|
|
50
|
+
reason: "A source projection needs macOS and fuse-t, which this platform"
|
|
51
|
+
+ ` (${platform}) cannot provide.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!exists(fuseLibraryPath)) {
|
|
55
|
+
return {
|
|
56
|
+
available: false,
|
|
57
|
+
reason: "A source projection needs fuse-t, which is not installed."
|
|
58
|
+
+ " Install it with `brew install --cask fuse-t`.",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { available: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function projectionUnavailableWarning(imageId, reason) {
|
|
65
|
+
return `${reason} Image ${imageId} is running with its Pharo tools and no`
|
|
66
|
+
+ " source projection; read and edit its code through the tools.";
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
// The class-side PIFSServer messages this plugin sends into the image. The
|
|
28
70
|
// release automation checks that a published PIFS still implements them: a pin
|
|
29
71
|
// bump that leaves a removed selector in place passes every test here, because
|
|
@@ -106,48 +148,6 @@ export function imageCreationPlan(listed, imageId) {
|
|
|
106
148
|
return "none";
|
|
107
149
|
}
|
|
108
150
|
|
|
109
|
-
// The launcher resolves a template name against its default group only, so an
|
|
110
|
-
// image naming a template from any other group cannot be created until the
|
|
111
|
-
// project config says which group that is. Find it once, on first creation.
|
|
112
|
-
export async function resolveTemplateCategory(projectRoot, launcher, imageId) {
|
|
113
|
-
const config = readProjectConfig(projectRoot);
|
|
114
|
-
const images = Array.isArray(config.images) ? config.images : [];
|
|
115
|
-
const configured = images.find((image) => image.id === imageId);
|
|
116
|
-
const create = configured?.create;
|
|
117
|
-
if (!create?.templateName || create.templateCategory) {
|
|
118
|
-
return create?.templateCategory;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const listed = await launcher.listTemplates();
|
|
122
|
-
const templates = Array.isArray(listed?.templates) ? listed.templates : [];
|
|
123
|
-
const categories = [
|
|
124
|
-
...new Set(
|
|
125
|
-
templates
|
|
126
|
-
.filter((template) => template?.name === create.templateName)
|
|
127
|
-
.map((template) => template?.templateCategory)
|
|
128
|
-
.filter(Boolean),
|
|
129
|
-
),
|
|
130
|
-
];
|
|
131
|
-
if (categories.length === 0) {
|
|
132
|
-
throw new Error(`No template named ${create.templateName} in any category.`);
|
|
133
|
-
}
|
|
134
|
-
if (categories.length > 1) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
`Template ${create.templateName} exists in several categories: ${categories.join(", ")}.`
|
|
137
|
-
+ ` Set create.templateCategory for image ${imageId} to choose one.`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const [templateCategory] = categories;
|
|
142
|
-
writeProjectConfig(projectRoot, {
|
|
143
|
-
...config,
|
|
144
|
-
images: images.map((image) => (image.id === imageId
|
|
145
|
-
? { ...image, create: { ...image.create, templateCategory } }
|
|
146
|
-
: image)),
|
|
147
|
-
});
|
|
148
|
-
return templateCategory;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
151
|
export function describeLifecycleFailure(failure) {
|
|
152
152
|
const stderr = failure.launcherResult?.raw?.stderr?.trim();
|
|
153
153
|
return [
|
|
@@ -207,12 +207,14 @@ export async function startAgentImage(projectRoot, imageId) {
|
|
|
207
207
|
writeRunningImages(projectRoot, runningImages);
|
|
208
208
|
restarted = true;
|
|
209
209
|
}
|
|
210
|
-
|
|
210
|
+
if (projectionAvailability().available) {
|
|
211
|
+
fetchPifsRelease(pifsCloneDirectory(projectRoot));
|
|
212
|
+
}
|
|
211
213
|
const launcher = new ScopedPharoLauncher({ projectRoot });
|
|
212
214
|
const plan = imageCreationPlan(await launcher.listImages(), resolved);
|
|
213
215
|
const created = plan !== "none";
|
|
214
216
|
if (created) {
|
|
215
|
-
|
|
217
|
+
// PLexus resolves which category the template lives in.
|
|
216
218
|
await launcher.createImage(resolved);
|
|
217
219
|
}
|
|
218
220
|
const started = await runLifecycle(
|
|
@@ -237,6 +239,24 @@ export async function startAgentImage(projectRoot, imageId) {
|
|
|
237
239
|
writeRunningImages(projectRoot, { ...runningImages, [resolved]: record });
|
|
238
240
|
return { ...record, created, warning: pharoMcpWarning(withoutMcp) };
|
|
239
241
|
}
|
|
242
|
+
const unappliedPin = unappliedPharoMcpPin(image);
|
|
243
|
+
const projection = projectionAvailability();
|
|
244
|
+
if (!projection.available) {
|
|
245
|
+
// The image and its tools are the point; the projection is the extra this
|
|
246
|
+
// package bundles where the platform allows it.
|
|
247
|
+
const record = { statePath: started.statePath, imageId: image.id, mcpServerId };
|
|
248
|
+
writeRunningImages(projectRoot, { ...runningImages, [resolved]: record });
|
|
249
|
+
return {
|
|
250
|
+
imageId: image.id,
|
|
251
|
+
created,
|
|
252
|
+
...(restarted ? { restarted } : {}),
|
|
253
|
+
warning: [
|
|
254
|
+
projectionUnavailableWarning(image.id, projection.reason),
|
|
255
|
+
unappliedPin,
|
|
256
|
+
].filter(Boolean).join("\n"),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
240
260
|
const mountPath = resolveMountPath(projectRoot, readConfig(projectRoot), image.id);
|
|
241
261
|
fs.mkdirSync(path.dirname(mountPath), { recursive: true });
|
|
242
262
|
const record = {
|
|
@@ -255,7 +275,6 @@ export async function startAgentImage(projectRoot, imageId) {
|
|
|
255
275
|
throw error;
|
|
256
276
|
}
|
|
257
277
|
writeRunningImages(projectRoot, { ...runningImages, [resolved]: { ...record, projectionPort } });
|
|
258
|
-
const unappliedPin = unappliedPharoMcpPin(image);
|
|
259
278
|
return {
|
|
260
279
|
imageId: image.id,
|
|
261
280
|
created,
|
package/src/mcpServer.js
CHANGED
|
@@ -39,8 +39,9 @@ const lifecycleTools = [
|
|
|
39
39
|
name: "image_start",
|
|
40
40
|
description:
|
|
41
41
|
"Start one of this project's Pharo images, creating it from its template "
|
|
42
|
-
+ "when it does not exist yet
|
|
43
|
-
+ "
|
|
42
|
+
+ "when it does not exist yet. The image's own tools are listed once it "
|
|
43
|
+
+ "answers. On macOS with fuse-t its source also mounts as files; "
|
|
44
|
+
+ "elsewhere the image starts without that and says so.",
|
|
44
45
|
inputSchema: {
|
|
45
46
|
type: "object",
|
|
46
47
|
properties: {
|
|
@@ -61,7 +62,7 @@ const lifecycleTools = [
|
|
|
61
62
|
{
|
|
62
63
|
name: "image_stop",
|
|
63
64
|
description:
|
|
64
|
-
"Stop a running image and
|
|
65
|
+
"Stop a running image, and any source projection this project started for it.",
|
|
65
66
|
inputSchema: {
|
|
66
67
|
type: "object",
|
|
67
68
|
properties: {
|
package/src/project.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
// The config file is PLexus's, so its name and its path come from PLexus.
|
|
4
|
+
// Spelling them here made this package a second definition of another
|
|
5
|
+
// package's format, free to drift from it without anything noticing.
|
|
6
|
+
import {
|
|
7
|
+
createProjectConfigFile,
|
|
8
|
+
loadProjectConfig,
|
|
9
|
+
plexusProjectConfigFileName,
|
|
10
|
+
projectConfigPath as plexusProjectConfigPath,
|
|
11
|
+
} from "@evref-bl/plexus";
|
|
3
12
|
|
|
4
|
-
export const projectConfigFileName =
|
|
13
|
+
export const projectConfigFileName = plexusProjectConfigFileName;
|
|
5
14
|
|
|
6
15
|
function projectRootAbove(start) {
|
|
7
16
|
let directory = path.resolve(start);
|
|
@@ -47,43 +56,26 @@ export function enclosingProjectRoot(start) {
|
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
export function projectConfigPath(projectRoot) {
|
|
50
|
-
return
|
|
59
|
+
return plexusProjectConfigPath(projectRoot);
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export function pluginDirectory(projectRoot) {
|
|
54
63
|
return path.join(projectRoot, ".pharo-agent");
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.replaceAll(/[^A-Za-z0-9._-]+/g, "-")
|
|
61
|
-
.replaceAll(/^-+|-+$/g, "")
|
|
62
|
-
.toLowerCase();
|
|
63
|
-
return { id: id || "pharo-project", name };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
+
// Validated, not merely parsed: a malformed plexus.json then fails naming
|
|
67
|
+
// what is wrong with it, rather than reaching PLexus as a shape it refuses
|
|
68
|
+
// further in.
|
|
66
69
|
export function readProjectConfig(projectRoot) {
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
70
|
-
} catch (error) {
|
|
71
|
-
throw new Error(`Cannot read ${configPath}: ${error.message}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function writeProjectConfig(projectRoot, config) {
|
|
76
|
-
fs.writeFileSync(projectConfigPath(projectRoot), `${JSON.stringify(config, null, 2)}\n`);
|
|
70
|
+
return loadProjectConfig(projectRoot);
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
export function createProjectConfig(projectRoot) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return config;
|
|
74
|
+
return createProjectConfigFile(projectRoot, {
|
|
75
|
+
name: path.basename(path.resolve(projectRoot)),
|
|
76
|
+
});
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
export function configuredImages(projectRoot) {
|
|
87
|
-
|
|
88
|
-
return Array.isArray(config.images) ? config.images : [];
|
|
80
|
+
return readProjectConfig(projectRoot).images;
|
|
89
81
|
}
|
package/src/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { loadProjectState } from "@evref-bl/plexus";
|
|
3
4
|
import { configuredImages, pluginDirectory } from "./project.js";
|
|
4
5
|
|
|
5
6
|
export function runtimeDirectory(projectRoot) {
|
|
@@ -114,13 +115,10 @@ export function imageProcessIsAlive(pid) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export function recordedImageIsRunning(record) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
const image = (state.images ?? []).find((candidate) => candidate.id === record.imageId);
|
|
118
|
+
// PLexus owns the state file, so PLexus reads it: a hand-rolled JSON.parse
|
|
119
|
+
// here was this package deciding what another package's format means.
|
|
120
|
+
const state = loadProjectState(record.statePath);
|
|
121
|
+
const image = state?.images.find((candidate) => candidate.id === record.imageId);
|
|
124
122
|
return image?.status === "running" && imageProcessIsAlive(image.pid);
|
|
125
123
|
}
|
|
126
124
|
|