@freetesh/canon 0.15.0 → 0.17.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +24 -7
- package/cli/dist/api.d.ts +2 -2
- package/cli/dist/api.js +2 -2
- package/cli/dist/checks/{external.js → commands.js} +2 -0
- package/cli/dist/checks/designs.d.ts +5 -0
- package/cli/dist/checks/designs.js +33 -0
- package/cli/dist/checks/index.js +6 -3
- package/cli/dist/checks/stale-copies.d.ts +2 -0
- package/cli/dist/checks/stale-copies.js +18 -0
- package/cli/dist/checks/{copies.d.ts → standards-copies.d.ts} +0 -1
- package/cli/dist/checks/{copies.js → standards-copies.js} +0 -16
- package/cli/dist/commands/add.d.ts +1 -1
- package/cli/dist/commands/add.js +1 -1
- package/cli/dist/commands/check.d.ts +1 -1
- package/cli/dist/commands/check.js +2 -2
- package/cli/dist/commands/copies.d.ts +1 -1
- package/cli/dist/commands/hooks.d.ts +1 -1
- package/cli/dist/commands/hooks.js +2 -2
- package/cli/dist/commands/init.d.ts +1 -1
- package/cli/dist/commands/init.js +2 -2
- package/cli/dist/commands/learn.d.ts +1 -1
- package/cli/dist/commands/learn.js +2 -2
- package/cli/dist/commands/pipeline.d.ts +1 -1
- package/cli/dist/{search → commands}/search.js +1 -1
- package/cli/dist/commands/standards.d.ts +1 -1
- package/cli/dist/commands/status.d.ts +1 -1
- package/cli/dist/commands/sync.d.ts +1 -1
- package/cli/dist/commands/sync.js +1 -1
- package/cli/dist/commands/ui.d.ts +1 -1
- package/cli/dist/commands/ui.js +2 -2
- package/cli/dist/config/load.js +18 -6
- package/cli/dist/config/schema.d.ts +24 -3
- package/cli/dist/config/schema.js +17 -3
- package/cli/dist/format.d.ts +1 -1
- package/cli/dist/index.js +2 -2
- package/cli/dist/init.js +2 -2
- package/cli/dist/packs.d.ts +3 -1
- package/cli/dist/standards.d.ts +11 -6
- package/cli/dist/standards.js +37 -17
- package/cli/dist/status.d.ts +1 -1
- package/cli/dist/status.js +1 -1
- package/package.json +3 -3
- package/packs/expo/pack.yaml +1 -1
- package/packs/firebase/pack.yaml +1 -1
- package/packs/react/pack.yaml +1 -1
- package/packs/revenuecat/pack.yaml +1 -1
- package/packs/uniwind/pack.yaml +1 -1
- package/scripts/check-designs.py +709 -0
- package/standards/README.md +23 -6
- package/standards/agent-contract.md +21 -6
- package/standards/context-layout.md +41 -6
- package/standards/library/design-sets.md +857 -0
- package/standards/library/expo.md +54 -0
- package/standards/library/frontend.md +44 -0
- package/standards/library/performance.md +45 -0
- package/standards/library/pipeline.md +42 -0
- package/standards/library/secrets.md +37 -0
- package/standards/library/store-listing.md +122 -0
- /package/cli/dist/{run.d.ts → check.d.ts} +0 -0
- /package/cli/dist/{run.js → check.js} +0 -0
- /package/cli/dist/checks/{external.d.ts → commands.d.ts} +0 -0
- /package/cli/dist/checks/{metrics.d.ts → metrics-definitions.d.ts} +0 -0
- /package/cli/dist/checks/{metrics.js → metrics-definitions.js} +0 -0
- /package/cli/dist/{search → commands}/mcp.d.ts +0 -0
- /package/cli/dist/{search → commands}/mcp.js +0 -0
- /package/cli/dist/{search → commands}/search.d.ts +0 -0
- /package/cli/dist/{commands → lib}/env.d.ts +0 -0
- /package/cli/dist/{commands → lib}/env.js +0 -0
- /package/cli/dist/{search → lib}/qmd.d.ts +0 -0
- /package/cli/dist/{search → lib}/qmd.js +0 -0
package/README.md
CHANGED
|
@@ -47,9 +47,11 @@ is a separate paid product built on this CLI.
|
|
|
47
47
|
listing tree, dead `.context/` paths, prices with no canonical home, shared
|
|
48
48
|
files, the import architecture, pipeline gates, and every checker your config
|
|
49
49
|
declares. `canon hooks install` makes every push run it.
|
|
50
|
-
- **Standards with one home.** canon ships defaults
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
- **Standards with one home.** canon ships defaults every repo follows, and a
|
|
51
|
+
library a repo imports by name (`standards: [design-sets, pipeline]`): design
|
|
52
|
+
sets, the build pipeline, Expo, frontend, performance, secrets, store listings.
|
|
53
|
+
A repo's own rules can sit in a folder it adds. `canon standards` shows what
|
|
54
|
+
applies.
|
|
53
55
|
- **A repo names only what it uses.** Modules, packs and shared-file sets are
|
|
54
56
|
opt-in; there is no skip list and no setting that turns a check off.
|
|
55
57
|
|
|
@@ -97,9 +99,9 @@ repo whose `canon.yaml` says `estate: true`, or `CANON_ESTATE`.
|
|
|
97
99
|
```yaml
|
|
98
100
|
# <estate>/.claude/canon.yaml — shared by every repo below it
|
|
99
101
|
estate: true
|
|
100
|
-
standards: [.canon/standards] # estate rules; same name replaces a canon default
|
|
101
102
|
commands: # your own checkers, run by `canon check`
|
|
102
|
-
- { id:
|
|
103
|
+
- { id: lint-copy, run: ./scripts/lint-copy.sh, if: .context/designs, module: design }
|
|
104
|
+
- { id: designs-first, run: test -d .context/designs, kinds: [mobile, web] } # only repos that ship screens
|
|
103
105
|
copies: # files products copy from one source repo, in named sets
|
|
104
106
|
source: design-system
|
|
105
107
|
sets:
|
|
@@ -110,8 +112,9 @@ copies: # files products copy from one source repo,
|
|
|
110
112
|
|
|
111
113
|
```yaml
|
|
112
114
|
# <repo>/.claude/canon.yaml — written by `canon init`
|
|
113
|
-
canon: 0.
|
|
114
|
-
kind:
|
|
115
|
+
canon: 0.17.0
|
|
116
|
+
kind: mobile # mobile | web | mobile-and-web | package | content
|
|
117
|
+
standards: [design-sets, pipeline, expo] # canon's library standards this repo follows; ./rules adds a folder of its own
|
|
115
118
|
modules: [docs, knowledge, code, listing, refs]
|
|
116
119
|
packs: [expo]
|
|
117
120
|
name: Acme # how the product is written; `named` copies swap it in
|
|
@@ -138,6 +141,20 @@ evals/ plugin evals and the fixture product
|
|
|
138
141
|
cli/ the canon CLI (TypeScript, vitest); `@freetesh/canon/api` for tools built on it
|
|
139
142
|
```
|
|
140
143
|
|
|
144
|
+
Inside `cli/src/`:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
index.ts the program: registers every command
|
|
148
|
+
api.ts what other tools import (`@freetesh/canon/api`)
|
|
149
|
+
commands/ one file per command — options and output only
|
|
150
|
+
checks/ one file per check, named by its id; index.ts lists them
|
|
151
|
+
config/ .claude/canon.yaml: schema, layered loading, estate discovery
|
|
152
|
+
lib/ shared helpers: files, frontmatter, env, qmd, system calls
|
|
153
|
+
<name>.ts the logic the commands call: check (+ format for its output), init, packs (add), sync, …
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Tests mirror it in `cli/test/`.
|
|
157
|
+
|
|
141
158
|
## Develop
|
|
142
159
|
|
|
143
160
|
`pnpm check` runs `canon check` on this repo, the typecheck and every test.
|
package/cli/dist/api.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { findEstate } from "./config/load.ts";
|
|
2
|
-
export { findProducts } from "./
|
|
3
|
-
export { qmdQuery, qmdRefresh } from "./
|
|
2
|
+
export { findProducts } from "./check.ts";
|
|
3
|
+
export { qmdQuery, qmdRefresh } from "./lib/qmd.ts";
|
package/cli/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// What other tools build on: the canon knowledge site reads repos the way `canon check --all` finds them.
|
|
2
2
|
export { findEstate } from "./config/load.js";
|
|
3
|
-
export { findProducts } from "./
|
|
4
|
-
export { qmdQuery, qmdRefresh } from "./
|
|
3
|
+
export { findProducts } from "./check.js";
|
|
4
|
+
export { qmdQuery, qmdRefresh } from "./lib/qmd.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { kindCovers } from "../config/schema.js";
|
|
4
5
|
const MAX_OUTPUT_LINES = 15;
|
|
5
6
|
function tail(text) {
|
|
6
7
|
return text.trim().split("\n").slice(-MAX_OUTPUT_LINES).join("\n");
|
|
@@ -18,6 +19,7 @@ export const commandChecks = {
|
|
|
18
19
|
return config.commands
|
|
19
20
|
.filter((command) => !command.if || existsSync(join(repo, command.if)))
|
|
20
21
|
.filter((command) => !command.module || config.modules.includes(command.module))
|
|
22
|
+
.filter((command) => !command.kinds || command.kinds.some((kind) => kindCovers(config.kind, kind)))
|
|
21
23
|
.flatMap((command) => {
|
|
22
24
|
const result = spawnSync("sh", ["-c", command.run], { cwd: repo, encoding: "utf8" });
|
|
23
25
|
if (result.error) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Check } from "./types.ts";
|
|
2
|
+
/** Every drawn surface: a folder under .context/designs/ with a routes.js manifest. */
|
|
3
|
+
export declare function designSurfaces(repo: string): string[];
|
|
4
|
+
/** Runs canon's design-set checker (scripts/check-designs.py) once per drawn surface. */
|
|
5
|
+
export declare const designs: Check;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const DESIGNS = ".context/designs";
|
|
5
|
+
const MAX_OUTPUT_LINES = 15;
|
|
6
|
+
/** Every drawn surface: a folder under .context/designs/ with a routes.js manifest. */
|
|
7
|
+
export function designSurfaces(repo) {
|
|
8
|
+
const root = join(repo, DESIGNS);
|
|
9
|
+
if (!existsSync(root))
|
|
10
|
+
return [];
|
|
11
|
+
return readdirSync(root, { withFileTypes: true })
|
|
12
|
+
.filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, "routes.js")))
|
|
13
|
+
.map((entry) => `${DESIGNS}/${entry.name}`)
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
/** Runs canon's design-set checker (scripts/check-designs.py) once per drawn surface. */
|
|
17
|
+
export const designs = {
|
|
18
|
+
id: "designs",
|
|
19
|
+
module: "design",
|
|
20
|
+
description: "every drawn surface follows the design-sets standard (scripts/check-designs.py)",
|
|
21
|
+
run({ repo, canonRoot }) {
|
|
22
|
+
const script = join(canonRoot, "scripts", "check-designs.py");
|
|
23
|
+
return designSurfaces(repo).flatMap((surface) => {
|
|
24
|
+
const result = spawnSync("python3", [script, surface], { cwd: repo, encoding: "utf8" });
|
|
25
|
+
if (result.error)
|
|
26
|
+
return [{ check: "designs", level: "error", file: surface, message: `could not run python3: ${result.error.message}` }];
|
|
27
|
+
if (result.status === 0)
|
|
28
|
+
return [];
|
|
29
|
+
const output = `${result.stdout}\n${result.stderr}`.trim().split("\n").slice(-MAX_OUTPUT_LINES).join("\n");
|
|
30
|
+
return [{ check: "designs", level: "error", file: surface, message: output }];
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
package/cli/dist/checks/index.js
CHANGED
|
@@ -2,11 +2,13 @@ import { architecture } from "./architecture.js";
|
|
|
2
2
|
import { canonicalValues } from "./canonical-values.js";
|
|
3
3
|
import { contextLayout } from "./context-layout.js";
|
|
4
4
|
import { contextSize } from "./context-size.js";
|
|
5
|
-
import { staleCopies
|
|
6
|
-
import {
|
|
5
|
+
import { staleCopies } from "./stale-copies.js";
|
|
6
|
+
import { standardsCopies } from "./standards-copies.js";
|
|
7
|
+
import { commandChecks } from "./commands.js";
|
|
8
|
+
import { designs } from "./designs.js";
|
|
7
9
|
import { features } from "./features.js";
|
|
8
10
|
import { listing } from "./listing.js";
|
|
9
|
-
import { metricsDefinitions } from "./metrics.js";
|
|
11
|
+
import { metricsDefinitions } from "./metrics-definitions.js";
|
|
10
12
|
import { pipeline } from "./pipeline.js";
|
|
11
13
|
import { refs } from "./refs.js";
|
|
12
14
|
import { sharedFiles } from "./shared-files.js";
|
|
@@ -29,5 +31,6 @@ export const ALL_CHECKS = [
|
|
|
29
31
|
metricsDefinitions,
|
|
30
32
|
sources,
|
|
31
33
|
pipeline,
|
|
34
|
+
designs,
|
|
32
35
|
commandChecks,
|
|
33
36
|
];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// Instruction files for agents other than Claude Code. They are copies of
|
|
4
|
+
// AGENTS.md that nobody reads, so they only drift.
|
|
5
|
+
const OTHER_AGENT_FILES = [".github/copilot-instructions.md", ".cursorrules", ".windsurfrules"];
|
|
6
|
+
export const staleCopies = {
|
|
7
|
+
id: "stale-copies",
|
|
8
|
+
module: null,
|
|
9
|
+
description: "no instruction files for agents that are not used (Copilot, Cursor, Windsurf)",
|
|
10
|
+
run({ repo }) {
|
|
11
|
+
return OTHER_AGENT_FILES.filter((file) => existsSync(join(repo, file))).map((file) => ({
|
|
12
|
+
check: "stale-copies",
|
|
13
|
+
level: "warn",
|
|
14
|
+
file,
|
|
15
|
+
message: "instructions for an agent not in use — delete, Claude reads AGENTS.md",
|
|
16
|
+
}));
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -1,25 +1,9 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
// Instruction files for agents other than Claude Code. They are copies of
|
|
4
|
-
// AGENTS.md that nobody reads, so they only drift.
|
|
5
|
-
const OTHER_AGENT_FILES = [".github/copilot-instructions.md", ".cursorrules", ".windsurfrules"];
|
|
6
3
|
// Fraction of a shared standard's lines that must appear in a product file
|
|
7
4
|
// before it counts as a copy rather than a reference.
|
|
8
5
|
const COPY_THRESHOLD = 0.6;
|
|
9
6
|
const STANDARD_COPIES = [{ file: ".context/wiki/CLAUDE.md", standard: "standards/wiki-schema.md" }];
|
|
10
|
-
export const staleCopies = {
|
|
11
|
-
id: "stale-copies",
|
|
12
|
-
module: null,
|
|
13
|
-
description: "no instruction files for agents that are not used (Copilot, Cursor, Windsurf)",
|
|
14
|
-
run({ repo }) {
|
|
15
|
-
return OTHER_AGENT_FILES.filter((file) => existsSync(join(repo, file))).map((file) => ({
|
|
16
|
-
check: "stale-copies",
|
|
17
|
-
level: "warn",
|
|
18
|
-
file,
|
|
19
|
-
message: "instructions for an agent not in use — delete, Claude reads AGENTS.md",
|
|
20
|
-
}));
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
7
|
function meaningfulLines(text) {
|
|
24
8
|
return new Set(text
|
|
25
9
|
.split("\n")
|
package/cli/dist/commands/add.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { CACHE_DIR, fetchText, runCommand } from "../lib/system.js";
|
|
3
3
|
import { addPack, fetchPackDocs, findPack, listPacks } from "../packs.js";
|
|
4
|
-
import { requireEstate } from "
|
|
4
|
+
import { requireEstate } from "../lib/env.js";
|
|
5
5
|
export function registerAdd(program, env) {
|
|
6
6
|
program
|
|
7
7
|
.command("add")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { exitCode, formatReports } from "../format.js";
|
|
3
|
-
import { findProducts, runChecks } from "../
|
|
4
|
-
import { requireEstate } from "
|
|
3
|
+
import { findProducts, runChecks } from "../check.js";
|
|
4
|
+
import { requireEstate } from "../lib/env.js";
|
|
5
5
|
export function registerCheck(program, env) {
|
|
6
6
|
program
|
|
7
7
|
.command("check")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { type Env } from "
|
|
2
|
+
import { type Env } from "../lib/env.ts";
|
|
3
3
|
export declare const HOOK = "#!/bin/sh\n# installed by `canon hooks install` \u2014 run it again rather than editing this file.\n# A machine without canon pushes unchecked, as it would without any hook.\ncommand -v canon >/dev/null 2>&1 || exit 0\n# The product is the folder that owns .git, not the folder being pushed from: a worktree shares these hooks.\nroot=$(cd \"$(git rev-parse --git-common-dir)/..\" && pwd) || exit 0\nexec canon check \"$root\"\n";
|
|
4
4
|
/** Writes the pre-push hook into one repo. Returns a one-line outcome. */
|
|
5
5
|
export declare function installHook(repo: string): string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { findProducts } from "../
|
|
5
|
-
import { requireEstate } from "
|
|
4
|
+
import { findProducts } from "../check.js";
|
|
5
|
+
import { requireEstate } from "../lib/env.js";
|
|
6
6
|
const MARK = "installed by `canon hooks install`";
|
|
7
7
|
// Hooks written by the tools this command replaced (the estate script, then zeno); they are ours to overwrite.
|
|
8
8
|
const OLD_MARKS = [MARK, "installed by ~/git/bin/install-hooks", "installed by `zeno hooks install`"];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, resolve } from "node:path";
|
|
2
2
|
import * as prompts from "@clack/prompts";
|
|
3
3
|
import { Option } from "commander";
|
|
4
|
-
import { KINDS, MODULES } from "../config/schema.js";
|
|
4
|
+
import { KINDS, kindCovers, MODULES } from "../config/schema.js";
|
|
5
5
|
import { RULES_FILES } from "../checks/architecture.js";
|
|
6
6
|
import { applyInit, parseChoices, planInit } from "../init.js";
|
|
7
7
|
import { listPacks } from "../packs.js";
|
|
@@ -29,7 +29,7 @@ async function askAnswers(inferred, packs) {
|
|
|
29
29
|
});
|
|
30
30
|
if (cancelled(modules))
|
|
31
31
|
return null;
|
|
32
|
-
const forKind = packs.filter((pack) => pack.kinds.
|
|
32
|
+
const forKind = packs.filter((pack) => pack.kinds.some((wanted) => kindCovers(kind, wanted)));
|
|
33
33
|
if (forKind.length === 0)
|
|
34
34
|
return { kind, modules, packs: [] };
|
|
35
35
|
const chosen = await prompts.multiselect({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { proposals, readDecisions, readVerdicts, recordDecision, summarise } from "../learn.js";
|
|
2
|
-
import { findProducts } from "../
|
|
3
|
-
import { requireEstate } from "
|
|
2
|
+
import { findProducts } from "../check.js";
|
|
3
|
+
import { requireEstate } from "../lib/env.js";
|
|
4
4
|
const DEFAULT_THRESHOLD = 3;
|
|
5
5
|
export function registerLearn(program, env) {
|
|
6
6
|
program
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { exitCode, formatReports } from "../format.js";
|
|
3
3
|
import { CACHE_DIR, fetchText, runCommand } from "../lib/system.js";
|
|
4
|
-
import { runChecks } from "../
|
|
4
|
+
import { runChecks } from "../check.js";
|
|
5
5
|
import { syncRepo } from "../sync.js";
|
|
6
6
|
export function registerSync(program, env) {
|
|
7
7
|
program
|
package/cli/dist/commands/ui.js
CHANGED
|
@@ -4,9 +4,9 @@ import { join } from "node:path";
|
|
|
4
4
|
import { architectureGraph } from "../checks/architecture.js";
|
|
5
5
|
import { formatReports } from "../format.js";
|
|
6
6
|
import { CACHE_DIR } from "../lib/system.js";
|
|
7
|
-
import { findProducts, runChecks } from "../
|
|
7
|
+
import { findProducts, runChecks } from "../check.js";
|
|
8
8
|
import { buildSite, quartzConfig } from "../ui.js";
|
|
9
|
-
import { requireEstate } from "
|
|
9
|
+
import { requireEstate } from "../lib/env.js";
|
|
10
10
|
// Quartz v5 has no tagged release yet, so pin the exact commit: an unpinned
|
|
11
11
|
// clone could change the site under us on the next fresh install.
|
|
12
12
|
const QUARTZ_REPO = "https://github.com/jackyzha0/quartz";
|
package/cli/dist/config/load.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
|
-
import { MODULES_BY_KIND, layerSchema } from "./schema.js";
|
|
4
|
+
import { isStandardName, MODULES_BY_KIND, layerSchema } from "./schema.js";
|
|
5
5
|
const EXPO_MARKERS = ["app.config.ts", "app.config.js", "app.json"];
|
|
6
6
|
const WEB_MARKERS = ["next.config.ts", "next.config.js", "next.config.mjs", "vite.config.ts"];
|
|
7
7
|
function configPath(folder) {
|
|
@@ -36,14 +36,26 @@ function appRoots(repo) {
|
|
|
36
36
|
const children = readdirSync(apps, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
37
37
|
return [repo, ...children.map((entry) => join(apps, entry.name))];
|
|
38
38
|
}
|
|
39
|
+
/** A root package.json that ships a CLI or exports a library. */
|
|
40
|
+
function isPackage(repo) {
|
|
41
|
+
const file = join(repo, "package.json");
|
|
42
|
+
if (!existsSync(file))
|
|
43
|
+
return false;
|
|
44
|
+
const pkg = JSON.parse(readFileSync(file, "utf8"));
|
|
45
|
+
return Boolean(pkg.bin || pkg.exports || pkg.main) && !pkg.private;
|
|
46
|
+
}
|
|
39
47
|
export function inferKind(repo) {
|
|
40
48
|
const roots = appRoots(repo);
|
|
41
49
|
const anyHas = (markers) => roots.some((root) => markers.some((file) => existsSync(join(root, file))));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
50
|
+
const mobile = anyHas(EXPO_MARKERS);
|
|
51
|
+
const web = anyHas(WEB_MARKERS);
|
|
52
|
+
if (mobile && web)
|
|
53
|
+
return "mobile-and-web";
|
|
54
|
+
if (mobile)
|
|
55
|
+
return "mobile";
|
|
56
|
+
if (web)
|
|
45
57
|
return "web";
|
|
46
|
-
return "content";
|
|
58
|
+
return isPackage(repo) ? "package" : "content";
|
|
47
59
|
}
|
|
48
60
|
export function inferModules(repo, kind) {
|
|
49
61
|
const modules = MODULES_BY_KIND[kind];
|
|
@@ -82,7 +94,7 @@ export function loadConfig(repo, options = {}) {
|
|
|
82
94
|
modules: lastDefined(pick("modules")) ?? inferModules(repo, kind),
|
|
83
95
|
expo: Object.assign({}, ...pick("expo")),
|
|
84
96
|
packs: [...new Set(pick("packs").flatMap((packs) => packs ?? []))].sort(),
|
|
85
|
-
standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((
|
|
97
|
+
standards: [...new Set(layers.flatMap((layer) => (layer.data.standards ?? []).map((entry) => (isStandardName(entry) ? entry : resolve(layer.base, entry)))))],
|
|
86
98
|
commands: mergeBy(pick("commands").map((list) => list ?? []), (command) => command.id),
|
|
87
99
|
doctor: { tools: mergeBy(pick("doctor").map((doctor) => doctor?.tools ?? []), (tool) => tool.tool) },
|
|
88
100
|
weigh: { weights: Object.assign({}, ...pick("weigh").map((weigh) => weigh?.weights)) },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
/** What a repo ships: an app to the stores, a site to the web, both, a package to a registry, or no code at all. */
|
|
3
|
+
export declare const KINDS: readonly ["mobile", "web", "mobile-and-web", "package", "content"];
|
|
3
4
|
export declare const MODULES: readonly ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing", "refs"];
|
|
4
5
|
/**
|
|
5
6
|
* The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
|
|
@@ -10,6 +11,8 @@ export declare const WIKI_FOLDER_BY_TYPE: Record<string, string>;
|
|
|
10
11
|
export declare const REMOVED_WIKI_FOLDERS: Record<string, string>;
|
|
11
12
|
/** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
|
|
12
13
|
export declare const CONTEXT_FOLDERS: string[];
|
|
14
|
+
/** A `standards:` entry naming a library standard rather than a folder path. */
|
|
15
|
+
export declare function isStandardName(entry: string): boolean;
|
|
13
16
|
export type Kind = (typeof KINDS)[number];
|
|
14
17
|
export type Module = (typeof MODULES)[number];
|
|
15
18
|
declare const command: z.ZodObject<{
|
|
@@ -26,6 +29,13 @@ declare const command: z.ZodObject<{
|
|
|
26
29
|
metrics: "metrics";
|
|
27
30
|
refs: "refs";
|
|
28
31
|
}>>;
|
|
32
|
+
kinds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
33
|
+
content: "content";
|
|
34
|
+
mobile: "mobile";
|
|
35
|
+
"mobile-and-web": "mobile-and-web";
|
|
36
|
+
package: "package";
|
|
37
|
+
web: "web";
|
|
38
|
+
}>>>;
|
|
29
39
|
}, z.core.$strict>;
|
|
30
40
|
declare const wikiType: z.ZodObject<{
|
|
31
41
|
type: z.ZodString;
|
|
@@ -60,7 +70,9 @@ export declare const layerSchema: z.ZodObject<{
|
|
|
60
70
|
canon: z.ZodOptional<z.ZodString>;
|
|
61
71
|
kind: z.ZodOptional<z.ZodEnum<{
|
|
62
72
|
content: "content";
|
|
63
|
-
|
|
73
|
+
mobile: "mobile";
|
|
74
|
+
"mobile-and-web": "mobile-and-web";
|
|
75
|
+
package: "package";
|
|
64
76
|
web: "web";
|
|
65
77
|
}>>;
|
|
66
78
|
modules: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -92,6 +104,13 @@ export declare const layerSchema: z.ZodObject<{
|
|
|
92
104
|
metrics: "metrics";
|
|
93
105
|
refs: "refs";
|
|
94
106
|
}>>;
|
|
107
|
+
kinds: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
108
|
+
content: "content";
|
|
109
|
+
mobile: "mobile";
|
|
110
|
+
"mobile-and-web": "mobile-and-web";
|
|
111
|
+
package: "package";
|
|
112
|
+
web: "web";
|
|
113
|
+
}>>>;
|
|
95
114
|
}, z.core.$strict>>>;
|
|
96
115
|
doctor: z.ZodOptional<z.ZodObject<{
|
|
97
116
|
tools: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -150,7 +169,7 @@ export interface Config {
|
|
|
150
169
|
sdk?: number;
|
|
151
170
|
};
|
|
152
171
|
packs: string[];
|
|
153
|
-
/**
|
|
172
|
+
/** Library standard names and absolute folders, estate first; a later file of the same name wins. */
|
|
154
173
|
standards: string[];
|
|
155
174
|
commands: CommandSpec[];
|
|
156
175
|
doctor: {
|
|
@@ -178,4 +197,6 @@ export interface Config {
|
|
|
178
197
|
};
|
|
179
198
|
}
|
|
180
199
|
export declare const MODULES_BY_KIND: Record<Kind, Module[]>;
|
|
200
|
+
/** A repo that ships both counts as mobile and as web: it gets both kinds' packs. */
|
|
201
|
+
export declare function kindCovers(kind: Kind, wanted: Kind): boolean;
|
|
181
202
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
|
|
2
|
+
/** What a repo ships: an app to the stores, a site to the web, both, a package to a registry, or no code at all. */
|
|
3
|
+
export const KINDS = ["mobile", "web", "mobile-and-web", "package", "content"];
|
|
3
4
|
export const MODULES = ["docs", "design", "knowledge", "metrics", "code", "architecture", "listing", "refs"];
|
|
4
5
|
/**
|
|
5
6
|
* The wiki page types every repo has, and the folder each lives in. A repo adds more under `wiki.types`.
|
|
@@ -18,6 +19,10 @@ export const REMOVED_WIKI_FOLDERS = {
|
|
|
18
19
|
};
|
|
19
20
|
/** The `.context/` folders every repo may have. A repo adds more under `context.folders`. */
|
|
20
21
|
export const CONTEXT_FOLDERS = ["raw", "wiki", "features", "designs", "listing", "marketing", "metrics", "support", "legal", "ops"];
|
|
22
|
+
/** A `standards:` entry naming a library standard rather than a folder path. */
|
|
23
|
+
export function isStandardName(entry) {
|
|
24
|
+
return /^[a-z0-9-]+$/.test(entry);
|
|
25
|
+
}
|
|
21
26
|
const principleId = z.string().regex(/^C-\d{2}$/, "principle IDs look like C-07");
|
|
22
27
|
const command = z
|
|
23
28
|
.object({
|
|
@@ -28,6 +33,8 @@ const command = z
|
|
|
28
33
|
if: z.string().optional(),
|
|
29
34
|
/** Only run in a repo that uses this module. */
|
|
30
35
|
module: z.enum(MODULES).optional(),
|
|
36
|
+
/** Only run in a repo of one of these kinds. */
|
|
37
|
+
kinds: z.array(z.enum(KINDS)).optional(),
|
|
31
38
|
})
|
|
32
39
|
.strict();
|
|
33
40
|
const kebab = z.string().regex(/^[a-z0-9-]+$/, "use lowercase-kebab");
|
|
@@ -80,7 +87,7 @@ export const layerSchema = z
|
|
|
80
87
|
modules: z.array(z.enum(MODULES)),
|
|
81
88
|
expo: z.object({ sdk: z.number().int().positive() }).partial(),
|
|
82
89
|
packs: z.array(z.string().regex(/^[a-z0-9-]+$/, "pack names are lowercase-kebab")),
|
|
83
|
-
/**
|
|
90
|
+
/** Standards to add: a bare name imports canon's library standard (`design-sets`); a path (`./rules`) is a folder of the repo's own. */
|
|
84
91
|
standards: z.array(z.string()),
|
|
85
92
|
commands: z.array(command),
|
|
86
93
|
doctor: z.object({ tools: z.array(tool) }).partial(),
|
|
@@ -100,7 +107,14 @@ export const layerSchema = z
|
|
|
100
107
|
.partial()
|
|
101
108
|
.strict();
|
|
102
109
|
export const MODULES_BY_KIND = {
|
|
103
|
-
|
|
110
|
+
mobile: ["docs", "knowledge", "code", "listing", "refs"],
|
|
104
111
|
web: ["docs", "knowledge", "code", "refs"],
|
|
112
|
+
"mobile-and-web": ["docs", "knowledge", "code", "listing", "refs"],
|
|
113
|
+
// A package's docs describe other repos' .context/ paths, so refs would flag them.
|
|
114
|
+
package: ["docs", "knowledge", "code"],
|
|
105
115
|
content: ["docs", "knowledge", "refs"],
|
|
106
116
|
};
|
|
117
|
+
/** A repo that ships both counts as mobile and as web: it gets both kinds' packs. */
|
|
118
|
+
export function kindCovers(kind, wanted) {
|
|
119
|
+
return kind === wanted || (kind === "mobile-and-web" && (wanted === "mobile" || wanted === "web"));
|
|
120
|
+
}
|
package/cli/dist/format.d.ts
CHANGED
package/cli/dist/index.js
CHANGED
|
@@ -16,8 +16,8 @@ import { registerSync } from "./commands/sync.js";
|
|
|
16
16
|
import { registerUi } from "./commands/ui.js";
|
|
17
17
|
import { registerWeigh } from "./commands/weigh.js";
|
|
18
18
|
import { findEstate } from "./config/load.js";
|
|
19
|
-
import { registerMcp } from "./
|
|
20
|
-
import { registerSearch } from "./
|
|
19
|
+
import { registerMcp } from "./commands/mcp.js";
|
|
20
|
+
import { registerSearch } from "./commands/search.js";
|
|
21
21
|
import { canonVersion } from "./status.js";
|
|
22
22
|
const env = {
|
|
23
23
|
canonRoot: resolve(import.meta.dirname, "..", ".."),
|
package/cli/dist/init.js
CHANGED
|
@@ -4,7 +4,7 @@ import { stringify } from "yaml";
|
|
|
4
4
|
import { RULES_FILES } from "./checks/architecture.js";
|
|
5
5
|
import { requiredContext } from "./checks/context-layout.js";
|
|
6
6
|
import { inferKind, inferModules } from "./config/load.js";
|
|
7
|
-
import { MODULES_BY_KIND } from "./config/schema.js";
|
|
7
|
+
import { kindCovers, MODULES_BY_KIND } from "./config/schema.js";
|
|
8
8
|
export const ARCHITECTURE_BASE = ".dependency-cruiser.base.cjs";
|
|
9
9
|
const ARCHITECTURE_TEMPLATES = join("templates", "dependency-cruiser");
|
|
10
10
|
const WORKSPACE_PARENTS = ["apps", "packages"];
|
|
@@ -86,7 +86,7 @@ export function planInit(repo, answers) {
|
|
|
86
86
|
}
|
|
87
87
|
const kind = answers.kind ?? inferKind(repo);
|
|
88
88
|
const modules = answers.modules ?? (answers.kind ? MODULES_BY_KIND[kind] : inferModules(repo, kind));
|
|
89
|
-
const sdk = kind
|
|
89
|
+
const sdk = kindCovers(kind, "mobile") ? detectExpoSdk(repo) : undefined;
|
|
90
90
|
const packs = answers.packs?.length ? { packs: answers.packs } : {};
|
|
91
91
|
const config = { canon: answers.canonVersion, kind, modules, ...(sdk ? { expo: { sdk } } : {}), ...packs };
|
|
92
92
|
const create = requiredContext(modules).filter((name) => !existsSync(join(repo, ".context", name))).map((name) => FIRST_FILE[name]);
|
package/cli/dist/packs.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ declare const packSchema: z.ZodObject<{
|
|
|
4
4
|
description: z.ZodString;
|
|
5
5
|
kinds: z.ZodArray<z.ZodEnum<{
|
|
6
6
|
content: "content";
|
|
7
|
-
|
|
7
|
+
mobile: "mobile";
|
|
8
|
+
"mobile-and-web": "mobile-and-web";
|
|
9
|
+
package: "package";
|
|
8
10
|
web: "web";
|
|
9
11
|
}>>;
|
|
10
12
|
skill: z.ZodOptional<z.ZodString>;
|