@howells/boundaries 0.1.1 → 0.1.2

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/AGENTS.md ADDED
@@ -0,0 +1,46 @@
1
+ # @howells/boundaries
2
+
3
+ Package-level boundary conventions for Turborepo workspaces. The npm package is `@howells/boundaries`; the executable is `boundaries`.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ npm test # Run unit and CLI contract tests
9
+ npm run validate:skill # Validate bundled Codex skill metadata
10
+ npm run smoke:install # Pack, install in a temp project, run the bin
11
+ npm pack --dry-run # Inspect publish contents
12
+ node src/cli.js --schema # Print machine-readable command schema
13
+ node src/cli.js init --dry-run # Preview generated Turbo boundary files
14
+ ```
15
+
16
+ ## Key Files
17
+
18
+ - `src/cli.js`: command parsing and output formatting.
19
+ - `src/init.js`: workspace discovery and config generation.
20
+ - `src/check.js`: local config validation and `turbo boundaries` delegation.
21
+ - `src/core.js`: tag inference and dependency rule evaluation.
22
+ - `src/schema.js`: machine-readable CLI schema.
23
+ - `skills/howells-boundaries/SKILL.md`: bundled Codex skill.
24
+
25
+ ## Conventions
26
+
27
+ - Keep the tool package-level. Do not add package-internal layer enforcement here.
28
+ - Prefer Turbo `boundaries` as the backend; this package is a convention layer.
29
+ - CLI commands must stay non-interactive.
30
+ - Any new command output should support `--json` when an agent may consume it.
31
+ - Mutating commands should support `--dry-run` before they write files.
32
+
33
+ ## Permissions
34
+
35
+ Always:
36
+ - Read and edit files in this package.
37
+ - Run `npm test`, `npm run validate:skill`, and `npm pack --dry-run`.
38
+
39
+ Ask first:
40
+ - Publish a new npm version.
41
+ - Change the default boundary policy.
42
+ - Add runtime dependencies.
43
+
44
+ Never:
45
+ - Weaken generated boundary rules just to silence violations.
46
+ - Edit user repos outside this package unless explicitly asked.
package/README.md CHANGED
@@ -12,6 +12,13 @@ pnpm add -D @howells/boundaries
12
12
 
13
13
  ## Use
14
14
 
15
+ Try without installing:
16
+
17
+ ```sh
18
+ npx @howells/boundaries --help
19
+ npx @howells/boundaries init --dry-run
20
+ ```
21
+
15
22
  Initialize boundary config:
16
23
 
17
24
  ```sh
@@ -43,6 +50,14 @@ pnpm exec boundaries explain apps/web packages/ui
43
50
  pnpm exec boundaries explain apps/web apps/admin
