@kontourai/flow-agents 1.0.0 → 1.0.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/CHANGELOG.md +8 -0
- package/build/src/cli/flow-kit.js +135 -5
- package/build/src/cli/validate-source-tree.js +19 -2
- package/build/src/runtime-adapters.js +1 -1
- package/build/src/tools/validate-source-tree.js +1 -1
- package/docs/fixture-ownership.md +1 -1
- package/evals/integration/test_activate_npx_context.sh +134 -0
- package/evals/integration/test_flow_kit_install_git.sh +163 -0
- package/package.json +1 -1
- package/src/cli/flow-kit.ts +123 -4
- package/src/cli/validate-source-tree.ts +7 -1
- package/src/runtime-adapters.ts +1 -1
- package/src/tools/validate-source-tree.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.1](https://github.com/kontourai/flow-agents/compare/v1.0.0...v1.0.1) (2026-06-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixes
|
|
7
|
+
|
|
8
|
+
* resolve three kit-distribution blockers ([#55](https://github.com/kontourai/flow-agents/issues/55) [#56](https://github.com/kontourai/flow-agents/issues/56) [#57](https://github.com/kontourai/flow-agents/issues/57)) ([3350cb1](https://github.com/kontourai/flow-agents/commit/3350cb15f44bff92d8d9c57f447761d0e1a1b20c))
|
|
9
|
+
* resolve three kit-distribution blockers ([#55](https://github.com/kontourai/flow-agents/issues/55), [#56](https://github.com/kontourai/flow-agents/issues/56), [#57](https://github.com/kontourai/flow-agents/issues/57)) ([13bf732](https://github.com/kontourai/flow-agents/commit/13bf732ff365efa84423e9ea46042e501d202db8))
|
|
10
|
+
|
|
3
11
|
## [1.0.0](https://github.com/kontourai/flow-agents/compare/v0.4.0...v1.0.0) (2026-06-12)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
3
5
|
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
5
8
|
import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
|
|
6
9
|
import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
|
|
@@ -27,6 +30,22 @@ function contentHash(root) {
|
|
|
27
30
|
}
|
|
28
31
|
return `sha256:${hash.digest("hex")}`;
|
|
29
32
|
}
|
|
33
|
+
/** Content hash that excludes .git and other VCS/cache directories (for install-git clones). */
|
|
34
|
+
function kitContentHash(root) {
|
|
35
|
+
const EXCLUDE_DIRS = new Set([".git", "__pycache__", ".pytest_cache"]);
|
|
36
|
+
const hash = crypto.createHash("sha256");
|
|
37
|
+
for (const file of walkFiles(root)) {
|
|
38
|
+
const parts = path.relative(root, file).split(path.sep);
|
|
39
|
+
if (parts.some((p) => EXCLUDE_DIRS.has(p)))
|
|
40
|
+
continue;
|
|
41
|
+
const rel = parts.join("/");
|
|
42
|
+
hash.update(rel);
|
|
43
|
+
hash.update("\0");
|
|
44
|
+
hash.update(fs.readFileSync(file));
|
|
45
|
+
hash.update("\0");
|
|
46
|
+
}
|
|
47
|
+
return `sha256:${hash.digest("hex")}`;
|
|
48
|
+
}
|
|
30
49
|
function installLocal(argv) {
|
|
31
50
|
const args = parseArgs(argv);
|
|
32
51
|
const source = path.resolve(args.positionals[0] ?? "");
|
|
@@ -36,13 +55,11 @@ function installLocal(argv) {
|
|
|
36
55
|
manifest = assertKitRepository(source);
|
|
37
56
|
}
|
|
38
57
|
catch (error) {
|
|
39
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
40
58
|
console.log("Flow Kit repository validation failed:");
|
|
41
59
|
for (const diagnostic of (error.diagnostics ?? [error.message]))
|
|
42
60
|
console.log(` - ${diagnostic}`);
|
|
43
61
|
return 1;
|
|
44
62
|
}
|
|
45
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
46
63
|
const kitId = String(manifest.id);
|
|
47
64
|
const hash = contentHash(source);
|
|
48
65
|
const registry = loadRegistry(dest);
|
|
@@ -163,10 +180,107 @@ function inspect(argv) {
|
|
|
163
180
|
console.log(JSON.stringify(result, null, 2));
|
|
164
181
|
return result.conformance.k0 ? 0 : 1;
|
|
165
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>] [--force] [--update]
|
|
185
|
+
*
|
|
186
|
+
* Shallow-clones a remote git repository to a temporary directory, validates the kit
|
|
187
|
+
* container with the same logic used by install-local, then delegates to the existing
|
|
188
|
+
* install path. Supports an optional #ref fragment in the URL or a separate --ref flag.
|
|
189
|
+
*
|
|
190
|
+
* Implements kontourai/flow-agents#56 (git-ref install surface).
|
|
191
|
+
*/
|
|
192
|
+
function installGit(argv) {
|
|
193
|
+
const args = parseArgs(argv);
|
|
194
|
+
const rawUrl = args.positionals[0] ?? "";
|
|
195
|
+
if (!rawUrl) {
|
|
196
|
+
console.error("install-git: missing <repo-url> argument");
|
|
197
|
+
console.error("usage: flow-kit install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>]");
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
// Parse ref: #fragment in URL takes precedence over --ref flag.
|
|
201
|
+
let repoUrl = rawUrl;
|
|
202
|
+
let ref = null;
|
|
203
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
204
|
+
if (hashIdx !== -1) {
|
|
205
|
+
repoUrl = rawUrl.slice(0, hashIdx);
|
|
206
|
+
ref = rawUrl.slice(hashIdx + 1) || null;
|
|
207
|
+
}
|
|
208
|
+
if (!ref)
|
|
209
|
+
ref = flagString(args.flags, "ref") ?? null;
|
|
210
|
+
const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
|
|
211
|
+
const force = flagBool(args.flags, "force") ?? false;
|
|
212
|
+
const update = flagBool(args.flags, "update") ?? false;
|
|
213
|
+
// Shallow-clone into a temporary directory.
|
|
214
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "flow-kit-git-"));
|
|
215
|
+
try {
|
|
216
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
217
|
+
if (ref)
|
|
218
|
+
cloneArgs.push("--branch", ref);
|
|
219
|
+
cloneArgs.push("--", repoUrl, tmpBase);
|
|
220
|
+
try {
|
|
221
|
+
child_process.execFileSync("git", cloneArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const msg = err instanceof Error && err.stderr
|
|
225
|
+
? err.stderr.toString().trim()
|
|
226
|
+
: String(err);
|
|
227
|
+
console.error(`install-git: git clone failed: ${msg}`);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
// Validate the cloned kit using the same logic as install-local.
|
|
231
|
+
let manifest;
|
|
232
|
+
try {
|
|
233
|
+
manifest = assertKitRepository(tmpBase);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
console.log("Flow Kit repository validation failed:");
|
|
237
|
+
for (const diagnostic of (error.diagnostics ?? [error.message])) {
|
|
238
|
+
console.log(` - ${diagnostic}`);
|
|
239
|
+
}
|
|
240
|
+
return 1;
|
|
241
|
+
}
|
|
242
|
+
// Delegate to the shared install logic (copy + registry update).
|
|
243
|
+
const kitId = String(manifest.id);
|
|
244
|
+
const hash = kitContentHash(tmpBase);
|
|
245
|
+
const registry = loadRegistry(dest);
|
|
246
|
+
const existing = registry.kits.find((entry) => entry.id === kitId);
|
|
247
|
+
const target = installedPath(dest, kitId);
|
|
248
|
+
assertPathContained(dest, target);
|
|
249
|
+
const sourceText = repoUrl + (ref ? `#${ref}` : "");
|
|
250
|
+
if (existing && existing.source !== sourceText && !update) {
|
|
251
|
+
console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
if (existing && existing.source === sourceText && existing.hash === hash && fs.existsSync(target) && !force) {
|
|
255
|
+
console.log(`kit '${kitId}' is already installed from ${sourceText}`);
|
|
256
|
+
return 0;
|
|
257
|
+
}
|
|
258
|
+
copyDir(tmpBase, target);
|
|
259
|
+
const entry = {
|
|
260
|
+
id: kitId,
|
|
261
|
+
source: sourceText,
|
|
262
|
+
hash,
|
|
263
|
+
installed_at: existing && existing.source === sourceText && !update ? existing.installed_at : isoNow(),
|
|
264
|
+
installed_path: target,
|
|
265
|
+
state: "installed",
|
|
266
|
+
};
|
|
267
|
+
if (typeof manifest.version === "string" && manifest.version)
|
|
268
|
+
entry.version = manifest.version;
|
|
269
|
+
registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
|
|
270
|
+
writeJson(registryPath(dest), registry);
|
|
271
|
+
console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
|
|
272
|
+
return 0;
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
166
278
|
export function main(argv = process.argv.slice(2)) {
|
|
167
279
|
const [command, ...rest] = argv;
|
|
168
280
|
if (command === "install-local")
|
|
169
281
|
return installLocal(rest);
|
|
282
|
+
if (command === "install-git")
|
|
283
|
+
return installGit(rest);
|
|
170
284
|
if (command === "list")
|
|
171
285
|
return list(rest);
|
|
172
286
|
if (command === "status")
|
|
@@ -175,8 +289,24 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
175
289
|
return activate(rest);
|
|
176
290
|
if (command === "inspect")
|
|
177
291
|
return inspect(rest);
|
|
178
|
-
console.error("usage: flow-kit <install-local|list|status|activate|inspect> ...");
|
|
292
|
+
console.error("usage: flow-kit <install-local|install-git|list|status|activate|inspect> ...");
|
|
179
293
|
return 2;
|
|
180
294
|
}
|
|
181
|
-
|
|
182
|
-
|
|
295
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
296
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
297
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
298
|
+
const _selfRealPath = (() => { try {
|
|
299
|
+
return fs.realpathSync(fileURLToPath(import.meta.url));
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return fileURLToPath(import.meta.url);
|
|
303
|
+
} })();
|
|
304
|
+
const _argv1RealPath = (() => { try {
|
|
305
|
+
return fs.realpathSync(process.argv[1]);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return process.argv[1];
|
|
309
|
+
} })();
|
|
310
|
+
if (_selfRealPath === _argv1RealPath) {
|
|
311
|
+
process.exitCode = main();
|
|
312
|
+
}
|
|
@@ -27,5 +27,22 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
27
27
|
console.log("Source tree validation passed");
|
|
28
28
|
return 0;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
31
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS).
|
|
32
|
+
import * as _fsVST from "node:fs";
|
|
33
|
+
import { fileURLToPath as _ftpVST } from "node:url";
|
|
34
|
+
const _selfVST = (() => { try {
|
|
35
|
+
return _fsVST.realpathSync(_ftpVST(import.meta.url));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return _ftpVST(import.meta.url);
|
|
39
|
+
} })();
|
|
40
|
+
const _argv1VST = (() => { try {
|
|
41
|
+
return _fsVST.realpathSync(process.argv[1]);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return process.argv[1];
|
|
45
|
+
} })();
|
|
46
|
+
if (_selfVST === _argv1VST) {
|
|
47
|
+
process.exitCode = main();
|
|
48
|
+
}
|
|
@@ -74,7 +74,7 @@ export function readKitInventory(sourceRoot, dest) {
|
|
|
74
74
|
const assets = [];
|
|
75
75
|
const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
|
|
76
76
|
if (!fs.existsSync(catalogPath))
|
|
77
|
-
|
|
77
|
+
warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
|
|
78
78
|
else {
|
|
79
79
|
const catalog = readJson(catalogPath);
|
|
80
80
|
const kits = catalog.kits;
|
|
@@ -84,7 +84,7 @@ const fixtureOwnerPolicies = new Map([
|
|
|
84
84
|
["evals/fixtures/backlog-provider-settings", { owners: ["evals/integration/test_effective_backlog_settings.sh"], classification: "settings precedence fixtures" }],
|
|
85
85
|
["evals/fixtures/builder-kit-workflow-state", { owners: ["evals/static/test_workflow_skills.sh"], classification: "Builder Kit workflow-state fixtures" }],
|
|
86
86
|
["evals/fixtures/console-learning-projection", { owners: ["evals/integration/test_console_learning_projection.sh"], classification: "console learning projection fixtures" }],
|
|
87
|
-
["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
|
|
87
|
+
["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/integration/test_activate_npx_context.sh", "evals/integration/test_flow_kit_install_git.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
|
|
88
88
|
["evals/fixtures/kit-conformance-levels", { owners: ["evals/integration/test_kit_conformance_levels.sh"], classification: "K-level conformance and consumer-target derivation fixtures" }],
|
|
89
89
|
["evals/fixtures/hook-influence", { owners: ["evals/integration/test_hook_influence_cases.sh", "evals/static/test_workflow_skills.sh", "scripts/validate-hook-influence-cases.js"], classification: "hook influence behavioral cases" }],
|
|
90
90
|
["evals/fixtures/pull-work-provider", { owners: ["evals/integration/test_pull_work_provider.sh"], classification: "work item provider normalization fixtures" }],
|
|
@@ -16,7 +16,7 @@ run `npm run validate:source --` and `npm run fixture:retirement-audit --`.
|
|
|
16
16
|
| `evals/fixtures/backlog-provider-settings` | settings precedence fixtures | `evals/integration/test_effective_backlog_settings.sh` | Keep while backlog provider settings resolution supports global defaults and project overrides. |
|
|
17
17
|
| `evals/fixtures/builder-kit-workflow-state` | Builder Kit workflow-state fixtures | `evals/static/test_workflow_skills.sh` | Keep while Builder Kit state contract and resume behavior are documented in workflow skill contracts. |
|
|
18
18
|
| `evals/fixtures/console-learning-projection` | console learning projection fixtures | `evals/integration/test_console_learning_projection.sh` | Keep while learning projection supports correction and open-route examples. |
|
|
19
|
-
| `evals/fixtures/flow-kit-repository` | Flow Kit repository contract fixtures | `evals/integration/test_flow_kit_repository.sh`, `evals/integration/test_local_flow_kit_install.sh`, `evals/integration/test_runtime_adapter_activation.sh`, `evals/static/test_workflow_skills.sh` | Keep valid and invalid cases paired with the Flow Kit repository contract. |
|
|
19
|
+
| `evals/fixtures/flow-kit-repository` | Flow Kit repository contract fixtures | `evals/integration/test_flow_kit_repository.sh`, `evals/integration/test_local_flow_kit_install.sh`, `evals/integration/test_runtime_adapter_activation.sh`, `evals/integration/test_activate_npx_context.sh`, `evals/integration/test_flow_kit_install_git.sh`, `evals/static/test_workflow_skills.sh` | Keep valid and invalid cases paired with the Flow Kit repository contract. |
|
|
20
20
|
| `evals/fixtures/kit-conformance-levels` | K-level conformance and consumer-target derivation fixtures | `evals/integration/test_kit_conformance_levels.sh` | Keep while K-level derivation, degradation invariant, and consumer-target badge rules are tested. |
|
|
21
21
|
| `evals/fixtures/hook-influence` | hook influence behavioral cases | `evals/integration/test_hook_influence_cases.sh`, `evals/static/test_workflow_skills.sh`, `scripts/validate-hook-influence-cases.js` | Keep while hook influence cases define agent guidance behavior. |
|
|
22
22
|
| `evals/fixtures/pull-work-provider` | work item provider normalization fixtures | `evals/integration/test_pull_work_provider.sh` | Keep while provider normalization preserves blockers, artifact refs, board membership, and freshness metadata. |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_activate_npx_context.sh — Verify that activate exits 0 in an npx-like context
|
|
3
|
+
# where there is no kits/catalog.json at the source-root.
|
|
4
|
+
# Implements acceptance criteria for kontourai/flow-agents#57.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
source "$ROOT/evals/lib/node.sh"
|
|
9
|
+
|
|
10
|
+
errors=0
|
|
11
|
+
TMP_DIR="$(mktemp -d)"
|
|
12
|
+
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
13
|
+
|
|
14
|
+
pass() { echo " ✓ $1"; }
|
|
15
|
+
fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
16
|
+
|
|
17
|
+
CLI="$ROOT/scripts/flow-kit.js"
|
|
18
|
+
MIXED_SRC="$ROOT/evals/fixtures/flow-kit-repository/mixed-runtime-kit"
|
|
19
|
+
|
|
20
|
+
echo "=== activate npx-context Checks (Issue #57) ==="
|
|
21
|
+
|
|
22
|
+
# Simulate the npx context: an empty source-root (no kits/catalog.json present).
|
|
23
|
+
NPX_SIMULATED_ROOT="$TMP_DIR/simulated-npx-root"
|
|
24
|
+
mkdir -p "$NPX_SIMULATED_ROOT"
|
|
25
|
+
DEST="$TMP_DIR/dest"
|
|
26
|
+
mkdir -p "$DEST"
|
|
27
|
+
|
|
28
|
+
# Install a kit into the destination workspace first.
|
|
29
|
+
install_out="$TMP_DIR/install.out"
|
|
30
|
+
if flow_agents_node "$CLI" install-local "$MIXED_SRC" --dest "$DEST" >"$install_out" 2>&1; then
|
|
31
|
+
pass "mixed-runtime-kit installs into workspace"
|
|
32
|
+
else
|
|
33
|
+
fail "install failed (prerequisite step)"
|
|
34
|
+
sed -n '1,80p' "$install_out"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# --- Test 1: activate with no catalog.json at source-root exits 0 ---
|
|
38
|
+
# This simulates the npx context: source-root points at an npm cache dir with no kits/catalog.json.
|
|
39
|
+
activate_out="$TMP_DIR/activate-npx.out"
|
|
40
|
+
if flow_agents_node "$CLI" activate --dest "$DEST" --source-root "$NPX_SIMULATED_ROOT" >"$activate_out" 2>&1; then
|
|
41
|
+
pass "activate exits 0 when source-root has no catalog.json (npx context)"
|
|
42
|
+
else
|
|
43
|
+
fail "activate exits non-zero when source-root has no catalog.json — false failure in npx context"
|
|
44
|
+
sed -n '1,120p' "$activate_out"
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# --- Test 2: missing catalog warning appears but not as an error ---
|
|
48
|
+
if node - "$activate_out" <<'NODE'
|
|
49
|
+
const fs = require("node:fs");
|
|
50
|
+
const raw = fs.readFileSync(process.argv[2], "utf8").trim();
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
// errors must be empty — missing catalog is not an error
|
|
53
|
+
if (Array.isArray(data.errors) && data.errors.length > 0) {
|
|
54
|
+
throw new Error(`activate reported errors: ${JSON.stringify(data.errors)}`);
|
|
55
|
+
}
|
|
56
|
+
// warnings may mention catalog (but must not be errors)
|
|
57
|
+
const catalogWarning = Array.isArray(data.warnings) && data.warnings.some((w) => w.includes("catalog") || w.includes("Catalog"));
|
|
58
|
+
// It's fine either way — the key invariant is errors=[].
|
|
59
|
+
console.log(`warnings: ${JSON.stringify(data.warnings)}`);
|
|
60
|
+
console.log(`catalog mentioned in warnings: ${catalogWarning}`);
|
|
61
|
+
console.log("ok");
|
|
62
|
+
NODE
|
|
63
|
+
then
|
|
64
|
+
pass "activate with missing catalog produces no errors (warnings only)"
|
|
65
|
+
else
|
|
66
|
+
fail "activate diagnostic structure incorrect"
|
|
67
|
+
sed -n '1,120p' "$activate_out"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# --- Test 3: user-installed kits ARE activated even without catalog.json ---
|
|
71
|
+
if node - "$activate_out" "$DEST" <<'NODE'
|
|
72
|
+
const fs = require("node:fs");
|
|
73
|
+
const path = require("node:path");
|
|
74
|
+
const raw = fs.readFileSync(process.argv[2], "utf8").trim();
|
|
75
|
+
const data = JSON.parse(raw);
|
|
76
|
+
const dest = process.argv[3];
|
|
77
|
+
// mixed-runtime-kit has a flow with id "mixed.runtime" — it should be in generated_runtime_files
|
|
78
|
+
const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
|
|
79
|
+
if (!ids.has("mixed.runtime")) {
|
|
80
|
+
throw new Error(`user-installed kit flow not activated; generated ids: ${[...ids].join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
// The generated file must exist on disk
|
|
83
|
+
for (const item of data.generated_runtime_files) {
|
|
84
|
+
if (item.asset_class === "activation-manifest") continue;
|
|
85
|
+
const generatedPath = path.join(dest, item.path);
|
|
86
|
+
if (!fs.existsSync(generatedPath)) throw new Error(`generated file missing on disk: ${generatedPath}`);
|
|
87
|
+
}
|
|
88
|
+
console.log("ok");
|
|
89
|
+
NODE
|
|
90
|
+
then
|
|
91
|
+
pass "user-installed kits are activated correctly even without catalog.json"
|
|
92
|
+
else
|
|
93
|
+
fail "user-installed kit flows missing from activation output"
|
|
94
|
+
sed -n '1,120p' "$activate_out"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# --- Test 4: built-in kits also activate when catalog.json IS present (regression guard) ---
|
|
98
|
+
activate_builtin_out="$TMP_DIR/activate-builtin.out"
|
|
99
|
+
if flow_agents_node "$CLI" activate --dest "$DEST" --source-root "$ROOT" >"$activate_builtin_out" 2>&1; then
|
|
100
|
+
pass "activate with catalog.json present still exits 0 (regression guard)"
|
|
101
|
+
else
|
|
102
|
+
fail "activate with catalog.json present failed (regression)"
|
|
103
|
+
sed -n '1,120p' "$activate_builtin_out"
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if node - "$activate_builtin_out" <<'NODE'
|
|
107
|
+
const fs = require("node:fs");
|
|
108
|
+
const raw = fs.readFileSync(process.argv[2], "utf8").trim();
|
|
109
|
+
const data = JSON.parse(raw);
|
|
110
|
+
if (Array.isArray(data.errors) && data.errors.length > 0) {
|
|
111
|
+
throw new Error(`regression: activate with catalog.json reported errors: ${JSON.stringify(data.errors)}`);
|
|
112
|
+
}
|
|
113
|
+
// Should have builder kit flows from catalog
|
|
114
|
+
const ids = new Set(data.generated_runtime_files.map((item) => item.asset_id));
|
|
115
|
+
if (!ids.has("builder.shape") && !ids.has("builder.build")) {
|
|
116
|
+
throw new Error(`built-in kit flows missing when catalog.json is present`);
|
|
117
|
+
}
|
|
118
|
+
console.log("ok");
|
|
119
|
+
NODE
|
|
120
|
+
then
|
|
121
|
+
pass "built-in kit flows present when catalog.json is available (regression)"
|
|
122
|
+
else
|
|
123
|
+
fail "built-in kit flows missing — regression introduced"
|
|
124
|
+
sed -n '1,120p' "$activate_builtin_out"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
echo ""
|
|
128
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
129
|
+
echo "activate npx-context checks passed."
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
echo "activate npx-context checks failed: $errors issue(s)."
|
|
134
|
+
exit 1
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_flow_kit_install_git.sh — Exercise flow-kit install-git with a file:// git URL fixture.
|
|
3
|
+
# No network required: the fixture is a bare git repo created from the existing valid-local-kit.
|
|
4
|
+
# Implements acceptance criteria for kontourai/flow-agents#56.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
8
|
+
source "$ROOT/evals/lib/node.sh"
|
|
9
|
+
|
|
10
|
+
errors=0
|
|
11
|
+
TMP_DIR="$(mktemp -d)"
|
|
12
|
+
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
13
|
+
|
|
14
|
+
pass() { echo " ✓ $1"; }
|
|
15
|
+
fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
16
|
+
|
|
17
|
+
CLI="$ROOT/scripts/flow-kit.js"
|
|
18
|
+
VALID_SRC="$ROOT/evals/fixtures/flow-kit-repository/valid-local-kit"
|
|
19
|
+
DEST="$TMP_DIR/install-dest"
|
|
20
|
+
mkdir -p "$DEST"
|
|
21
|
+
|
|
22
|
+
echo "=== install-git Checks (Issue #56) ==="
|
|
23
|
+
|
|
24
|
+
# --- Fixture setup: create a bare git repo from the valid-local-kit fixture ---
|
|
25
|
+
FIXTURE_REPO="$TMP_DIR/fixture-bare-repo"
|
|
26
|
+
FIXTURE_WORKING="$TMP_DIR/fixture-working"
|
|
27
|
+
cp -R "$VALID_SRC" "$FIXTURE_WORKING"
|
|
28
|
+
(cd "$FIXTURE_WORKING" && git init -q && git config user.email "test@test.local" && git config user.name "test" && git add -A && git commit -q -m "init")
|
|
29
|
+
git clone -q --bare "$FIXTURE_WORKING" "$FIXTURE_REPO"
|
|
30
|
+
FILE_URL="file://$FIXTURE_REPO"
|
|
31
|
+
|
|
32
|
+
echo " (fixture repo: $FILE_URL)"
|
|
33
|
+
|
|
34
|
+
# --- Test 1: basic install-git from file:// URL ---
|
|
35
|
+
install_out="$TMP_DIR/install-git.out"
|
|
36
|
+
if flow_agents_node "$CLI" install-git "$FILE_URL" --dest "$DEST" >"$install_out" 2>&1; then
|
|
37
|
+
pass "install-git from file:// URL succeeds"
|
|
38
|
+
else
|
|
39
|
+
fail "install-git from file:// URL failed"
|
|
40
|
+
sed -n '1,80p' "$install_out"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
REGISTRY="$DEST/kits/local/installed-kits.json"
|
|
44
|
+
if [[ -f "$REGISTRY" ]]; then
|
|
45
|
+
pass "install-git writes registry file"
|
|
46
|
+
else
|
|
47
|
+
fail "install-git did not write registry file"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if node - "$REGISTRY" "$FILE_URL" <<'NODE'
|
|
51
|
+
const fs = require("node:fs");
|
|
52
|
+
const registry = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
53
|
+
const entry = registry.kits[0];
|
|
54
|
+
if (!entry) throw new Error("no kits in registry");
|
|
55
|
+
for (const key of ["id", "source", "hash", "installed_at", "installed_path", "state"]) {
|
|
56
|
+
if (!(key in entry)) throw new Error(`missing metadata key: ${key}`);
|
|
57
|
+
}
|
|
58
|
+
if (entry.id !== "example-kit") throw new Error(`unexpected id: ${entry.id}`);
|
|
59
|
+
if (!entry.hash.startsWith("sha256:")) throw new Error("hash should include sha256 prefix");
|
|
60
|
+
if (Number.isNaN(Date.parse(entry.installed_at))) throw new Error("installed_at should be ISO parseable");
|
|
61
|
+
if (!fs.existsSync(entry.installed_path + "/kit.json")) throw new Error("installed kit copy missing kit.json");
|
|
62
|
+
const expectedSource = process.argv[3];
|
|
63
|
+
if (!entry.source.startsWith(expectedSource.replace(/\/$/, ""))) throw new Error(`source mismatch: ${entry.source}`);
|
|
64
|
+
console.log("ok");
|
|
65
|
+
NODE
|
|
66
|
+
then
|
|
67
|
+
pass "install-git records correct provenance metadata"
|
|
68
|
+
else
|
|
69
|
+
fail "install-git metadata is incomplete"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# --- Test 2: idempotent re-install from same URL ---
|
|
73
|
+
idempotent_out="$TMP_DIR/idempotent.out"
|
|
74
|
+
registry_hash_before="$(shasum -a 256 "$REGISTRY" | awk '{print $1}')"
|
|
75
|
+
if flow_agents_node "$CLI" install-git "$FILE_URL" --dest "$DEST" >"$idempotent_out" 2>&1 \
|
|
76
|
+
&& grep -q "already installed" "$idempotent_out" \
|
|
77
|
+
&& [[ "$registry_hash_before" == "$(shasum -a 256 "$REGISTRY" | awk '{print $1}')" ]]; then
|
|
78
|
+
pass "install-git same-URL reinstall is idempotent"
|
|
79
|
+
else
|
|
80
|
+
fail "install-git same-URL reinstall was not idempotent"
|
|
81
|
+
sed -n '1,80p' "$idempotent_out"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# --- Test 3: #ref fragment syntax ---
|
|
85
|
+
ref_out="$TMP_DIR/ref.out"
|
|
86
|
+
DEST2="$TMP_DIR/dest-with-ref"
|
|
87
|
+
mkdir -p "$DEST2"
|
|
88
|
+
# Re-create fixture repo with a tagged commit so we can test #ref
|
|
89
|
+
FIXTURE_WORKING2="$TMP_DIR/fixture-working2"
|
|
90
|
+
FIXTURE_REPO2="$TMP_DIR/fixture-bare-repo2"
|
|
91
|
+
cp -R "$VALID_SRC" "$FIXTURE_WORKING2"
|
|
92
|
+
(cd "$FIXTURE_WORKING2" && git init -q && git config user.email "test@test.local" && git config user.name "test" && git add -A && git commit -q -m "init" && git tag v1.0)
|
|
93
|
+
git clone -q --bare "$FIXTURE_WORKING2" "$FIXTURE_REPO2"
|
|
94
|
+
FILE_URL2="file://$FIXTURE_REPO2"
|
|
95
|
+
|
|
96
|
+
if flow_agents_node "$CLI" install-git "${FILE_URL2}#v1.0" --dest "$DEST2" >"$ref_out" 2>&1; then
|
|
97
|
+
pass "install-git with #ref fragment succeeds"
|
|
98
|
+
else
|
|
99
|
+
fail "install-git with #ref fragment failed"
|
|
100
|
+
sed -n '1,80p' "$ref_out"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if node - "$DEST2/kits/local/installed-kits.json" "${FILE_URL2}#v1.0" <<'NODE'
|
|
104
|
+
const fs = require("node:fs");
|
|
105
|
+
const registry = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
106
|
+
const entry = registry.kits.find((e) => e.id === "example-kit");
|
|
107
|
+
if (!entry) throw new Error("kit not found in registry");
|
|
108
|
+
const expectedSource = process.argv[3];
|
|
109
|
+
if (entry.source !== expectedSource) throw new Error(`source mismatch: expected ${expectedSource}, got ${entry.source}`);
|
|
110
|
+
console.log("ok");
|
|
111
|
+
NODE
|
|
112
|
+
then
|
|
113
|
+
pass "install-git #ref stored in source metadata"
|
|
114
|
+
else
|
|
115
|
+
fail "install-git #ref not stored correctly in source metadata"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# --- Test 4: --ref flag syntax ---
|
|
119
|
+
ref_flag_out="$TMP_DIR/ref-flag.out"
|
|
120
|
+
DEST3="$TMP_DIR/dest-with-ref-flag"
|
|
121
|
+
mkdir -p "$DEST3"
|
|
122
|
+
if flow_agents_node "$CLI" install-git "$FILE_URL2" --ref v1.0 --dest "$DEST3" >"$ref_flag_out" 2>&1; then
|
|
123
|
+
pass "install-git with --ref flag succeeds"
|
|
124
|
+
else
|
|
125
|
+
fail "install-git with --ref flag failed"
|
|
126
|
+
sed -n '1,80p' "$ref_flag_out"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- Test 5: missing git URL exits non-zero ---
|
|
130
|
+
missing_url_out="$TMP_DIR/missing-url.out"
|
|
131
|
+
if flow_agents_node "$CLI" install-git --dest "$DEST" >"$missing_url_out" 2>&1; then
|
|
132
|
+
fail "install-git with no URL should exit non-zero"
|
|
133
|
+
sed -n '1,40p' "$missing_url_out"
|
|
134
|
+
else
|
|
135
|
+
pass "install-git with no URL exits non-zero with usage message"
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# --- Test 6: invalid git URL exits non-zero ---
|
|
139
|
+
invalid_url_out="$TMP_DIR/invalid-url.out"
|
|
140
|
+
if flow_agents_node "$CLI" install-git "file:///nonexistent-repo-that-does-not-exist" --dest "$DEST" >"$invalid_url_out" 2>&1; then
|
|
141
|
+
fail "install-git with invalid URL should exit non-zero"
|
|
142
|
+
sed -n '1,40p' "$invalid_url_out"
|
|
143
|
+
else
|
|
144
|
+
pass "install-git with invalid URL exits non-zero"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# --- Test 7: catalog.json not mutated ---
|
|
148
|
+
CATALOG_HASH_BEFORE="$(shasum -a 256 "$ROOT/kits/catalog.json" | awk '{print $1}')"
|
|
149
|
+
# (all operations above have already run)
|
|
150
|
+
if [[ "$CATALOG_HASH_BEFORE" == "$(shasum -a 256 "$ROOT/kits/catalog.json" | awk '{print $1}')" ]]; then
|
|
151
|
+
pass "install-git does not mutate source kits/catalog.json"
|
|
152
|
+
else
|
|
153
|
+
fail "source kits/catalog.json changed during install-git test"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
echo ""
|
|
157
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
158
|
+
echo "install-git checks passed."
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
echo "install-git checks failed: $errors issue(s)."
|
|
163
|
+
exit 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kontourai/flow-agents",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Flow Agents — a Kontour product that applies Flow and Veritas discipline as a portable process layer inside the agent tools you already use: Claude Code, Codex, Kiro, opencode, pi, and GitHub Actions — with framework adapters (AWS Strands preview) on the same policy-engine contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
package/src/cli/flow-kit.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import * as child_process from "node:child_process";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
3
5
|
import * as path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
4
7
|
import { parseArgs, flagBool, flagString } from "../lib/args.js";
|
|
5
8
|
import { assertPathContained, copyDir, isoNow, readJson, walkFiles, writeJson } from "../lib/fs.js";
|
|
6
9
|
import { assertKitRepository, deriveKitTargets } from "../flow-kit/validate.js";
|
|
@@ -31,6 +34,22 @@ function contentHash(root: string): string {
|
|
|
31
34
|
return `sha256:${hash.digest("hex")}`;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/** Content hash that excludes .git and other VCS/cache directories (for install-git clones). */
|
|
38
|
+
function kitContentHash(root: string): string {
|
|
39
|
+
const EXCLUDE_DIRS = new Set([".git", "__pycache__", ".pytest_cache"]);
|
|
40
|
+
const hash = crypto.createHash("sha256");
|
|
41
|
+
for (const file of walkFiles(root)) {
|
|
42
|
+
const parts = path.relative(root, file).split(path.sep);
|
|
43
|
+
if (parts.some((p) => EXCLUDE_DIRS.has(p))) continue;
|
|
44
|
+
const rel = parts.join("/");
|
|
45
|
+
hash.update(rel);
|
|
46
|
+
hash.update("\0");
|
|
47
|
+
hash.update(fs.readFileSync(file));
|
|
48
|
+
hash.update("\0");
|
|
49
|
+
}
|
|
50
|
+
return `sha256:${hash.digest("hex")}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
function installLocal(argv: string[]): number {
|
|
35
54
|
const args = parseArgs(argv);
|
|
36
55
|
const source = path.resolve(args.positionals[0] ?? "");
|
|
@@ -39,12 +58,10 @@ function installLocal(argv: string[]): number {
|
|
|
39
58
|
try {
|
|
40
59
|
manifest = assertKitRepository(source);
|
|
41
60
|
} catch (error) {
|
|
42
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
43
61
|
console.log("Flow Kit repository validation failed:");
|
|
44
62
|
for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) console.log(` - ${diagnostic}`);
|
|
45
63
|
return 1;
|
|
46
64
|
}
|
|
47
|
-
console.log("warning: Flow validation surface unavailable; local kit check uses the minimal Flow Definition fallback");
|
|
48
65
|
const kitId = String(manifest.id);
|
|
49
66
|
const hash = contentHash(source);
|
|
50
67
|
const registry = loadRegistry(dest);
|
|
@@ -169,15 +186,117 @@ function inspect(argv: string[]): number {
|
|
|
169
186
|
return result.conformance.k0 ? 0 : 1;
|
|
170
187
|
}
|
|
171
188
|
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>] [--force] [--update]
|
|
192
|
+
*
|
|
193
|
+
* Shallow-clones a remote git repository to a temporary directory, validates the kit
|
|
194
|
+
* container with the same logic used by install-local, then delegates to the existing
|
|
195
|
+
* install path. Supports an optional #ref fragment in the URL or a separate --ref flag.
|
|
196
|
+
*
|
|
197
|
+
* Implements kontourai/flow-agents#56 (git-ref install surface).
|
|
198
|
+
*/
|
|
199
|
+
function installGit(argv: string[]): number {
|
|
200
|
+
const args = parseArgs(argv);
|
|
201
|
+
const rawUrl = args.positionals[0] ?? "";
|
|
202
|
+
if (!rawUrl) {
|
|
203
|
+
console.error("install-git: missing <repo-url> argument");
|
|
204
|
+
console.error("usage: flow-kit install-git <repo-url>[#ref] [--ref <branch|tag|sha>] [--dest <path>]");
|
|
205
|
+
return 2;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Parse ref: #fragment in URL takes precedence over --ref flag.
|
|
209
|
+
let repoUrl = rawUrl;
|
|
210
|
+
let ref: string | null = null;
|
|
211
|
+
const hashIdx = rawUrl.indexOf("#");
|
|
212
|
+
if (hashIdx !== -1) {
|
|
213
|
+
repoUrl = rawUrl.slice(0, hashIdx);
|
|
214
|
+
ref = rawUrl.slice(hashIdx + 1) || null;
|
|
215
|
+
}
|
|
216
|
+
if (!ref) ref = flagString(args.flags, "ref") ?? null;
|
|
217
|
+
|
|
218
|
+
const dest = path.resolve(flagString(args.flags, "dest", ".") ?? ".");
|
|
219
|
+
const force = flagBool(args.flags, "force") ?? false;
|
|
220
|
+
const update = flagBool(args.flags, "update") ?? false;
|
|
221
|
+
|
|
222
|
+
// Shallow-clone into a temporary directory.
|
|
223
|
+
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "flow-kit-git-"));
|
|
224
|
+
try {
|
|
225
|
+
const cloneArgs = ["clone", "--depth", "1"];
|
|
226
|
+
if (ref) cloneArgs.push("--branch", ref);
|
|
227
|
+
cloneArgs.push("--", repoUrl, tmpBase);
|
|
228
|
+
try {
|
|
229
|
+
child_process.execFileSync("git", cloneArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const msg = err instanceof Error && (err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr
|
|
232
|
+
? ((err as NodeJS.ErrnoException & { stderr?: Buffer }).stderr as Buffer).toString().trim()
|
|
233
|
+
: String(err);
|
|
234
|
+
console.error(`install-git: git clone failed: ${msg}`);
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Validate the cloned kit using the same logic as install-local.
|
|
239
|
+
let manifest: Record<string, unknown>;
|
|
240
|
+
try {
|
|
241
|
+
manifest = assertKitRepository(tmpBase);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.log("Flow Kit repository validation failed:");
|
|
244
|
+
for (const diagnostic of ((error as Error & { diagnostics?: string[] }).diagnostics ?? [(error as Error).message])) {
|
|
245
|
+
console.log(` - ${diagnostic}`);
|
|
246
|
+
}
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Delegate to the shared install logic (copy + registry update).
|
|
251
|
+
const kitId = String(manifest.id);
|
|
252
|
+
const hash = kitContentHash(tmpBase);
|
|
253
|
+
const registry = loadRegistry(dest);
|
|
254
|
+
const existing = registry.kits.find((entry) => entry.id === kitId);
|
|
255
|
+
const target = installedPath(dest, kitId);
|
|
256
|
+
assertPathContained(dest, target);
|
|
257
|
+
const sourceText = repoUrl + (ref ? `#${ref}` : "");
|
|
258
|
+
if (existing && existing.source !== sourceText && !update) {
|
|
259
|
+
console.log(`conflict: kit '${kitId}' is already installed from ${existing.source}; rerun with --update to replace it`);
|
|
260
|
+
return 2;
|
|
261
|
+
}
|
|
262
|
+
if (existing && existing.source === sourceText && existing.hash === hash && fs.existsSync(target) && !force) {
|
|
263
|
+
console.log(`kit '${kitId}' is already installed from ${sourceText}`);
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
copyDir(tmpBase, target);
|
|
267
|
+
const entry: Record<string, unknown> = {
|
|
268
|
+
id: kitId,
|
|
269
|
+
source: sourceText,
|
|
270
|
+
hash,
|
|
271
|
+
installed_at: existing && existing.source === sourceText && !update ? existing.installed_at : isoNow(),
|
|
272
|
+
installed_path: target,
|
|
273
|
+
state: "installed",
|
|
274
|
+
};
|
|
275
|
+
if (typeof manifest.version === "string" && manifest.version) entry.version = manifest.version;
|
|
276
|
+
registry.kits = existing ? registry.kits.map((item) => item.id === kitId ? entry : item) : [...registry.kits, entry];
|
|
277
|
+
writeJson(registryPath(dest), registry);
|
|
278
|
+
console.log(`${existing ? "updated" : "installed"} git kit '${kitId}' from ${sourceText} at ${target}`);
|
|
279
|
+
return 0;
|
|
280
|
+
} finally {
|
|
281
|
+
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
172
285
|
export function main(argv = process.argv.slice(2)): number {
|
|
173
286
|
const [command, ...rest] = argv;
|
|
174
287
|
if (command === "install-local") return installLocal(rest);
|
|
288
|
+
if (command === "install-git") return installGit(rest);
|
|
175
289
|
if (command === "list") return list(rest);
|
|
176
290
|
if (command === "status") return status(rest);
|
|
177
291
|
if (command === "activate") return activate(rest);
|
|
178
292
|
if (command === "inspect") return inspect(rest);
|
|
179
|
-
console.error("usage: flow-kit <install-local|list|status|activate|inspect> ...");
|
|
293
|
+
console.error("usage: flow-kit <install-local|install-git|list|status|activate|inspect> ...");
|
|
180
294
|
return 2;
|
|
181
295
|
}
|
|
182
296
|
|
|
183
|
-
|
|
297
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
298
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS) so the
|
|
299
|
+
// entry-point guard fires correctly when the module is loaded directly as a script.
|
|
300
|
+
const _selfRealPath = (() => { try { return fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return fileURLToPath(import.meta.url); } })();
|
|
301
|
+
const _argv1RealPath = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
302
|
+
if (_selfRealPath === _argv1RealPath) { process.exitCode = main(); }
|
|
@@ -27,4 +27,10 @@ export function main(argv = process.argv.slice(2)): number {
|
|
|
27
27
|
return 0;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Use process.exitCode (not process.exit) to allow stdout to be flushed before exit.
|
|
31
|
+
// Resolve real paths to handle symlinks (e.g. /tmp -> /private/tmp on macOS).
|
|
32
|
+
import * as _fsVST from "node:fs";
|
|
33
|
+
import { fileURLToPath as _ftpVST } from "node:url";
|
|
34
|
+
const _selfVST = (() => { try { return _fsVST.realpathSync(_ftpVST(import.meta.url)); } catch { return _ftpVST(import.meta.url); } })();
|
|
35
|
+
const _argv1VST = (() => { try { return _fsVST.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
36
|
+
if (_selfVST === _argv1VST) { process.exitCode = main(); }
|
package/src/runtime-adapters.ts
CHANGED
|
@@ -86,7 +86,7 @@ export function readKitInventory(sourceRoot: string, dest: string): KitInventory
|
|
|
86
86
|
const errors: string[] = [];
|
|
87
87
|
const assets: KitAsset[] = [];
|
|
88
88
|
const catalogPath = path.join(sourceRoot, "kits", "catalog.json");
|
|
89
|
-
if (!fs.existsSync(catalogPath))
|
|
89
|
+
if (!fs.existsSync(catalogPath)) warnings.push(`${catalogPath}: built-in Kit Catalog not found; skipping built-in kits (this is normal when running outside a flow-agents checkout)`);
|
|
90
90
|
else {
|
|
91
91
|
const catalog = readJson(catalogPath) as Record<string, unknown>;
|
|
92
92
|
const kits = catalog.kits;
|
|
@@ -84,7 +84,7 @@ const fixtureOwnerPolicies = new Map<string, { owners: string[]; classification:
|
|
|
84
84
|
["evals/fixtures/backlog-provider-settings", { owners: ["evals/integration/test_effective_backlog_settings.sh"], classification: "settings precedence fixtures" }],
|
|
85
85
|
["evals/fixtures/builder-kit-workflow-state", { owners: ["evals/static/test_workflow_skills.sh"], classification: "Builder Kit workflow-state fixtures" }],
|
|
86
86
|
["evals/fixtures/console-learning-projection", { owners: ["evals/integration/test_console_learning_projection.sh"], classification: "console learning projection fixtures" }],
|
|
87
|
-
["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
|
|
87
|
+
["evals/fixtures/flow-kit-repository", { owners: ["evals/integration/test_flow_kit_repository.sh", "evals/integration/test_local_flow_kit_install.sh", "evals/integration/test_runtime_adapter_activation.sh", "evals/integration/test_activate_npx_context.sh", "evals/integration/test_flow_kit_install_git.sh", "evals/static/test_workflow_skills.sh"], classification: "Flow Kit repository contract fixtures" }],
|
|
88
88
|
["evals/fixtures/kit-conformance-levels", { owners: ["evals/integration/test_kit_conformance_levels.sh"], classification: "K-level conformance and consumer-target derivation fixtures" }],
|
|
89
89
|
["evals/fixtures/hook-influence", { owners: ["evals/integration/test_hook_influence_cases.sh", "evals/static/test_workflow_skills.sh", "scripts/validate-hook-influence-cases.js"], classification: "hook influence behavioral cases" }],
|
|
90
90
|
["evals/fixtures/pull-work-provider", { owners: ["evals/integration/test_pull_work_provider.sh"], classification: "work item provider normalization fixtures" }],
|