@invariant-ui/cli 0.0.0-stage → 0.1.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/LICENSE +105 -0
- package/README.md +248 -2
- package/dist/agent-kit/apply.js +232 -0
- package/dist/agent-kit/guidance.js +236 -0
- package/dist/agent-kit/index.js +5 -0
- package/dist/agent-kit/integrations.js +121 -0
- package/dist/agent-kit/plugin.js +115 -0
- package/dist/artifact-generator/index.js +75 -0
- package/dist/browser-verify/driver.js +2 -0
- package/dist/browser-verify/index.js +4 -0
- package/dist/browser-verify/playwright.js +82 -0
- package/dist/browser-verify/runner.js +175 -0
- package/dist/browser-verify/spec.js +103 -0
- package/dist/cli/args.js +77 -0
- package/dist/cli/baseline.js +64 -0
- package/dist/cli/cli.js +121 -0
- package/dist/cli/commands-browser.js +104 -0
- package/dist/cli/commands-discover.js +312 -0
- package/dist/cli/commands-enforcement.js +35 -0
- package/dist/cli/commands-governance.js +179 -0
- package/dist/cli/commands-hook.js +269 -0
- package/dist/cli/commands-patterns.js +324 -0
- package/dist/cli/commands-setup.js +485 -0
- package/dist/cli/commands-validate.js +157 -0
- package/dist/cli/commands.js +517 -0
- package/dist/cli/config.js +197 -0
- package/dist/cli/diff.js +66 -0
- package/dist/cli/enforcement-log.js +83 -0
- package/dist/cli/enforcement-report.js +96 -0
- package/dist/cli/errors.js +10 -0
- package/dist/cli/format.js +22 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/io.js +2 -0
- package/dist/cli/main.js +31 -0
- package/dist/cli/pipeline.js +262 -0
- package/dist/cli/publish.js +77 -0
- package/dist/cli/report.js +127 -0
- package/dist/cli/starter-rules.js +57 -0
- package/dist/cli/store.js +60 -0
- package/dist/cli/version.js +5 -0
- package/dist/cli/watch.js +68 -0
- package/dist/cli/workflow.js +52 -0
- package/dist/compiler-angular/adapter.js +115 -0
- package/dist/compiler-angular/analyze.js +222 -0
- package/dist/compiler-angular/classes.js +446 -0
- package/dist/compiler-angular/declared.js +285 -0
- package/dist/compiler-angular/emit.js +183 -0
- package/dist/compiler-angular/index.js +4 -0
- package/dist/compiler-angular/template.js +142 -0
- package/dist/compiler-core/adapter.js +2 -0
- package/dist/compiler-core/adapters/inventory.js +312 -0
- package/dist/compiler-core/compile.js +148 -0
- package/dist/compiler-core/component-facts.js +123 -0
- package/dist/compiler-core/discovery/docs.js +66 -0
- package/dist/compiler-core/discovery/entries.js +270 -0
- package/dist/compiler-core/discovery/exports.js +207 -0
- package/dist/compiler-core/discovery/graph.js +59 -0
- package/dist/compiler-core/discovery/index.js +103 -0
- package/dist/compiler-core/discovery/packages.js +148 -0
- package/dist/compiler-core/discovery/schema.js +144 -0
- package/dist/compiler-core/discovery/storybook.js +232 -0
- package/dist/compiler-core/discovery/support.js +82 -0
- package/dist/compiler-core/discovery/tokens.js +186 -0
- package/dist/compiler-core/discovery/tooling.js +146 -0
- package/dist/compiler-core/fs.js +89 -0
- package/dist/compiler-core/glob.js +3 -0
- package/dist/compiler-core/index.js +16 -0
- package/dist/compiler-core/ir-builder.js +68 -0
- package/dist/compiler-core/json-schema.js +11 -0
- package/dist/compiler-core/testing/index.js +81 -0
- package/dist/compiler-core/ts/ast.js +166 -0
- package/dist/compiler-core/ts/members.js +163 -0
- package/dist/compiler-core/ts/program.js +81 -0
- package/dist/compiler-core/ts/static-eval.js +145 -0
- package/dist/compiler-core/ts/tsconfig-paths.js +60 -0
- package/dist/compiler-core/walk.js +46 -0
- package/dist/compiler-ir/index.js +4 -0
- package/dist/compiler-ir/ir.js +67 -0
- package/dist/compiler-ir/json-schema.js +6 -0
- package/dist/compiler-ir/validate.js +116 -0
- package/dist/compiler-react/adapter.js +147 -0
- package/dist/compiler-react/analyze.js +199 -0
- package/dist/compiler-react/ast.js +105 -0
- package/dist/compiler-react/behavior.js +75 -0
- package/dist/compiler-react/detect.js +558 -0
- package/dist/compiler-react/emit.js +188 -0
- package/dist/compiler-react/index.js +2 -0
- package/dist/compiler-react/program.js +58 -0
- package/dist/compiler-react/props.js +157 -0
- package/dist/compiler-react/styling.js +138 -0
- package/dist/compiler-react/usage.js +116 -0
- package/dist/compiler-react/variants.js +111 -0
- package/dist/compiler-storybook/adapter.js +253 -0
- package/dist/compiler-storybook/csf.js +521 -0
- package/dist/compiler-storybook/emit.js +202 -0
- package/dist/compiler-storybook/index-json.js +41 -0
- package/dist/compiler-storybook/index.js +5 -0
- package/dist/compiler-storybook/mdx.js +120 -0
- package/dist/compiler-storybook/static-eval.js +145 -0
- package/dist/compiler-tokens/adapter.js +158 -0
- package/dist/compiler-tokens/css.js +59 -0
- package/dist/compiler-tokens/index.js +5 -0
- package/dist/compiler-tokens/json-lines.js +106 -0
- package/dist/compiler-tokens/json-tokens.js +52 -0
- package/dist/compiler-vue/adapter.js +107 -0
- package/dist/compiler-vue/analyze.js +344 -0
- package/dist/compiler-vue/declared.js +343 -0
- package/dist/compiler-vue/emit.js +129 -0
- package/dist/compiler-vue/index.js +5 -0
- package/dist/compiler-vue/script.js +472 -0
- package/dist/compiler-vue/sfc.js +44 -0
- package/dist/compiler-vue/template.js +73 -0
- package/dist/contract/archetype-checks.js +153 -0
- package/dist/contract/components.js +102 -0
- package/dist/contract/conflicts.js +39 -0
- package/dist/contract/contract.js +46 -0
- package/dist/contract/evidence.js +84 -0
- package/dist/contract/fact-path.js +39 -0
- package/dist/contract/facts.js +62 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/integrity.js +21 -0
- package/dist/contract/json-schema.js +11 -0
- package/dist/contract/knowledge.js +95 -0
- package/dist/contract/pattern-checks.js +254 -0
- package/dist/contract/patterns.js +225 -0
- package/dist/contract/primitives.js +81 -0
- package/dist/contract/sources.js +30 -0
- package/dist/contract/tokens.js +41 -0
- package/dist/contract/validate.js +253 -0
- package/dist/contract-query/archetype.js +188 -0
- package/dist/contract-query/index.js +6 -0
- package/dist/contract-query/patterns.js +298 -0
- package/dist/contract-query/scan.js +164 -0
- package/dist/contract-query/search.js +132 -0
- package/dist/contract-query/system.js +148 -0
- package/dist/contract-query/validate-code.js +239 -0
- package/dist/contract-query/validate-usage.js +150 -0
- package/dist/contract-query/views.js +288 -0
- package/dist/governance/decisions.js +229 -0
- package/dist/governance/index.js +5 -0
- package/dist/governance/patterns-file.js +156 -0
- package/dist/governance/patterns.js +510 -0
- package/dist/governance/policies.js +148 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/loader.js +70 -0
- package/dist/mcp/protocol.js +27 -0
- package/dist/mcp/resources.js +138 -0
- package/dist/mcp/server.js +177 -0
- package/dist/mcp/stdio.js +28 -0
- package/dist/mcp/tools.js +267 -0
- package/dist/reconciliation/code.js +62 -0
- package/dist/reconciliation/components.js +401 -0
- package/dist/reconciliation/conflicts.js +126 -0
- package/dist/reconciliation/facts.js +41 -0
- package/dist/reconciliation/index.js +7 -0
- package/dist/reconciliation/reconcile.js +134 -0
- package/dist/reconciliation/resolve.js +158 -0
- package/dist/reconciliation/review.js +81 -0
- package/dist/reconciliation/storybook.js +370 -0
- package/dist/reconciliation/tokens.js +179 -0
- package/dist/reconciliation/util.js +55 -0
- package/dist/semantic-engine/additive.js +51 -0
- package/dist/semantic-engine/compounds.js +91 -0
- package/dist/semantic-engine/documents.js +157 -0
- package/dist/semantic-engine/enrich.js +42 -0
- package/dist/semantic-engine/index.js +7 -0
- package/dist/semantic-engine/infer.js +38 -0
- package/dist/semantic-engine/tokens.js +120 -0
- package/dist/shared/canonical.js +65 -0
- package/dist/shared/diagnostic.js +78 -0
- package/dist/shared/glob.js +98 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/json.js +2 -0
- package/dist/validation/card.js +50 -0
- package/dist/validation/discovery/arrangements.js +162 -0
- package/dist/validation/discovery/discover.js +70 -0
- package/dist/validation/discovery/index.js +8 -0
- package/dist/validation/discovery/model.js +127 -0
- package/dist/validation/discovery/promote.js +24 -0
- package/dist/validation/discovery/reviews.js +63 -0
- package/dist/validation/discovery/shapes.js +47 -0
- package/dist/validation/discovery/similar-files.js +146 -0
- package/dist/validation/discovery/util.js +47 -0
- package/dist/validation/index.js +11 -0
- package/dist/validation/interaction-facts.js +341 -0
- package/dist/validation/interaction-workflows.js +633 -0
- package/dist/validation/literals.js +251 -0
- package/dist/validation/pattern-rules.js +608 -0
- package/dist/validation/policy.js +241 -0
- package/dist/validation/report.js +163 -0
- package/dist/validation/rules.js +269 -0
- package/dist/validation/scan.js +295 -0
- package/dist/validation/text.js +34 -0
- package/dist/validation/validate-code.js +513 -0
- package/dist/validation/validate-usage.js +196 -0
- package/package.json +38 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Matt Carlucci
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,249 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark-light.svg">
|
|
5
|
+
<img alt="Invariant" src="https://raw.githubusercontent.com/mcarlucci/invariant/main/site/assets/logo-wordmark-light.svg" width="280">
|
|
6
|
+
</picture>
|
|
7
|
+
</p>
|
|
2
8
|
|
|
3
|
-
|
|
9
|
+
<p align="center"><strong>Turn your design system into a contract that AI agents can read and check.</strong></p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://mcarlucci.github.io/invariant/">Website</a> ·
|
|
13
|
+
<a href="https://mcarlucci.github.io/invariant/docs/">Documentation</a> ·
|
|
14
|
+
<a href="https://github.com/mcarlucci/invariant/blob/main/docs/specifications">Specifications</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Invariant scans your existing design system (React, Vue, Angular, Storybook, design tokens) and compiles it into one machine-readable file: the **contract**. AI coding tools then use that contract to learn which components, props and tokens really exist, and to check that generated code follows them.
|
|
18
|
+
|
|
19
|
+
No cloud service, no account, no network access. Everything runs locally.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Why use it?
|
|
24
|
+
|
|
25
|
+
AI assistants guess at design systems. They invent props, hard-code colors and use the wrong component. Invariant gives them the facts:
|
|
26
|
+
|
|
27
|
+
- **Accurate.** Components, props, events and tokens are extracted from your source code, not made up by an LLM.
|
|
28
|
+
- **Traceable.** Every fact points back to the file and line it came from.
|
|
29
|
+
- **Honest.** Each fact is labeled `discovered`, `documented`, `inferred` or `approved`. Guesses are never passed off as facts, and conflicting sources are reported, not hidden.
|
|
30
|
+
- **Portable.** The contract is plain JSON. It doesn't depend on a frontend framework or on MCP.
|
|
31
|
+
- **Safe.** Your code is analyzed statically. Nothing from your project is executed.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Your sources Invariant Consumers
|
|
37
|
+
┌──────────────┐ ┌───────────────────┐ ┌────────────────────┐
|
|
38
|
+
│ React / Vue │ │ 1. Discover │ │ AI agents (MCP) │
|
|
39
|
+
│ Angular │ ───▶ │ 2. Extract to IR │ ───▶ │ CLI queries │
|
|
40
|
+
│ Storybook │ │ 3. Reconcile │ │ Usage validation │
|
|
41
|
+
│ Token files │ │ 4. Write contract │ │ HTML report │
|
|
42
|
+
└──────────────┘ └───────────────────┘ └────────────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each source adapter turns what it finds into a shared intermediate representation (IR). A framework-independent compiler then merges those into the contract.
|
|
46
|
+
|
|
47
|
+
## Requirements
|
|
48
|
+
|
|
49
|
+
- Node.js 20 or newer to run it. To work on Invariant itself: Node 24 (see `.nvmrc`) and [pnpm](https://pnpm.io).
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
Fastest path, with nothing to install first. In your project:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx @invariant-ui/cli setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It compiles your design system, finds the patterns your code already repeats, and shows one plan. Answer yes once and it approves the patterns, turns on starter rules (as warnings), connects your AI tool, baselines existing violations and adds a GitHub Action. In a design system repository it also makes the published package carry the contract. Nothing is approved until you confirm the contract. To keep the command, run `npm install -g @invariant-ui/cli`, then `invariant setup`.
|
|
60
|
+
|
|
61
|
+
From a source checkout, or one step at a time:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# 1. Install and build (from a source checkout)
|
|
65
|
+
pnpm install
|
|
66
|
+
pnpm build
|
|
67
|
+
|
|
68
|
+
# 2. Go to the project you want to analyze and create a config
|
|
69
|
+
pnpm invariant init --cwd /path/to/your/project
|
|
70
|
+
|
|
71
|
+
# 3. Compile the contract
|
|
72
|
+
pnpm invariant compile --cwd /path/to/your/project
|
|
73
|
+
|
|
74
|
+
# 4. Explore it
|
|
75
|
+
pnpm invariant components list --cwd /path/to/your/project
|
|
76
|
+
pnpm invariant components inspect Button --cwd /path/to/your/project
|
|
77
|
+
pnpm invariant tokens list --cwd /path/to/your/project
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`pnpm invariant` runs `node apps/cli/dist/main.js`.
|
|
81
|
+
|
|
82
|
+
Compiling writes these files to the `invariant/` folder:
|
|
83
|
+
|
|
84
|
+
| File | What it is |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `invariant.contract.json` | The contract (the source of truth) |
|
|
87
|
+
| `invariant.evidence.json` | Where each fact came from |
|
|
88
|
+
| `invariant.diagnostics.json` | Anything unsupported, incomplete or wrong |
|
|
89
|
+
| `invariant.review.json` | Conflicts and guesses waiting for a human decision |
|
|
90
|
+
| `invariant.report.html` | A static, self-contained summary for people |
|
|
91
|
+
| `invariant.manifest.json` | Tool version and a hash of every file |
|
|
92
|
+
|
|
93
|
+
Compiling unchanged sources gives identical output, so the files are safe to commit and diff.
|
|
94
|
+
|
|
95
|
+
## Use it in your project
|
|
96
|
+
|
|
97
|
+
- **The design system is in your repo** (a package in a monorepo, or the repo itself): run `npx @invariant-ui/cli setup` at the root.
|
|
98
|
+
- **Your app uses a design system from npm:** the design system's repo compiles a contract and publishes its `invariant/` folder with the package. In your app, install the package and run `npx @invariant-ui/cli setup`. It finds the shipped contract, adds your own patterns on top, and writes a config with `"extends": { "contract": "node_modules/<package>/invariant" }`. `verify --fresh` flags the contract as stale when you upgrade the library.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
`invariant init` writes `invariant.config.json` for you. You can also edit it by hand:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"name": "Acme Design System",
|
|
107
|
+
"contractId": "acme-design-system",
|
|
108
|
+
"sources": [
|
|
109
|
+
{ "id": "ui-library", "type": "react", "path": "packages/ui" },
|
|
110
|
+
{ "id": "storybook", "type": "storybook", "path": "packages/ui" },
|
|
111
|
+
{ "id": "tokens", "type": "tokens" }
|
|
112
|
+
],
|
|
113
|
+
"output": { "directory": "invariant" }
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Supported source types: `react`, `vue`, `angular`, `storybook`, `tokens`. Figma and zeroheight are not supported yet.
|
|
118
|
+
|
|
119
|
+
Add sources from the command line with `invariant source add <type> [path]`.
|
|
120
|
+
|
|
121
|
+
Optional sections: `validation` (see `invariant rules`; `ignore` globs, `rules` severity overrides, `failOn`), `governance` (approver lists and the governance files' paths) and `enforcement` (`hooks`: `gate-errors` by default, `warn` or `off` — what the Claude Code hooks do when an agent writes code that breaks the contract).
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
| Command | What it does |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `init` | Detect your project and write `invariant.config.json` |
|
|
128
|
+
| `source add` / `source remove` / `source list` | Manage configured sources |
|
|
129
|
+
| `analyze` | Extract and print what was found, without building a contract |
|
|
130
|
+
| `compile` | Run the full pipeline and write the contract and other files (`--watch` recompiles on change) |
|
|
131
|
+
| `components list` / `components inspect <name>` | Browse components (add `--evidence` to see source locations) |
|
|
132
|
+
| `tokens list`, `conflicts list` | Browse tokens and conflicts |
|
|
133
|
+
| `patterns list` / `inspect` / `resolve` / `search` / `relationships` | Browse patterns: parts, states, constraints, relationships, approval status and evidence; `resolve` reads a page archetype with everything it composes |
|
|
134
|
+
| `browser run` | Opt-in: check a running app in a headless browser (needs Playwright in your project). Runtime results, kept apart from `validate` |
|
|
135
|
+
| `setup` | One command: detect sources, compile, show what was found, propose patterns from your code, connect your AI tool, baseline existing violations and offer a GitHub Action that checks every pull request commit. Asks a few questions; nothing is approved until you confirm the contract |
|
|
136
|
+
| `discover <path...>` | Find structures that recur in your application code and write them as candidates to review. Nothing it finds is approved or enforced |
|
|
137
|
+
| `candidates list\|show\|accept\|reject\|defer\|promote` | Review candidates; `promote` adds an accepted one to `invariant.patterns.json` without approving it |
|
|
138
|
+
| `validate <path...>` | Check your application code against the contract and the structural rules of approved patterns |
|
|
139
|
+
| `verify` | Check the compiled output is intact (add `--fresh` to also detect stale sources and see what changed) |
|
|
140
|
+
| `rules`, `policy list` | List validation rules and approved policies |
|
|
141
|
+
| `review list\|show\|approve\|reject\|resolve\|accept\|dismiss` | Record human decisions on conflicts and inferred facts |
|
|
142
|
+
| `mcp` | Serve the contract to AI tools over stdio (read-only) |
|
|
143
|
+
| `hook` | Check an agent's Write/Edit against the contract and answer in its hook format (Claude Code runs this for you) |
|
|
144
|
+
| `agent init <client\|all>` | Write MCP config, skills, rules and enforcement hooks for Claude Code, Cursor, Codex and Windsurf |
|
|
145
|
+
| `export --to <dir>` | Copy the verified artifacts and JSON Schemas elsewhere |
|
|
146
|
+
|
|
147
|
+
Read commands accept `--json`. Global options: `--cwd`, `--config`, `--dir`, `--trust-config`.
|
|
148
|
+
|
|
149
|
+
Exit codes: `0` success, `1` findings or invalid output, `2` bad usage or input.
|
|
150
|
+
|
|
151
|
+
## Use it with an AI agent
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
invariant compile
|
|
155
|
+
invariant agent init claude-code # or cursor, codex, windsurf, all
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
With Claude Code, you watch the contract being enforced. Ask an agent for a settings page with a delete button and every file it writes is checked before it lands:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
1. invariant blocked app/pages/Settings.tsx: 1 error against the @acme/ui contract
|
|
162
|
+
✗ prop-value-not-allowed: Button.variant is "danger", which is not one of "primary", "secondary", "ghost" (line 8)
|
|
163
|
+
fix: Use one of "primary", "secondary", "ghost".
|
|
164
|
+
2. ✗ color-not-in-tokens: #ff0000 is a literal color that is not the value of any token (line 8)
|
|
165
|
+
3. ✗ pattern-required-child-missing: Modal at line 11 has no Button (pattern pattern:page.settings) (line 11)
|
|
166
|
+
4. invariant ✓ app/pages/Settings.tsx follows the @acme/ui contract (6 component uses checked)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Run `pnpm demo` in a checkout to replay exactly that, and `invariant enforcement report` (add `--format html --to report.html`) to get a forwardable timeline: `4 writes checked · 3 violations caught · 3 fixed · 0 outstanding`. See [See it working in your agent chat](https://mcarlucci.github.io/invariant/docs/#enforcement).
|
|
170
|
+
|
|
171
|
+
`agent init` installs the hooks for Claude Code: a write that would introduce an error-severity violation is blocked with the exact finding and fix; warnings show up right after the write; clean passes get a green line in the chat. The hooks fail open (if invariant cannot run, the write proceeds) and respect your baseline and rule overrides. Set `enforcement.hooks` to `warn` or `off` in `invariant.config.json` to soften or disable this.
|
|
172
|
+
|
|
173
|
+
| Client | How the contract is applied | Blocks a write |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| Claude Code | Hooks on Write and Edit, plus MCP and skills | Yes |
|
|
176
|
+
| Cursor | MCP and rules; the agent is asked to validate | No |
|
|
177
|
+
| Codex, Windsurf | MCP and rules; the agent is asked to validate | No |
|
|
178
|
+
| Any client | `invariant validate` in CI | At pull request time |
|
|
179
|
+
|
|
180
|
+
The MCP server is read-only and gives the agent tools such as `get_component`, `get_design_tokens`, `search_design_system` and `validate_component_usage`. After a recompile, the server picks up the new contract without a restart. Whether the agent calls `validate_design_system_compliance` itself or a hook fires on its write, the chat shows the same enforcement card: the findings, the fixes, and what to do next. The enforcement report records hook checks only, not the agent's own MCP calls.
|
|
181
|
+
|
|
182
|
+
Contract text comes from your source and docs, so the server marks every result as untrusted content. Generated agent config never includes free-form descriptions from your code.
|
|
183
|
+
|
|
184
|
+
## Repository layout
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
apps/
|
|
188
|
+
cli/ The `invariant` command
|
|
189
|
+
packages/
|
|
190
|
+
compiler-ir/ Shared intermediate representation
|
|
191
|
+
compiler-core/ Pure compiler: IR to contract
|
|
192
|
+
compiler-react/ React adapter
|
|
193
|
+
compiler-vue/ Vue adapter
|
|
194
|
+
compiler-angular/ Angular adapter
|
|
195
|
+
compiler-storybook/ Storybook adapter
|
|
196
|
+
compiler-tokens/ Design token adapter
|
|
197
|
+
contract/ Contract schema and validation
|
|
198
|
+
contract-query/ Read API over a compiled contract
|
|
199
|
+
reconciliation/ Merging sources and surfacing conflicts
|
|
200
|
+
semantic-engine/ Classification and inference, with evidence
|
|
201
|
+
validation/ Usage validators and rules, and pattern discovery
|
|
202
|
+
browser-verify/ Opt-in runtime checks in a browser
|
|
203
|
+
governance/ Approvals, policies and decisions
|
|
204
|
+
artifact-generator/ Writes the output files
|
|
205
|
+
mcp/ MCP server
|
|
206
|
+
agent-kit/ Client configs, skills and rules
|
|
207
|
+
shared/ Shared utilities and structured diagnostics
|
|
208
|
+
fixtures/ Test projects and golden files
|
|
209
|
+
docs/ Architecture, ADRs and specifications
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pnpm build # tsc -b
|
|
216
|
+
pnpm test # vitest
|
|
217
|
+
pnpm lint # biome
|
|
218
|
+
pnpm typecheck
|
|
219
|
+
pnpm check:boundaries # enforce package boundaries
|
|
220
|
+
pnpm verify # lint + typecheck + boundaries + build + test
|
|
221
|
+
pnpm update:golden # regenerate golden fixtures
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Documentation
|
|
225
|
+
|
|
226
|
+
- [Website](https://mcarlucci.github.io/invariant/) and [documentation site](https://mcarlucci.github.io/invariant/docs/)
|
|
227
|
+
- [System architecture](https://github.com/mcarlucci/invariant/blob/main/docs/architecture/system-architecture.md)
|
|
228
|
+
- [Pattern definitions](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/pattern-definitions-v1.md): write layouts, interactions and page archetypes in `invariant.patterns.json`
|
|
229
|
+
- [Browser verification](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/browser-verification-v1.md): opt-in runtime checks for focus, keyboard order, rendered composition and responsive layout
|
|
230
|
+
- [Pattern discovery](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/pattern-discovery-v1.md): find recurring structures in your code and review them as candidates
|
|
231
|
+
- [ADR 0001: Foundational architecture](https://github.com/mcarlucci/invariant/blob/main/docs/decisions/0001-architecture.md)
|
|
232
|
+
- Specifications in [`docs/specifications/`](https://github.com/mcarlucci/invariant/blob/main/docs/specifications): [CLI](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/cli-v1.md), [contract](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/contract-v1.md), [MCP and agents](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/mcp-and-agents-v1.md), [validation and governance](https://github.com/mcarlucci/invariant/blob/main/docs/specifications/validation-and-governance-v1.md), and more
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
Invariant is source-available under the [Functional Source License, Version 1.1, ALv2 Future License](https://github.com/mcarlucci/invariant/blob/main/LICENSE) (FSL-1.1-ALv2).
|
|
237
|
+
|
|
238
|
+
You can read, run, modify and redistribute it for any purpose except a Competing Use: offering it in a commercial product or service that substitutes for Invariant or offers the same or similar functionality. Internal use, non-commercial education and research, and professional services for a licensee are always allowed.
|
|
239
|
+
|
|
240
|
+
Each version converts to Apache-2.0 two years after it is released.
|
|
241
|
+
|
|
242
|
+
Copyright 2026 Matt Carlucci.
|
|
243
|
+
|
|
244
|
+
## Principles
|
|
245
|
+
|
|
246
|
+
- The contract is the single source of truth.
|
|
247
|
+
- Extract deterministically. LLMs may help classify, but never supply APIs, props or token values.
|
|
248
|
+
- Keep evidence for every fact.
|
|
249
|
+
- Never hide conflicts, and never turn a guess into an approved rule automatically.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MARKER } from "./guidance.js";
|
|
4
|
+
const BEGIN = "<!-- invariant:begin -->";
|
|
5
|
+
const END = "<!-- invariant:end -->";
|
|
6
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
7
|
+
/** Resolves a project-relative path, refusing anything that would land outside the root, symlinks included. */
|
|
8
|
+
export function resolveInside(root, rel) {
|
|
9
|
+
if (rel === "" || path.isAbsolute(rel) || rel.split(/[\\/]/).includes(".."))
|
|
10
|
+
throw new Error(`unsafe path: ${rel}`);
|
|
11
|
+
const target = path.resolve(root, rel);
|
|
12
|
+
const realRoot = realpathSync(root);
|
|
13
|
+
let probe = target;
|
|
14
|
+
while (!existsSync(probe))
|
|
15
|
+
probe = path.dirname(probe);
|
|
16
|
+
const real = realpathSync(probe);
|
|
17
|
+
if (real !== realRoot && !real.startsWith(realRoot + path.sep))
|
|
18
|
+
throw new Error(`path escapes the project: ${rel}`);
|
|
19
|
+
if (existsSync(target) && lstatSync(target).isSymbolicLink())
|
|
20
|
+
throw new Error(`refusing to write through a symbolic link: ${rel}`);
|
|
21
|
+
return target;
|
|
22
|
+
}
|
|
23
|
+
function write(file, text, dryRun) {
|
|
24
|
+
if (dryRun)
|
|
25
|
+
return;
|
|
26
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
writeFileSync(file, text, "utf8");
|
|
28
|
+
}
|
|
29
|
+
function jsonMerge(file, f, force, dryRun) {
|
|
30
|
+
const rel = f.path;
|
|
31
|
+
let doc = {};
|
|
32
|
+
const existed = existsSync(file);
|
|
33
|
+
if (existed) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
36
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
37
|
+
throw new Error("not an object");
|
|
38
|
+
doc = parsed;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {
|
|
42
|
+
path: rel,
|
|
43
|
+
action: "skipped",
|
|
44
|
+
reason: "the file is not plain JSON (comments or trailing commas?), so it was left alone. Add the entry by hand."
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let node = doc;
|
|
49
|
+
for (const key of f.key.slice(0, -1)) {
|
|
50
|
+
const next = node[key];
|
|
51
|
+
if (next === undefined) {
|
|
52
|
+
const created = {};
|
|
53
|
+
node[key] = created;
|
|
54
|
+
node = created;
|
|
55
|
+
}
|
|
56
|
+
else if (typeof next === "object" && next !== null && !Array.isArray(next))
|
|
57
|
+
node = next;
|
|
58
|
+
else
|
|
59
|
+
return { path: rel, action: "skipped", reason: `"${key}" is not an object, so the file was left alone` };
|
|
60
|
+
}
|
|
61
|
+
const last = f.key[f.key.length - 1];
|
|
62
|
+
if (same(node[last], f.value))
|
|
63
|
+
return { path: rel, action: "unchanged" };
|
|
64
|
+
if (node[last] !== undefined && !force)
|
|
65
|
+
return {
|
|
66
|
+
path: rel,
|
|
67
|
+
action: "skipped",
|
|
68
|
+
reason: `an entry "${last}" already exists with different settings; use --force to replace it`
|
|
69
|
+
};
|
|
70
|
+
node[last] = f.value;
|
|
71
|
+
write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
|
|
72
|
+
return { path: rel, action: existed ? "updated" : "created" };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Makes sure an array at `key` holds our elements, leaving any the user configured alone. An element that
|
|
76
|
+
* carries our `identity` is ours: identical means unchanged, different means replace with `--force`, and no
|
|
77
|
+
* identity anywhere means append. A non-array at the key is never touched.
|
|
78
|
+
*/
|
|
79
|
+
function jsonArrayAppend(file, f, force, dryRun) {
|
|
80
|
+
const rel = f.path;
|
|
81
|
+
let doc = {};
|
|
82
|
+
const existed = existsSync(file);
|
|
83
|
+
if (existed) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
86
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
87
|
+
throw new Error("not an object");
|
|
88
|
+
doc = parsed;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {
|
|
92
|
+
path: rel,
|
|
93
|
+
action: "skipped",
|
|
94
|
+
reason: "the file is not plain JSON (comments or trailing commas?), so it was left alone. Add the entry by hand."
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let node = doc;
|
|
99
|
+
for (const key of f.key.slice(0, -1)) {
|
|
100
|
+
const next = node[key];
|
|
101
|
+
if (next === undefined) {
|
|
102
|
+
const created = {};
|
|
103
|
+
node[key] = created;
|
|
104
|
+
node = created;
|
|
105
|
+
}
|
|
106
|
+
else if (typeof next === "object" && next !== null && !Array.isArray(next))
|
|
107
|
+
node = next;
|
|
108
|
+
else
|
|
109
|
+
return { path: rel, action: "skipped", reason: `"${key}" is not an object, so the file was left alone` };
|
|
110
|
+
}
|
|
111
|
+
const last = f.key[f.key.length - 1];
|
|
112
|
+
const arr = node[last];
|
|
113
|
+
if (arr === undefined) {
|
|
114
|
+
node[last] = f.value;
|
|
115
|
+
write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
|
|
116
|
+
return { path: rel, action: existed ? "updated" : "created" };
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(arr))
|
|
119
|
+
return { path: rel, action: "skipped", reason: `"${last}" is not an array, so the file was left alone` };
|
|
120
|
+
if (f.value.every((el) => arr.some((x) => same(x, el))))
|
|
121
|
+
return { path: rel, action: "unchanged" };
|
|
122
|
+
const isOurs = (el) => JSON.stringify(el).includes(f.identity);
|
|
123
|
+
if (arr.some(isOurs)) {
|
|
124
|
+
if (!force)
|
|
125
|
+
return {
|
|
126
|
+
path: rel,
|
|
127
|
+
action: "skipped",
|
|
128
|
+
reason: "an invariant hook already exists with different settings; use --force to replace it"
|
|
129
|
+
};
|
|
130
|
+
node[last] = [...arr.filter((el) => !isOurs(el)), ...f.value];
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
node[last] = [...arr, ...f.value];
|
|
134
|
+
write(file, `${JSON.stringify(doc, null, 2)}\n`, dryRun);
|
|
135
|
+
return { path: rel, action: existed ? "updated" : "created" };
|
|
136
|
+
}
|
|
137
|
+
function tomlTable(file, f, force, dryRun) {
|
|
138
|
+
if (!existsSync(file)) {
|
|
139
|
+
write(file, f.text, dryRun);
|
|
140
|
+
return { path: f.path, action: "created" };
|
|
141
|
+
}
|
|
142
|
+
const text = readFileSync(file, "utf8");
|
|
143
|
+
const header = `[${f.table}]`;
|
|
144
|
+
const lines = text.split("\n");
|
|
145
|
+
const start = lines.findIndex((l) => l.trim() === header);
|
|
146
|
+
if (start < 0) {
|
|
147
|
+
write(file, `${text}${text.endsWith("\n") || text === "" ? "" : "\n"}\n${f.text}`, dryRun);
|
|
148
|
+
return { path: f.path, action: "updated" };
|
|
149
|
+
}
|
|
150
|
+
let end = start + 1;
|
|
151
|
+
while (end < lines.length && !/^\s*\[/.test(lines[end]))
|
|
152
|
+
end++;
|
|
153
|
+
const current = lines.slice(start, end).join("\n").trimEnd();
|
|
154
|
+
if (current === f.text.trimEnd())
|
|
155
|
+
return { path: f.path, action: "unchanged" };
|
|
156
|
+
if (!force)
|
|
157
|
+
return {
|
|
158
|
+
path: f.path,
|
|
159
|
+
action: "skipped",
|
|
160
|
+
reason: `a [${f.table}] table already exists with different settings; use --force to replace it`
|
|
161
|
+
};
|
|
162
|
+
const next = [...lines.slice(0, start), ...f.text.trimEnd().split("\n"), "", ...lines.slice(end)].join("\n");
|
|
163
|
+
write(file, next, dryRun);
|
|
164
|
+
return { path: f.path, action: "updated" };
|
|
165
|
+
}
|
|
166
|
+
function generated(file, f, force, dryRun) {
|
|
167
|
+
if (!existsSync(file)) {
|
|
168
|
+
write(file, f.content, dryRun);
|
|
169
|
+
return { path: f.path, action: "created" };
|
|
170
|
+
}
|
|
171
|
+
const current = readFileSync(file, "utf8");
|
|
172
|
+
if (current === f.content)
|
|
173
|
+
return { path: f.path, action: "unchanged" };
|
|
174
|
+
if (!current.includes(MARKER) && !force)
|
|
175
|
+
return {
|
|
176
|
+
path: f.path,
|
|
177
|
+
action: "skipped",
|
|
178
|
+
reason: "the file exists and was not generated by invariant; use --force to replace it"
|
|
179
|
+
};
|
|
180
|
+
write(file, f.content, dryRun);
|
|
181
|
+
return { path: f.path, action: "updated" };
|
|
182
|
+
}
|
|
183
|
+
function markdownSection(file, f, dryRun) {
|
|
184
|
+
const block = `${BEGIN}\n<!-- ${MARKER}; edits between the markers are replaced when it is regenerated -->\n${f.content.trimEnd()}\n${END}\n`;
|
|
185
|
+
if (!existsSync(file)) {
|
|
186
|
+
write(file, block, dryRun);
|
|
187
|
+
return { path: f.path, action: "created" };
|
|
188
|
+
}
|
|
189
|
+
const text = readFileSync(file, "utf8");
|
|
190
|
+
const b = text.indexOf(BEGIN);
|
|
191
|
+
const e = text.indexOf(END);
|
|
192
|
+
if (b >= 0 && e > b) {
|
|
193
|
+
const next = `${text.slice(0, b)}${block.trimEnd()}${text.slice(e + END.length)}`;
|
|
194
|
+
if (next === text)
|
|
195
|
+
return { path: f.path, action: "unchanged" };
|
|
196
|
+
write(file, next, dryRun);
|
|
197
|
+
return { path: f.path, action: "updated" };
|
|
198
|
+
}
|
|
199
|
+
if (b >= 0 || e >= 0)
|
|
200
|
+
return {
|
|
201
|
+
path: f.path,
|
|
202
|
+
action: "skipped",
|
|
203
|
+
reason: "the invariant markers are unbalanced, so the file was left alone"
|
|
204
|
+
};
|
|
205
|
+
write(file, `${text}${text.endsWith("\n") || text === "" ? "" : "\n"}\n${block}`, dryRun);
|
|
206
|
+
return { path: f.path, action: "updated" };
|
|
207
|
+
}
|
|
208
|
+
/** Writes planned files under `root`. Nothing outside the root is touched, and user files are merged or left alone. */
|
|
209
|
+
export function applyPlan(root, files, options = {}) {
|
|
210
|
+
const force = options.force === true;
|
|
211
|
+
const dryRun = options.dryRun === true;
|
|
212
|
+
const one = (f) => {
|
|
213
|
+
let file;
|
|
214
|
+
try {
|
|
215
|
+
file = resolveInside(root, f.path);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return { path: f.path, action: "skipped", reason: error.message };
|
|
219
|
+
}
|
|
220
|
+
if (f.kind === "json-merge")
|
|
221
|
+
return jsonMerge(file, f, force, dryRun);
|
|
222
|
+
if (f.kind === "json-array-append")
|
|
223
|
+
return jsonArrayAppend(file, f, force, dryRun);
|
|
224
|
+
if (f.kind === "toml-table")
|
|
225
|
+
return tomlTable(file, f, force, dryRun);
|
|
226
|
+
if (f.kind === "generated")
|
|
227
|
+
return generated(file, f, force, dryRun);
|
|
228
|
+
return markdownSection(file, f, dryRun);
|
|
229
|
+
};
|
|
230
|
+
return files.map(one);
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=apply.js.map
|