44
51
  ```
45
52
 
53
+ Machine-readable output:
54
+
55
+ ```sh
56
+ pnpm exec boundaries --schema
57
+ pnpm exec boundaries init --dry-run --json
58
+ pnpm exec boundaries explain apps/web packages/ui --json
59
+ ```
60
+
46
61
  ## Default Policy
47
62
 
48
63
  The default tags are:
package/package.json CHANGED
@@ -1,19 +1,39 @@
1
1
  {
2
2
  "name": "@howells/boundaries",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Opinionated Turborepo package boundary conventions.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "boundaries": "src/cli.js"
8
8
  },
9
9
  "license": "MIT",
10
+ "keywords": [
11
+ "turborepo",
12
+ "monorepo",
13
+ "boundaries",
14
+ "architecture",
15
+ "cli"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/howells/boundaries.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/howells/boundaries/issues"
23
+ },
24
+ "homepage": "https://www.npmjs.com/package/@howells/boundaries",
10
25
  "files": [
26
+ "AGENTS.md",
11
27
  "README.md",
28
+ "scripts",
12
29
  "src",
13
30
  "skills"
14
31
  ],
15
32
  "scripts": {
16
- "test": "node --test"
33
+ "prepack": "npm test && npm run validate:skill",
34
+ "smoke:install": "node scripts/smoke-install.mjs",
35
+ "test": "node --test",
36
+ "validate:skill": "node scripts/validate-skill.mjs"
17
37
  },
18
38
  "engines": {
19
39
  "node": ">=20"
@@ -0,0 +1,32 @@
1
+ import { execFile } from "node:child_process";
2
+ import { mkdtemp, unlink } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const root = new URL("..", import.meta.url);
9
+ const temp = await mkdtemp(join(tmpdir(), "boundaries-smoke-"));
10
+
11
+ const { stdout: packOutput } = await execFileAsync("npm", ["pack", "--pack-destination", temp], {
12
+ cwd: root,
13
+ });
14
+ const filename = packOutput
15
+ .split("\n")
16
+ .map((line) => line.trim())
17
+ .find((line) => line.endsWith(".tgz"));
18
+ if (!filename) {
19
+ throw new Error(`Could not find packed tarball in npm pack output:\n${packOutput}`);
20
+ }
21
+ const tarball = join(temp, filename);
22
+
23
+ await execFileAsync("npm", ["init", "-y"], { cwd: temp });
24
+ await execFileAsync("npm", ["install", tarball, "--ignore-scripts"], { cwd: temp });
25
+
26
+ const { stdout } = await execFileAsync("npx", ["boundaries", "--help"], { cwd: temp });
27
+ if (!stdout.includes("Usage: boundaries <command>")) {
28
+ throw new Error("Installed boundaries binary did not print expected help.");
29
+ }
30
+
31
+ process.stdout.write(`Smoke install passed in ${temp}\n`);
32
+ await unlink(tarball);
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ const skillPath = new URL("../skills/howells-boundaries/SKILL.md", import.meta.url);
4
+ const skill = await readFile(skillPath, "utf8");
5
+
6
+ if (!skill.startsWith("---\n")) {
7
+ fail("SKILL.md must start with YAML frontmatter.");
8
+ }
9
+
10
+ const end = skill.indexOf("\n---\n", 4);
11
+ if (end === -1) {
12
+ fail("SKILL.md frontmatter is not closed.");
13
+ }
14
+
15
+ const frontmatter = skill.slice(4, end);
16
+ for (const field of ["name:", "description:"]) {
17
+ if (!frontmatter.includes(field)) {
18
+ fail(`SKILL.md frontmatter is missing ${field}`);
19
+ }
20
+ }
21
+
22
+ if (!skill.includes("boundaries init") || !skill.includes("boundaries check")) {
23
+ fail("SKILL.md must document init and check workflows.");
24
+ }
25
+
26
+ process.stdout.write("Skill is valid.\n");
27
+
28
+ function fail(message) {
29
+ process.stderr.write(`${message}\n`);
30
+ process.exit(1);
31
+ }
package/src/check.js CHANGED
@@ -9,6 +9,7 @@ export async function checkRepository({
9
9
  runTurbo = true,
10
10
  stdout = process.stdout,
11
11
  stderr = process.stderr,
12
+ quiet = false,
12
13
  } = {}) {
13
14
  const rootPackageJson = await readJson(join(root, "package.json"));
14
15
  const rootTurboJson = await readJson(join(root, "turbo.json"), {});
@@ -16,22 +17,29 @@ export async function checkRepository({
16
17
  const errors = await validateBoundarySetup(root, rootTurboJson, workspaces);
17
18
 
18
19
  if (errors.length > 0) {
19
- for (const error of errors) {
20
- stderr.write(`boundaries: ${error}\n`);
20
+ if (!quiet) {
21
+ for (const error of errors) {
22
+ stderr.write(`boundaries: ${error.message}\n`);
23
+ }
21
24
  }
22
25
  return { ok: false, exitCode: 1, errors };
23
26
  }
24
27
 
25
28
  if (!runTurbo) {
26
- stdout.write(`Boundary configuration is valid for ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"}.\n`);
27
- return { ok: true, exitCode: 0, errors: [] };
29
+ if (!quiet) {
30
+ stdout.write(`Boundary configuration is valid for ${workspaces.length} workspace${workspaces.length === 1 ? "" : "s"}.\n`);
31
+ }
32
+ return { ok: true, exitCode: 0, errors: [], workspaces };
28
33
  }
