@sapiom/harness 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +44 -43
- package/dist/cli/auth.d.ts +26 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +39 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +163 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/consent.d.ts +15 -0
- package/dist/cli/consent.d.ts.map +1 -0
- package/dist/cli/consent.js +81 -0
- package/dist/cli/consent.js.map +1 -0
- package/dist/cli/doctor.d.ts +27 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +74 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/machine-id.d.ts +19 -0
- package/dist/cli/machine-id.d.ts.map +1 -0
- package/dist/cli/machine-id.js +75 -0
- package/dist/cli/machine-id.js.map +1 -0
- package/dist/cli/paths.d.ts +3 -0
- package/dist/cli/paths.d.ts.map +1 -0
- package/dist/cli/paths.js +11 -0
- package/dist/cli/paths.js.map +1 -0
- package/dist/cli/settings.d.ts +26 -0
- package/dist/cli/settings.d.ts.map +1 -0
- package/dist/cli/settings.js +123 -0
- package/dist/cli/settings.js.map +1 -0
- package/dist/core/adapters/claude-code.d.ts +41 -0
- package/dist/core/adapters/claude-code.d.ts.map +1 -0
- package/dist/core/adapters/claude-code.js +238 -0
- package/dist/core/adapters/claude-code.js.map +1 -0
- package/dist/core/adapters/codex.d.ts +59 -0
- package/dist/core/adapters/codex.d.ts.map +1 -0
- package/dist/core/adapters/codex.js +319 -0
- package/dist/core/adapters/codex.js.map +1 -0
- package/dist/core/canvas-body.d.ts +45 -0
- package/dist/core/canvas-body.d.ts.map +1 -0
- package/dist/core/canvas-body.js +94 -0
- package/dist/core/canvas-body.js.map +1 -0
- package/dist/core/canvas-cache.d.ts +23 -0
- package/dist/core/canvas-cache.d.ts.map +1 -0
- package/dist/core/canvas-cache.js +64 -0
- package/dist/core/canvas-cache.js.map +1 -0
- package/dist/core/canvas-enrich.d.ts +106 -0
- package/dist/core/canvas-enrich.d.ts.map +1 -0
- package/dist/core/canvas-enrich.js +178 -0
- package/dist/core/canvas-enrich.js.map +1 -0
- package/dist/core/canvas-enrichment.d.ts +144 -0
- package/dist/core/canvas-enrichment.d.ts.map +1 -0
- package/dist/core/canvas-enrichment.js +192 -0
- package/dist/core/canvas-enrichment.js.map +1 -0
- package/dist/core/canvas-graph.d.ts +71 -0
- package/dist/core/canvas-graph.d.ts.map +1 -0
- package/dist/core/canvas-graph.js +111 -0
- package/dist/core/canvas-graph.js.map +1 -0
- package/dist/core/canvas-index-classify.d.ts +41 -0
- package/dist/core/canvas-index-classify.d.ts.map +1 -0
- package/dist/core/canvas-index-classify.js +63 -0
- package/dist/core/canvas-index-classify.js.map +1 -0
- package/dist/core/canvas-interconnections.d.ts +24 -0
- package/dist/core/canvas-interconnections.d.ts.map +1 -0
- package/dist/core/canvas-interconnections.js +96 -0
- package/dist/core/canvas-interconnections.js.map +1 -0
- package/dist/core/canvas-manifest-check.d.ts +15 -0
- package/dist/core/canvas-manifest-check.d.ts.map +1 -0
- package/dist/core/canvas-manifest-check.js +90 -0
- package/dist/core/canvas-manifest-check.js.map +1 -0
- package/dist/core/canvas-render.d.ts +73 -0
- package/dist/core/canvas-render.d.ts.map +1 -0
- package/dist/core/canvas-render.js +145 -0
- package/dist/core/canvas-render.js.map +1 -0
- package/dist/core/canvas-svg.d.ts +50 -0
- package/dist/core/canvas-svg.d.ts.map +1 -0
- package/dist/core/canvas-svg.js +240 -0
- package/dist/core/canvas-svg.js.map +1 -0
- package/dist/core/canvas-template.d.ts +43 -0
- package/dist/core/canvas-template.d.ts.map +1 -0
- package/dist/core/canvas-template.js +365 -0
- package/dist/core/canvas-template.js.map +1 -0
- package/dist/core/canvas-watcher.d.ts +25 -0
- package/dist/core/canvas-watcher.d.ts.map +1 -0
- package/dist/core/canvas-watcher.js +144 -0
- package/dist/core/canvas-watcher.js.map +1 -0
- package/dist/core/collector/batcher.d.ts +57 -0
- package/dist/core/collector/batcher.d.ts.map +1 -0
- package/dist/core/collector/batcher.js +143 -0
- package/dist/core/collector/batcher.js.map +1 -0
- package/dist/core/collector/codex-tailer.d.ts +85 -0
- package/dist/core/collector/codex-tailer.d.ts.map +1 -0
- package/dist/core/collector/codex-tailer.js +270 -0
- package/dist/core/collector/codex-tailer.js.map +1 -0
- package/dist/core/collector/normalizer.d.ts +33 -0
- package/dist/core/collector/normalizer.d.ts.map +1 -0
- package/dist/core/collector/normalizer.js +94 -0
- package/dist/core/collector/normalizer.js.map +1 -0
- package/dist/core/collector/seq.d.ts +14 -0
- package/dist/core/collector/seq.d.ts.map +1 -0
- package/dist/core/collector/seq.js +20 -0
- package/dist/core/collector/seq.js.map +1 -0
- package/dist/core/collector/store.d.ts +16 -0
- package/dist/core/collector/store.d.ts.map +1 -0
- package/dist/core/collector/store.js +30 -0
- package/dist/core/collector/store.js.map +1 -0
- package/dist/core/collector/transcript.d.ts +38 -0
- package/dist/core/collector/transcript.d.ts.map +1 -0
- package/dist/core/collector/transcript.js +110 -0
- package/dist/core/collector/transcript.js.map +1 -0
- package/dist/core/event-bus.d.ts +9 -0
- package/dist/core/event-bus.d.ts.map +1 -0
- package/dist/core/event-bus.js +25 -0
- package/dist/core/event-bus.js.map +1 -0
- package/dist/core/example-seed.d.ts +34 -0
- package/dist/core/example-seed.d.ts.map +1 -0
- package/dist/core/example-seed.js +277 -0
- package/dist/core/example-seed.js.map +1 -0
- package/dist/core/inject/claude-settings.d.ts +34 -0
- package/dist/core/inject/claude-settings.d.ts.map +1 -0
- package/dist/core/inject/claude-settings.js +152 -0
- package/dist/core/inject/claude-settings.js.map +1 -0
- package/dist/core/inject/mcp-config.d.ts +26 -0
- package/dist/core/inject/mcp-config.d.ts.map +1 -0
- package/dist/core/inject/mcp-config.js +39 -0
- package/dist/core/inject/mcp-config.js.map +1 -0
- package/dist/core/inject/retention.d.ts +66 -0
- package/dist/core/inject/retention.d.ts.map +1 -0
- package/dist/core/inject/retention.js +119 -0
- package/dist/core/inject/retention.js.map +1 -0
- package/dist/core/inject/system-prompt.d.ts +11 -0
- package/dist/core/inject/system-prompt.d.ts.map +1 -0
- package/dist/core/inject/system-prompt.js +22 -0
- package/dist/core/inject/system-prompt.js.map +1 -0
- package/dist/core/macro-runner.d.ts +36 -0
- package/dist/core/macro-runner.d.ts.map +1 -0
- package/dist/core/macro-runner.js +66 -0
- package/dist/core/macro-runner.js.map +1 -0
- package/dist/core/macros.d.ts +10 -0
- package/dist/core/macros.d.ts.map +1 -0
- package/dist/core/macros.js +59 -0
- package/dist/core/macros.js.map +1 -0
- package/dist/core/path-safety.d.ts +33 -0
- package/dist/core/path-safety.d.ts.map +1 -0
- package/dist/core/path-safety.js +63 -0
- package/dist/core/path-safety.js.map +1 -0
- package/dist/core/paths.d.ts +27 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +43 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/port-detector.d.ts +69 -0
- package/dist/core/port-detector.d.ts.map +1 -0
- package/dist/core/port-detector.js +121 -0
- package/dist/core/port-detector.js.map +1 -0
- package/dist/core/session-manager.d.ts +275 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +729 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/task-manager.d.ts +138 -0
- package/dist/core/task-manager.d.ts.map +1 -0
- package/dist/core/task-manager.js +286 -0
- package/dist/core/task-manager.js.map +1 -0
- package/dist/core/task-stream.d.ts +32 -0
- package/dist/core/task-stream.d.ts.map +1 -0
- package/dist/core/task-stream.js +87 -0
- package/dist/core/task-stream.js.map +1 -0
- package/dist/core/workflow-registry.d.ts +36 -0
- package/dist/core/workflow-registry.d.ts.map +1 -0
- package/dist/core/workflow-registry.js +220 -0
- package/dist/core/workflow-registry.js.map +1 -0
- package/dist/core/workspace-context.d.ts +43 -0
- package/dist/core/workspace-context.d.ts.map +1 -0
- package/dist/core/workspace-context.js +108 -0
- package/dist/core/workspace-context.js.map +1 -0
- package/dist/core/workspace-watcher.d.ts +26 -0
- package/dist/core/workspace-watcher.d.ts.map +1 -0
- package/dist/core/workspace-watcher.js +174 -0
- package/dist/core/workspace-watcher.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/profiles/canvas-guidelines.d.ts +20 -0
- package/dist/profiles/canvas-guidelines.d.ts.map +1 -0
- package/dist/profiles/canvas-guidelines.js +48 -0
- package/dist/profiles/canvas-guidelines.js.map +1 -0
- package/dist/profiles/default.d.ts +10 -0
- package/dist/profiles/default.d.ts.map +1 -0
- package/dist/profiles/default.js +70 -0
- package/dist/profiles/default.js.map +1 -0
- package/dist/server/auth.d.ts +9 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +28 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/canvas-render.d.ts +18 -0
- package/dist/server/canvas-render.d.ts.map +1 -0
- package/dist/server/canvas-render.js +24 -0
- package/dist/server/canvas-render.js.map +1 -0
- package/dist/server/canvas.d.ts +12 -0
- package/dist/server/canvas.d.ts.map +1 -0
- package/dist/server/canvas.js +174 -0
- package/dist/server/canvas.js.map +1 -0
- package/dist/server/events-ws.d.ts +11 -0
- package/dist/server/events-ws.d.ts.map +1 -0
- package/dist/server/events-ws.js +24 -0
- package/dist/server/events-ws.js.map +1 -0
- package/dist/server/fs.d.ts +4 -0
- package/dist/server/fs.d.ts.map +1 -0
- package/dist/server/fs.js +88 -0
- package/dist/server/fs.js.map +1 -0
- package/dist/server/index.d.ts +83 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/ingest.d.ts +74 -0
- package/dist/server/ingest.d.ts.map +1 -0
- package/dist/server/ingest.js +74 -0
- package/dist/server/ingest.js.map +1 -0
- package/dist/server/macros.d.ts +32 -0
- package/dist/server/macros.d.ts.map +1 -0
- package/dist/server/macros.js +76 -0
- package/dist/server/macros.js.map +1 -0
- package/dist/server/rest.d.ts +75 -0
- package/dist/server/rest.d.ts.map +1 -0
- package/dist/server/rest.js +235 -0
- package/dist/server/rest.js.map +1 -0
- package/dist/server/static.d.ts +8 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +36 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/terminal-ws.d.ts +9 -0
- package/dist/server/terminal-ws.d.ts.map +1 -0
- package/dist/server/terminal-ws.js +59 -0
- package/dist/server/terminal-ws.js.map +1 -0
- package/dist/server/ws-router.d.ts +16 -0
- package/dist/server/ws-router.d.ts.map +1 -0
- package/dist/server/ws-router.js +26 -0
- package/dist/server/ws-router.js.map +1 -0
- package/dist/shared/types.d.ts +485 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +80 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/web/assets/index-B9R4TzH7.css +32 -0
- package/dist/web/assets/index-Dh97QAgq.js +192 -0
- package/dist/web/index.html +27 -0
- package/package.json +84 -7
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow registry (workstream W2's backend slice).
|
|
3
|
+
*
|
|
4
|
+
* Discovers orchestration projects by scanning a directory tree (bounded
|
|
5
|
+
* depth) for `sapiom.json` marker files, tracks manually-connected paths,
|
|
6
|
+
* and persists the combined list to HARNESS_PATHS.workflows. Exposes an
|
|
7
|
+
* Express router implementing the /api/workflows surface from
|
|
8
|
+
* src/shared/types.ts; the integrator mounts it (and express.json()) on the
|
|
9
|
+
* shared app.
|
|
10
|
+
*/
|
|
11
|
+
import { type Router as ExpressRouter } from "express";
|
|
12
|
+
import { type WorkflowInfo } from "../shared/types.js";
|
|
13
|
+
export declare class WorkflowRegistry {
|
|
14
|
+
private readonly registryPath;
|
|
15
|
+
private workflows;
|
|
16
|
+
private loaded;
|
|
17
|
+
constructor(registryPath?: string);
|
|
18
|
+
private ensureLoaded;
|
|
19
|
+
private persist;
|
|
20
|
+
list(): Promise<WorkflowInfo[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Drops entries whose `path` no longer exists on disk and persists the
|
|
23
|
+
* result — users delete projects, and a crashed run can leave a temp
|
|
24
|
+
* directory registered. Deliberately narrow: only a confirmed-missing
|
|
25
|
+
* path (ENOENT/ENOTDIR) is pruned. A directory that exists but is
|
|
26
|
+
* merely unbuilt, unreadable (permissions), or temporarily unstattable
|
|
27
|
+
* stays registered. Returns what was pruned so the caller can log it.
|
|
28
|
+
*/
|
|
29
|
+
prune(): Promise<WorkflowInfo[]>;
|
|
30
|
+
/** Scans `root` and merges discovered projects into the persisted registry. */
|
|
31
|
+
scan(root: string): Promise<WorkflowInfo[]>;
|
|
32
|
+
/** Registers an arbitrary path (the "+ Connect" flow); marker is optional at connect time. */
|
|
33
|
+
connectPath(inputPath: string): Promise<WorkflowInfo>;
|
|
34
|
+
}
|
|
35
|
+
export declare function createWorkflowsRouter(registry: WorkflowRegistry): ExpressRouter;
|
|
36
|
+
//# sourceMappingURL=workflow-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-registry.d.ts","sourceRoot":"","sources":["../../src/core/workflow-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,EAAU,KAAK,MAAM,IAAI,aAAa,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAmFtE,qBAAa,gBAAgB;IAIf,OAAO,CAAC,QAAQ,CAAC,YAAY;IAHzC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,MAAM,CAAS;gBAEM,YAAY,GAAE,MAA4C;YAEzE,YAAY;YAWZ,OAAO;IAKf,IAAI,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAKrC;;;;;;;OAOG;IACG,KAAK,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAqBtC,+EAA+E;IACzE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAiBjD,8FAA8F;IACxF,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAgB5D;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,aAAa,CAkC/E"}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow registry (workstream W2's backend slice).
|
|
3
|
+
*
|
|
4
|
+
* Discovers orchestration projects by scanning a directory tree (bounded
|
|
5
|
+
* depth) for `sapiom.json` marker files, tracks manually-connected paths,
|
|
6
|
+
* and persists the combined list to HARNESS_PATHS.workflows. Exposes an
|
|
7
|
+
* Express router implementing the /api/workflows surface from
|
|
8
|
+
* src/shared/types.ts; the integrator mounts it (and express.json()) on the
|
|
9
|
+
* shared app.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { Router } from "express";
|
|
15
|
+
import { HARNESS_PATHS } from "../shared/types.js";
|
|
16
|
+
import { hasTraversalSegment, resolveWithinRoot } from "./path-safety.js";
|
|
17
|
+
const MAX_SCAN_DEPTH = 3;
|
|
18
|
+
const SKIP_DIR_NAMES = new Set(["node_modules", ".git"]);
|
|
19
|
+
function expandHome(inputPath) {
|
|
20
|
+
if (inputPath === "~")
|
|
21
|
+
return os.homedir();
|
|
22
|
+
if (inputPath.startsWith("~/"))
|
|
23
|
+
return path.join(os.homedir(), inputPath.slice(2));
|
|
24
|
+
return inputPath;
|
|
25
|
+
}
|
|
26
|
+
// `dir` reaching these sinks is always a resolved absolute path (from
|
|
27
|
+
// path.resolve in scan/connectPath, or a confined descent in scanDir), so a
|
|
28
|
+
// `..` segment can never survive. Asserting it anyway keeps the no-traversal
|
|
29
|
+
// guarantee explicit and local to each fs read, and covers the arbitrary
|
|
30
|
+
// path connectPath accepts (which has no scan root to confine it to).
|
|
31
|
+
async function readMarker(dir) {
|
|
32
|
+
if (hasTraversalSegment(dir))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const raw = await fs.readFile(path.join(dir, "sapiom.json"), "utf8");
|
|
36
|
+
return JSON.parse(raw);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function nameFor(dir) {
|
|
43
|
+
if (hasTraversalSegment(dir))
|
|
44
|
+
return path.basename(dir);
|
|
45
|
+
try {
|
|
46
|
+
const raw = await fs.readFile(path.join(dir, "package.json"), "utf8");
|
|
47
|
+
const pkg = JSON.parse(raw);
|
|
48
|
+
if (pkg.name)
|
|
49
|
+
return pkg.name;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// No package.json (or it doesn't parse) — fall back to the directory name.
|
|
53
|
+
}
|
|
54
|
+
return path.basename(dir);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Depth-first scan; a directory carrying a marker is registered and not
|
|
58
|
+
* descended into. Every directory touched is confined to `root` (the tree the
|
|
59
|
+
* caller asked to scan): the initial call is `root` itself, and recursion only
|
|
60
|
+
* ever descends into a direct child, so no crafted entry name can walk the
|
|
61
|
+
* scan outside `root`. Symlinked entries report `isDirectory() === false`
|
|
62
|
+
* (withFileTypes uses raw dirent info, not a followed stat) and so are never
|
|
63
|
+
* descended into either.
|
|
64
|
+
*/
|
|
65
|
+
async function scanDir(root, dir, depth, found) {
|
|
66
|
+
if (depth > MAX_SCAN_DEPTH)
|
|
67
|
+
return;
|
|
68
|
+
const safeDir = resolveWithinRoot(root, dir);
|
|
69
|
+
if (!safeDir)
|
|
70
|
+
return;
|
|
71
|
+
const marker = await readMarker(safeDir);
|
|
72
|
+
if (marker) {
|
|
73
|
+
found.push({
|
|
74
|
+
name: await nameFor(safeDir),
|
|
75
|
+
path: safeDir,
|
|
76
|
+
definitionId: marker.definitionId ?? null,
|
|
77
|
+
source: "scan",
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let entries;
|
|
82
|
+
try {
|
|
83
|
+
entries = await fs.readdir(safeDir, { withFileTypes: true });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (!entry.isDirectory() || SKIP_DIR_NAMES.has(entry.name))
|
|
90
|
+
continue;
|
|
91
|
+
await scanDir(root, path.join(safeDir, entry.name), depth + 1, found);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export class WorkflowRegistry {
|
|
95
|
+
constructor(registryPath = expandHome(HARNESS_PATHS.workflows)) {
|
|
96
|
+
this.registryPath = registryPath;
|
|
97
|
+
this.workflows = [];
|
|
98
|
+
this.loaded = false;
|
|
99
|
+
}
|
|
100
|
+
async ensureLoaded() {
|
|
101
|
+
if (this.loaded)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
const raw = await fs.readFile(this.registryPath, "utf8");
|
|
105
|
+
this.workflows = JSON.parse(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
this.workflows = [];
|
|
109
|
+
}
|
|
110
|
+
this.loaded = true;
|
|
111
|
+
}
|
|
112
|
+
async persist() {
|
|
113
|
+
await fs.mkdir(path.dirname(this.registryPath), { recursive: true });
|
|
114
|
+
await fs.writeFile(this.registryPath, JSON.stringify(this.workflows, null, 2));
|
|
115
|
+
}
|
|
116
|
+
async list() {
|
|
117
|
+
await this.ensureLoaded();
|
|
118
|
+
return this.workflows;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Drops entries whose `path` no longer exists on disk and persists the
|
|
122
|
+
* result — users delete projects, and a crashed run can leave a temp
|
|
123
|
+
* directory registered. Deliberately narrow: only a confirmed-missing
|
|
124
|
+
* path (ENOENT/ENOTDIR) is pruned. A directory that exists but is
|
|
125
|
+
* merely unbuilt, unreadable (permissions), or temporarily unstattable
|
|
126
|
+
* stays registered. Returns what was pruned so the caller can log it.
|
|
127
|
+
*/
|
|
128
|
+
async prune() {
|
|
129
|
+
await this.ensureLoaded();
|
|
130
|
+
const kept = [];
|
|
131
|
+
const pruned = [];
|
|
132
|
+
for (const workflow of this.workflows) {
|
|
133
|
+
try {
|
|
134
|
+
await fs.stat(workflow.path);
|
|
135
|
+
kept.push(workflow);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const code = err.code;
|
|
139
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
140
|
+
pruned.push(workflow);
|
|
141
|
+
else
|
|
142
|
+
kept.push(workflow);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (pruned.length > 0) {
|
|
146
|
+
this.workflows = kept;
|
|
147
|
+
await this.persist();
|
|
148
|
+
}
|
|
149
|
+
return pruned;
|
|
150
|
+
}
|
|
151
|
+
/** Scans `root` and merges discovered projects into the persisted registry. */
|
|
152
|
+
async scan(root) {
|
|
153
|
+
await this.ensureLoaded();
|
|
154
|
+
const absoluteRoot = path.resolve(expandHome(root));
|
|
155
|
+
const found = [];
|
|
156
|
+
await scanDir(absoluteRoot, absoluteRoot, 0, found);
|
|
157
|
+
const byPath = new Map(this.workflows.map((workflow) => [workflow.path, workflow]));
|
|
158
|
+
for (const workflow of found) {
|
|
159
|
+
const existing = byPath.get(workflow.path);
|
|
160
|
+
// A manually-connected entry keeps its `source`; a scan only refreshes name/definitionId.
|
|
161
|
+
byPath.set(workflow.path, existing ? { ...existing, ...workflow, source: existing.source } : workflow);
|
|
162
|
+
}
|
|
163
|
+
this.workflows = Array.from(byPath.values());
|
|
164
|
+
await this.persist();
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
/** Registers an arbitrary path (the "+ Connect" flow); marker is optional at connect time. */
|
|
168
|
+
async connectPath(inputPath) {
|
|
169
|
+
await this.ensureLoaded();
|
|
170
|
+
const absolutePath = path.resolve(expandHome(inputPath));
|
|
171
|
+
const marker = await readMarker(absolutePath);
|
|
172
|
+
const info = {
|
|
173
|
+
name: await nameFor(absolutePath),
|
|
174
|
+
path: absolutePath,
|
|
175
|
+
definitionId: marker?.definitionId ?? null,
|
|
176
|
+
source: "connect",
|
|
177
|
+
};
|
|
178
|
+
const idx = this.workflows.findIndex((workflow) => workflow.path === absolutePath);
|
|
179
|
+
if (idx >= 0)
|
|
180
|
+
this.workflows[idx] = info;
|
|
181
|
+
else
|
|
182
|
+
this.workflows.push(info);
|
|
183
|
+
await this.persist();
|
|
184
|
+
return info;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
export function createWorkflowsRouter(registry) {
|
|
188
|
+
const router = Router();
|
|
189
|
+
router.get("/api/workflows", async (_req, res) => {
|
|
190
|
+
res.json(await registry.list());
|
|
191
|
+
});
|
|
192
|
+
router.post("/api/workflows/connect", async (req, res) => {
|
|
193
|
+
const inputPath = req.body?.path;
|
|
194
|
+
if (typeof inputPath !== "string" || !inputPath.trim()) {
|
|
195
|
+
res.status(400).json({ error: "path is required" });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
res.json(await registry.connectPath(inputPath));
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
res.status(500).json({ error: err.message });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
router.post("/api/workflows/scan", async (req, res) => {
|
|
206
|
+
const root = req.body?.root;
|
|
207
|
+
if (typeof root !== "string" || !root.trim()) {
|
|
208
|
+
res.status(400).json({ error: "root is required" });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
res.json(await registry.scan(root));
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
res.status(500).json({ error: err.message });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return router;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=workflow-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-registry.js","sourceRoot":"","sources":["../../src/core/workflow-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes HARNESS_CONTEXT_FILE (`.sapiom/harness-context.json`) in a
|
|
3
|
+
* session's cwd — the agent-legible mirror of this workspace's UI state:
|
|
4
|
+
* which workflow (if any) the session is bound to, every workflow the
|
|
5
|
+
* registry currently knows about, and the session's own identity. Called
|
|
6
|
+
* unconditionally from `SessionManager.create()` so the file exists for
|
|
7
|
+
* every session regardless of entry point (REST, `autoCreateSession`), as a
|
|
8
|
+
* backfill from `SessionManager.resume()` when it's missing entirely, on
|
|
9
|
+
* every `PATCH /api/sessions/:id/workflow`, and whenever the workflow
|
|
10
|
+
* registry changes (scan/connect) — see server/index.ts's
|
|
11
|
+
* `writeSessionContext`/`scanWorkflowsAndBroadcast` for how those call
|
|
12
|
+
* sites are wired, and both of which can legitimately fire concurrent
|
|
13
|
+
* writes to the *same* destination (a scan's rewrite-all-open-sessions step
|
|
14
|
+
* racing a user's live bind click, for instance) — see `withPerPathQueue`.
|
|
15
|
+
* Unbinding writes `boundWorkflow: null` rather than deleting the file, so a
|
|
16
|
+
* concurrent read from the agent never races a momentary ENOENT.
|
|
17
|
+
*/
|
|
18
|
+
import { type HarnessKind, type WorkflowInfo } from "../shared/types.js";
|
|
19
|
+
export interface WorkspaceContextSession {
|
|
20
|
+
id: string;
|
|
21
|
+
cwd: string;
|
|
22
|
+
harness: HarnessKind;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Atomically writes `<session.cwd>/.sapiom/harness-context.json`.
|
|
26
|
+
* Best-effort: a session's cwd could in principle be unwritable
|
|
27
|
+
* (permissions, deleted out from under the session) — that must never fail
|
|
28
|
+
* the caller that triggered it, so errors are logged, not thrown.
|
|
29
|
+
*
|
|
30
|
+
* `workflows` is sorted by path before writing (deterministic, independent
|
|
31
|
+
* of registry scan/insertion order) so an agent re-reading the file across
|
|
32
|
+
* turns can diff it cheaply instead of re-parsing a reordered blob every
|
|
33
|
+
* time.
|
|
34
|
+
*/
|
|
35
|
+
export declare function writeHarnessContext(session: WorkspaceContextSession, boundWorkflow: WorkflowInfo | null, workflows: WorkflowInfo[]): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* True if `<cwd>/.sapiom/harness-context.json` already exists. Used by
|
|
38
|
+
* `SessionManager.resume()` to decide whether a backfill write is needed —
|
|
39
|
+
* resume must never clobber a file that could already reflect a real
|
|
40
|
+
* binding.
|
|
41
|
+
*/
|
|
42
|
+
export declare function harnessContextFileExists(cwd: string): Promise<boolean>;
|
|
43
|
+
//# sourceMappingURL=workspace-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-context.d.ts","sourceRoot":"","sources":["../../src/core/workspace-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,EAEL,KAAK,WAAW,EAGhB,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAM5B,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,WAAW,CAAC;CACtB;AAiCD;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,uBAAuB,EAChC,aAAa,EAAE,YAAY,GAAG,IAAI,EAClC,SAAS,EAAE,YAAY,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO5E"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes HARNESS_CONTEXT_FILE (`.sapiom/harness-context.json`) in a
|
|
3
|
+
* session's cwd — the agent-legible mirror of this workspace's UI state:
|
|
4
|
+
* which workflow (if any) the session is bound to, every workflow the
|
|
5
|
+
* registry currently knows about, and the session's own identity. Called
|
|
6
|
+
* unconditionally from `SessionManager.create()` so the file exists for
|
|
7
|
+
* every session regardless of entry point (REST, `autoCreateSession`), as a
|
|
8
|
+
* backfill from `SessionManager.resume()` when it's missing entirely, on
|
|
9
|
+
* every `PATCH /api/sessions/:id/workflow`, and whenever the workflow
|
|
10
|
+
* registry changes (scan/connect) — see server/index.ts's
|
|
11
|
+
* `writeSessionContext`/`scanWorkflowsAndBroadcast` for how those call
|
|
12
|
+
* sites are wired, and both of which can legitimately fire concurrent
|
|
13
|
+
* writes to the *same* destination (a scan's rewrite-all-open-sessions step
|
|
14
|
+
* racing a user's live bind click, for instance) — see `withPerPathQueue`.
|
|
15
|
+
* Unbinding writes `boundWorkflow: null` rather than deleting the file, so a
|
|
16
|
+
* concurrent read from the agent never races a momentary ENOENT.
|
|
17
|
+
*/
|
|
18
|
+
import * as crypto from "node:crypto";
|
|
19
|
+
import * as fs from "node:fs/promises";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { HARNESS_CONTEXT_FILE, } from "../shared/types.js";
|
|
22
|
+
function toContextWorkflowEntry(workflow) {
|
|
23
|
+
return { name: workflow.name, path: workflow.path, definitionId: workflow.definitionId };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Serializes writes per destination path. Two independent triggers can
|
|
27
|
+
* legitimately race on the same `harness-context.json` (a workflow scan's
|
|
28
|
+
* rewrite-all-open-sessions step, a bind/unbind, a fresh session's initial
|
|
29
|
+
* write) — without this, concurrent writers could both compute the same
|
|
30
|
+
* `Date.now()`-based tmp filename (confirmed via repro: a tight burst of
|
|
31
|
+
* concurrent writes to one destination reliably collides within the same
|
|
32
|
+
* millisecond) and steal each other's tmp file out from under a pending
|
|
33
|
+
* `rename`, which fails with ENOENT. Serializing per path also fixes a
|
|
34
|
+
* subtler issue beyond the crash: without it, whichever concurrent write's
|
|
35
|
+
* disk I/O happens to finish first wins, which can silently apply writes
|
|
36
|
+
* out of the order they were actually triggered in. Keyed by absolute
|
|
37
|
+
* path; an entry is removed once nothing else has chained onto it, so this
|
|
38
|
+
* never grows unbounded over a long-running server's lifetime.
|
|
39
|
+
*/
|
|
40
|
+
const writeQueues = new Map();
|
|
41
|
+
async function withPerPathQueue(filePath, task) {
|
|
42
|
+
const previous = writeQueues.get(filePath) ?? Promise.resolve();
|
|
43
|
+
// `task` never throws (its own try/catch logs and swallows) — chaining
|
|
44
|
+
// onto both branches of `previous` is defense in depth, so one write's
|
|
45
|
+
// failure can never wedge the queue for the next one to this same path.
|
|
46
|
+
const current = previous.then(task, task);
|
|
47
|
+
writeQueues.set(filePath, current);
|
|
48
|
+
try {
|
|
49
|
+
await current;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
if (writeQueues.get(filePath) === current)
|
|
53
|
+
writeQueues.delete(filePath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Atomically writes `<session.cwd>/.sapiom/harness-context.json`.
|
|
58
|
+
* Best-effort: a session's cwd could in principle be unwritable
|
|
59
|
+
* (permissions, deleted out from under the session) — that must never fail
|
|
60
|
+
* the caller that triggered it, so errors are logged, not thrown.
|
|
61
|
+
*
|
|
62
|
+
* `workflows` is sorted by path before writing (deterministic, independent
|
|
63
|
+
* of registry scan/insertion order) so an agent re-reading the file across
|
|
64
|
+
* turns can diff it cheaply instead of re-parsing a reordered blob every
|
|
65
|
+
* time.
|
|
66
|
+
*/
|
|
67
|
+
export async function writeHarnessContext(session, boundWorkflow, workflows) {
|
|
68
|
+
const filePath = path.join(session.cwd, HARNESS_CONTEXT_FILE);
|
|
69
|
+
const sortedWorkflows = [...workflows].sort((a, b) => a.path.localeCompare(b.path)).map(toContextWorkflowEntry);
|
|
70
|
+
const context = {
|
|
71
|
+
boundWorkflow: boundWorkflow ? toContextWorkflowEntry(boundWorkflow) : null,
|
|
72
|
+
workflows: sortedWorkflows,
|
|
73
|
+
session: { id: session.id, cwd: session.cwd, harness: session.harness },
|
|
74
|
+
updatedAt: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
await withPerPathQueue(filePath, async () => {
|
|
77
|
+
try {
|
|
78
|
+
const dir = path.dirname(filePath);
|
|
79
|
+
await fs.mkdir(dir, { recursive: true });
|
|
80
|
+
// A random suffix, not just pid+Date.now(): millisecond resolution is
|
|
81
|
+
// not fine enough to stay unique across a burst of concurrent writes
|
|
82
|
+
// to the same destination (confirmed via repro). Collision-proof
|
|
83
|
+
// regardless of timing, independent of the queue above.
|
|
84
|
+
const tmpPath = path.join(dir, `.harness-context.json.tmp-${process.pid}-${crypto.randomUUID()}`);
|
|
85
|
+
await fs.writeFile(tmpPath, JSON.stringify(context, null, 2) + "\n", "utf8");
|
|
86
|
+
await fs.rename(tmpPath, filePath);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error(`[harness] failed to write ${filePath}:`, err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* True if `<cwd>/.sapiom/harness-context.json` already exists. Used by
|
|
95
|
+
* `SessionManager.resume()` to decide whether a backfill write is needed —
|
|
96
|
+
* resume must never clobber a file that could already reflect a real
|
|
97
|
+
* binding.
|
|
98
|
+
*/
|
|
99
|
+
export async function harnessContextFileExists(cwd) {
|
|
100
|
+
try {
|
|
101
|
+
await fs.access(path.join(cwd, HARNESS_CONTEXT_FILE));
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=workspace-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-context.js","sourceRoot":"","sources":["../../src/core/workspace-context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fingerprint of the set of workflow-marker directories under `root` (sorted,
|
|
3
|
+
* bounded depth, ignored subtrees skipped). Changes exactly when a workflow is
|
|
4
|
+
* added, removed, or renamed — not when unrelated files are edited. Exported
|
|
5
|
+
* for the polling fallback and for direct testing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function snapshotWorkspaceWorkflows(root: string): string;
|
|
8
|
+
export interface WorkspaceWatcherManagerDeps {
|
|
9
|
+
/** Debounced per-session notification that the workspace's workflow set may
|
|
10
|
+
* have changed — the integrator re-scans that session's cwd and broadcasts
|
|
11
|
+
* `workflows.changed` if the list actually changed. */
|
|
12
|
+
onChange(harnessSessionId: string): void;
|
|
13
|
+
}
|
|
14
|
+
/** Registry of one SessionWorkspaceWatcher per active harness session. */
|
|
15
|
+
export declare class WorkspaceWatcherManager {
|
|
16
|
+
private readonly deps;
|
|
17
|
+
private readonly watchers;
|
|
18
|
+
constructor(deps: WorkspaceWatcherManagerDeps);
|
|
19
|
+
/** Idempotent: replaces any existing watcher for this session. */
|
|
20
|
+
start(harnessSessionId: string, cwd: string): void;
|
|
21
|
+
stop(harnessSessionId: string): void;
|
|
22
|
+
stopAll(): void;
|
|
23
|
+
/** Test/debug helper — how many sessions currently have an active watcher. */
|
|
24
|
+
get size(): number;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=workspace-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-watcher.d.ts","sourceRoot":"","sources":["../../src/core/workspace-watcher.ts"],"names":[],"mappings":"AAkDA;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAyB/D;AAsED,MAAM,WAAW,2BAA2B;IAC1C;;4DAEwD;IACxD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,0EAA0E;AAC1E,qBAAa,uBAAuB;IAGtB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8C;gBAE1C,IAAI,EAAE,2BAA2B;IAE9D,kEAAkE;IAClE,KAAK,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAQlD,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI;IAKpC,OAAO,IAAI,IAAI;IAIf,8EAA8E;IAC9E,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace watcher: notices when workflows are added to / removed from a
|
|
3
|
+
* session's workspace directory DURING a session, so the rail can be re-scanned
|
|
4
|
+
* and re-broadcast instead of staying frozen at whatever the boot/session-create
|
|
5
|
+
* scan found. Users scaffold new workflows (and delete old ones) mid-session and
|
|
6
|
+
* expect the rail to keep up.
|
|
7
|
+
*
|
|
8
|
+
* Modeled on core/canvas-watcher.ts (same recursive-fs.watch-with-polling-
|
|
9
|
+
* fallback shape, same per-session lifecycle), but tuned to STRUCTURAL change
|
|
10
|
+
* rather than content change. It fires only when the SET of workflow-marker
|
|
11
|
+
* directories under the workspace actually changes (a workflow appearing,
|
|
12
|
+
* disappearing, or being renamed) — never for ordinary file edits:
|
|
13
|
+
*
|
|
14
|
+
* - A raw watch event only *arms* a check; the debounced check recomputes
|
|
15
|
+
* the workflow-marker fingerprint and fires `onChange` iff it differs from
|
|
16
|
+
* the last one. This is deliberately NOT keyed off the `fs.watch` event
|
|
17
|
+
* type: recursive `fs.watch` on macOS reports `rename` for plain content
|
|
18
|
+
* writes too, so an event-type filter would spam rescans on every save.
|
|
19
|
+
* The fingerprint diff is the reliable, cross-platform signal.
|
|
20
|
+
* - High-churn / irrelevant subtrees (`node_modules`, `.git`, `.sapiom`,
|
|
21
|
+
* build output) are skipped when arming AND when fingerprinting: a
|
|
22
|
+
* workflow marker never lives there, and `.sapiom/canvas/renders` in
|
|
23
|
+
* particular is rewritten on every render.
|
|
24
|
+
*
|
|
25
|
+
* The polling fallback (Linux, or a watcher runtime error) diffs the same
|
|
26
|
+
* fingerprint on an interval, so both paths share one notion of "changed".
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from "node:fs";
|
|
29
|
+
import * as path from "node:path";
|
|
30
|
+
const DEBOUNCE_MS = 250;
|
|
31
|
+
const POLL_INTERVAL_MS = 1000;
|
|
32
|
+
/** Kept in sync with core/workflow-registry.ts's scan: the marker file that
|
|
33
|
+
* makes a directory a workflow, and how deep the scan looks for it. */
|
|
34
|
+
const WORKFLOW_MARKER = "sapiom.json";
|
|
35
|
+
const MAX_SCAN_DEPTH = 3;
|
|
36
|
+
/** Directories a workflow marker never lives in — skipped both when
|
|
37
|
+
* fingerprinting and when deciding whether a watch event is relevant.
|
|
38
|
+
* `.sapiom` covers the canvas renders that are rewritten on every render. */
|
|
39
|
+
const IGNORED_DIR_NAMES = new Set(["node_modules", ".git", ".sapiom", "dist", "build", ".next"]);
|
|
40
|
+
function firstSegmentIgnored(relPath) {
|
|
41
|
+
for (const segment of relPath.split(path.sep)) {
|
|
42
|
+
if (segment && IGNORED_DIR_NAMES.has(segment))
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fingerprint of the set of workflow-marker directories under `root` (sorted,
|
|
49
|
+
* bounded depth, ignored subtrees skipped). Changes exactly when a workflow is
|
|
50
|
+
* added, removed, or renamed — not when unrelated files are edited. Exported
|
|
51
|
+
* for the polling fallback and for direct testing.
|
|
52
|
+
*/
|
|
53
|
+
export function snapshotWorkspaceWorkflows(root) {
|
|
54
|
+
const markerDirs = [];
|
|
55
|
+
const walk = (dir, depth) => {
|
|
56
|
+
if (depth > MAX_SCAN_DEPTH)
|
|
57
|
+
return;
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// A directory carrying the marker is itself a workflow — record it and
|
|
66
|
+
// don't descend (matches the registry's scan semantics).
|
|
67
|
+
if (entries.some((entry) => entry.isFile() && entry.name === WORKFLOW_MARKER)) {
|
|
68
|
+
markerDirs.push(dir);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory() || IGNORED_DIR_NAMES.has(entry.name))
|
|
73
|
+
continue;
|
|
74
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(root, 0);
|
|
78
|
+
return markerDirs.sort().join("|");
|
|
79
|
+
}
|
|
80
|
+
/** One session's workspace watcher. */
|
|
81
|
+
class SessionWorkspaceWatcher {
|
|
82
|
+
constructor(cwd, harnessSessionId, onChange) {
|
|
83
|
+
this.cwd = cwd;
|
|
84
|
+
this.harnessSessionId = harnessSessionId;
|
|
85
|
+
this.onChange = onChange;
|
|
86
|
+
this.watcher = null;
|
|
87
|
+
this.pollTimer = null;
|
|
88
|
+
this.debounceTimer = null;
|
|
89
|
+
this.closed = false;
|
|
90
|
+
this.lastSnapshot = "";
|
|
91
|
+
this.lastSnapshot = snapshotWorkspaceWorkflows(this.cwd);
|
|
92
|
+
this.arm();
|
|
93
|
+
}
|
|
94
|
+
/** Debounced check: recompute the fingerprint and fire only on a real change. */
|
|
95
|
+
scheduleCheck() {
|
|
96
|
+
if (this.closed)
|
|
97
|
+
return;
|
|
98
|
+
if (this.debounceTimer)
|
|
99
|
+
clearTimeout(this.debounceTimer);
|
|
100
|
+
this.debounceTimer = setTimeout(() => this.checkNow(), DEBOUNCE_MS);
|
|
101
|
+
}
|
|
102
|
+
checkNow() {
|
|
103
|
+
if (this.closed)
|
|
104
|
+
return;
|
|
105
|
+
const snapshot = snapshotWorkspaceWorkflows(this.cwd);
|
|
106
|
+
if (snapshot === this.lastSnapshot)
|
|
107
|
+
return;
|
|
108
|
+
this.lastSnapshot = snapshot;
|
|
109
|
+
this.onChange(this.harnessSessionId);
|
|
110
|
+
}
|
|
111
|
+
isRelevantPath(filename) {
|
|
112
|
+
// A `null` filename means the platform couldn't say what changed — check
|
|
113
|
+
// to be safe rather than miss a new/removed workflow.
|
|
114
|
+
if (!filename)
|
|
115
|
+
return true;
|
|
116
|
+
return !firstSegmentIgnored(filename);
|
|
117
|
+
}
|
|
118
|
+
arm() {
|
|
119
|
+
if (this.closed)
|
|
120
|
+
return;
|
|
121
|
+
try {
|
|
122
|
+
this.watcher = fs.watch(this.cwd, { recursive: true }, (_event, filename) => {
|
|
123
|
+
if (this.isRelevantPath(filename))
|
|
124
|
+
this.scheduleCheck();
|
|
125
|
+
});
|
|
126
|
+
this.watcher.on("error", () => this.fallBackToPolling());
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// `recursive` isn't supported on this platform (notably Linux).
|
|
130
|
+
this.fallBackToPolling();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
fallBackToPolling() {
|
|
134
|
+
if (this.closed || this.pollTimer)
|
|
135
|
+
return;
|
|
136
|
+
this.watcher?.close();
|
|
137
|
+
this.watcher = null;
|
|
138
|
+
this.pollTimer = setInterval(() => this.checkNow(), POLL_INTERVAL_MS);
|
|
139
|
+
}
|
|
140
|
+
close() {
|
|
141
|
+
this.closed = true;
|
|
142
|
+
if (this.debounceTimer)
|
|
143
|
+
clearTimeout(this.debounceTimer);
|
|
144
|
+
if (this.pollTimer)
|
|
145
|
+
clearInterval(this.pollTimer);
|
|
146
|
+
this.watcher?.close();
|
|
147
|
+
this.watcher = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Registry of one SessionWorkspaceWatcher per active harness session. */
|
|
151
|
+
export class WorkspaceWatcherManager {
|
|
152
|
+
constructor(deps) {
|
|
153
|
+
this.deps = deps;
|
|
154
|
+
this.watchers = new Map();
|
|
155
|
+
}
|
|
156
|
+
/** Idempotent: replaces any existing watcher for this session. */
|
|
157
|
+
start(harnessSessionId, cwd) {
|
|
158
|
+
this.stop(harnessSessionId);
|
|
159
|
+
this.watchers.set(harnessSessionId, new SessionWorkspaceWatcher(cwd, harnessSessionId, (id) => this.deps.onChange(id)));
|
|
160
|
+
}
|
|
161
|
+
stop(harnessSessionId) {
|
|
162
|
+
this.watchers.get(harnessSessionId)?.close();
|
|
163
|
+
this.watchers.delete(harnessSessionId);
|
|
164
|
+
}
|
|
165
|
+
stopAll() {
|
|
166
|
+
for (const harnessSessionId of [...this.watchers.keys()])
|
|
167
|
+
this.stop(harnessSessionId);
|
|
168
|
+
}
|
|
169
|
+
/** Test/debug helper — how many sessions currently have an active watcher. */
|
|
170
|
+
get size() {
|
|
171
|
+
return this.watchers.size;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=workspace-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-watcher.js","sourceRoot":"","sources":["../../src/core/workspace-watcher.ts"],"names":[],"mappings":"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"}
|