oris-skills 2.0.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/.cursor-plugin/plugin.json +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/agents/oris-loop-debriefer.md +29 -0
- package/agents/oris-loop-doctor.md +32 -0
- package/agents/oris-loop-executor.md +32 -0
- package/agents/oris-loop-verifier.md +31 -0
- package/docs/architecture.md +26 -0
- package/docs/distribution.md +38 -0
- package/docs/maintainer-guide.md +32 -0
- package/docs/user-guide.md +42 -0
- package/package.json +49 -0
- package/references/clean-code-checklist.md +107 -0
- package/references/conventions.md +39 -0
- package/references/doc-policy.md +24 -0
- package/references/loop-adapter.schema.json +126 -0
- package/references/loop-contract.md +119 -0
- package/references/loop.schema.json +143 -0
- package/references/questions.md +38 -0
- package/references/repo-map.md +53 -0
- package/references/repo-map.schema.json +198 -0
- package/references/settings.md +35 -0
- package/references/settings.schema.json +75 -0
- package/scripts/flow/oris-flow-clean-runtime.mjs +182 -0
- package/scripts/flow/oris-flow-layout.mjs +53 -0
- package/scripts/flow/oris-flow-scan.mjs +350 -0
- package/scripts/flow/oris-flow-version-control.mjs +42 -0
- package/scripts/flow/oris-gitignore.mjs +79 -0
- package/scripts/install/generate-agent-adapters.mjs +74 -0
- package/scripts/install/install-user-skills.mjs +271 -0
- package/scripts/install/uninstall-user-skills.mjs +163 -0
- package/scripts/loop/oris-loop-bootstrap.mjs +383 -0
- package/scripts/loop/oris-loop-bundle.mjs +22 -0
- package/scripts/loop/oris-loop-chat.mjs +408 -0
- package/scripts/loop/oris-loop-demo.mjs +180 -0
- package/scripts/loop/oris-loop-document.mjs +179 -0
- package/scripts/loop/oris-loop-dry-run.mjs +110 -0
- package/scripts/loop/oris-loop-fixtures.mjs +148 -0
- package/scripts/loop/oris-loop-list.mjs +81 -0
- package/scripts/loop/oris-loop-paths.mjs +232 -0
- package/scripts/loop/oris-loop-run.mjs +241 -0
- package/scripts/loop/oris-loop-stop.mjs +319 -0
- package/scripts/loop/oris-loop-templates.mjs +77 -0
- package/scripts/loop/oris-loop-verify.mjs +206 -0
- package/scripts/oris-skills.mjs +116 -0
- package/scripts/package-oris-skills.mjs +53 -0
- package/scripts/tests/run-all-tests.mjs +38 -0
- package/scripts/tests/test-agent-adapters.mjs +19 -0
- package/scripts/tests/test-oris-1-0-cleanliness.mjs +58 -0
- package/scripts/tests/test-oris-flow-clean-runtime.mjs +75 -0
- package/scripts/tests/test-oris-flow-scan.mjs +49 -0
- package/scripts/tests/test-oris-gitignore.mjs +35 -0
- package/scripts/tests/test-oris-loop-bootstrap.mjs +94 -0
- package/scripts/tests/test-oris-loop-document.mjs +112 -0
- package/scripts/tests/test-oris-loop-list.mjs +74 -0
- package/scripts/tests/test-oris-loop-run.mjs +45 -0
- package/scripts/tests/test-oris-loop-smoke.mjs +130 -0
- package/scripts/tests/test-oris-loop-stop.mjs +371 -0
- package/scripts/tests/test-routing-lifecycle.mjs +181 -0
- package/scripts/tests/test-schemas.mjs +55 -0
- package/skills/oris-flow/SKILL.md +55 -0
- package/skills/oris-flow/agents/openai.yaml +4 -0
- package/skills/oris-flow-criteria/SKILL.md +49 -0
- package/skills/oris-flow-discover/SKILL.md +58 -0
- package/skills/oris-flow-docs/SKILL.md +31 -0
- package/skills/oris-flow-fix/SKILL.md +42 -0
- package/skills/oris-flow-implement/SKILL.md +43 -0
- package/skills/oris-flow-plan/SKILL.md +51 -0
- package/skills/oris-flow-setup/SKILL.md +49 -0
- package/skills/oris-help/SKILL.md +30 -0
- package/skills/oris-help/agents/openai.yaml +4 -0
- package/skills/oris-loop/SKILL.md +66 -0
- package/skills/oris-loop/agents/openai.yaml +4 -0
- package/skills/oris-loop/references/craft.md +48 -0
- package/skills/oris-loop/references/improve.md +23 -0
- package/skills/oris-loop/references/run.md +30 -0
- package/skills/oris-loop/templates/debriefer.md +18 -0
- package/skills/oris-loop/templates/doctor.md +20 -0
- package/skills/oris-loop/templates/executor.md +19 -0
- package/skills/oris-loop/templates/orchestrator.md +33 -0
- package/skills/oris-loop/templates/verifier.md +21 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Oris Settings
|
|
2
|
+
|
|
3
|
+
User-local settings, never bundle content. Path: `~/.oris/settings.json` (all platforms).
|
|
4
|
+
The installer creates it when missing; it NEVER overwrites an existing file.
|
|
5
|
+
|
|
6
|
+
## Shape (v1)
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"version": 1,
|
|
11
|
+
"orisFlow": { "uiLanguage": "en", "artifactLanguage": "en" },
|
|
12
|
+
"defaultProfiles": { "testing": "default" },
|
|
13
|
+
"profiles": {
|
|
14
|
+
"default": {
|
|
15
|
+
"environment": "local",
|
|
16
|
+
"values": { "baseUrl": "" },
|
|
17
|
+
"secrets": { "username": "", "password": "" },
|
|
18
|
+
"flags": { "allowDataMutation": false }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `uiLanguage` → chat, menus, question labels, recaps (`en` | `it`, default `en`).
|
|
25
|
+
- `artifactLanguage` → generated documents (`en` | `it`, default `en`).
|
|
26
|
+
- `profiles` → repository-neutral test logins/config. The repo adapter maps profile fields to roles.
|
|
27
|
+
- `flags.allowDataMutation` → data mutation allowed ONLY with `environment: local` + loopback URLs + this flag true.
|
|
28
|
+
|
|
29
|
+
## Rules
|
|
30
|
+
|
|
31
|
+
1. READ settings before the first workflow question; missing/invalid → defaults above.
|
|
32
|
+
2. Current user instructions override settings for the turn.
|
|
33
|
+
3. Passwords: only in this file. NEVER committed, printed, put in docs, loop state, receipts, or CLI args. Mask as `***` everywhere.
|
|
34
|
+
4. CONFIRM before writing settings. Update via `/oris-flow` → Setup → language & profiles.
|
|
35
|
+
5. In an approved loop scope, use only the profile/commands/tools the loop declares; otherwise `blocked`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Oris Flow Settings",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"$id": "https://oris.dev/schemas/oris-flow-settings-v1.json",
|
|
6
|
+
"required": ["version", "orisFlow", "defaultProfiles", "profiles"],
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"version": {
|
|
10
|
+
"type": "integer",
|
|
11
|
+
"const": 1
|
|
12
|
+
},
|
|
13
|
+
"orisFlow": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["uiLanguage", "artifactLanguage"],
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"properties": {
|
|
18
|
+
"uiLanguage": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"enum": ["en", "it"]
|
|
21
|
+
},
|
|
22
|
+
"artifactLanguage": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"enum": ["it", "en"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"defaultProfiles": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"minProperties": 1,
|
|
31
|
+
"additionalProperties": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"minLength": 1
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"profiles": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"minProperties": 1,
|
|
39
|
+
"additionalProperties": {
|
|
40
|
+
"$ref": "#/$defs/profile"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"$defs": {
|
|
45
|
+
"profile": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["environment", "values", "secrets", "flags"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"environment": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"minLength": 1
|
|
53
|
+
},
|
|
54
|
+
"values": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": {
|
|
57
|
+
"type": ["string", "number", "boolean"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"secrets": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": {
|
|
63
|
+
"type": "string"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"flags": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": {
|
|
69
|
+
"type": "boolean"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
ORIS_FLOW_ADAPTER,
|
|
6
|
+
ORIS_FLOW_LOOPS_ROOT,
|
|
7
|
+
ORIS_FLOW_MANIFEST,
|
|
8
|
+
ORIS_FLOW_MAPS_ROOT,
|
|
9
|
+
ORIS_FLOW_RUNTIME,
|
|
10
|
+
} from "./oris-flow-layout.mjs";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RUNTIME_RELATIVE = ORIS_FLOW_RUNTIME;
|
|
13
|
+
|
|
14
|
+
function readJson(filePath) {
|
|
15
|
+
if (!fs.existsSync(filePath)) return null;
|
|
16
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertSafeRelativePath(relativePath, label) {
|
|
20
|
+
const normalized = String(relativePath ?? "").replace(/\\/g, "/");
|
|
21
|
+
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
|
|
22
|
+
throw new Error(`${label} must be a repository-relative path.`);
|
|
23
|
+
}
|
|
24
|
+
if (normalized.split("/").includes("..")) {
|
|
25
|
+
throw new Error(`${label} must not contain '..'.`);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertUnderRepository(repositoryRoot, targetPath) {
|
|
31
|
+
const root = path.resolve(repositoryRoot);
|
|
32
|
+
const resolved = path.resolve(targetPath);
|
|
33
|
+
const relative = path.relative(root, resolved);
|
|
34
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
35
|
+
throw new Error(`Refusing to clean path outside repository: ${targetPath}`);
|
|
36
|
+
}
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveCleanTargets(repositoryRoot) {
|
|
41
|
+
const root = path.resolve(repositoryRoot);
|
|
42
|
+
const adapter = readJson(path.join(root, ORIS_FLOW_ADAPTER));
|
|
43
|
+
const runtimeRelative = assertSafeRelativePath(
|
|
44
|
+
adapter?.paths?.runtime ?? DEFAULT_RUNTIME_RELATIVE,
|
|
45
|
+
"paths.runtime",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (runtimeRelative.replace(/\\/g, "/") === ORIS_FLOW_RUNTIME) {
|
|
49
|
+
return [assertUnderRepository(root, path.join(root, ...ORIS_FLOW_RUNTIME.split("/")))];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return [assertUnderRepository(root, path.join(root, ...runtimeRelative.split("/")))];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function listEntries(directoryPath) {
|
|
56
|
+
if (!fs.existsSync(directoryPath)) return [];
|
|
57
|
+
const entries = [];
|
|
58
|
+
const stack = [directoryPath];
|
|
59
|
+
while (stack.length > 0) {
|
|
60
|
+
const current = stack.pop();
|
|
61
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
62
|
+
const fullPath = path.join(current, entry.name);
|
|
63
|
+
entries.push(fullPath);
|
|
64
|
+
if (entry.isDirectory()) stack.push(fullPath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return entries.sort();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeEntry(entryPath) {
|
|
71
|
+
if (!fs.existsSync(entryPath)) return;
|
|
72
|
+
const stat = fs.lstatSync(entryPath);
|
|
73
|
+
if (stat.isDirectory()) {
|
|
74
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
fs.rmSync(entryPath, { force: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function cleanFlowRuntime(repositoryRoot, options = {}) {
|
|
81
|
+
const root = path.resolve(repositoryRoot);
|
|
82
|
+
const dryRun = Boolean(options.dryRun);
|
|
83
|
+
const targets = resolveCleanTargets(root);
|
|
84
|
+
const removed = [];
|
|
85
|
+
const preserved = [
|
|
86
|
+
ORIS_FLOW_MANIFEST,
|
|
87
|
+
`${ORIS_FLOW_MAPS_ROOT}/**`,
|
|
88
|
+
ORIS_FLOW_ADAPTER,
|
|
89
|
+
`${ORIS_FLOW_LOOPS_ROOT}/**`,
|
|
90
|
+
".cursor/hooks.json",
|
|
91
|
+
".cursor/hooks/oris-loop-stop.mjs",
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const target of targets) {
|
|
95
|
+
if (!fs.existsSync(target)) continue;
|
|
96
|
+
const stat = fs.lstatSync(target);
|
|
97
|
+
if (stat.isDirectory()) {
|
|
98
|
+
for (const entry of listEntries(target)) {
|
|
99
|
+
const relative = path.relative(root, entry).split(path.sep).join("/");
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
removed.push(relative);
|
|
102
|
+
} else {
|
|
103
|
+
removeEntry(entry);
|
|
104
|
+
removed.push(relative);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (dryRun) {
|
|
108
|
+
removed.push(path.relative(root, target).split(path.sep).join("/"));
|
|
109
|
+
} else {
|
|
110
|
+
removeEntry(target);
|
|
111
|
+
removed.push(path.relative(root, target).split(path.sep).join("/"));
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const relative = path.relative(root, target).split(path.sep).join("/");
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
removed.push(relative);
|
|
118
|
+
} else {
|
|
119
|
+
removeEntry(target);
|
|
120
|
+
removed.push(relative);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
repositoryRoot: root,
|
|
126
|
+
dryRun,
|
|
127
|
+
targets: targets.map((target) => path.relative(root, target).split(path.sep).join("/")),
|
|
128
|
+
removed,
|
|
129
|
+
preserved,
|
|
130
|
+
cleaned: removed.length > 0,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseArgs(argv) {
|
|
135
|
+
const options = {
|
|
136
|
+
repositoryRoot: process.cwd(),
|
|
137
|
+
dryRun: false,
|
|
138
|
+
json: false,
|
|
139
|
+
};
|
|
140
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
141
|
+
const raw = argv[i];
|
|
142
|
+
if (raw === "--dry-run") options.dryRun = true;
|
|
143
|
+
else if (raw === "--json") options.json = true;
|
|
144
|
+
else if (raw.startsWith("--repository-root=")) options.repositoryRoot = raw.slice("--repository-root=".length);
|
|
145
|
+
else if (raw === "--repository-root" && argv[i + 1]) {
|
|
146
|
+
options.repositoryRoot = argv[i + 1];
|
|
147
|
+
i += 1;
|
|
148
|
+
} else if (!raw.startsWith("-")) {
|
|
149
|
+
options.repositoryRoot = raw;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return options;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function printCleanReport(result) {
|
|
156
|
+
console.log(`Oris Flow runtime clean${result.dryRun ? " (dry run)" : ""}`);
|
|
157
|
+
console.log(`Repository: ${result.repositoryRoot}`);
|
|
158
|
+
console.log("");
|
|
159
|
+
console.log("Targets:");
|
|
160
|
+
for (const target of result.targets) console.log(`- ${target}`);
|
|
161
|
+
console.log("");
|
|
162
|
+
if (result.removed.length === 0) {
|
|
163
|
+
console.log("No ephemeral runtime files found.");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`${result.dryRun ? "Would remove" : "Removed"} ${result.removed.length} path(s):`);
|
|
167
|
+
for (const entry of result.removed) console.log(`- ${entry}`);
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log("Preserved by design:");
|
|
170
|
+
for (const entry of result.preserved) console.log(`- ${entry}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
174
|
+
const options = parseArgs(process.argv.slice(2));
|
|
175
|
+
const result = cleanFlowRuntime(options.repositoryRoot, { dryRun: options.dryRun });
|
|
176
|
+
if (options.json) {
|
|
177
|
+
console.log(JSON.stringify(result, null, 2));
|
|
178
|
+
} else {
|
|
179
|
+
printCleanReport(result);
|
|
180
|
+
}
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const ORIS_FLOW_ROOT = ".oris-flow";
|
|
5
|
+
export const ORIS_FLOW_MANIFEST = ".oris-flow/manifest.json";
|
|
6
|
+
export const ORIS_FLOW_MAPS_ROOT = ".oris-flow/maps";
|
|
7
|
+
export const ORIS_FLOW_ADAPTER = ".oris-flow/adapter.json";
|
|
8
|
+
export const ORIS_FLOW_LOOPS_ROOT = ".oris-flow/loops";
|
|
9
|
+
export const ORIS_FLOW_RUNTIME = ".oris-flow/runtime";
|
|
10
|
+
export const ORIS_HOME = path.join(os.homedir(), ".oris");
|
|
11
|
+
export const ORIS_BUNDLE_ROOT = path.join(ORIS_HOME, "oris-skills");
|
|
12
|
+
export const ORIS_SKILLS_SETTINGS = path.join(ORIS_HOME, "settings.json");
|
|
13
|
+
export const ORIS_LOOP_SCHEMA = "https://oris.dev/schemas/oris-loop-adapter-v1.json";
|
|
14
|
+
export const ORIS_LOOP_SCHEMA_VERSION = 1;
|
|
15
|
+
export const ORIS_LOOP_DOCUMENT_VERSION = 2;
|
|
16
|
+
export const ORIS_FLOW_MAP_SCHEMA = "https://oris.dev/schemas/oris-flow-map-v1.json";
|
|
17
|
+
export const ORIS_FLOW_MAP_SCHEMA_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
export function toPosixPath(value) {
|
|
20
|
+
return String(value).replace(/\\/g, "/");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function safeSlug(value, fallback = "loop") {
|
|
24
|
+
const raw = path.basename(toPosixPath(value || fallback));
|
|
25
|
+
const slug = raw.replace(/[^A-Za-z0-9_.-]/g, "-").replace(/^-+|-+$/g, "");
|
|
26
|
+
return slug || fallback;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function assertRepositoryRelative(value, label) {
|
|
30
|
+
const normalized = toPosixPath(value ?? "");
|
|
31
|
+
if (!normalized || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
|
|
32
|
+
throw new Error(`${label} must be a repository-relative path.`);
|
|
33
|
+
}
|
|
34
|
+
if (normalized.split("/").includes("..")) {
|
|
35
|
+
throw new Error(`${label} must not contain '..'.`);
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveInsideRepository(repositoryRoot, relativePath, label = "path") {
|
|
41
|
+
const root = path.resolve(repositoryRoot);
|
|
42
|
+
const normalized = assertRepositoryRelative(relativePath, label);
|
|
43
|
+
const resolved = path.resolve(root, ...normalized.split("/"));
|
|
44
|
+
const relative = path.relative(root, resolved);
|
|
45
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
46
|
+
throw new Error(`${label} must be below repository root: ${relativePath}`);
|
|
47
|
+
}
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function relativeToRepository(repositoryRoot, targetPath) {
|
|
52
|
+
return toPosixPath(path.relative(path.resolve(repositoryRoot), path.resolve(targetPath)));
|
|
53
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import {
|
|
8
|
+
ORIS_FLOW_MANIFEST,
|
|
9
|
+
ORIS_FLOW_MAP_SCHEMA,
|
|
10
|
+
ORIS_FLOW_MAP_SCHEMA_VERSION,
|
|
11
|
+
ORIS_FLOW_MAPS_ROOT,
|
|
12
|
+
relativeToRepository,
|
|
13
|
+
resolveInsideRepository,
|
|
14
|
+
toPosixPath,
|
|
15
|
+
} from "./oris-flow-layout.mjs";
|
|
16
|
+
|
|
17
|
+
const ignoredDirectories = new Set([
|
|
18
|
+
".git",
|
|
19
|
+
".hg",
|
|
20
|
+
".svn",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist",
|
|
23
|
+
"build",
|
|
24
|
+
"bin",
|
|
25
|
+
"obj",
|
|
26
|
+
".next",
|
|
27
|
+
".nuxt",
|
|
28
|
+
".turbo",
|
|
29
|
+
".venv",
|
|
30
|
+
"venv",
|
|
31
|
+
"__pycache__",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const stackDetectors = [
|
|
35
|
+
{ id: "node", label: "Node.js", match: (file) => file.name === "package.json" },
|
|
36
|
+
{ id: "pnpm", label: "pnpm", match: (file) => file.name === "pnpm-lock.yaml" },
|
|
37
|
+
{ id: "yarn", label: "Yarn", match: (file) => file.name === "yarn.lock" },
|
|
38
|
+
{ id: "dotnet", label: ".NET", match: (file) => /\.(sln|csproj|fsproj|vbproj)$/i.test(file.name) },
|
|
39
|
+
{ id: "python", label: "Python", match: (file) => ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"].includes(file.name) },
|
|
40
|
+
{ id: "java", label: "Java", match: (file) => ["pom.xml", "build.gradle", "build.gradle.kts"].includes(file.name) },
|
|
41
|
+
{ id: "go", label: "Go", match: (file) => file.name === "go.mod" },
|
|
42
|
+
{ id: "rust", label: "Rust", match: (file) => file.name === "Cargo.toml" },
|
|
43
|
+
{ id: "playwright", label: "Playwright", match: (file) => /^playwright\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
44
|
+
{ id: "vitest", label: "Vitest", match: (file) => /^vitest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) },
|
|
45
|
+
{ id: "jest", label: "Jest", match: (file) => /^jest\.config\.(ts|js|mjs|cjs)$/i.test(file.name) || file.name === "jest.config.json" },
|
|
46
|
+
{ id: "docker", label: "Docker", match: (file) => file.name === "Dockerfile" || file.name === "docker-compose.yml" || file.name === "compose.yaml" },
|
|
47
|
+
{ id: "azure-pipelines", label: "Azure Pipelines", match: (file) => /^azure-pipelines.*\.ya?ml$/i.test(file.name) },
|
|
48
|
+
{ id: "github-actions", label: "GitHub Actions", match: (file) => file.relative.startsWith(".github/workflows/") && /\.ya?ml$/i.test(file.name) },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function fail(message) {
|
|
52
|
+
console.error(message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
const options = {
|
|
58
|
+
repositoryRoot: process.cwd(),
|
|
59
|
+
write: false,
|
|
60
|
+
json: false,
|
|
61
|
+
area: "",
|
|
62
|
+
};
|
|
63
|
+
const readValue = (index, raw) => {
|
|
64
|
+
if (raw.includes("=")) return [index, raw.slice(raw.indexOf("=") + 1)];
|
|
65
|
+
if (index + 1 >= argv.length) fail(`Missing value for ${raw}.`);
|
|
66
|
+
return [index + 1, argv[index + 1]];
|
|
67
|
+
};
|
|
68
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
69
|
+
const raw = argv[i];
|
|
70
|
+
const key = raw.toLowerCase();
|
|
71
|
+
if (key === "--repository-root" || key.startsWith("--repository-root=")) [i, options.repositoryRoot] = readValue(i, raw);
|
|
72
|
+
else if (key === "--write") options.write = true;
|
|
73
|
+
else if (key === "--json") options.json = true;
|
|
74
|
+
else if (key === "--area" || key.startsWith("--area=")) [i, options.area] = readValue(i, raw);
|
|
75
|
+
else fail(`Unknown argument: ${raw}`);
|
|
76
|
+
}
|
|
77
|
+
return options;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function walk(root, options = {}) {
|
|
81
|
+
const maxFiles = Number(options.maxFiles ?? 6000);
|
|
82
|
+
const files = [];
|
|
83
|
+
const stack = [root];
|
|
84
|
+
while (stack.length > 0 && files.length < maxFiles) {
|
|
85
|
+
const current = stack.pop();
|
|
86
|
+
let entries = [];
|
|
87
|
+
try {
|
|
88
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (ignoredDirectories.has(entry.name)) continue;
|
|
95
|
+
const full = path.join(current, entry.name);
|
|
96
|
+
const relative = relativeToRepository(root, full);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
stack.push(full);
|
|
99
|
+
} else if (entry.isFile()) {
|
|
100
|
+
files.push({ full, relative, name: entry.name });
|
|
101
|
+
}
|
|
102
|
+
if (files.length >= maxFiles) break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return files;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readJson(filePath) {
|
|
109
|
+
if (!fs.existsSync(filePath)) return null;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function detectStacks(files) {
|
|
118
|
+
const stacks = new Map();
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
for (const detector of stackDetectors) {
|
|
121
|
+
if (!detector.match(file)) continue;
|
|
122
|
+
const current = stacks.get(detector.id) ?? { id: detector.id, label: detector.label, evidence: [] };
|
|
123
|
+
current.evidence.push(file.relative);
|
|
124
|
+
stacks.set(detector.id, current);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return [...stacks.values()].map((stack) => ({
|
|
128
|
+
...stack,
|
|
129
|
+
evidence: stack.evidence.slice(0, 8),
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function findPackageCommands(files) {
|
|
134
|
+
const commands = [];
|
|
135
|
+
for (const file of files.filter((item) => item.name === "package.json")) {
|
|
136
|
+
const pkg = readJson(file.full);
|
|
137
|
+
const scripts = pkg?.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
138
|
+
const cwd = path.dirname(file.relative) === "." ? "." : toPosixPath(path.dirname(file.relative));
|
|
139
|
+
for (const name of ["check", "test", "lint", "build", "typecheck"]) {
|
|
140
|
+
if (scripts[name]) {
|
|
141
|
+
commands.push({
|
|
142
|
+
name,
|
|
143
|
+
cwd,
|
|
144
|
+
command: cwd === "." ? `npm run ${name}` : `npm run ${name} --prefix ${cwd}`,
|
|
145
|
+
evidence: file.relative,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return commands;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasAny(files, predicate) {
|
|
154
|
+
return files.some(predicate);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function makeSection(pathValue, summary, details) {
|
|
158
|
+
return {
|
|
159
|
+
pointer: {
|
|
160
|
+
path: pathValue,
|
|
161
|
+
confidence: details.confidence ?? "detected",
|
|
162
|
+
lastScannedAt: details.scannedAt,
|
|
163
|
+
stale: false,
|
|
164
|
+
summary,
|
|
165
|
+
},
|
|
166
|
+
markdown: details.markdown,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildSections(root, files, stacks, commands, scannedAt) {
|
|
171
|
+
const sections = {};
|
|
172
|
+
const topFiles = files.map((file) => file.relative);
|
|
173
|
+
|
|
174
|
+
if (hasAny(files, (file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING)\.md$/i.test(file.relative) || file.relative.startsWith("docs/"))) {
|
|
175
|
+
const docs = topFiles.filter((file) => /(^|\/)(README|CONTEXT|CHANGELOG|CONTRIBUTING)\.md$/i.test(file) || file.startsWith("docs/")).slice(0, 20);
|
|
176
|
+
sections.docs = makeSection(".oris-flow/maps/docs.md", "Documentation and task context detected.", {
|
|
177
|
+
scannedAt,
|
|
178
|
+
markdown: [
|
|
179
|
+
"# Docs",
|
|
180
|
+
"",
|
|
181
|
+
"Detected documentation paths:",
|
|
182
|
+
...docs.map((file) => `- ${file}`),
|
|
183
|
+
"",
|
|
184
|
+
].join("\n"),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (hasAny(files, (file) => file.relative.startsWith(".cursor/skills/") || file.relative.startsWith(".cursor/rules/"))) {
|
|
189
|
+
const cursorFiles = topFiles.filter((file) => file.startsWith(".cursor/skills/") || file.startsWith(".cursor/rules/")).slice(0, 20);
|
|
190
|
+
sections.cursorGuidance = makeSection(".oris-flow/maps/cursor-guidance.md", "Project-local Cursor skills or rules detected.", {
|
|
191
|
+
scannedAt,
|
|
192
|
+
markdown: [
|
|
193
|
+
"# Cursor Guidance",
|
|
194
|
+
"",
|
|
195
|
+
"Detected local Cursor guidance:",
|
|
196
|
+
...cursorFiles.map((file) => `- ${file}`),
|
|
197
|
+
"",
|
|
198
|
+
].join("\n"),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (stacks.length > 0) {
|
|
203
|
+
sections.stack = makeSection(".oris-flow/maps/stack.md", `Detected ${stacks.length} stack signal(s).`, {
|
|
204
|
+
scannedAt,
|
|
205
|
+
markdown: [
|
|
206
|
+
"# Stack",
|
|
207
|
+
"",
|
|
208
|
+
...stacks.flatMap((stack) => [
|
|
209
|
+
`## ${stack.label}`,
|
|
210
|
+
"",
|
|
211
|
+
...stack.evidence.map((file) => `- ${file}`),
|
|
212
|
+
"",
|
|
213
|
+
]),
|
|
214
|
+
].join("\n"),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (commands.length > 0) {
|
|
219
|
+
sections.commands = makeSection(".oris-flow/maps/commands.md", `Detected ${commands.length} package command(s).`, {
|
|
220
|
+
scannedAt,
|
|
221
|
+
markdown: [
|
|
222
|
+
"# Commands",
|
|
223
|
+
"",
|
|
224
|
+
...commands.map((command) => `- \`${command.command}\` (${command.evidence})`),
|
|
225
|
+
"",
|
|
226
|
+
].join("\n"),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (hasAny(files, (file) => /(^|\/)(src|app|apps|packages)\//.test(file.relative))) {
|
|
231
|
+
const codeRoots = [...new Set(topFiles
|
|
232
|
+
.filter((file) => /(^|\/)(src|app|apps|packages)\//.test(file))
|
|
233
|
+
.map((file) => file.split("/").slice(0, 2).join("/")))]
|
|
234
|
+
.slice(0, 20);
|
|
235
|
+
sections.code = makeSection(".oris-flow/maps/code.md", "Source code areas detected.", {
|
|
236
|
+
scannedAt,
|
|
237
|
+
markdown: [
|
|
238
|
+
"# Code",
|
|
239
|
+
"",
|
|
240
|
+
"Detected source areas:",
|
|
241
|
+
...codeRoots.map((file) => `- ${file}`),
|
|
242
|
+
"",
|
|
243
|
+
].join("\n"),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasAny(files, (file) => /(^|\/)(test|tests|spec|specs|e2e)(\/|$)|\.(test|spec)\.[jt]sx?$/i.test(file.relative))) {
|
|
248
|
+
const tests = topFiles.filter((file) => /(^|\/)(test|tests|spec|specs|e2e)(\/|$)|\.(test|spec)\.[jt]sx?$/i.test(file)).slice(0, 20);
|
|
249
|
+
sections.tests = makeSection(".oris-flow/maps/tests.md", "Test assets detected.", {
|
|
250
|
+
scannedAt,
|
|
251
|
+
markdown: [
|
|
252
|
+
"# Tests",
|
|
253
|
+
"",
|
|
254
|
+
"Detected test paths:",
|
|
255
|
+
...tests.map((file) => `- ${file}`),
|
|
256
|
+
"",
|
|
257
|
+
].join("\n"),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return sections;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function scanRepository(repositoryRoot, options = {}) {
|
|
265
|
+
const root = path.resolve(repositoryRoot);
|
|
266
|
+
const scannedAt = new Date().toISOString();
|
|
267
|
+
const files = walk(root, options);
|
|
268
|
+
const stacks = detectStacks(files);
|
|
269
|
+
const commands = findPackageCommands(files);
|
|
270
|
+
const sections = buildSections(root, files, stacks, commands, scannedAt);
|
|
271
|
+
const sectionPointers = Object.fromEntries(
|
|
272
|
+
Object.entries(sections).map(([key, value]) => [key, value.pointer]),
|
|
273
|
+
);
|
|
274
|
+
const manifest = {
|
|
275
|
+
$schema: ORIS_FLOW_MAP_SCHEMA,
|
|
276
|
+
schemaVersion: ORIS_FLOW_MAP_SCHEMA_VERSION,
|
|
277
|
+
repository: path.basename(root),
|
|
278
|
+
generatedAt: scannedAt,
|
|
279
|
+
updatedAt: scannedAt,
|
|
280
|
+
setup: {
|
|
281
|
+
mode: options.mode ?? "create",
|
|
282
|
+
scanDepth: options.scanDepth ?? "balanced",
|
|
283
|
+
confirmedDecisions: [],
|
|
284
|
+
versionControl: {
|
|
285
|
+
orisFlowMap: options.versionControl ?? "commit",
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
refreshPolicy: {
|
|
289
|
+
mode: "manual",
|
|
290
|
+
},
|
|
291
|
+
stacks,
|
|
292
|
+
commands,
|
|
293
|
+
sections: sectionPointers,
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
repositoryRoot: root,
|
|
297
|
+
scannedAt,
|
|
298
|
+
fileCount: files.length,
|
|
299
|
+
stacks,
|
|
300
|
+
commands,
|
|
301
|
+
sections,
|
|
302
|
+
manifest,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function writeScanResult(scan) {
|
|
307
|
+
const root = scan.repositoryRoot;
|
|
308
|
+
const manifestPath = resolveInsideRepository(root, ORIS_FLOW_MANIFEST, "Oris Flow manifest");
|
|
309
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
310
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(scan.manifest, null, 2)}\n`, "utf8");
|
|
311
|
+
|
|
312
|
+
for (const section of Object.values(scan.sections)) {
|
|
313
|
+
const sectionPath = resolveInsideRepository(root, section.pointer.path, "Oris Flow section");
|
|
314
|
+
fs.mkdirSync(path.dirname(sectionPath), { recursive: true });
|
|
315
|
+
fs.writeFileSync(sectionPath, section.markdown, "utf8");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const readmePath = resolveInsideRepository(root, `${ORIS_FLOW_MAPS_ROOT}/README.md`, "Oris Flow map README");
|
|
319
|
+
if (!fs.existsSync(readmePath)) {
|
|
320
|
+
fs.mkdirSync(path.dirname(readmePath), { recursive: true });
|
|
321
|
+
fs.writeFileSync(readmePath, "# Oris Flow Maps\n\nGenerated by `oris-flow-scan.mjs`. Sections are present only when repository evidence supports them.\n", "utf8");
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
manifestPath: relativeToRepository(root, manifestPath),
|
|
325
|
+
sectionPaths: Object.values(scan.sections).map((section) => section.pointer.path),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
330
|
+
const options = parseArgs(process.argv.slice(2));
|
|
331
|
+
const scan = scanRepository(options.repositoryRoot, options);
|
|
332
|
+
const written = options.write ? writeScanResult(scan) : null;
|
|
333
|
+
const payload = {
|
|
334
|
+
repositoryRoot: toPosixPath(scan.repositoryRoot),
|
|
335
|
+
fileCount: scan.fileCount,
|
|
336
|
+
stacks: scan.stacks,
|
|
337
|
+
commands: scan.commands,
|
|
338
|
+
sections: scan.manifest.sections,
|
|
339
|
+
written,
|
|
340
|
+
};
|
|
341
|
+
if (options.json || options.write) {
|
|
342
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
343
|
+
} else {
|
|
344
|
+
console.log(`Oris Flow scan: ${payload.repositoryRoot}`);
|
|
345
|
+
console.log(`Files scanned: ${payload.fileCount}`);
|
|
346
|
+
console.log(`Stacks: ${payload.stacks.map((stack) => stack.label).join(", ") || "none"}`);
|
|
347
|
+
console.log(`Sections: ${Object.keys(payload.sections).join(", ") || "none"}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|