29
34
 
30
- const turboExitCode = await runTurboBoundaries({ root });
35
+ const turboResult = await runTurboBoundaries({ root, quiet });
36
+ const turboExitCode = turboResult.exitCode;
31
37
  return {
32
38
  ok: turboExitCode === 0,
33
39
  exitCode: turboExitCode,
34
- errors: [],
40
+ errors: turboExitCode === 0 ? [] : [turboResult.error],
41
+ turbo: turboResult,
42
+ workspaces,
35
43
  };
36
44
  }
37
45
 
@@ -39,36 +47,78 @@ async function validateBoundarySetup(root, rootTurboJson, workspaces) {
39
47
  const errors = [];
40
48
 
41
49
  if (!rootTurboJson.boundaries?.tags) {
42
- errors.push("root turbo.json is missing boundaries.tags; run `boundaries init`.");
50
+ errors.push({
51
+ code: "ROOT_BOUNDARIES_MISSING",
52
+ message: "root turbo.json is missing boundaries.tags; run `boundaries init`.",
53
+ file: "turbo.json",
54
+ is_retriable: false,
55
+ suggestions: ["Run `boundaries init`."],
56
+ });
43
57
  }
44
58
 
45
59
  for (const workspace of workspaces) {
46
60
  const packageTurboJson = await readJson(join(root, workspace.path, "turbo.json"), {});
47
61
  if (!Array.isArray(packageTurboJson.tags) || packageTurboJson.tags.length === 0) {
48
- errors.push(`${workspace.path}/turbo.json is missing package boundary tags.`);
62
+ errors.push({
63
+ code: "PACKAGE_TAGS_MISSING",
64
+ message: `${workspace.path}/turbo.json is missing package boundary tags.`,
65
+ file: `${workspace.path}/turbo.json`,
66
+ is_retriable: false,
67
+ suggestions: ["Run `boundaries init`."],
68
+ });
49
69
  }
50
70
  }
51
71
 
52
72
  return errors;
53
73
  }
54
74
 
