project-iris 0.0.7 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +294 -264
- package/dist/bridge/agent-runner.js +190 -0
- package/dist/bridge/connector-factory.js +4 -0
- package/dist/bridge/connectors/in-process-connector.js +29 -0
- package/dist/bridge/filesystem-connector.js +5 -0
- package/dist/cli.js +10 -2
- package/dist/commands/ask.js +150 -23
- package/dist/commands/bridge.js +8 -0
- package/dist/commands/flow.js +301 -0
- package/dist/commands/framework.js +273 -0
- package/dist/commands/generate.js +59 -0
- package/dist/commands/install.js +72 -29
- package/dist/commands/pack.js +7 -1
- package/dist/commands/run.js +195 -13
- package/dist/commands/status.js +9 -0
- package/dist/commands/uninstall.js +3 -1
- package/dist/commands/use.js +20 -0
- package/dist/commands/validate.js +80 -65
- package/dist/framework/framework-loader.js +97 -0
- package/dist/framework/framework-paths.js +48 -0
- package/dist/framework/framework-types.js +15 -0
- package/dist/iris/artifacts/config.js +68 -0
- package/dist/iris/artifacts/generator.js +88 -0
- package/dist/iris/artifacts/types.js +1 -0
- package/dist/iris/bundle.js +44 -0
- package/dist/iris/doctrine/collector.js +124 -0
- package/dist/iris/fixer.js +28 -22
- package/dist/iris/flows/manifest.js +124 -0
- package/dist/iris/framework-context.js +49 -0
- package/dist/iris/framework-manager.js +215 -0
- package/dist/iris/fs/atomic.js +22 -0
- package/dist/iris/importers/index.js +9 -0
- package/dist/iris/importers/types.js +8 -0
- package/dist/iris/importers/writer.js +139 -0
- package/dist/iris/installer.js +105 -40
- package/dist/iris/interactive/env.js +21 -0
- package/dist/iris/interactive/intent-interview.js +345 -0
- package/dist/iris/interactive/intent-schema.js +28 -0
- package/dist/iris/interactive/interview-io.js +22 -0
- package/dist/iris/interview/config.js +71 -0
- package/dist/iris/interview/types.js +16 -0
- package/dist/iris/interview/utils.js +38 -0
- package/dist/iris/packer.js +69 -47
- package/dist/iris/parsers/unit-parser.js +43 -0
- package/dist/iris/paths.js +18 -0
- package/dist/iris/policy.js +122 -17
- package/dist/iris/proc.js +56 -0
- package/dist/iris/resolver.js +3 -0
- package/dist/iris/routes.js +180 -11
- package/dist/iris/run-state.js +3 -0
- package/dist/iris/state.js +37 -9
- package/dist/iris/templates.js +70 -0
- package/dist/iris/tmp.js +24 -0
- package/dist/iris/uninstaller.js +24 -9
- package/dist/iris/utils/interpolate.js +42 -0
- package/dist/iris/validator.js +72 -10
- package/dist/iris/workflow/config.js +51 -0
- package/dist/iris/workflow/engine.js +129 -0
- package/dist/iris/workflow/steps.js +448 -0
- package/dist/iris/workflow/types.js +1 -0
- package/dist/utils/logo.js +17 -0
- package/dist/workflows/intent-inception.js +87 -65
- package/package.json +8 -6
- package/src/iris_bundle/.iris/aidlc/README.md +0 -16
- package/src/iris_bundle/.iris/aidlc/agents/iris-construction-agent.md +0 -35
- package/src/iris_bundle/.iris/aidlc/agents/iris-inception-agent.md +0 -30
- package/src/iris_bundle/.iris/aidlc/agents/iris-master-agent.md +0 -35
- package/src/iris_bundle/.iris/aidlc/agents/iris-operations-agent.md +0 -29
- package/src/iris_bundle/.iris/aidlc/commands/iris-construction-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-inception-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-master-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/commands/iris-operations-agent.md +0 -18
- package/src/iris_bundle/.iris/aidlc/context/context-map.md +0 -25
- package/src/iris_bundle/.iris/aidlc/context/exclusion-rules.md +0 -13
- package/src/iris_bundle/.iris/aidlc/context/load-order.md +0 -25
- package/src/iris_bundle/.iris/aidlc/memory/intent-rules.md +0 -9
- package/src/iris_bundle/.iris/aidlc/memory/log-rules.md +0 -5
- package/src/iris_bundle/.iris/aidlc/memory/memory-bank.yaml +0 -39
- package/src/iris_bundle/.iris/aidlc/memory/unit-rules.md +0 -9
- package/src/iris_bundle/.iris/aidlc/quick-start.md +0 -24
- package/src/iris_bundle/.iris/aidlc/skills/execution/implementation.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/execution/refactoring.md +0 -13
- package/src/iris_bundle/.iris/aidlc/skills/execution/scaffold-generation.md +0 -15
- package/src/iris_bundle/.iris/aidlc/skills/governance/escalation.md +0 -13
- package/src/iris_bundle/.iris/aidlc/skills/governance/quality-gates.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/governance/stop-conditions.md +0 -11
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/decomposition.md +0 -23
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/risk-analysis.md +0 -14
- package/src/iris_bundle/.iris/aidlc/skills/reasoning/verification.md +0 -21
- package/src/iris_bundle/.iris/aidlc/standards/artifacts-registry.md +0 -38
- package/src/iris_bundle/.iris/aidlc/standards/decision-logging.md +0 -16
- package/src/iris_bundle/.iris/aidlc/standards/doctrine-structure.md +0 -31
- package/src/iris_bundle/.iris/aidlc/standards/documentation-rules.md +0 -15
- package/src/iris_bundle/.iris/aidlc/standards/file-structure.md +0 -21
- package/src/iris_bundle/.iris/aidlc/standards/naming-conventions.md +0 -18
- package/src/iris_bundle/.iris/aidlc/standards/phases-and-gates.md +0 -25
- package/src/iris_bundle/.iris/aidlc/standards/routes-and-routing.md +0 -35
- package/src/iris_bundle/.iris/aidlc/standards/tool-wrappers.md +0 -32
- package/src/iris_bundle/.iris/aidlc/templates/bolt.md +0 -23
- package/src/iris_bundle/.iris/aidlc/templates/doctrine-doc-template.md +0 -33
- package/src/iris_bundle/.iris/aidlc/templates/intent.md +0 -23
- package/src/iris_bundle/.iris/aidlc/templates/log.md +0 -24
- package/src/iris_bundle/.iris/aidlc/templates/review.md +0 -21
- package/src/iris_bundle/.iris/aidlc/templates/unit.md +0 -31
- package/src/iris_bundle/.iris/aidlc/validation/failure-modes.md +0 -16
- package/src/iris_bundle/.iris/aidlc/validation/phase-preconditions.md +0 -21
- package/src/iris_bundle/.iris/aidlc/validation/quality-checklist.md +0 -20
- package/src/iris_bundle/.iris/policy.yaml +0 -27
- package/src/iris_bundle/.iris/routes.yaml +0 -98
- package/src/iris_bundle/.iris/state.yaml +0 -7
- package/src/iris_bundle/.iris/tools/claude/.claude/claude.md +0 -9
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/compare-specs.md +0 -203
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-construction-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-inception-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-master-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-operations-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/codex/AGENTS.md +0 -15
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-construction-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-inception-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-master-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-operations-agent.md +0 -25
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-construction-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-inception-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-master-agent.toml +0 -29
- package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-operations-agent.toml +0 -29
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
// --- Errors ---
|
|
5
|
+
export class FlowNotFoundError extends Error {
|
|
6
|
+
flowId;
|
|
7
|
+
path;
|
|
8
|
+
constructor(flowId, path) {
|
|
9
|
+
super(`Flow '${flowId}' not found at ${path}`);
|
|
10
|
+
this.flowId = flowId;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.name = "FlowNotFoundError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class FlowManifestValidationError extends Error {
|
|
16
|
+
flowId;
|
|
17
|
+
constructor(flowId, message) {
|
|
18
|
+
super(`Invalid manifest for flow '${flowId}': ${message}`);
|
|
19
|
+
this.flowId = flowId;
|
|
20
|
+
this.name = "FlowManifestValidationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// --- Constants & Defaults ---
|
|
24
|
+
const DEFAULT_ENTRYPOINTS = {
|
|
25
|
+
doctrine_dir: "doctrine",
|
|
26
|
+
policy_overlay: "policy.overlay.yaml",
|
|
27
|
+
routes_overlay: "routes.overlay.yaml"
|
|
28
|
+
};
|
|
29
|
+
// --- Helpers ---
|
|
30
|
+
export function getFlowsDir(root) {
|
|
31
|
+
return path.join(root, ".iris/flows");
|
|
32
|
+
}
|
|
33
|
+
export function getFlowDir(root, flowId) {
|
|
34
|
+
return path.join(getFlowsDir(root), flowId);
|
|
35
|
+
}
|
|
36
|
+
export function getFlowDoctrineRoot(root, flowId, manifest) {
|
|
37
|
+
return path.join(getFlowDir(root, flowId), manifest.entrypoints.doctrine_dir);
|
|
38
|
+
}
|
|
39
|
+
export function getFlowPolicyOverlayPath(root, flowId, manifest) {
|
|
40
|
+
return path.join(getFlowDir(root, flowId), manifest.entrypoints.policy_overlay);
|
|
41
|
+
}
|
|
42
|
+
export function getFlowRoutesOverlayPath(root, flowId, manifest) {
|
|
43
|
+
return path.join(getFlowDir(root, flowId), manifest.entrypoints.routes_overlay);
|
|
44
|
+
}
|
|
45
|
+
// --- Loader ---
|
|
46
|
+
export function loadFlowManifest(root, flowId) {
|
|
47
|
+
const flowDir = getFlowDir(root, flowId);
|
|
48
|
+
const manifestPath = path.join(flowDir, "flow.yaml");
|
|
49
|
+
if (!fs.existsSync(manifestPath)) {
|
|
50
|
+
throw new FlowNotFoundError(flowId, manifestPath);
|
|
51
|
+
}
|
|
52
|
+
let raw;
|
|
53
|
+
try {
|
|
54
|
+
raw = yaml.load(fs.readFileSync(manifestPath, "utf8"));
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
throw new FlowManifestValidationError(flowId, `Failed to parse YAML: ${e.message}`);
|
|
58
|
+
}
|
|
59
|
+
if (!raw || typeof raw !== "object") {
|
|
60
|
+
throw new FlowManifestValidationError(flowId, "Manifest is not a valid object");
|
|
61
|
+
}
|
|
62
|
+
// Validation: Required Fields
|
|
63
|
+
if (!raw.id || typeof raw.id !== "string" || !raw.id.trim()) {
|
|
64
|
+
throw new FlowManifestValidationError(flowId, "Missing or empty 'id'");
|
|
65
|
+
}
|
|
66
|
+
if (!raw.name || typeof raw.name !== "string" || !raw.name.trim()) {
|
|
67
|
+
throw new FlowManifestValidationError(flowId, "Missing or empty 'name'");
|
|
68
|
+
}
|
|
69
|
+
if (!raw.version || typeof raw.version !== "string" || !raw.version.trim()) {
|
|
70
|
+
throw new FlowManifestValidationError(flowId, "Missing or empty 'version'");
|
|
71
|
+
}
|
|
72
|
+
// Validation: ID Match
|
|
73
|
+
if (raw.id !== flowId) {
|
|
74
|
+
throw new FlowManifestValidationError(flowId, `Manifest ID '${raw.id}' does not match flow ID '${flowId}'`);
|
|
75
|
+
}
|
|
76
|
+
// Defaults & Entrypoint Validation
|
|
77
|
+
const entrypoints = { ...DEFAULT_ENTRYPOINTS, ...(raw.entrypoints || {}) };
|
|
78
|
+
// Validate Paths
|
|
79
|
+
validateRelativePath(flowId, "doctrine_dir", entrypoints.doctrine_dir);
|
|
80
|
+
validateRelativePath(flowId, "policy_overlay", entrypoints.policy_overlay);
|
|
81
|
+
validateRelativePath(flowId, "routes_overlay", entrypoints.routes_overlay);
|
|
82
|
+
return {
|
|
83
|
+
id: raw.id,
|
|
84
|
+
name: raw.name,
|
|
85
|
+
version: raw.version,
|
|
86
|
+
description: raw.description,
|
|
87
|
+
entrypoints
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function validateRelativePath(flowId, field, p) {
|
|
91
|
+
if (!p || typeof p !== "string" || !p.trim()) {
|
|
92
|
+
throw new FlowManifestValidationError(flowId, `Entrypoint '${field}' must be a non-empty string`);
|
|
93
|
+
}
|
|
94
|
+
if (path.isAbsolute(p)) {
|
|
95
|
+
throw new FlowManifestValidationError(flowId, `Entrypoint '${field}' must be relative`);
|
|
96
|
+
}
|
|
97
|
+
const normalized = path.normalize(p);
|
|
98
|
+
if (normalized.startsWith("..") || normalized === ".." || normalized.includes(path.sep + "..")) {
|
|
99
|
+
throw new FlowManifestValidationError(flowId, `Entrypoint '${field}' cannot traverse upwards`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function listInstalledFlows(root) {
|
|
103
|
+
const flowsDir = getFlowsDir(root);
|
|
104
|
+
if (!fs.existsSync(flowsDir))
|
|
105
|
+
return [];
|
|
106
|
+
const results = [];
|
|
107
|
+
const entries = fs.readdirSync(flowsDir, { withFileTypes: true });
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.isDirectory())
|
|
110
|
+
continue;
|
|
111
|
+
const flowId = entry.name;
|
|
112
|
+
try {
|
|
113
|
+
// This will validate schema and ID match
|
|
114
|
+
const manifest = loadFlowManifest(root, flowId);
|
|
115
|
+
results.push(manifest);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// Ignore invalid/partial flows during listing unless specific debug needed?
|
|
119
|
+
// User requested: "don't crash on partial folders"
|
|
120
|
+
// We sip it.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { loadState, getActiveFramework } from "./state.js";
|
|
3
|
+
import { loadFramework } from "../framework/framework-loader.js";
|
|
4
|
+
import { FrameworkError } from "../framework/framework-types.js";
|
|
5
|
+
import { repoRoot } from "../lib.js";
|
|
6
|
+
/**
|
|
7
|
+
* Resolves the currently active framework from state.
|
|
8
|
+
* Centralizes logic for commands to obtain the framework context.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* 1. Reads .iris/state.yaml for active ID.
|
|
12
|
+
* 2. Attempts to load framework.
|
|
13
|
+
* 3. Returns structured context (success or failure).
|
|
14
|
+
* 4. Prints warning to stderr if load fails (but returns context so caller can decide fallback).
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveActiveFramework(root) {
|
|
17
|
+
const r = root || repoRoot();
|
|
18
|
+
const state = loadState(); // loadState() takes no args currently
|
|
19
|
+
const activeFw = getActiveFramework(state);
|
|
20
|
+
try {
|
|
21
|
+
const resolution = await loadFramework(activeFw.current, { repoRoot: r });
|
|
22
|
+
return {
|
|
23
|
+
activeId: activeFw.current,
|
|
24
|
+
activeVersion: activeFw.version,
|
|
25
|
+
resolution,
|
|
26
|
+
error: null
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
// If it's a known FrameworkError, return it.
|
|
31
|
+
// If generic error, wrap it? loadFramework usually throws FrameworkError or Error.
|
|
32
|
+
let fwError;
|
|
33
|
+
if (err instanceof FrameworkError) {
|
|
34
|
+
fwError = err;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Generic error wrapper
|
|
38
|
+
fwError = new FrameworkError("INVALID_YAML", activeFw.current, `Failed to load framework '${activeFw.current}': ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
// Emit Warning as requested
|
|
41
|
+
console.error(kleur.yellow(`IRIS_WARNING IRIS_FRAMEWORK_LOAD_FAILED: Could not load active framework '${activeFw.current}'. Reason: ${fwError.message}`));
|
|
42
|
+
return {
|
|
43
|
+
activeId: activeFw.current,
|
|
44
|
+
activeVersion: activeFw.version,
|
|
45
|
+
resolution: null,
|
|
46
|
+
error: fwError
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { FrameworkError } from "../framework/framework-types.js";
|
|
4
|
+
import { loadFramework } from "../framework/framework-loader.js";
|
|
5
|
+
import { getBundledFrameworksDir } from "./bundle.js";
|
|
6
|
+
import { copyDir, ensureDir } from "../lib.js";
|
|
7
|
+
import { loadState, getActiveFramework } from "./state.js";
|
|
8
|
+
// --- Validation ---
|
|
9
|
+
/**
|
|
10
|
+
* Validates a framework ID.
|
|
11
|
+
* Defaults to strict mode: ^[a-z0-9][a-z0-9-_]*$
|
|
12
|
+
* Future: allowScoped option.
|
|
13
|
+
*/
|
|
14
|
+
export function validateFrameworkId(id, options = {}) {
|
|
15
|
+
const { allowScoped = false } = options;
|
|
16
|
+
if (!id || typeof id !== 'string')
|
|
17
|
+
return false;
|
|
18
|
+
// Strict mode (current default)
|
|
19
|
+
// Starts with alphanumeric, followed by alphanumeric, -, _
|
|
20
|
+
// TODO: Support scoped IDs (e.g. @scope/pkg) in future version.
|
|
21
|
+
// Suggestion: Store scoped IDs under .iris/frameworks/@scope__pkg (encoded) to keep flat structure.
|
|
22
|
+
const strictRegex = /^[a-z0-9][a-z0-9-_]*$/;
|
|
23
|
+
return strictRegex.test(id);
|
|
24
|
+
}
|
|
25
|
+
// --- Listing ---
|
|
26
|
+
export async function listInstalledFrameworks(root) {
|
|
27
|
+
const frameworksDir = path.join(root, ".iris/frameworks");
|
|
28
|
+
const state = loadState();
|
|
29
|
+
const activeFw = getActiveFramework(state);
|
|
30
|
+
if (!fs.existsSync(frameworksDir)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const entries = fs.readdirSync(frameworksDir, { withFileTypes: true });
|
|
34
|
+
const results = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isDirectory())
|
|
37
|
+
continue;
|
|
38
|
+
// Ignore temp directories from failed atomic installs
|
|
39
|
+
if (entry.name.startsWith('.tmp-'))
|
|
40
|
+
continue;
|
|
41
|
+
const fwPath = path.join(frameworksDir, entry.name);
|
|
42
|
+
// Ensure active checking uses the folder name derived ID if logical ID is missing?
|
|
43
|
+
// Actually framework state uses the ID.
|
|
44
|
+
const isActive = activeFw.current === entry.name;
|
|
45
|
+
try {
|
|
46
|
+
const resolution = await loadFramework(entry.name, { repoRoot: root });
|
|
47
|
+
results.push({
|
|
48
|
+
id: resolution.manifest.id || entry.name, // Source of truth fallback
|
|
49
|
+
version: resolution.manifest.version,
|
|
50
|
+
path: fwPath,
|
|
51
|
+
valid: true,
|
|
52
|
+
active: isActive,
|
|
53
|
+
manifest: resolution.manifest
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
results.push({
|
|
58
|
+
id: entry.name, // Fallback to folder name
|
|
59
|
+
version: null,
|
|
60
|
+
path: fwPath,
|
|
61
|
+
valid: false,
|
|
62
|
+
active: isActive,
|
|
63
|
+
error: {
|
|
64
|
+
code: error.code || 'UNKNOWN',
|
|
65
|
+
message: error.message || String(error)
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
export async function listBundledFrameworks() {
|
|
73
|
+
const bundleDir = getBundledFrameworksDir();
|
|
74
|
+
if (!fs.existsSync(bundleDir)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const entries = fs.readdirSync(bundleDir, { withFileTypes: true });
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (!entry.isDirectory())
|
|
81
|
+
continue;
|
|
82
|
+
const fwPath = path.join(bundleDir, entry.name);
|
|
83
|
+
try {
|
|
84
|
+
// Load explicitly from path
|
|
85
|
+
const resolution = await loadFramework(fwPath, { repoRoot: "/" });
|
|
86
|
+
results.push({
|
|
87
|
+
id: resolution.manifest.id || entry.name,
|
|
88
|
+
path: fwPath,
|
|
89
|
+
version: resolution.manifest.version,
|
|
90
|
+
valid: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
results.push({
|
|
95
|
+
id: entry.name,
|
|
96
|
+
path: fwPath,
|
|
97
|
+
valid: false,
|
|
98
|
+
error: e.message
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
// --- Installation ---
|
|
105
|
+
export async function installFrameworkFromPath(root, sourcePath, options = {}) {
|
|
106
|
+
const frameworksDir = path.join(root, ".iris/frameworks");
|
|
107
|
+
ensureDir(frameworksDir);
|
|
108
|
+
// 1. Validate Source & Get ID
|
|
109
|
+
// We load it to ensure it's a valid framework AND to get the canonical ID
|
|
110
|
+
let resolution;
|
|
111
|
+
try {
|
|
112
|
+
resolution = await loadFramework(sourcePath, { repoRoot: root });
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
throw new FrameworkError('INSTALL_FAILED', sourcePath, `Failed to load source framework: ${e.message}`, e.hint);
|
|
116
|
+
}
|
|
117
|
+
// Ensure ID is present
|
|
118
|
+
const id = resolution.manifest.id;
|
|
119
|
+
if (!id) {
|
|
120
|
+
throw new FrameworkError('INVALID_SCHEMA', sourcePath, `Framework manifest missing required 'id' field.`);
|
|
121
|
+
}
|
|
122
|
+
// 2. Validate ID (Strict)
|
|
123
|
+
if (!validateFrameworkId(id)) {
|
|
124
|
+
throw new FrameworkError('INVALID_ID', id, `Framework ID '${id}' is invalid.`, "IDs must match ^[a-z0-9][a-z0-9-_]*$");
|
|
125
|
+
}
|
|
126
|
+
// 3. Resolve Target Path & Safety Guard
|
|
127
|
+
const targetPath = path.resolve(frameworksDir, id);
|
|
128
|
+
// Guard: Path Traversal
|
|
129
|
+
if (!targetPath.startsWith(path.resolve(frameworksDir) + path.sep)) {
|
|
130
|
+
throw new FrameworkError('SECURITY_VIOLATION', id, `Invalid framework ID '${id}': Resulting path traverses outside frameworks directory.`);
|
|
131
|
+
}
|
|
132
|
+
// 4. Check Existence
|
|
133
|
+
if (fs.existsSync(targetPath)) {
|
|
134
|
+
if (!options.force) {
|
|
135
|
+
throw new FrameworkError('ALREADY_EXISTS', targetPath, `Framework '${id}' is already installed.`, "Use --force to overwrite.");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 5. Atomic Install
|
|
139
|
+
// Copy to .tmp-<id>-<random>
|
|
140
|
+
const tempName = `.tmp-${id}-${Math.random().toString(36).substring(7)}`;
|
|
141
|
+
const tempPath = path.join(frameworksDir, tempName);
|
|
142
|
+
try {
|
|
143
|
+
// Copy recursive
|
|
144
|
+
copyDir(resolution.rootDir, tempPath);
|
|
145
|
+
// 6. Rename / Swap
|
|
146
|
+
if (fs.existsSync(targetPath)) {
|
|
147
|
+
// Remove old (Safe because verified inside frameworksDir)
|
|
148
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
fs.renameSync(tempPath, targetPath);
|
|
151
|
+
return {
|
|
152
|
+
id,
|
|
153
|
+
version: resolution.manifest.version,
|
|
154
|
+
path: targetPath,
|
|
155
|
+
valid: true,
|
|
156
|
+
active: false,
|
|
157
|
+
manifest: resolution.manifest
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
// Cleanup temp
|
|
162
|
+
if (fs.existsSync(tempPath)) {
|
|
163
|
+
fs.rmSync(tempPath, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
throw new FrameworkError('INSTALL_FAILED', targetPath, `Failed to install framework: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function installBundledFramework(root, id, options = {}) {
|
|
169
|
+
const bundleDir = getBundledFrameworksDir();
|
|
170
|
+
// Fallback logic to find bundled framework
|
|
171
|
+
let sourcePath = path.join(bundleDir, id);
|
|
172
|
+
if (!fs.existsSync(sourcePath)) {
|
|
173
|
+
const allBundled = await listBundledFrameworks();
|
|
174
|
+
const found = allBundled.find(b => b.id === id && b.valid);
|
|
175
|
+
if (found) {
|
|
176
|
+
sourcePath = found.path;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
throw new FrameworkError('NOT_FOUND', id, `Bundled framework '${id}' not found.`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return installFrameworkFromPath(root, sourcePath, options);
|
|
183
|
+
}
|
|
184
|
+
// --- Removal ---
|
|
185
|
+
export function removeFramework(root, id) {
|
|
186
|
+
const frameworksDir = path.join(root, ".iris/frameworks");
|
|
187
|
+
// 1. Guard: Frameworks dir exists?
|
|
188
|
+
if (!fs.existsSync(frameworksDir)) {
|
|
189
|
+
throw new FrameworkError('NOT_FOUND', frameworksDir, "No frameworks installed.");
|
|
190
|
+
}
|
|
191
|
+
// 2. Resolve Target & Safety
|
|
192
|
+
// Use regex first
|
|
193
|
+
if (!validateFrameworkId(id)) {
|
|
194
|
+
throw new FrameworkError('INVALID_ID', id, `Invalid framework ID format.`);
|
|
195
|
+
}
|
|
196
|
+
const targetPath = path.resolve(frameworksDir, id);
|
|
197
|
+
if (!targetPath.startsWith(path.resolve(frameworksDir) + path.sep)) {
|
|
198
|
+
throw new FrameworkError('SECURITY_VIOLATION', id, `Path traversal detected.`);
|
|
199
|
+
}
|
|
200
|
+
if (!fs.existsSync(targetPath)) {
|
|
201
|
+
throw new FrameworkError('NOT_FOUND', targetPath, `Framework '${id}' is not installed.`);
|
|
202
|
+
}
|
|
203
|
+
// 3. Guard: Active
|
|
204
|
+
const state = loadState();
|
|
205
|
+
if (state.framework.current === id) {
|
|
206
|
+
throw new FrameworkError('ACTIVE_FRAMEWORK', id, `Cannot remove active framework '${id}'.`, "Switch to another framework first (e.g. 'iris framework use iris-core').");
|
|
207
|
+
}
|
|
208
|
+
// 4. Delete
|
|
209
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
210
|
+
}
|
|
211
|
+
// --- Validation Wrapper ---
|
|
212
|
+
export async function validateFrameworkParams(input, root) {
|
|
213
|
+
// Just a wrapper for loadFramework but enables consistent error handling for CLI
|
|
214
|
+
return loadFramework(input, { repoRoot: root });
|
|
215
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function writeJsonAtomic(filePath, data) {
|
|
4
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
5
|
+
const dir = path.dirname(filePath);
|
|
6
|
+
if (!fs.existsSync(dir)) {
|
|
7
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
11
|
+
fs.renameSync(tmpPath, filePath);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
// Attempt cleanup
|
|
15
|
+
try {
|
|
16
|
+
if (fs.existsSync(tmpPath))
|
|
17
|
+
fs.unlinkSync(tmpPath);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const REGISTRY = {};
|
|
2
|
+
export function registerImporter(importer) {
|
|
3
|
+
REGISTRY[importer.id] = importer;
|
|
4
|
+
}
|
|
5
|
+
// No importers registered - IRIS ships only iris-core
|
|
6
|
+
export function getImporter(id) {
|
|
7
|
+
return REGISTRY[id] || null;
|
|
8
|
+
}
|
|
9
|
+
export const IMPORTERS = REGISTRY;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// Safe extensions to import
|
|
4
|
+
const SAFE_EXTENSIONS = ['.md', '.txt', '.yml', '.yaml', '.json'];
|
|
5
|
+
const IGNORED_DIRS = ['node_modules', '.git', 'dist', 'build', 'coverage'];
|
|
6
|
+
export function ensureDoctrineOnlyFlowInstalled(opts) {
|
|
7
|
+
const result = {
|
|
8
|
+
flowId: opts.flowId,
|
|
9
|
+
installedDir: '', // set below
|
|
10
|
+
copiedFiles: 0,
|
|
11
|
+
skippedFiles: 0,
|
|
12
|
+
overwrittenFiles: 0,
|
|
13
|
+
};
|
|
14
|
+
const flowDir = path.join(opts.repoRoot, '.iris', 'flows', opts.flowId);
|
|
15
|
+
result.installedDir = flowDir;
|
|
16
|
+
if (opts.verbose) {
|
|
17
|
+
console.log(`Preparing to install flow '${opts.flowId}' into ${flowDir}`);
|
|
18
|
+
}
|
|
19
|
+
// 1. Check if flow already exists
|
|
20
|
+
if (fs.existsSync(flowDir) && !opts.force) {
|
|
21
|
+
// If not creating/updating, we might still want to report what happened,
|
|
22
|
+
// but typically we'd expect the caller to handle "already exists" checks if they wanted to abort early.
|
|
23
|
+
// However, the requirement is "overwrite if force", which implies "skip/merge if not force".
|
|
24
|
+
// For simplicity, if it exists and !force, we might just return early if we consider the flow "installed".
|
|
25
|
+
// But let's proceed to copy templates with "skip existing" semantics.
|
|
26
|
+
}
|
|
27
|
+
if (opts.dryRun) {
|
|
28
|
+
console.log(`[Dry Run] Would create/update flow directory: ${flowDir}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
fs.mkdirSync(flowDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
// 2. Ensure flow.yaml exists
|
|
34
|
+
const flowYamlPath = path.join(flowDir, 'flow.yaml');
|
|
35
|
+
if (opts.dryRun) {
|
|
36
|
+
if (!fs.existsSync(flowYamlPath) || opts.force) {
|
|
37
|
+
console.log(`[Dry Run] Would write flow.yaml for ${opts.flowId}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
if (!fs.existsSync(flowYamlPath) || opts.force) {
|
|
42
|
+
const flowYamlContent = `# Generated by iris flow import
|
|
43
|
+
id: ${opts.flowId}
|
|
44
|
+
name: ${opts.flowId}
|
|
45
|
+
description: Imported from ${opts.displayName}
|
|
46
|
+
version: imported
|
|
47
|
+
`;
|
|
48
|
+
fs.writeFileSync(flowYamlPath, flowYamlContent);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. Ensure overlays exist (empty)
|
|
52
|
+
const overlays = ['policy.overlay.yaml', 'routes.overlay.yaml'];
|
|
53
|
+
for (const file of overlays) {
|
|
54
|
+
const filePath = path.join(flowDir, file);
|
|
55
|
+
if (opts.dryRun) {
|
|
56
|
+
if (!fs.existsSync(filePath) || opts.force) {
|
|
57
|
+
console.log(`[Dry Run] Would ensure empty ${file}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (!fs.existsSync(filePath) || opts.force) {
|
|
62
|
+
fs.writeFileSync(filePath, '{}\n');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 4. Copy templates
|
|
67
|
+
// Destination: .iris/flows/<id>/doctrine/templates/<id>/...
|
|
68
|
+
// We nest under <id> again inside templates/ to avoid collision if multiple flows share generic names,
|
|
69
|
+
// and to keep it namespaced.
|
|
70
|
+
const destTemplateRoot = path.join(flowDir, 'doctrine', 'templates', opts.flowId);
|
|
71
|
+
if (opts.dryRun) {
|
|
72
|
+
console.log(`[Dry Run] Would copy templates to ${destTemplateRoot}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
fs.mkdirSync(destTemplateRoot, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
for (const srcDir of opts.sourceTemplateDirs) {
|
|
78
|
+
copyRecursive(srcDir, destTemplateRoot, opts, result);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function copyRecursive(src, dest, opts, result, relPath = '') {
|
|
83
|
+
if (!fs.existsSync(src))
|
|
84
|
+
return;
|
|
85
|
+
const stats = fs.statSync(src);
|
|
86
|
+
if (stats.isDirectory()) {
|
|
87
|
+
// Check ignore list
|
|
88
|
+
const dirName = path.basename(src);
|
|
89
|
+
if (IGNORED_DIRS.includes(dirName))
|
|
90
|
+
return;
|
|
91
|
+
if (!opts.dryRun) {
|
|
92
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
const children = fs.readdirSync(src);
|
|
95
|
+
for (const child of children) {
|
|
96
|
+
copyRecursive(path.join(src, child), path.join(dest, child), opts, result, path.join(relPath, child));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (stats.isFile()) {
|
|
100
|
+
// Filter extensions
|
|
101
|
+
const ext = path.extname(src).toLowerCase();
|
|
102
|
+
if (!SAFE_EXTENSIONS.includes(ext)) {
|
|
103
|
+
if (opts.verbose)
|
|
104
|
+
console.log(`Skipping unsafe/irrelevant file type: ${src}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (opts.dryRun) {
|
|
108
|
+
// Just log what would happen
|
|
109
|
+
if (fs.existsSync(dest)) {
|
|
110
|
+
if (opts.force) {
|
|
111
|
+
console.log(`[Dry Run] Would overwrite ${relPath}`);
|
|
112
|
+
result.overwrittenFiles++;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(`[Dry Run] Would skip existing ${relPath}`);
|
|
116
|
+
result.skippedFiles++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(`[Dry Run] Would copy ${relPath}`);
|
|
121
|
+
result.copiedFiles++;
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (fs.existsSync(dest)) {
|
|
126
|
+
if (opts.force) {
|
|
127
|
+
fs.copyFileSync(src, dest);
|
|
128
|
+
result.overwrittenFiles++;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
result.skippedFiles++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
fs.copyFileSync(src, dest);
|
|
136
|
+
result.copiedFiles++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|