@yansirplus/cli 0.5.17

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.
Files changed (47) hide show
  1. package/PUBLIC_API.md +22 -0
  2. package/README.md +34 -0
  3. package/dist/build/agent-authoring/config.d.ts +177 -0
  4. package/dist/build/agent-authoring/config.js +607 -0
  5. package/dist/build/agent-authoring/manifest-compiler.d.ts +159 -0
  6. package/dist/build/agent-authoring/manifest-compiler.js +737 -0
  7. package/dist/build/agent-authoring/shared.d.ts +10 -0
  8. package/dist/build/agent-authoring/shared.js +57 -0
  9. package/dist/build/agent-authoring/static-target.d.ts +59 -0
  10. package/dist/build/agent-authoring/static-target.js +1857 -0
  11. package/dist/build/agent-authoring.d.ts +9 -0
  12. package/dist/build/agent-authoring.js +5 -0
  13. package/dist/build/build-cli.d.ts +2 -0
  14. package/dist/build/build-cli.js +264 -0
  15. package/dist/check/algorithmic/architecture-checks.mjs +971 -0
  16. package/dist/check/algorithmic/client-boundary-checks.mjs +337 -0
  17. package/dist/check/algorithmic/convergence-smoke-checks.mjs +608 -0
  18. package/dist/check/algorithmic/distribution-checks.mjs +919 -0
  19. package/dist/check/algorithmic/owner-checks.mjs +647 -0
  20. package/dist/check/algorithmic/package-boundary-checks.mjs +985 -0
  21. package/dist/check/algorithmic/projection-boundary-checks.mjs +302 -0
  22. package/dist/check/algorithmic/repo-surface-checks.mjs +267 -0
  23. package/dist/check/algorithmic/runtime-structural-checks.mjs +264 -0
  24. package/dist/check/algorithmic/source-alias-checks.mjs +106 -0
  25. package/dist/check/algorithmic/static-target-checks.mjs +447 -0
  26. package/dist/check/algorithmic-checks.mjs +482 -0
  27. package/dist/check/check-coverage.mjs +231 -0
  28. package/dist/check/command-runner.mjs +22 -0
  29. package/dist/check/default-gate.mjs +51 -0
  30. package/dist/check/gate-selector.mjs +305 -0
  31. package/dist/check/manifest-rules.mjs +223 -0
  32. package/dist/check/package-graph.mjs +464 -0
  33. package/dist/generate/generate-agent-docs.mjs +435 -0
  34. package/dist/generate/generate-carrier-reference.mjs +514 -0
  35. package/dist/generate/generate-docs.mjs +345 -0
  36. package/dist/generate/generate-effect-skill-manifests.mjs +193 -0
  37. package/dist/generate/project-docs-site.mjs +190 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.js +25 -0
  40. package/dist/lib/agent-docs-model.mjs +888 -0
  41. package/dist/lib/boundary-rules.mjs +63 -0
  42. package/dist/lib/capability-routes.mjs +354 -0
  43. package/dist/lib/projection-sink.mjs +113 -0
  44. package/dist/lib/public-api-model.mjs +306 -0
  45. package/dist/main.mjs +233 -0
  46. package/dist/runner.mjs +127 -0
  47. package/package.json +32 -0