55
- function runTurboBoundaries({ root }) {
75
+ function runTurboBoundaries({ root, quiet }) {
56
76
  return new Promise((resolve) => {
57
77
  const child = spawn("turbo", ["boundaries"], {
58
78
  cwd: root,
59
79
  shell: process.platform === "win32",
60
- stdio: "inherit",
80
+ stdio: quiet ? ["ignore", "pipe", "pipe"] : "inherit",
61
81
  });
82
+ let stdout = "";
83
+ let stderr = "";
84
+
85
+ if (quiet) {
86
+ child.stdout?.on("data", (chunk) => {
87
+ stdout += chunk.toString();
88
+ });
89
+ child.stderr?.on("data", (chunk) => {
90
+ stderr += chunk.toString();
91
+ });
92
+ }
62
93
 
63
94
  child.on("error", (error) => {
64
- process.stderr.write(
65
- `boundaries: could not run \`turbo boundaries\` (${error.message}). Install turbo or run \`boundaries check --no-turbo\`.\n`,
66
- );
67
- resolve(1);
95
+ const problem = {
96
+ code: "TURBO_NOT_FOUND",
97
+ message: `could not run \`turbo boundaries\` (${error.message}).`,
98
+ is_retriable: false,
99
+ suggestions: ["Install turbo or run `boundaries check --no-turbo`."],
100
+ };
101
+ if (!quiet) {
102
+ process.stderr.write(`boundaries: ${problem.message} ${problem.suggestions[0]}\n`);
103
+ }
104
+ resolve({ exitCode: 69, stdout, stderr, error: problem });
68
105
  });
69
106
 
70
107
  child.on("close", (code) => {
71
- resolve(code ?? 1);
108
+ const exitCode = code ?? 1;
109
+ resolve({
110
+ exitCode,
111
+ stdout,
112
+ stderr,
113
+ error: exitCode === 0
114
+ ? undefined
115
+ : {
116
+ code: "TURBO_BOUNDARIES_FAILED",
117
+ message: "`turbo boundaries` reported violations.",
118
+ is_retriable: false,
119
+ suggestions: ["Read the captured Turbo output and fix package imports or dependency declarations."],
120
+ },
121
+ });
72
122
  });
73
123
  });
74
124
  }
package/src/cli.js CHANGED
@@ -5,6 +5,15 @@ import { join } from "node:path";
5
5
  import { checkRepository } from "./check.js";
6
6
  import { evaluateDependency } from "./core.js";
7
7
  import { discoverWorkspaces, initRepository } from "./init.js";
8
+ import {
9
+ EXIT_CODES,
10
+ failure,
11
+ hasFlag,
12
+ stripFlags,
13
+ success,
14
+ writeJson,
15
+ } from "./output.js";
16
+ import { commandSchema } from "./schema.js";
8
17
 
9
18
  const HELP = `Usage: boundaries <command>
10
19
 
@@ -17,40 +26,101 @@ Commands:
17
26
 
18
27
  async function main(argv) {
19
28
  const [command, ...args] = argv;
29
+ const json = hasFlag(argv, "--json");
30
+ const schema = hasFlag(argv, "--schema");
31
+
32
+ if (schema) {
33
+ writeJson(process.stdout, success(commandSchema));
34
+ return EXIT_CODES.OK;
35
+ }
20
36
 
21
37
  if (!command || command === "help" || command === "--help" || command === "-h") {
38
+ if (json) {
39
+ writeJson(process.stdout, success(commandSchema));
40
+ return EXIT_CODES.OK;
41
+ }
22
42
  process.stdout.write(HELP);
23
- return 0;
43
+ return EXIT_CODES.OK;
24
44
  }
25
45
 
26
46
  if (command === "init") {
27
- const result = await initRepository();
28
- process.stdout.write(
29
- `Initialized boundaries for ${result.workspaces.length} workspace${result.workspaces.length === 1 ? "" : "s"}.\n`,
30
- );
31
- return 0;
47
+ const result = await initRepository({ dryRun: hasFlag(args, "--dry-run") });
48
+ if (json) {
49
+ writeJson(process.stdout, success(summarizeInitResult(result)));
50
+ } else {
51
+ process.stdout.write(
52
+ `${result.dryRun ? "Planned" : "Initialized"} boundaries for ${result.workspaces.length} workspace${result.workspaces.length === 1 ? "" : "s"}.\n`,
53
+ );
54
+ }
55
+ return EXIT_CODES.OK;
32
56
  }
33
57
 
34
58
  if (command === "check") {
35
59
  const result = await checkRepository({
36
60
  runTurbo: !args.includes("--no-turbo"),
61
+ quiet: json,
37
62
  });
63
+ if (json) {
64
+ if (result.ok) {
65
+ writeJson(process.stdout, success({
66
+ ok: true,
67
+ workspaceCount: result.workspaces?.length ?? 0,
68
+ ranTurbo: !args.includes("--no-turbo"),
69
+ turbo: result.turbo
70
+ ? { exitCode: result.turbo.exitCode, stdout: result.turbo.stdout, stderr: result.turbo.stderr }
71
+ : undefined,
72
+ }));
73
+ } else {
74
+ const primary = result.errors[0] ?? {
75
+ code: "BOUNDARY_CHECK_FAILED",
76
+ message: "Boundary check failed.",
77
+ is_retriable: false,
78
+ suggestions: ["Inspect command output and retry."],
79
+ };
80
+ writeJson(process.stderr, failure(primary, {
81
+ errors: result.errors,
82
+ turbo: result.turbo
83
+ ? { exitCode: result.turbo.exitCode, stdout: result.turbo.stdout, stderr: result.turbo.stderr }
84
+ : undefined,
85
+ }));
86
+ }
87
+ }
38
88
  return result.exitCode;
39
89
  }
40
90
 
41
91
  if (command === "explain") {
42
- return explain(args);
92
+ return explain(stripFlags(args, ["--json"]), { json });
43
93
  }
44
94
 
45
- process.stderr.write(`Unknown command: ${command}\n\n${HELP}`);
46
- return 1;
95
+ const error = {
96
+ code: "UNKNOWN_COMMAND",
97
+ message: `Unknown command: ${command}`,
98
+ is_retriable: false,
99
+ suggestions: ["Run `boundaries --help` for available commands."],
100
+ };
101
+ if (json) {
102
+ writeJson(process.stderr, failure(error));
103
+ } else {
104
+ process.stderr.write(`${error.message}\n\n${HELP}`);
105
+ }
106
+ return EXIT_CODES.USAGE;
47
107
  }
48
108
 
49
- async function explain(args) {
109
+ async function explain(args, { json = false } = {}) {
50
110
  const [fromSelector, toSelector] = args;
51
111
  if (!fromSelector || !toSelector) {
52
- process.stderr.write("Usage: boundaries explain <from> <to>\n");
53
- return 1;
112
+ const error = {
113
+ code: "USAGE_ERROR",
114
+ message: "Usage: boundaries explain <from> <to>",
115
+ is_retriable: false,
116
+ suggestions: ["Provide both source and target workspace selectors."],
117
+ };
118
+ if (json) {
119
+ writeJson(process.stderr, failure(error));
120
+ } else {
121
+ process.stderr.write(`${error.message}\n`);
122
+ }
123
+ return EXIT_CODES.USAGE;
54
124
  }
55
125
 
56
126
  const root = process.cwd();
@@ -61,8 +131,18 @@ async function explain(args) {
61
131
  const to = findWorkspace(workspaces, toSelector);
62
132
 
63
133
  if (!from || !to) {
64
- process.stderr.write("Could not find both workspaces. Use a package name or workspace path.\n");
65
- return 1;
134
+ const error = {
135
+ code: "WORKSPACE_NOT_FOUND",
136
+ message: "Could not find both workspaces. Use a package name or workspace path.",
137
+ is_retriable: false,
138
+ suggestions: ["Run `boundaries init --dry-run --json` to inspect discovered workspaces."],
139
+ };
140
+ if (json) {
141
+ writeJson(process.stderr, failure(error));
142
+ } else {
143
+ process.stderr.write(`${error.message}\n`);
144
+ }
145
+ return EXIT_CODES.DATA;
66
146
  }
67
147
 
68
148
  const decision = evaluateDependency({
@@ -73,13 +153,55 @@ async function explain(args) {
73
153
  toTags: await readTags(root, to),
74
154
  });
75
155
 
76
- process.stdout.write(
77
- `${from.name ?? from.path} -> ${to.name ?? to.path}: ${decision.allowed ? "allowed" : "blocked"}\n${decision.reason}\n`,
78
- );
156
+ const data = {
157
+ allowed: decision.allowed,
158
+ reason: decision.reason,
159
+ from: describeWorkspace(from, await readTags(root, from)),
160
+ to: describeWorkspace(to, await readTags(root, to)),
161
+ };
162
+
163
+ if (json) {
164
+ if (decision.allowed) {
165
+ writeJson(process.stdout, success(data));
166
+ } else {
167
+ writeJson(process.stdout, failure({
168
+ code: "BOUNDARY_BLOCKED",
169
+ message: decision.reason,
170
+ is_retriable: false,
171
+ suggestions: ["Move shared code into a package or adjust package boundary tags intentionally."],
172
+ }, data));
173
+ }
174
+ } else {
175
+ process.stdout.write(
176
+ `${from.name ?? from.path} -> ${to.name ?? to.path}: ${decision.allowed ? "allowed" : "blocked"}\n${decision.reason}\n`,
177
+ );
178
+ }
79
179
 
80
180
  return decision.allowed ? 0 : 1;
81
181
  }
82
182
 
183
+ function summarizeInitResult(result) {
184
+ return {
185
+ dryRun: result.dryRun,
186
+ workspaces: result.workspaces.map((workspace) => ({
187
+ name: workspace.name,
188
+ path: workspace.path,
189
+ })),
190
+ plannedWrites: result.plannedWrites.map((write) => ({
191
+ path: write.path,
192
+ kind: write.kind,
193
+ })),
194
+ };
195
+ }
196
+
197
+ function describeWorkspace(workspace, tags) {
198
+ return {
199
+ name: workspace.name,
200
+ path: workspace.path,
201
+ tags,
202
+ };
203
+ }
204
+
83
205
  function findWorkspace(workspaces, selector) {
84
206
  return workspaces.find((workspace) => {
85
207
  return workspace.name === selector || workspace.path === selector || workspace.path.endsWith(`/${selector}`);
@@ -107,6 +229,17 @@ main(process.argv.slice(2))
107
229
  process.exitCode = exitCode;
108
230
  })
109
231
  .catch((error) => {
110
- process.stderr.write(`boundaries: ${error.message}\n`);
111
- process.exitCode = 1;
232
+ const json = process.argv.includes("--json");
233
+ const problem = {
234
+ code: error.code === "ENOENT" ? "FILE_NOT_FOUND" : "UNHANDLED_ERROR",
235
+ message: error.message,
236
+ is_retriable: false,
237
+ suggestions: ["Check that you are running from a workspace root with package.json."],
238
+ };
239
+ if (json) {
240
+ writeJson(process.stderr, failure(problem));
241
+ } else {
242
+ process.stderr.write(`boundaries: ${error.message}\n`);
243
+ }
244
+ process.exitCode = error.code === "ENOENT" ? EXIT_CODES.DATA : EXIT_CODES.SOFTWARE;
112
245
  });
package/src/init.js CHANGED
@@ -7,29 +7,48 @@ import {
7
7
  inferTagsForWorkspace,
8
8
  } from "./core.js";
9
9
 
10
- export async function initRepository({ root = process.cwd() } = {}) {
10
+ export async function initRepository({ root = process.cwd(), dryRun = false } = {}) {
11
11
  const rootPackageJsonPath = join(root, "package.json");
12
12
  const rootTurboJsonPath = join(root, "turbo.json");
13
13
  const rootPackageJson = await readJson(rootPackageJsonPath);
14
14
  const rootTurboJson = await readJson(rootTurboJsonPath, {});
15
15
  const workspaces = await discoverWorkspaces(root, rootPackageJson);
16
+ const plannedWrites = [];
16
17
 
17
18
  rootPackageJson.scripts = {
18
19
  ...(rootPackageJson.scripts ?? {}),
19
20
  boundaries: rootPackageJson.scripts?.boundaries ?? "boundaries check",
20
21
  };
21
22
 
22
- await writeJson(rootPackageJsonPath, rootPackageJson);
23
- await writeJson(rootTurboJsonPath, applyRootBoundaryConfig(rootTurboJson));
23
+ await planJsonWrite({
24
+ root,
25
+ filePath: rootPackageJsonPath,
26
+ value: rootPackageJson,
27
+ dryRun,
28
+ plannedWrites,
29
+ });
30
+ await planJsonWrite({
31
+ root,
32
+ filePath: rootTurboJsonPath,
33
+ value: applyRootBoundaryConfig(rootTurboJson),
34
+ dryRun,
35
+ plannedWrites,
36
+ });
24
37
 
25
38
  for (const workspace of workspaces) {
26
39
  const turboJsonPath = join(root, workspace.path, "turbo.json");
27
40
  const currentTurboJson = await readJson(turboJsonPath, {});
28
41
  const tags = inferTagsForWorkspace(workspace);
29
- await writeJson(turboJsonPath, createPackageTurboConfig(currentTurboJson, tags));
42
+ await planJsonWrite({
43
+ root,
44
+ filePath: turboJsonPath,
45
+ value: createPackageTurboConfig(currentTurboJson, tags),
46
+ dryRun,
47
+ plannedWrites,
48
+ });
30
49
  }
31
50
 
32
- return { workspaces };
51
+ return { dryRun, workspaces, plannedWrites };
33
52
  }
34
53
 
35
54
  export async function discoverWorkspaces(root, rootPackageJson = undefined) {
@@ -162,3 +181,19 @@ async function writeJson(filePath, value) {
162
181
  await mkdir(dirname(filePath), { recursive: true });
163
182
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
164
183
  }
184
+
185
+ async function planJsonWrite({ root, filePath, value, dryRun, plannedWrites }) {
186
+ plannedWrites.push({
187
+ path: relativePath(root, filePath),
188
+ kind: "json",
189
+ content: `${JSON.stringify(value, null, 2)}\n`,
190
+ });
191
+
192
+ if (!dryRun) {
193
+ await writeJson(filePath, value);
194
+ }
195
+ }
196
+
197
+ function relativePath(root, filePath) {
198
+ return filePath.startsWith(`${root}/`) ? filePath.slice(root.length + 1) : filePath;
199
+ }
package/src/output.js ADDED
@@ -0,0 +1,51 @@
1
+ export const EXIT_CODES = {
2
+ OK: 0,
3
+ USAGE: 64,
4
+ DATA: 65,
5
+ UNAVAILABLE: 69,
6
+ SOFTWARE: 70,
7
+ };
8
+
9
+ export function success(data, metadata = undefined) {
10
+ return {
11
+ success: true,
12
+ data,
13
+ ...(metadata ? { metadata } : {}),
14
+ };
15
+ }
16
+
17
+ export function failure(error, data = undefined) {
18
+ return {
19
+ success: false,
20
+ ...(data ? { data } : {}),
21
+ error: problem(error),
22
+ };
23
+ }
24
+
25
+ export function problem({
26
+ code,
27
+ message,
28
+ is_retriable = false,
29
+ suggestions = [],
30
+ file = undefined,
31
+ }) {
32
+ return {
33
+ code,
34
+ message,
35
+ is_retriable,
36
+ suggestions,
37
+ ...(file ? { file } : {}),
38
+ };
39
+ }
40
+
41
+ export function writeJson(stream, value) {
42
+ stream.write(`${JSON.stringify(value)}\n`);
43
+ }
44
+
45
+ export function hasFlag(args, flag) {
46
+ return args.includes(flag);
47
+ }
48
+
49
+ export function stripFlags(args, flags) {
50
+ return args.filter((arg) => !flags.includes(arg));
51
+ }
package/src/schema.js ADDED
@@ -0,0 +1,63 @@
1
+ export const commandSchema = {
2
+ name: "boundaries",
3
+ description: "Opinionated Turborepo package boundary conventions.",
4
+ commands: [
5
+ {
6
+ name: "init",
7
+ description: "Add root Turbo boundary rules and per-package tags.",
8
+ options: [
9
+ option("--json", "Print a machine-readable response envelope."),
10
+ option("--dry-run", "Print planned file writes without changing files."),
11
+ ],
12
+ output: "InitResult",
13
+ },
14
+ {
15
+ name: "check",
16
+ description: "Validate boundary config and optionally run turbo boundaries.",
17
+ options: [
18
+ option("--json", "Print a machine-readable response envelope."),
19
+ option("--no-turbo", "Validate convention config without running turbo boundaries."),
20
+ ],
21
+ output: "CheckResult",
22
+ },
23
+ {
24
+ name: "explain",
25
+ description: "Explain whether one workspace may depend on another.",
26
+ arguments: [
27
+ argument("from", "Package name, workspace path, or workspace basename."),
28
+ argument("to", "Package name, workspace path, or workspace basename."),
29
+ ],
30
+ options: [option("--json", "Print a machine-readable response envelope.")],
31
+ output: "ExplainResult",
32
+ },
33
+ {
34
+ name: "help",
35
+ description: "Show command help.",
36
+ options: [
37
+ option("--json", "Print a machine-readable response envelope."),
38
+ option("--schema", "Print this command schema as JSON."),
39
+ ],
40
+ output: "CommandSchema",
41
+ },
42
+ ],
43
+ responseEnvelope: {
44
+ success: "boolean",
45
+ data: "object | undefined",
46
+ error: "BoundaryProblem | undefined",
47
+ },
48
+ errorShape: {
49
+ code: "string",
50
+ message: "string",
51
+ is_retriable: "boolean",
52
+ suggestions: "string[]",
53
+ file: "string | undefined",
54
+ },
55
+ };
56
+
57
+ function option(name, description) {
58
+ return { name, description };
59
+ }
60
+
61
+ function argument(name, description) {
62
+ return { name, description, required: true };
63
+ }