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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +869 -0
  3. package/dist/src/checks/run.d.ts +3 -0
  4. package/dist/src/checks/run.js +173 -0
  5. package/dist/src/checks/run.js.map +1 -0
  6. package/dist/src/cli.d.ts +2 -0
  7. package/dist/src/cli.js +406 -0
  8. package/dist/src/cli.js.map +1 -0
  9. package/dist/src/config.d.ts +11 -0
  10. package/dist/src/config.js +585 -0
  11. package/dist/src/config.js.map +1 -0
  12. package/dist/src/context/build.d.ts +2 -0
  13. package/dist/src/context/build.js +147 -0
  14. package/dist/src/context/build.js.map +1 -0
  15. package/dist/src/context/render.d.ts +7 -0
  16. package/dist/src/context/render.js +74 -0
  17. package/dist/src/context/render.js.map +1 -0
  18. package/dist/src/engine/apply.d.ts +2 -0
  19. package/dist/src/engine/apply.js +96 -0
  20. package/dist/src/engine/apply.js.map +1 -0
  21. package/dist/src/engine/diff.d.ts +2 -0
  22. package/dist/src/engine/diff.js +100 -0
  23. package/dist/src/engine/diff.js.map +1 -0
  24. package/dist/src/engine/plan.d.ts +2 -0
  25. package/dist/src/engine/plan.js +259 -0
  26. package/dist/src/engine/plan.js.map +1 -0
  27. package/dist/src/errors.d.ts +14 -0
  28. package/dist/src/errors.js +31 -0
  29. package/dist/src/errors.js.map +1 -0
  30. package/dist/src/filesystem/paths.d.ts +2 -0
  31. package/dist/src/filesystem/paths.js +47 -0
  32. package/dist/src/filesystem/paths.js.map +1 -0
  33. package/dist/src/hooks/run.d.ts +10 -0
  34. package/dist/src/hooks/run.js +105 -0
  35. package/dist/src/hooks/run.js.map +1 -0
  36. package/dist/src/index.d.ts +7 -0
  37. package/dist/src/index.js +7 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/lock.d.ts +2 -0
  40. package/dist/src/lock.js +16 -0
  41. package/dist/src/lock.js.map +1 -0
  42. package/dist/src/onboarding.d.ts +26 -0
  43. package/dist/src/onboarding.js +212 -0
  44. package/dist/src/onboarding.js.map +1 -0
  45. package/dist/src/parsers/index.d.ts +18 -0
  46. package/dist/src/parsers/index.js +194 -0
  47. package/dist/src/parsers/index.js.map +1 -0
  48. package/dist/src/parsers/json.d.ts +17 -0
  49. package/dist/src/parsers/json.js +214 -0
  50. package/dist/src/parsers/json.js.map +1 -0
  51. package/dist/src/parsers/merge.d.ts +1 -0
  52. package/dist/src/parsers/merge.js +17 -0
  53. package/dist/src/parsers/merge.js.map +1 -0
  54. package/dist/src/project/discover.d.ts +2 -0
  55. package/dist/src/project/discover.js +144 -0
  56. package/dist/src/project/discover.js.map +1 -0
  57. package/dist/src/rules/json-pointer.d.ts +3 -0
  58. package/dist/src/rules/json-pointer.js +114 -0
  59. package/dist/src/rules/json-pointer.js.map +1 -0
  60. package/dist/src/snapshots/canonical.d.ts +11 -0
  61. package/dist/src/snapshots/canonical.js +150 -0
  62. package/dist/src/snapshots/canonical.js.map +1 -0
  63. package/dist/src/sources/process.d.ts +3 -0
  64. package/dist/src/sources/process.js +28 -0
  65. package/dist/src/sources/process.js.map +1 -0
  66. package/dist/src/sources/resolve.d.ts +3 -0
  67. package/dist/src/sources/resolve.js +144 -0
  68. package/dist/src/sources/resolve.js.map +1 -0
  69. package/dist/src/template/init.d.ts +1 -0
  70. package/dist/src/template/init.js +62 -0
  71. package/dist/src/template/init.js.map +1 -0
  72. package/dist/src/template/manifest.d.ts +2 -0
  73. package/dist/src/template/manifest.js +80 -0
  74. package/dist/src/template/manifest.js.map +1 -0
  75. package/dist/src/template/release.d.ts +3 -0
  76. package/dist/src/template/release.js +8 -0
  77. package/dist/src/template/release.js.map +1 -0
  78. package/dist/src/template/validate.d.ts +2 -0
  79. package/dist/src/template/validate.js +62 -0
  80. package/dist/src/template/validate.js.map +1 -0
  81. package/dist/src/types.d.ts +242 -0
  82. package/dist/src/types.js +4 -0
  83. package/dist/src/types.js.map +1 -0
  84. package/dist/src/version.d.ts +2 -0
  85. package/dist/src/version.js +3 -0
  86. package/dist/src/version.js.map +1 -0
  87. 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.