knitto 0.0.1
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 +21 -0
- package/README.md +869 -0
- package/dist/src/checks/run.d.ts +3 -0
- package/dist/src/checks/run.js +173 -0
- package/dist/src/checks/run.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +406 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.js +585 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context/build.d.ts +2 -0
- package/dist/src/context/build.js +147 -0
- package/dist/src/context/build.js.map +1 -0
- package/dist/src/context/render.d.ts +7 -0
- package/dist/src/context/render.js +74 -0
- package/dist/src/context/render.js.map +1 -0
- package/dist/src/engine/apply.d.ts +2 -0
- package/dist/src/engine/apply.js +96 -0
- package/dist/src/engine/apply.js.map +1 -0
- package/dist/src/engine/diff.d.ts +2 -0
- package/dist/src/engine/diff.js +100 -0
- package/dist/src/engine/diff.js.map +1 -0
- package/dist/src/engine/plan.d.ts +2 -0
- package/dist/src/engine/plan.js +259 -0
- package/dist/src/engine/plan.js.map +1 -0
- package/dist/src/errors.d.ts +14 -0
- package/dist/src/errors.js +31 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/filesystem/paths.d.ts +2 -0
- package/dist/src/filesystem/paths.js +47 -0
- package/dist/src/filesystem/paths.js.map +1 -0
- package/dist/src/hooks/run.d.ts +10 -0
- package/dist/src/hooks/run.js +105 -0
- package/dist/src/hooks/run.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lock.d.ts +2 -0
- package/dist/src/lock.js +16 -0
- package/dist/src/lock.js.map +1 -0
- package/dist/src/onboarding.d.ts +26 -0
- package/dist/src/onboarding.js +212 -0
- package/dist/src/onboarding.js.map +1 -0
- package/dist/src/parsers/index.d.ts +18 -0
- package/dist/src/parsers/index.js +194 -0
- package/dist/src/parsers/index.js.map +1 -0
- package/dist/src/parsers/json.d.ts +17 -0
- package/dist/src/parsers/json.js +214 -0
- package/dist/src/parsers/json.js.map +1 -0
- package/dist/src/parsers/merge.d.ts +1 -0
- package/dist/src/parsers/merge.js +17 -0
- package/dist/src/parsers/merge.js.map +1 -0
- package/dist/src/project/discover.d.ts +2 -0
- package/dist/src/project/discover.js +144 -0
- package/dist/src/project/discover.js.map +1 -0
- package/dist/src/rules/json-pointer.d.ts +3 -0
- package/dist/src/rules/json-pointer.js +114 -0
- package/dist/src/rules/json-pointer.js.map +1 -0
- package/dist/src/snapshots/canonical.d.ts +11 -0
- package/dist/src/snapshots/canonical.js +150 -0
- package/dist/src/snapshots/canonical.js.map +1 -0
- package/dist/src/sources/process.d.ts +3 -0
- package/dist/src/sources/process.js +28 -0
- package/dist/src/sources/process.js.map +1 -0
- package/dist/src/sources/resolve.d.ts +3 -0
- package/dist/src/sources/resolve.js +144 -0
- package/dist/src/sources/resolve.js.map +1 -0
- package/dist/src/template/init.d.ts +1 -0
- package/dist/src/template/init.js +62 -0
- package/dist/src/template/init.js.map +1 -0
- package/dist/src/template/manifest.d.ts +2 -0
- package/dist/src/template/manifest.js +80 -0
- package/dist/src/template/manifest.js.map +1 -0
- package/dist/src/template/release.d.ts +3 -0
- package/dist/src/template/release.js +8 -0
- package/dist/src/template/release.js.map +1 -0
- package/dist/src/template/validate.d.ts +2 -0
- package/dist/src/template/validate.js +62 -0
- package/dist/src/template/validate.js.map +1 -0
- package/dist/src/types.d.ts +242 -0
- package/dist/src/types.js +4 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.js +3 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
# Knitto
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/assets/knitto.png" alt="Knitto knitted K logo" width="320">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Knitto is a Unix-first declarative filesystem reconciler.
|
|
8
|
+
|
|
9
|
+
It renders versioned Handlebars templates into a desired directory state,
|
|
10
|
+
compares that state with existing files, presents a reviewable plan, and
|
|
11
|
+
applies changes safely.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
knitto check ./project
|
|
15
|
+
knitto plan ./project
|
|
16
|
+
knitto apply ./project
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Knitto supports:
|
|
20
|
+
|
|
21
|
+
- Whole-file generation and deletion.
|
|
22
|
+
- Structured JSON, YAML, INI, and package manifest reconciliation.
|
|
23
|
+
- Preserving unmanaged content.
|
|
24
|
+
- Explicit overrides and exclusions.
|
|
25
|
+
- Required project inputs.
|
|
26
|
+
- Reproducible, content-addressed template versions.
|
|
27
|
+
- Atomic updates and stale-change protection.
|
|
28
|
+
- Local, remote archive, and source-control template transports.
|
|
29
|
+
|
|
30
|
+
In shorter terms, Knitto is a template-driven tool for keeping directory trees
|
|
31
|
+
aligned with a declared standard: `rsync`-style convergence with Handlebars
|
|
32
|
+
templates and structured-file awareness.
|
|
33
|
+
|
|
34
|
+
The core engine operates on directories. Source control, pull requests, CI
|
|
35
|
+
systems, and future hosted services can integrate with the engine without
|
|
36
|
+
defining its behavior.
|
|
37
|
+
|
|
38
|
+
## Status
|
|
39
|
+
|
|
40
|
+
This repository contains an early proof of concept. It supports:
|
|
41
|
+
|
|
42
|
+
- Handlebars whole-file templates.
|
|
43
|
+
- Deep JSON merges, exact subtrees, template-local `{{remove}}`, and optional managed JSON Pointer fields.
|
|
44
|
+
- Standard RFC 6902 JSON Patch output for structured plans.
|
|
45
|
+
- Text, JSON, package.json, YAML, and INI parser modes.
|
|
46
|
+
- Root and workspace scopes with package-level configuration overlays.
|
|
47
|
+
- Layered manifests, partials, conditional rules, templated paths, and glob removals.
|
|
48
|
+
- Required-package, unwanted-package, engine, and file-content checks.
|
|
49
|
+
- Digest-gated Unix hooks for custom context, parsers, and checks.
|
|
50
|
+
- Local directories, HTTP archives, Git HTTPS, and Git SSH template sources.
|
|
51
|
+
- Content-addressed SHA-256 template snapshots and an XDG cache.
|
|
52
|
+
- Explicit project variables, rule exclusions, field exclusions, and field overrides.
|
|
53
|
+
- Read-only checks and plans.
|
|
54
|
+
- Stale-plan detection, atomic file replacement, and rollback after application errors.
|
|
55
|
+
|
|
56
|
+
## Template Structure
|
|
57
|
+
|
|
58
|
+
A template is an ordinary directory:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
template/
|
|
62
|
+
├── template.json
|
|
63
|
+
└── files/
|
|
64
|
+
├── LICENSE.hbs
|
|
65
|
+
└── package.managed.json.hbs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The manifest defines stable rules and their ownership:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"schemaVersion": 1,
|
|
73
|
+
"name": "node-service",
|
|
74
|
+
"inputs": [
|
|
75
|
+
"package.json"
|
|
76
|
+
],
|
|
77
|
+
"variables": {
|
|
78
|
+
"license": "MIT"
|
|
79
|
+
},
|
|
80
|
+
"rules": [
|
|
81
|
+
{
|
|
82
|
+
"id": "license",
|
|
83
|
+
"type": "file",
|
|
84
|
+
"template": "files/LICENSE.hbs",
|
|
85
|
+
"destination": "LICENSE"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "package-metadata",
|
|
89
|
+
"type": "content",
|
|
90
|
+
"parser": "package-json",
|
|
91
|
+
"template": "files/package.managed.json.hbs",
|
|
92
|
+
"destination": "package.json",
|
|
93
|
+
"exact": [
|
|
94
|
+
"/repository"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A whole-file rule owns the complete destination. Structured parsers deeply merge rendered content by default. `exact` or an optional pointer allowlist narrows or strengthens ownership, while `{{remove}}` expresses property removal inside the template.
|
|
102
|
+
|
|
103
|
+
Whole-file deletion is never inferred. A template must declare a `delete` rule to remove a file.
|
|
104
|
+
|
|
105
|
+
## Handlebars Context
|
|
106
|
+
|
|
107
|
+
Templates receive a platform-neutral context:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"project": {
|
|
112
|
+
"path": "/projects/example",
|
|
113
|
+
"name": "example"
|
|
114
|
+
},
|
|
115
|
+
"files": {
|
|
116
|
+
"package.json": {
|
|
117
|
+
"text": "{ ... }",
|
|
118
|
+
"json": {
|
|
119
|
+
"name": "example"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"metadata": {},
|
|
124
|
+
"variables": {}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For example:
|
|
129
|
+
|
|
130
|
+
```handlebars
|
|
131
|
+
{
|
|
132
|
+
"name": {{json files.[package.json].json.name}},
|
|
133
|
+
"license": {{json variables.license}},
|
|
134
|
+
"repository": {
|
|
135
|
+
"type": "git",
|
|
136
|
+
"url": {{json metadata.url}}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The built-in deterministic helpers include `json`, `obj`, `extGlob`, `join`, `pluck`, `quote`, `last`, `lowercase`, `uppercase`, `basename`, `default`, `appendMissingLines`, `lte`, `eq`, and `semverRangeMajor`. `appendMissingLines` remains supported for compatibility with immutable template snapshots even when newer templates use authoritative whole-file rules. Rendering uses strict Handlebars mode with prototype access disabled.
|
|
142
|
+
|
|
143
|
+
Templates register partials through an explicit name-to-file map:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"partials": {
|
|
148
|
+
"managedScripts": "files/_managed-scripts.hbs",
|
|
149
|
+
"licenseHeader": "files/shared/license-header.hbs"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Names are arbitrary and are not derived from filenames. `{{> managedScripts}}` resolves exactly to the file assigned to the `managedScripts` key.
|
|
155
|
+
|
|
156
|
+
## Conditional Structured Property Deletion
|
|
157
|
+
|
|
158
|
+
JSON and YAML templates define desired values and deletions in one place. `{{remove}}` renders an internal marker that structured parsers consume before writing; it never appears in the destination file.
|
|
159
|
+
|
|
160
|
+
```handlebars
|
|
161
|
+
{
|
|
162
|
+
"license": "MIT",
|
|
163
|
+
"type": {{#if esm}}"module"{{else}}{{remove}}{{/if}},
|
|
164
|
+
"scripts": {
|
|
165
|
+
"lint": "eslint .",
|
|
166
|
+
"format": {{#if prettier}}"prettier . --check"{{else}}{{remove}}{{/if}},
|
|
167
|
+
"template-copy": {{remove}}
|
|
168
|
+
},
|
|
169
|
+
"repository": {
|
|
170
|
+
"type": "git",
|
|
171
|
+
"url": {{json metadata.url}}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"id": "package-json",
|
|
179
|
+
"type": "content",
|
|
180
|
+
"parser": "package-json",
|
|
181
|
+
"template": "package-json.hbs",
|
|
182
|
+
"destination": "package.json",
|
|
183
|
+
"exact": [
|
|
184
|
+
"/repository"
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- The rendered object is deeply merged into the existing document.
|
|
190
|
+
- `exact` replaces those subtrees completely, removing unmentioned properties.
|
|
191
|
+
- `{{remove}}` removes the property at its rendered location.
|
|
192
|
+
- Handlebars conditionals decide whether a property receives a value or is deleted.
|
|
193
|
+
- Pointer exclusions prevent the corresponding exact, merge, or `{{remove}}` operation.
|
|
194
|
+
- Project overrides are applied after template reconciliation.
|
|
195
|
+
|
|
196
|
+
When `prettier` is false, planning produces standard RFC 6902 operations:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
[
|
|
200
|
+
{
|
|
201
|
+
"op": "remove",
|
|
202
|
+
"path": "/scripts/format"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"op": "remove",
|
|
206
|
+
"path": "/scripts/template-copy"
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"op": "remove",
|
|
210
|
+
"path": "/standard"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"op": "remove",
|
|
214
|
+
"path": "/templateVersion"
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
To enforce that an entire object has no unmentioned properties, place its pointer in `exact`. For example, `"/repository"` means the rendered `repository` object is authoritative. An empty pointer, `""`, makes the complete rendered document authoritative.
|
|
220
|
+
|
|
221
|
+
JSON Schema remains available through the optional `schema` property for validation and advanced closure rules such as `additionalProperties: false`. It is not required for ordinary merges and deletions.
|
|
222
|
+
|
|
223
|
+
The JSON and YAML reconcilers use JSON Pointer for exact ownership and project exceptions, while plans emit standard JSON Patch operations over the parsed document. `{{remove}}` is an authoring convenience translated into an RFC 6902 `remove` operation.
|
|
224
|
+
|
|
225
|
+
## Typed Parsers
|
|
226
|
+
|
|
227
|
+
Rules can select how rendered content is parsed and reconciled:
|
|
228
|
+
|
|
229
|
+
| Parser | Behavior |
|
|
230
|
+
| --- | --- |
|
|
231
|
+
| `text` | Replace the complete file |
|
|
232
|
+
| `json` | Replace the complete JSON document |
|
|
233
|
+
| `json-merge` | Deeply merge objects; source arrays replace target arrays |
|
|
234
|
+
| `package-json` | Deep merge and serialize common package fields in conventional order |
|
|
235
|
+
| `yaml` | Replace a YAML document with JSON-parity exact, removal, exclusion, override, schema, and patch semantics |
|
|
236
|
+
| `yaml-merge` | Deeply merge YAML data with JSON-parity exact, removal, exclusion, override, schema, and patch semantics |
|
|
237
|
+
| `ini` | Replace an INI document |
|
|
238
|
+
| `ini-merge` | Deeply merge INI data |
|
|
239
|
+
| `hook` | Delegate preparation to an explicitly trusted Unix hook |
|
|
240
|
+
|
|
241
|
+
YAML behavior is selected by the rule parser, not the destination extension, so `.yml` and `.yaml` files have identical support.
|
|
242
|
+
|
|
243
|
+
Every parser feeds the same file plan and stale-write protection. Structured JSON and YAML parsers attach their RFC 6902 operations to JSON plan output.
|
|
244
|
+
|
|
245
|
+
## Layered Templates
|
|
246
|
+
|
|
247
|
+
A template manifest can extend other manifests in the same immutable snapshot:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"schemaVersion": 1,
|
|
252
|
+
"name": "acme-node-service",
|
|
253
|
+
"extends": [
|
|
254
|
+
"base/node/template.json",
|
|
255
|
+
"base/open-source/template.json"
|
|
256
|
+
],
|
|
257
|
+
"variables": {
|
|
258
|
+
"codeowner": "@acme/platform"
|
|
259
|
+
},
|
|
260
|
+
"rules": []
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Inputs, partials, variables, checks, and hooks are combined. A later rule, check, or hook replaces an earlier entry with the same stable ID.
|
|
265
|
+
|
|
266
|
+
## Workspaces and Scopes
|
|
267
|
+
|
|
268
|
+
Node workspaces are discovered from the root `package.json`. Rules and checks can run against:
|
|
269
|
+
|
|
270
|
+
- `root`: only the repository root.
|
|
271
|
+
- `workspace`: every selected workspace.
|
|
272
|
+
- `all`: the root and every workspace.
|
|
273
|
+
|
|
274
|
+
Rules normally write relative to the package being evaluated. `target: "root"` lets a workspace generate a repository-root file:
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"id": "workspace-ci",
|
|
279
|
+
"type": "file",
|
|
280
|
+
"scope": "workspace",
|
|
281
|
+
"target": "root",
|
|
282
|
+
"template": "files/ci.yml.hbs",
|
|
283
|
+
"destination": ".github/workflows/ci-{{pkgNameFs}}.yml"
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Root and workspace `package.json` files may contain `knitto` configuration. Workspace values overlay root values for variables, metadata, exclusions, and overrides.
|
|
288
|
+
|
|
289
|
+
Derived Handlebars values include `isRoot`, `isWorkspace`, `isMono`, `isRootMono`, `repoDir`, `moduleDir`, `pkgName`, `pkgNameFs`, `pkgPath`, `workspacePaths`, `workspaceGlobs`, `isPrivate`, `isPublic`, `esm`, `cjsExt`, and `deleteJsExt`.
|
|
290
|
+
|
|
291
|
+
## Policy Checks
|
|
292
|
+
|
|
293
|
+
Templates may define checks independent of generated files:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"checks": [
|
|
298
|
+
{
|
|
299
|
+
"id": "required-tooling",
|
|
300
|
+
"type": "required-packages",
|
|
301
|
+
"scope": "all",
|
|
302
|
+
"packages": {
|
|
303
|
+
"devDependencies": [
|
|
304
|
+
"typescript@^5",
|
|
305
|
+
"eslint@^9"
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"id": "unwanted-tooling",
|
|
311
|
+
"type": "unwanted-packages",
|
|
312
|
+
"packages": [
|
|
313
|
+
"standard"
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
"id": "changelog-heading",
|
|
318
|
+
"type": "file-regex",
|
|
319
|
+
"path": "CHANGELOG.md",
|
|
320
|
+
"pattern": "^# Changelog\\\\n\\\\n#",
|
|
321
|
+
"solution": "Reformat the changelog heading."
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "dependency-engines",
|
|
325
|
+
"type": "engines"
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Checks support `scope` and Handlebars `when` conditions. Projects can explicitly exclude a check by stable ID.
|
|
332
|
+
|
|
333
|
+
## Trusted Unix Hooks
|
|
334
|
+
|
|
335
|
+
Declarative templates are the default. A template may optionally declare executable hooks for custom context derivation, parser behavior, or checks:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"hooks": [
|
|
340
|
+
{
|
|
341
|
+
"id": "derive-project-data",
|
|
342
|
+
"kind": "context",
|
|
343
|
+
"command": "hooks/derive-project-data"
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Hooks exchange JSON over stdin and stdout. They do not run until the exact template digest is trusted:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
knitto source trust ./project
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Trust is recorded in `.knitto.json` as a SHA-256 digest. A changed template produces a different digest and must be trusted again before its hooks can execute.
|
|
356
|
+
|
|
357
|
+
## Project Configuration
|
|
358
|
+
|
|
359
|
+
Each managed project contains a human-edited `.knitto.json`:
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"source": {
|
|
364
|
+
"type": "git",
|
|
365
|
+
"url": "git@github.com:acme/repository-templates.git",
|
|
366
|
+
"path": "templates/node-service",
|
|
367
|
+
"ref": "main"
|
|
368
|
+
},
|
|
369
|
+
"metadata": {
|
|
370
|
+
"url": "https://example.com/acme/service",
|
|
371
|
+
"issuesUrl": "https://example.com/acme/service/issues"
|
|
372
|
+
},
|
|
373
|
+
"variables": {
|
|
374
|
+
"license": "MIT"
|
|
375
|
+
},
|
|
376
|
+
"exclude": {
|
|
377
|
+
"rules": [],
|
|
378
|
+
"checks": [],
|
|
379
|
+
"pointers": {
|
|
380
|
+
"package-metadata": [
|
|
381
|
+
"/homepage"
|
|
382
|
+
]
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
"overrides": {
|
|
386
|
+
"package-metadata": {
|
|
387
|
+
"/repository/url": "https://example.com/custom/service"
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Projects can diverge explicitly:
|
|
394
|
+
|
|
395
|
+
- `variables` replace declared template variable defaults.
|
|
396
|
+
- `exclude.rules` opts out of complete rules.
|
|
397
|
+
- `exclude.pointers` opts out of selected fields in JSON rules.
|
|
398
|
+
- `overrides` replaces a managed JSON value for one project.
|
|
399
|
+
|
|
400
|
+
Unknown variables, rules, and pointers fail validation. Local edits to managed content remain drift unless the project declares an exclusion or override.
|
|
401
|
+
|
|
402
|
+
## Template Revisions
|
|
403
|
+
|
|
404
|
+
Knitto does not require template authors to publish packages, tags, or
|
|
405
|
+
releases. Templates and consumers without release configuration continue to
|
|
406
|
+
use branches, commits, local directories, or archives as before. Every resolved
|
|
407
|
+
template directory becomes an immutable snapshot identified by a digest:
|
|
408
|
+
|
|
409
|
+
```text
|
|
410
|
+
sha256:58eea85c...
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`.knitto.json` describes where the template comes from. Generated `.knitto.lock` records the exact content digest and source provenance:
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
{
|
|
417
|
+
"schemaVersion": 1,
|
|
418
|
+
"digest": "sha256:58eea85c...",
|
|
419
|
+
"source": {
|
|
420
|
+
"type": "git",
|
|
421
|
+
"url": "git@github.com:acme/repository-templates.git",
|
|
422
|
+
"path": "templates/node-service",
|
|
423
|
+
"ref": "main"
|
|
424
|
+
},
|
|
425
|
+
"engine": {
|
|
426
|
+
"package": "knitto",
|
|
427
|
+
"version": "0.0.1"
|
|
428
|
+
},
|
|
429
|
+
"provenance": {
|
|
430
|
+
"sourceType": "git",
|
|
431
|
+
"locator": "git@github.com:acme/repository-templates.git",
|
|
432
|
+
"templatePath": "templates/node-service",
|
|
433
|
+
"revision": "89ca41..."
|
|
434
|
+
},
|
|
435
|
+
"templateSchemaVersion": 1,
|
|
436
|
+
"resolvedAt": "2026-09-04T05:00:00.000Z"
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The SHA-256 digest is authoritative. A Git commit, HTTP ETag, or branch name is provenance, not the cross-transport version identity.
|
|
441
|
+
|
|
442
|
+
The optional `engine` field pins the exact public npm version used to produce
|
|
443
|
+
the plan and lock. Knitto refuses to operate with a different version and
|
|
444
|
+
prints the corresponding `npx knitto@<version>` command. Newly initialized
|
|
445
|
+
projects include this pin; older configurations without it remain supported.
|
|
446
|
+
|
|
447
|
+
### Opt-in template releases
|
|
448
|
+
|
|
449
|
+
A template may declare Release Please metadata:
|
|
450
|
+
|
|
451
|
+
```json
|
|
452
|
+
{
|
|
453
|
+
"schemaVersion": 1,
|
|
454
|
+
"name": "node-policy",
|
|
455
|
+
"engine": {
|
|
456
|
+
"package": "knitto",
|
|
457
|
+
"version": "1.0.0"
|
|
458
|
+
},
|
|
459
|
+
"release": {
|
|
460
|
+
"provider": "release-please",
|
|
461
|
+
"version": "2.3.0",
|
|
462
|
+
"tagFormat": "policy-v{version}"
|
|
463
|
+
},
|
|
464
|
+
"rules": []
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`tagFormat` is template-specific and must contain `{version}`. Release Please
|
|
469
|
+
updates `release.version` on its release pull request and creates the matching
|
|
470
|
+
tag after merge. Consumers pin that exact tag in `.knitto.json`; reverting the
|
|
471
|
+
consumer update restores both the prior tag and lock.
|
|
472
|
+
|
|
473
|
+
Release Please uses `0.0.0` as bootstrap metadata before the first release.
|
|
474
|
+
Knitto treats that version as unreleased, so Git consumers may continue using
|
|
475
|
+
the template's branch. Immutable-tag enforcement begins when
|
|
476
|
+
`release.version` becomes the first published version.
|
|
477
|
+
|
|
478
|
+
Move an existing Git consumer to a released tag before planning or applying:
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
npx knitto@1.0.0 source pin ./project --ref policy-v2.3.0
|
|
482
|
+
npx knitto@1.0.0 apply ./project --update
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`source pin` validates that the selected revision declares the requested tag
|
|
486
|
+
and updates the consumer's engine pin to the version required by that template.
|
|
487
|
+
The second command must use that newly pinned engine version. Templates that do
|
|
488
|
+
not declare `release`, and templates still bootstrapping at `0.0.0`, do not
|
|
489
|
+
require a release tag.
|
|
490
|
+
|
|
491
|
+
Snapshots are cached under:
|
|
492
|
+
|
|
493
|
+
```text
|
|
494
|
+
${XDG_CACHE_HOME:-$HOME/.cache}/knitto/snapshots/
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Locked checks can therefore remain reproducible and work from cache after initial resolution.
|
|
498
|
+
|
|
499
|
+
## Source Types
|
|
500
|
+
|
|
501
|
+
### Local directory
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"source": {
|
|
506
|
+
"type": "local",
|
|
507
|
+
"path": "../repository-templates/node-service"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### HTTP archive
|
|
513
|
+
|
|
514
|
+
The URL must return a tar or tar-gzip archive containing the template manifest:
|
|
515
|
+
|
|
516
|
+
```json
|
|
517
|
+
{
|
|
518
|
+
"source": {
|
|
519
|
+
"type": "http",
|
|
520
|
+
"url": "https://templates.example.com/node-service/latest.tar.gz"
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
An optional `path` selects a template beneath the extracted archive root.
|
|
526
|
+
|
|
527
|
+
### Git over HTTPS
|
|
528
|
+
|
|
529
|
+
```json
|
|
530
|
+
{
|
|
531
|
+
"source": {
|
|
532
|
+
"type": "git",
|
|
533
|
+
"url": "https://example.com/acme/repository-templates.git",
|
|
534
|
+
"path": "templates/node-service",
|
|
535
|
+
"ref": "main"
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Git over SSH
|
|
541
|
+
|
|
542
|
+
```json
|
|
543
|
+
{
|
|
544
|
+
"source": {
|
|
545
|
+
"type": "git",
|
|
546
|
+
"url": "git@example.com:acme/repository-templates.git",
|
|
547
|
+
"path": "templates/node-service",
|
|
548
|
+
"ref": "main"
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
SSH sources use the existing Unix SSH agent and Git configuration. Knitto does not store private keys.
|
|
554
|
+
|
|
555
|
+
All source types resolve through the same validation, canonicalization, hashing, caching, planning, and application pipeline.
|
|
556
|
+
|
|
557
|
+
## Commands
|
|
558
|
+
|
|
559
|
+
Scaffold a new self-managed template:
|
|
560
|
+
|
|
561
|
+
```bash
|
|
562
|
+
knitto init-template ./my-template
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
This creates:
|
|
566
|
+
|
|
567
|
+
```text
|
|
568
|
+
my-template/
|
|
569
|
+
├── .knitto.json
|
|
570
|
+
└── .knitto/
|
|
571
|
+
├── template.json
|
|
572
|
+
└── files/
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
The generated `.knitto.json` points to the local `.knitto` source, and
|
|
576
|
+
`template.json` starts with an empty `rules` array. The template name defaults
|
|
577
|
+
to the target directory name and can be set explicitly:
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
knitto init-template ./my-template --name shared-node-policy
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
The command refuses to replace an existing `.knitto` directory or
|
|
584
|
+
`.knitto.json`.
|
|
585
|
+
|
|
586
|
+
Initialize a project interactively:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
knitto init ./project
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Or initialize non-interactively:
|
|
593
|
+
|
|
594
|
+
```bash
|
|
595
|
+
knitto init ./project \
|
|
596
|
+
--type git \
|
|
597
|
+
--source git@example.com:acme/repository-templates.git \
|
|
598
|
+
--template-path templates/node-service \
|
|
599
|
+
--ref main
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
All reconciliation commands validate every referenced template file, partial,
|
|
603
|
+
schema, executable hook, and parser-hook reference before planning or applying
|
|
604
|
+
changes. Use `check` to additionally validate the project configuration, any
|
|
605
|
+
existing lock, and assert consumer compliance against its selected snapshot:
|
|
606
|
+
|
|
607
|
+
```bash
|
|
608
|
+
knitto check ./project
|
|
609
|
+
knitto check ./project --json
|
|
610
|
+
knitto check ./project --quiet
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Validation covers template assets even when their rules are currently disabled
|
|
614
|
+
by a `when` condition. Use the newest configured source instead of the lock:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
knitto check ./project --update
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`check` exits with status `1` when writes, deletions, or check violations are
|
|
621
|
+
planned.
|
|
622
|
+
|
|
623
|
+
Display the changes required by the locked snapshot:
|
|
624
|
+
|
|
625
|
+
```bash
|
|
626
|
+
knitto plan ./project
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
When `.knitto.json` exists but no lock has been created yet, `plan`
|
|
630
|
+
resolves the configured source as a bootstrap snapshot. The first successful
|
|
631
|
+
`apply` writes `.knitto.lock`; later commands use the pinned revision
|
|
632
|
+
unless `--update` is supplied.
|
|
633
|
+
|
|
634
|
+
A starter repository may bootstrap from an embedded local template and include
|
|
635
|
+
a rule that rewrites only `.knitto.json#source` to a canonical remote
|
|
636
|
+
source before deleting the embedded template. When an apply changes the source,
|
|
637
|
+
the generated lock records the post-apply source configuration.
|
|
638
|
+
|
|
639
|
+
Templates may declare prompts for arbitrary required non-secret project
|
|
640
|
+
settings under `metadata.*` or `variables.*`. Every command resolves the
|
|
641
|
+
requirements of its selected template revision, so a later `plan --update` can
|
|
642
|
+
request newly introduced information before reconciliation proceeds. Answers
|
|
643
|
+
are saved to `.knitto.json`. Text, numeric, confirmation, and select
|
|
644
|
+
prompts are supported, and `when` may conditionally enable a requirement.
|
|
645
|
+
Prompt defaults may be Handlebars templates, but
|
|
646
|
+
derived values are never implicit engine behavior. A template may explicitly
|
|
647
|
+
opt into a directory-derived package-name default with:
|
|
648
|
+
|
|
649
|
+
```json
|
|
650
|
+
{
|
|
651
|
+
"path": "metadata.name",
|
|
652
|
+
"type": "text",
|
|
653
|
+
"message": "Package name",
|
|
654
|
+
"default": "{{project.name}}",
|
|
655
|
+
"required": true,
|
|
656
|
+
"when": "{{#unless (eq project.name \"template-source\")}}true{{/unless}}"
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
For automation or non-interactive initialization, repeat `--set`:
|
|
661
|
+
|
|
662
|
+
```bash
|
|
663
|
+
knitto plan ./project --update \
|
|
664
|
+
--set metadata.name=my-service \
|
|
665
|
+
--set variables.region=iad
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Template inputs are for non-secret configuration. Credentials and tokens must
|
|
669
|
+
not be stored as prompt answers.
|
|
670
|
+
|
|
671
|
+
LLMs and CI should inspect requirements before planning:
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
knitto inputs --update --json
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The response identifies the selected template digest and every declared input,
|
|
678
|
+
including its path, type, message, choices, configured value or default,
|
|
679
|
+
`willPrompt`, and `missingRequired`. This lets automation construct explicit
|
|
680
|
+
`--set path=value` arguments without discovering prompts through an
|
|
681
|
+
interactive command.
|
|
682
|
+
|
|
683
|
+
CI never accepts prompt defaults or silently skips unresolved inputs. If an
|
|
684
|
+
enabled input is not already configured and was not explicitly provided with
|
|
685
|
+
`--set`, Knitto fails with instructions for creating a manual pull
|
|
686
|
+
request. From a local checkout, either collect and save the values
|
|
687
|
+
interactively:
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
knitto plan --update
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
or populate every value explicitly:
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
knitto plan --update \
|
|
697
|
+
--set 'metadata.name=<value>' \
|
|
698
|
+
--set 'metadata.description=<value>'
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Commit the resulting `.knitto.json` change through a pull request before
|
|
702
|
+
retrying CI.
|
|
703
|
+
|
|
704
|
+
Human-readable plans begin with the project, abbreviated template digest,
|
|
705
|
+
compliance status, change and policy counts, and exclusions. Each operation
|
|
706
|
+
then identifies its action, destination, owning rule, line summary, optional
|
|
707
|
+
structured patch paths, and unified diff. Use `--json` for the complete
|
|
708
|
+
machine-readable plan.
|
|
709
|
+
|
|
710
|
+
Resolve the newest source contents and preview an update without advancing the lock:
|
|
711
|
+
|
|
712
|
+
```bash
|
|
713
|
+
knitto plan ./project --update
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Apply the locked snapshot:
|
|
717
|
+
|
|
718
|
+
```bash
|
|
719
|
+
knitto apply ./project
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Apply the newest source snapshot and advance the lock only after successful writes:
|
|
723
|
+
|
|
724
|
+
```bash
|
|
725
|
+
knitto apply ./project --update
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
`apply` calculates the plan and applies it atomically in the same invocation.
|
|
729
|
+
CI can therefore use `knitto apply --update` as its single
|
|
730
|
+
reconciliation step before creating a pull request from the resulting changes.
|
|
731
|
+
|
|
732
|
+
Inspect the locked and current source revisions:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
knitto source inspect ./project
|
|
736
|
+
knitto source inspect ./project --json
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
Trust executable hooks from the currently locked template revision:
|
|
740
|
+
|
|
741
|
+
```bash
|
|
742
|
+
knitto source trust ./project
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
## Releases
|
|
746
|
+
|
|
747
|
+
The first public version is `0.0.1`. Release Please manages version updates,
|
|
748
|
+
`CHANGELOG.md`, release pull requests, and tags from Conventional Commits on
|
|
749
|
+
`main`. When a release is created, the workflow builds that exact version,
|
|
750
|
+
stages it through npm, and leaves final publication behind npm's human 2FA
|
|
751
|
+
approval boundary.
|
|
752
|
+
|
|
753
|
+
The workflow uses `GITHUB_TOKEN` by default. Set a `RELEASE_PLEASE_TOKEN`
|
|
754
|
+
repository secret to a fine-grained token with contents and pull-request write
|
|
755
|
+
access when release pull requests must trigger other GitHub Actions workflows.
|
|
756
|
+
Publishing requires npm trusted publishing or an `NPM_TOKEN` repository secret.
|
|
757
|
+
|
|
758
|
+
## Testing
|
|
759
|
+
|
|
760
|
+
The high-level CLI usage suite runs Knitto as a subprocess against isolated
|
|
761
|
+
temporary templates and projects:
|
|
762
|
+
|
|
763
|
+
```bash
|
|
764
|
+
npm run test:integration
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
It covers the local initialize-plan-check-apply lifecycle, structured-file
|
|
768
|
+
preservation, deletion, drift detection, immutable Git tag pinning, engine
|
|
769
|
+
pinning, lock creation and advancement, required CI inputs, workspace targets,
|
|
770
|
+
invalid inactive template assets, and executable-hook trust. The full
|
|
771
|
+
`npm test` command includes this suite. CI builds Knitto and runs the same suite
|
|
772
|
+
again through `dist/src/cli.js` to verify the compiled executable boundary:
|
|
773
|
+
|
|
774
|
+
```bash
|
|
775
|
+
npm run build
|
|
776
|
+
npm run test:integration:dist
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
## Railway Proof of Concept
|
|
780
|
+
|
|
781
|
+
[`examples/railway`](./examples/railway) contains:
|
|
782
|
+
|
|
783
|
+
- A shared Railway Node service template.
|
|
784
|
+
- The current `package.json` shapes from `railway-vikunja` and `railway-plausible`.
|
|
785
|
+
- A field-level opt-out for the Vikunja homepage.
|
|
786
|
+
- An explicit Plausible homepage override.
|
|
787
|
+
- A whole-file license rule.
|
|
788
|
+
- Legacy `standard`, `templateVersion`, formatter, template-copy, and repository fields that demonstrate pointer-driven property deletion.
|
|
789
|
+
- An RFC 6902 patch attached to the `package.json` plan operation.
|
|
790
|
+
- A compact `exact` policy plus conditional `{{remove}}` values; no duplicate package schema or second conditional is required.
|
|
791
|
+
|
|
792
|
+
`examples/railway/standards-template` is a deliberately small template for the
|
|
793
|
+
real `railway-plausible` and `railway-vikunja` repositories. It ensures both
|
|
794
|
+
`.DS_Store` and the generated `railway-plan.json` are ignored.
|
|
795
|
+
|
|
796
|
+
The `.gitignore` rule is authoritative, so both repositories converge to the
|
|
797
|
+
same ordering and newline layout rather than merely containing the same
|
|
798
|
+
entries. `railway-plan.json` may contain account-specific Railway details and
|
|
799
|
+
must remain local.
|
|
800
|
+
|
|
801
|
+
The same standards template owns the complete `package.json` shape for these
|
|
802
|
+
private Railway IaC repositories. It derives `name` from the repository
|
|
803
|
+
directory and standardizes ESM, Node.js 22, Railway commands, formatting,
|
|
804
|
+
package ordering, quality checks, and exact development dependency versions.
|
|
805
|
+
It intentionally omits a publish version and production dependencies.
|
|
806
|
+
|
|
807
|
+
It also owns `tsconfig.json` as a complete file. Both projects use NodeNext,
|
|
808
|
+
strict checking, no emit, JSON module resolution, ES2022 output targeting,
|
|
809
|
+
explicit Node.js types, and only compile `.railway/**/*.ts`.
|
|
810
|
+
|
|
811
|
+
The template also removes `.github/**`. These projects consume one shared local
|
|
812
|
+
template and intentionally do not carry repository-specific GitHub workflows
|
|
813
|
+
or metadata. Applying glob deletions prunes empty parent directories, so the
|
|
814
|
+
`.github` directory itself disappears after its final file is removed.
|
|
815
|
+
|
|
816
|
+
`.railway/README.md` is also explicitly absent. Railway-specific documentation
|
|
817
|
+
belongs in the repository root README, keeping the `.railway` directory limited
|
|
818
|
+
to generated metadata and executable infrastructure configuration.
|
|
819
|
+
|
|
820
|
+
After applying package dependency changes, regenerate the project lockfile with
|
|
821
|
+
`npm install --package-lock-only --ignore-scripts`. Lockfiles contain resolved
|
|
822
|
+
package-manager output and are not rendered as Handlebars templates.
|
|
823
|
+
|
|
824
|
+
After building the CLI:
|
|
825
|
+
|
|
826
|
+
```bash
|
|
827
|
+
node dist/src/cli.js init examples/railway/projects/railway-vikunja
|
|
828
|
+
node dist/src/cli.js init examples/railway/projects/railway-plausible
|
|
829
|
+
node dist/src/cli.js plan examples/railway/projects/railway-vikunja
|
|
830
|
+
node dist/src/cli.js plan examples/railway/projects/railway-plausible
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
The checked-in example configurations are intentionally unlocked. When a project already has `.knitto.json`, `init` validates its configured source and creates the initial lock without replacing the configuration.
|
|
834
|
+
|
|
835
|
+
The same template can be applied to actual local checkouts by adding project-specific `.knitto.json` files and running `plan` before `apply`. No Git remote or pull-request behavior is required.
|
|
836
|
+
|
|
837
|
+
## Development
|
|
838
|
+
|
|
839
|
+
Install dependencies:
|
|
840
|
+
|
|
841
|
+
```bash
|
|
842
|
+
npm install
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
Available validation commands:
|
|
846
|
+
|
|
847
|
+
```bash
|
|
848
|
+
npm run check
|
|
849
|
+
npm test
|
|
850
|
+
npm run build
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
The GitHub Actions workflow calls these package scripts; validation logic is not hidden in workflow YAML.
|
|
854
|
+
|
|
855
|
+
## Future Central Registry
|
|
856
|
+
|
|
857
|
+
A hosted or self-hosted registry can implement the HTTP source contract without changing the engine:
|
|
858
|
+
|
|
859
|
+
1. Accept a template directory or connect an external source.
|
|
860
|
+
2. Validate it and create an immutable content-addressed snapshot.
|
|
861
|
+
3. Expose mutable channels such as `stable` or `next` that resolve to digests.
|
|
862
|
+
4. Track which locked digest each project reports.
|
|
863
|
+
5. Coordinate update plans across many machines and projects.
|
|
864
|
+
|
|
865
|
+
The registry would improve discovery, history, access control, adoption reporting, and fleet coordination. It would not be required for local reconciliation.
|
|
866
|
+
|
|
867
|
+
GitHub, GitLab, CI, and pull-request integrations can likewise consume operation plans as adapters. The underlying primitive remains:
|
|
868
|
+
|
|
869
|
+
> Resolve a template snapshot, render it with project context, calculate a plan, and safely reconcile a Unix directory.
|