@@ -0,0 +1,306 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+
5
+ export const apiSourceMode = (pkg) => pkg.apiSourceMode ?? "manual";
6
+
7
+ export const sourceTsdocModes = new Set(["source-tsdoc"]);
8
+
9
+ const statusTags = new Set(["public", "experimental", "internal", "deprecated"]);
10
+
11
+ const hasExportModifier = (node) =>
12
+ node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
13
+
14
+ const hasDefaultModifier = (node) =>
15
+ node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true;
16
+
17
+ const nameFromDeclaration = (name) => (ts.isIdentifier(name) ? name.text : null);
18
+
19
+ const resolveRelativeModule = (fromFile, specifier) => {
20
+ const base = path.resolve(path.dirname(fromFile), specifier);
21
+ const candidates = [
22
+ base,
23
+ `${base}.ts`,
24
+ `${base}.tsx`,
25
+ path.join(base, "index.ts"),
26
+ path.join(base, "index.tsx"),
27
+ ];
28
+ for (const candidate of candidates) {
29
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
30
+ }
31
+ throw new Error(`cannot resolve export module ${specifier} from ${fromFile}`);
32
+ };
33
+
34
+ export const exportedNamesFromAst = (file, seen) => {
35
+ const source = fs.readFileSync(file, "utf8");
36
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
37
+ const names = new Set();
38
+
39
+ for (const statement of sourceFile.statements) {
40
+ if (ts.isExportDeclaration(statement)) {
41
+ const specifier =
42
+ statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
43
+ ? statement.moduleSpecifier.text
44
+ : null;
45
+
46
+ if (statement.exportClause === undefined) {
47
+ if (specifier !== null && specifier.startsWith(".")) {
48
+ const target = resolveRelativeModule(file, specifier);
49
+ for (const name of exportedNamesFromSource(target, seen)) names.add(name);
50
+ }
51
+ continue;
52
+ }
53
+
54
+ if (ts.isNamespaceExport(statement.exportClause)) {
55
+ names.add(statement.exportClause.name.text);
56
+ continue;
57
+ }
58
+
59
+ for (const element of statement.exportClause.elements) {
60
+ names.add(element.name.text);
61
+ }
62
+ continue;
63
+ }
64
+
65
+ if (ts.isExportAssignment(statement)) {
66
+ names.add(statement.isExportEquals === true ? "export=" : "default");
67
+ continue;
68
+ }
69
+
70
+ if (!hasExportModifier(statement)) continue;
71
+
72
+ if (hasDefaultModifier(statement)) {
73
+ names.add("default");
74
+ continue;
75
+ }
76
+
77
+ if (
78
+ ts.isInterfaceDeclaration(statement) ||
79
+ ts.isTypeAliasDeclaration(statement) ||
80
+ ts.isClassDeclaration(statement) ||
81
+ ts.isFunctionDeclaration(statement) ||
82
+ ts.isEnumDeclaration(statement)
83
+ ) {
84
+ const name = statement.name === undefined ? null : nameFromDeclaration(statement.name);
85
+ if (name !== null) names.add(name);
86
+ continue;
87
+ }
88
+
89
+ if (ts.isVariableStatement(statement)) {
90
+ for (const declaration of statement.declarationList.declarations) {
91
+ const name = nameFromDeclaration(declaration.name);
92
+ if (name !== null) names.add(name);
93
+ }
94
+ }
95
+ }
96
+
97
+ return names;
98
+ };
99
+
100
+ export const exportedNamesFromSource = (file, seen = new Set()) => {
101
+ const abs = path.resolve(file);
102
+ if (seen.has(abs)) return new Set();
103
+ seen.add(abs);
104
+ return exportedNamesFromAst(abs, seen);
105
+ };
106
+
107
+ const plainComment = (comment) => {
108
+ if (comment === undefined) return "";
109
+ if (typeof comment === "string") return comment;
110
+ if (Array.isArray(comment)) return comment.map((part) => part.text).join("");
111
+ return String(comment);
112
+ };
113
+
114
+ const tsdocForNode = (node, sourceFile) => {
115
+ const docs = node.jsDoc ?? [];
116
+ const doc = docs.at(-1);
117
+ if (doc === undefined) {
118
+ return { summary: "", tags: [] };
119
+ }
120
+
121
+ const tags = [];
122
+ for (const tag of doc.tags ?? []) {
123
+ tags.push({
124
+ name: tag.tagName.getText(sourceFile),
125
+ text: plainComment(tag.comment).trim(),
126
+ });
127
+ }
128
+
129
+ return {
130
+ summary: plainComment(doc.comment).replace(/\s+/gu, " ").trim(),
131
+ tags,
132
+ };
133
+ };
134
+
135
+ const sourceExportRecordsFromAst = (file, entrypoint, seen) => {
136
+ const abs = path.resolve(file);
137
+ if (seen.has(abs)) return [];
138
+ seen.add(abs);
139
+
140
+ const source = fs.readFileSync(abs, "utf8");
141
+ const sourceFile = ts.createSourceFile(abs, source, ts.ScriptTarget.Latest, true);
142
+ const records = [];
143
+
144
+ const pushRecord = (name, node) => {
145
+ const tsdoc = tsdocForNode(node, sourceFile);
146
+ const matchingTags = tsdoc.tags.filter((tag) => statusTags.has(tag.name));
147
+ const status = matchingTags.length === 1 ? matchingTags[0] : null;
148
+ records.push({
149
+ entrypoint,
150
+ name,
151
+ key: `${entrypoint}:${name}`,
152
+ file: abs,
153
+ summary: tsdoc.summary,
154
+ tags: tsdoc.tags,
155
+ status,
156
+ statusTags: matchingTags,
157
+ });
158
+ };
159
+
160
+ for (const statement of sourceFile.statements) {
161
+ if (ts.isExportDeclaration(statement)) {
162
+ const specifier =
163
+ statement.moduleSpecifier !== undefined && ts.isStringLiteral(statement.moduleSpecifier)
164
+ ? statement.moduleSpecifier.text
165
+ : null;
166
+ if (statement.exportClause === undefined && specifier !== null && specifier.startsWith(".")) {
167
+ records.push(
168
+ ...sourceExportRecordsFromAst(resolveRelativeModule(abs, specifier), entrypoint, seen),
169
+ );
170
+ }
171
+ continue;
172
+ }
173
+
174
+ if (ts.isExportAssignment(statement)) {
175
+ pushRecord(statement.isExportEquals === true ? "export=" : "default", statement);
176
+ continue;
177
+ }
178
+
179
+ if (!hasExportModifier(statement)) continue;
180
+
181
+ if (hasDefaultModifier(statement)) {
182
+ pushRecord("default", statement);
183
+ continue;
184
+ }
185
+
186
+ if (
187
+ ts.isInterfaceDeclaration(statement) ||
188
+ ts.isTypeAliasDeclaration(statement) ||
189
+ ts.isClassDeclaration(statement) ||
190
+ ts.isFunctionDeclaration(statement) ||
191
+ ts.isEnumDeclaration(statement)
192
+ ) {
193
+ const name = statement.name === undefined ? null : nameFromDeclaration(statement.name);
194
+ if (name !== null) pushRecord(name, statement);
195
+ continue;
196
+ }
197
+
198
+ if (ts.isVariableStatement(statement)) {
199
+ for (const declaration of statement.declarationList.declarations) {
200
+ const name = nameFromDeclaration(declaration.name);
201
+ if (name !== null) pushRecord(name, statement);
202
+ }
203
+ }
204
+ }
205
+
206
+ return records;
207
+ };
208
+
209
+ const packageEntrypoints = (root, pkg) => {
210
+ const pkgDir = path.join(root, pkg.path);
211
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
212
+ return Object.entries(manifest.exports ?? {})
213
+ .map(([entrypoint, exportSpec]) => {
214
+ const source = exportSpec?.default ?? exportSpec;
215
+ return typeof source === "string" && source.startsWith("./")
216
+ ? { entrypoint, file: path.join(pkgDir, source) }
217
+ : null;
218
+ })
219
+ .filter((entry) => entry !== null);
220
+ };
221
+
222
+ export const exportedNamesForPackage = (root, pkg) =>
223
+ packageEntrypoints(root, pkg).flatMap(({ entrypoint, file }) =>
224
+ [...exportedNamesFromSource(file)].map((name) => ({
225
+ entrypoint,
226
+ name: String(name),
227
+ key: `${entrypoint}:${String(name)}`,
228
+ })),
229
+ );
230
+
231
+ export const sourceTsdocRecordsForPackage = (root, pkg) =>
232
+ packageEntrypoints(root, pkg)
233
+ .flatMap(({ entrypoint, file }) => sourceExportRecordsFromAst(file, entrypoint, new Set()))
234
+ .sort((left, right) => left.key.localeCompare(right.key));
235
+
236
+ export const validateSourceTsdocRecords = (pkg, records) => {
237
+ const failures = [];
238
+ const seen = new Set();
239
+
240
+ for (const record of records) {
241
+ if (seen.has(record.key)) {
242
+ failures.push(`${pkg.name}: duplicate exported symbol record ${record.key}`);
243
+ continue;
244
+ }
245
+ seen.add(record.key);
246
+
247
+ if (record.summary.length === 0) {
248
+ failures.push(`${pkg.name}: ${record.key} is missing a TSDoc summary`);
249
+ }
250
+ if (record.statusTags.length !== 1) {
251
+ failures.push(
252
+ `${pkg.name}: ${record.key} must have exactly one API status tag (@public, @experimental, @internal, or @deprecated)`,
253
+ );
254
+ continue;
255
+ }
256
+ if (record.status?.name === "internal") {
257
+ failures.push(`${pkg.name}: ${record.key} is tagged @internal but exported`);
258
+ }
259
+ if (record.status?.name === "deprecated" && record.status.text.length === 0) {
260
+ failures.push(`${pkg.name}: ${record.key} has @deprecated without a reason`);
261
+ }
262
+ }
263
+
264
+ return failures;
265
+ };
266
+
267
+ const listSection = (records) =>
268
+ records.length === 0
269
+ ? "None."
270
+ : records.map((record) => `- \`${record.key}\` - ${record.summary}`).join("\n");
271
+
272
+ export const sourceTsdocApiMarkdown = (pkg, records) => {
273
+ const byStatus = (status) => records.filter((record) => record.status?.name === status);
274
+ const deprecated = byStatus("deprecated");
275
+ const deprecatedSection =
276
+ deprecated.length === 0
277
+ ? "None."
278
+ : deprecated
279
+ .map(
280
+ (record) => `- \`${record.key}\` - ${record.summary} Deprecated: ${record.status.text}`,
281
+ )
282
+ .join("\n");
283
+
284
+ return [
285
+ `# ${pkg.name} Public API Intent`,
286
+ "",
287
+ "<!-- generated by packages/cli/src/generate/generate-docs.mjs; edit exported TSDoc in package source -->",
288
+ "",
289
+ "## Public exports",
290
+ "",
291
+ listSection(byStatus("public")),
292
+ "",
293
+ "## Experimental exports",
294
+ "",
295
+ listSection(byStatus("experimental")),
296
+ "",
297
+ "## Deprecated exports",
298
+ "",
299
+ deprecatedSection,
300
+ "",
301
+ "## Internal-only exports",
302
+ "",
303
+ "Any package file or symbol not listed above.",
304
+ "",
305
+ ].join("\n");
306
+ };
package/dist/main.mjs ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import {
6
+ algorithmicCheckerAcceptsArgs,
7
+ hasAlgorithmicChecker,
8
+ runAlgorithmicChecker,
9
+ } from "./check/algorithmic-checks.mjs";
10
+ import {
11
+ deriveAffectedGates,
12
+ printAffectedGates,
13
+ runAffectedGates,
14
+ } from "./check/gate-selector.mjs";
15
+ import { runDefaultGate } from "./check/default-gate.mjs";
16
+ import { listGuards, runGroup, runGuard } from "./runner.mjs";
17
+
18
+ const version = "0.5.16";
19
+
20
+ const helpText = `agentOS repository CLI ${version}
21
+
22
+ Usage:
23
+ agentos --help
24
+ agentos --version
25
+ agentos build [--cwd <path>] [--config <path>] [--package-scope <scope>]
26
+ agentos check all
27
+ agentos check default
28
+ agentos check structural
29
+ agentos check affected [--base <ref>] [--head <ref>] [--json] [--explain] [--run]
30
+ agentos check docs
31
+ agentos check effect-manifests
32
+ agentos check release
33
+ agentos check site
34
+ agentos check guard-coverage
35
+ agentos check <algorithmic-check-id>
36
+ agentos check guard <rule-id>
37
+ agentos check guards
38
+ agentos generate docs
39
+ agentos generate effect-manifests
40
+ agentos generate site
41
+ agentos generate site --watch
42
+ `;
43
+
44
+ const printHelp = () => {
45
+ process.stdout.write(helpText);
46
+ };
47
+
48
+ const fail = (message) => {
49
+ process.stderr.write(`${message}\n\n`);
50
+ if (
51
+ message.startsWith("agentos:") ||
52
+ message.startsWith("agentos build:") ||
53
+ message.startsWith("agentos check:") ||
54
+ message.startsWith("agentos generate:")
55
+ ) {
56
+ printHelp();
57
+ }
58
+ process.exitCode = 1;
59
+ };
60
+
61
+ const expectNoExtraArgs = (args, command) => {
62
+ if (args.length > 0) {
63
+ throw new Error(`${command}: unexpected argument ${args.join(" ")}`);
64
+ }
65
+ };
66
+
67
+ const runBuild = async (args) => {
68
+ const runner = fileURLToPath(new URL("./build/build-cli.js", import.meta.url));
69
+ await new Promise((resolve, reject) => {
70
+ const child = spawn("bun", [runner, "build", ...args], { stdio: "inherit" });
71
+ child.on("error", (error) => {
72
+ reject(new Error(`agentos build: failed to start bun: ${error.message}`));
73
+ });
74
+ child.on("exit", (code, signal) => {
75
+ if (signal !== null) {
76
+ reject(new Error(`agentos build: build runner terminated by ${signal}`));
77
+ return;
78
+ }
79
+ if (code !== 0) {
80
+ reject(new Error(`agentos build: build runner failed with exit code ${code ?? 1}`));
81
+ return;
82
+ }
83
+ resolve();
84
+ });
85
+ });
86
+ };
87
+
88
+ const runCheck = async (args) => {
89
+ const [command, ...rest] = args;
90
+ switch (command) {
91
+ case "all":
92
+ expectNoExtraArgs(rest, "agentos check all");
93
+ await runGroup("all");
94
+ return;
95
+ case "default":
96
+ expectNoExtraArgs(rest, "agentos check default");
97
+ await runDefaultGate();
98
+ return;
99
+ case "structural":
100
+ expectNoExtraArgs(rest, "agentos check structural");
101
+ await runGroup("all");
102
+ return;
103
+ case "affected": {
104
+ let base;
105
+ let head;
106
+ let json = false;
107
+ let run = false;
108
+ for (let index = 0; index < rest.length; index += 1) {
109
+ const arg = rest[index];
110
+ if (arg === "--base") {
111
+ base = rest[index + 1];
112
+ if (base === undefined) throw new Error("agentos check affected: --base requires a ref");
113
+ index += 1;
114
+ } else if (arg === "--head") {
115
+ head = rest[index + 1];
116
+ if (head === undefined) throw new Error("agentos check affected: --head requires a ref");
117
+ index += 1;
118
+ } else if (arg === "--json") {
119
+ json = true;
120
+ } else if (arg === "--explain") {
121
+ json = false;
122
+ } else if (arg === "--run") {
123
+ run = true;
124
+ } else {
125
+ throw new Error(`agentos check affected: unexpected argument ${arg}`);
126
+ }
127
+ }
128
+ const result = deriveAffectedGates({ base, head });
129
+ printAffectedGates(result, { json });
130
+ if (run) runAffectedGates(result);
131
+ return;
132
+ }
133
+ case "docs":
134
+ expectNoExtraArgs(rest, "agentos check docs");
135
+ await runGroup("check-docs");
136
+ return;
137
+ case "effect-manifests":
138
+ expectNoExtraArgs(rest, "agentos check effect-manifests");
139
+ await runGroup("check-effect-manifests");
140
+ return;
141
+ case "release":
142
+ expectNoExtraArgs(rest, "agentos check release");
143
+ await runGroup("release");
144
+ return;
145
+ case "site":
146
+ expectNoExtraArgs(rest, "agentos check site");
147
+ await runGroup("check-site");
148
+ return;
149
+ case "guard-coverage":
150
+ expectNoExtraArgs(rest, "agentos check guard-coverage");
151
+ await runGroup("guard-coverage");
152
+ return;
153
+ case "guard": {
154
+ const [ruleId, ...extra] = rest;
155
+ if (ruleId === undefined) throw new Error("agentos check guard: missing <rule-id>");
156
+ expectNoExtraArgs(extra, "agentos check guard");
157
+ await runGuard(ruleId);
158
+ return;
159
+ }
160
+ case "guards":
161
+ expectNoExtraArgs(rest, "agentos check guards");
162
+ for (const id of listGuards()) console.log(id);
163
+ return;
164
+ default:
165
+ if (command !== undefined && hasAlgorithmicChecker(command)) {
166
+ if (!algorithmicCheckerAcceptsArgs(command)) {
167
+ expectNoExtraArgs(rest, `agentos check ${command}`);
168
+ }
169
+ await runAlgorithmicChecker(command, rest);
170
+ return;
171
+ }
172
+ throw new Error(
173
+ "agentos check: choose one of all, default, structural, affected, docs, effect-manifests, release, site, guard-coverage, guard, guards, or an algorithmic checker id",
174
+ );
175
+ }
176
+ };
177
+
178
+ const runGenerate = async (args) => {
179
+ const [command, ...rest] = args;
180
+ switch (command) {
181
+ case "docs":
182
+ expectNoExtraArgs(rest, "agentos generate docs");
183
+ await runGroup("generate-docs");
184
+ return;
185
+ case "effect-manifests":
186
+ expectNoExtraArgs(rest, "agentos generate effect-manifests");
187
+ await runGroup("generate-effect-manifests");
188
+ return;
189
+ case "site":
190
+ if (rest[0] === "--watch") {
191
+ expectNoExtraArgs(rest.slice(1), "agentos generate site --watch");
192
+ await runGroup("generate-site-watch");
193
+ } else {
194
+ expectNoExtraArgs(rest, "agentos generate site");
195
+ await runGroup("generate-site");
196
+ }
197
+ return;
198
+ default:
199
+ throw new Error("agentos generate: choose one of docs, effect-manifests, site");
200
+ }
201
+ };
202
+
203
+ const main = async () => {
204
+ const args = process.argv.slice(2);
205
+ const [command, ...rest] = args;
206
+ if (command === undefined || command === "--help" || command === "-h") {
207
+ printHelp();
208
+ return;
209
+ }
210
+ if (command === "--version" || command === "-v") {
211
+ console.log(version);
212
+ return;
213
+ }
214
+ switch (command) {
215
+ case "build":
216
+ await runBuild(rest);
217
+ return;
218
+ case "check":
219
+ await runCheck(rest);
220
+ return;
221
+ case "generate":
222
+ await runGenerate(rest);
223
+ return;
224
+ default:
225
+ throw new Error("agentos: choose one of build, check, generate");
226
+ }
227
+ };
228
+
229
+ try {
230
+ await main();
231
+ } catch (error) {
232
+ fail(error instanceof Error ? error.message : String(error));
233
+ }
@@ -0,0 +1,127 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { runCommand } from "./check/command-runner.mjs";
5
+ import { runRuleAcceptance, validateRuleAcceptance } from "./check/manifest-rules.mjs";
6
+
7
+ export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
8
+
9
+ const manifestPath = path.join(repoRoot, "docs/agent/boundary-rules.source.json");
10
+
11
+ export const loadBoundaryRules = () => {
12
+ const value = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
13
+ validateBoundaryRules(value);
14
+ return value;
15
+ };
16
+
17
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
18
+
19
+ const requiredRuleFields = [
20
+ "id",
21
+ "owner",
22
+ "invariantId",
23
+ "kind",
24
+ "acceptance",
25
+ "diagnostics",
26
+ "paths",
27
+ "commandGroup",
28
+ ];
29
+
30
+ const validateBoundaryRules = (value) => {
31
+ const failures = [];
32
+ if (!isRecord(value)) failures.push("boundary rules source must be an object");
33
+ if (value.schemaVersion !== 2) failures.push("boundary rules schemaVersion must be 2");
34
+ if (!isRecord(value.commandGroups)) failures.push("boundary rules must define commandGroups");
35
+ if (!Array.isArray(value.rules)) failures.push("boundary rules must define rules[]");
36
+
37
+ const commandGroups = isRecord(value.commandGroups) ? value.commandGroups : {};
38
+ for (const [groupId, steps] of Object.entries(commandGroups)) {
39
+ if (!Array.isArray(steps)) {
40
+ failures.push(`commandGroups.${groupId} must be an array`);
41
+ continue;
42
+ }
43
+ for (const [index, step] of steps.entries()) {
44
+ if (!isRecord(step)) {
45
+ failures.push(`commandGroups.${groupId}[${index}] must be an object`);
46
+ continue;
47
+ }
48
+ if (!["command", "group", "rule"].includes(step.type)) {
49
+ failures.push(`commandGroups.${groupId}[${index}] has invalid type`);
50
+ }
51
+ if (step.type === "command" && typeof step.command !== "string") {
52
+ failures.push(`commandGroups.${groupId}[${index}] command must be a string`);
53
+ }
54
+ if ((step.type === "group" || step.type === "rule") && typeof step.id !== "string") {
55
+ failures.push(`commandGroups.${groupId}[${index}] id must be a string`);
56
+ }
57
+ }
58
+ }
59
+
60
+ const seenRules = new Set();
61
+ for (const [index, rule] of (Array.isArray(value.rules) ? value.rules : []).entries()) {
62
+ const label = isRecord(rule) && typeof rule.id === "string" ? rule.id : `rules[${index}]`;
63
+ if (!isRecord(rule)) {
64
+ failures.push(`${label} must be an object`);
65
+ continue;
66
+ }
67
+ for (const field of requiredRuleFields) {
68
+ if (!(field in rule)) failures.push(`${label} missing ${field}`);
69
+ }
70
+ if (typeof rule.id !== "string" || rule.id.length === 0) {
71
+ failures.push(`${label} id must be non-empty`);
72
+ } else if (seenRules.has(rule.id)) {
73
+ failures.push(`duplicate boundary rule ${rule.id}`);
74
+ } else {
75
+ seenRules.add(rule.id);
76
+ }
77
+ validateRuleAcceptance(rule, failures);
78
+ if (!Array.isArray(rule.diagnostics)) failures.push(`${label} diagnostics must be an array`);
79
+ if (!Array.isArray(rule.paths)) failures.push(`${label} paths must be an array`);
80
+ if (typeof rule.commandGroup !== "string" || !(rule.commandGroup in commandGroups)) {
81
+ failures.push(`${label} commandGroup must reference commandGroups`);
82
+ }
83
+ }
84
+
85
+ if (failures.length > 0) {
86
+ throw new Error(failures.join("\n"));
87
+ }
88
+ };
89
+
90
+ const ruleById = (manifest, id) => manifest.rules.find((rule) => rule.id === id);
91
+
92
+ const runSteps = async (manifest, steps, stack = []) => {
93
+ for (const step of steps) {
94
+ if (step.type === "command") {
95
+ runCommand(step.command, { cwd: repoRoot });
96
+ continue;
97
+ }
98
+ if (step.type === "group") {
99
+ if (stack.includes(step.id)) {
100
+ throw new Error(`cyclic command group: ${[...stack, step.id].join(" -> ")}`);
101
+ }
102
+ const group = manifest.commandGroups[step.id];
103
+ if (group === undefined) throw new Error(`unknown command group ${step.id}`);
104
+ await runSteps(manifest, group, [...stack, step.id]);
105
+ continue;
106
+ }
107
+ if (step.type === "rule") {
108
+ await runGuard(step.id, manifest);
109
+ continue;
110
+ }
111
+ }
112
+ };
113
+
114
+ export const runGroup = async (id, manifest = loadBoundaryRules()) => {
115
+ const steps = manifest.commandGroups[id];
116
+ if (steps === undefined) throw new Error(`unknown command group ${id}`);
117
+ await runSteps(manifest, steps, [id]);
118
+ };
119
+
120
+ export const runGuard = async (id, manifest = loadBoundaryRules()) => {
121
+ const rule = ruleById(manifest, id);
122
+ if (rule === undefined) throw new Error(`unknown boundary rule ${id}`);
123
+ await runRuleAcceptance(rule);
124
+ };
125
+
126
+ export const listGuards = (manifest = loadBoundaryRules()) =>
127
+ manifest.rules.map((rule) => rule.id).sort((left, right) => left.localeCompare(right));
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@yansirplus/cli",
3
+ "version": "0.5.17",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "bin": {
12
+ "agentos": "./dist/main.mjs"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "PUBLIC_API.md"
24
+ ],
25
+ "dependencies": {
26
+ "@yansirplus/core": "0.5.17",
27
+ "@yansirplus/runtime": "0.5.17"
28
+ },
29
+ "peerDependencies": {
30
+ "effect": "^4.0.0-beta.84"
31
+ }
32
+ }