@trineui/cli 0.1.0-beta.1 → 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.
Files changed (39) hide show
  1. package/README.md +40 -0
  2. package/bin/trine.js +24 -0
  3. package/dist/add-button.js +15 -0
  4. package/dist/add-component.js +315 -0
  5. package/dist/index.js +73 -646
  6. package/package.json +23 -30
  7. package/templates/button/button.html +11 -0
  8. package/templates/button/button.skin.ts +128 -0
  9. package/templates/button/button.ts +39 -0
  10. package/templates/styles/tokens.css +102 -0
  11. package/templates/styles/trine-consumer.css +58 -0
  12. package/CHANGELOG.md +0 -30
  13. package/src/commands/add.ts +0 -101
  14. package/src/commands/diff.test.ts +0 -55
  15. package/src/commands/diff.ts +0 -104
  16. package/src/commands/eject.ts +0 -95
  17. package/src/commands/init.ts +0 -92
  18. package/src/commands/sync-interactive.ts +0 -108
  19. package/src/commands/sync.test.ts +0 -35
  20. package/src/commands/sync.ts +0 -113
  21. package/src/index.ts +0 -18
  22. package/src/types/manifest.ts +0 -14
  23. package/src/utils/__tests__/hash.test.ts +0 -35
  24. package/src/utils/__tests__/template.test.ts +0 -47
  25. package/src/utils/eject-merger.ts +0 -149
  26. package/src/utils/hash.ts +0 -43
  27. package/src/utils/manifest.ts +0 -43
  28. package/src/utils/template.ts +0 -26
  29. package/templates/button.blueprint.ts.hbs +0 -41
  30. package/templates/button.skin.ts.hbs +0 -35
  31. package/templates/checkbox.blueprint.ts.hbs +0 -57
  32. package/templates/checkbox.skin.ts.hbs +0 -44
  33. package/templates/dialog.blueprint.ts.hbs +0 -61
  34. package/templates/dialog.skin.ts.hbs +0 -27
  35. package/templates/input.blueprint.ts.hbs +0 -83
  36. package/templates/input.skin.ts.hbs +0 -29
  37. package/templates/select.blueprint.ts.hbs +0 -86
  38. package/templates/select.skin.ts.hbs +0 -53
  39. package/tsconfig.json +0 -10
package/package.json CHANGED
@@ -1,41 +1,34 @@
1
1
  {
2
2
  "name": "@trineui/cli",
3
- "version": "0.1.0-beta.1",
4
- "description": "Trine CLI — add, diff, sync, and eject Angular UI components",
5
- "keywords": ["angular", "cli", "trine", "ui-components", "code-generation"],
6
- "author": "Chatchawan Phrueksawan <pete.chatchawan@gmail.com>",
7
- "license": "MIT",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/petechatchawan/forge.git",
11
- "directory": "packages/cli"
12
- },
13
- "homepage": "https://docs.trine-ui.dev/cli/reference",
14
- "bugs": "https://github.com/petechatchawan/forge/issues",
3
+ "version": "0.1.0",
15
4
  "type": "module",
5
+ "description": "Copy-paste ownership CLI for Trine UI components.",
16
6
  "main": "./dist/index.js",
17
- "types": "./dist/index.d.ts",
18
7
  "bin": {
19
- "trine": "./dist/index.js"
20
- },
21
- "engines": {
22
- "node": ">=18.0.0"
8
+ "trine": "./bin/trine.js"
23
9
  },
24
10
  "scripts": {
25
- "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outdir=dist --external:commander --external:diff --external:handlebars --external:@inquirer/* --external:node:stream --external:node:readline --external:node:events --external:node:util --external:ts-morph"
11
+ "build": "tsc -p tsconfig.build.json",
12
+ "prepack": "pnpm build"
26
13
  },
27
- "dependencies": {
28
- "@inquirer/prompts": "^8.3.2",
29
- "commander": "^14.0.3",
30
- "diff": "^8.0.3",
31
- "handlebars": "^4.7.8",
32
- "ts-morph": "^27.0.2"
14
+ "files": [
15
+ "bin",
16
+ "dist",
17
+ "templates",
18
+ "README.md"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
33
22
  },
34
- "devDependencies": {
35
- "@types/diff": "^8.0.0",
36
- "@types/handlebars": "^4.1.0",
37
- "esbuild": "^0.27.4",
38
- "tsx": "^4.21.0",
39
- "vitest": "^4.1.0"
23
+ "keywords": [
24
+ "trine",
25
+ "angular",
26
+ "cli",
27
+ "components",
28
+ "copy-paste",
29
+ "ownership"
30
+ ],
31
+ "dependencies": {
32
+ "typescript": "^5.9.2"
40
33
  }
41
34
  }
@@ -0,0 +1,11 @@
1
+ <button
2
+ [attr.aria-busy]="loading() ? 'true' : null"
3
+ [class]="classes()"
4
+ [disabled]="isDisabled()"
5
+ [type]="type()"
6
+ >
7
+ @if (loading()) {
8
+ <span [class]="spinnerClasses()" aria-hidden="true"></span>
9
+ }
10
+ <ng-content></ng-content>
11
+ </button>
@@ -0,0 +1,128 @@
1
+ import { cva } from 'class-variance-authority';
2
+
3
+ export const buttonSkin = cva(
4
+ [
5
+ 'inline-flex items-center justify-center gap-2',
6
+ 'whitespace-nowrap align-middle',
7
+ 'font-medium leading-none',
8
+ 'select-none',
9
+ '[&:has(>.sr-only)]:gap-0 [&:has(>.sr-only)]:px-0',
10
+ 'rounded-[var(--trine-radius-md)] border',
11
+ 'transition-[background-color,border-color,color,box-shadow,transform] duration-[var(--trine-duration-base)] ease-[var(--trine-ease-default)]',
12
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--trine-color-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--trine-color-bg)]',
13
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:text-[var(--trine-color-text-disabled)] disabled:shadow-none',
14
+ ],
15
+ {
16
+ variants: {
17
+ variant: {
18
+ primary: [
19
+ 'bg-[var(--trine-color-primary-default)]',
20
+ 'border-[var(--trine-color-primary-default)]',
21
+ 'text-[var(--trine-color-primary-fg)]',
22
+ 'shadow-[var(--trine-shadow-sm)]',
23
+ 'hover:bg-[var(--trine-color-primary-hover)] hover:border-[var(--trine-color-primary-hover)]',
24
+ 'active:bg-[var(--trine-color-primary-active)] active:border-[var(--trine-color-primary-active)] active:translate-y-px active:shadow-none',
25
+ 'disabled:border-[var(--trine-color-primary-subtle)] disabled:bg-[var(--trine-color-primary-subtle)]',
26
+ ],
27
+ secondary: [
28
+ 'bg-[var(--trine-color-neutral-subtle)]',
29
+ 'border-[var(--trine-color-border)]',
30
+ 'text-[var(--trine-color-text)]',
31
+ 'hover:bg-[var(--trine-color-neutral-hover)] hover:border-[var(--trine-color-border-strong)]',
32
+ 'active:bg-[var(--trine-color-neutral-active)] active:border-[var(--trine-color-border-strong)] active:translate-y-px',
33
+ 'disabled:border-[var(--trine-color-border)] disabled:bg-[var(--trine-color-neutral-subtle)]',
34
+ ],
35
+ ghost: [
36
+ 'bg-transparent',
37
+ 'border-transparent',
38
+ 'text-[var(--trine-color-primary-default)]',
39
+ 'hover:bg-[var(--trine-color-primary-subtle)] hover:text-[var(--trine-color-primary-hover)]',
40
+ 'active:bg-[var(--trine-color-primary-subtle)] active:text-[var(--trine-color-primary-active)] active:translate-y-px',
41
+ 'disabled:bg-transparent disabled:border-transparent disabled:text-[var(--trine-color-text-disabled)]',
42
+ ],
43
+ danger: [
44
+ 'bg-[var(--trine-color-danger-default)]',
45
+ 'border-[var(--trine-color-danger-default)]',
46
+ 'text-[var(--trine-color-danger-fg)]',
47
+ 'shadow-[var(--trine-shadow-sm)]',
48
+ 'hover:bg-[var(--trine-color-danger-hover)] hover:border-[var(--trine-color-danger-hover)]',
49
+ 'active:bg-[var(--trine-color-danger-active)] active:border-[var(--trine-color-danger-active)] active:translate-y-px active:shadow-none',
50
+ 'disabled:border-[var(--trine-color-danger-subtle)] disabled:bg-[var(--trine-color-danger-subtle)]',
51
+ ],
52
+ },
53
+ size: {
54
+ sm: ['min-h-8 min-w-8 px-3 text-xs'],
55
+ md: ['min-h-9 min-w-9 px-4 text-sm'],
56
+ lg: ['min-h-11 min-w-11 px-6 text-base'],
57
+ },
58
+ loading: {
59
+ true: ['cursor-progress'],
60
+ false: null,
61
+ },
62
+ },
63
+ compoundVariants: [
64
+ {
65
+ variant: 'primary',
66
+ loading: true,
67
+ class: [
68
+ '[&[aria-busy=true]]:border-[var(--trine-color-primary-default)]',
69
+ '[&[aria-busy=true]]:bg-[var(--trine-color-primary-default)]',
70
+ '[&[aria-busy=true]]:text-[var(--trine-color-primary-fg)]',
71
+ '[&[aria-busy=true]]:shadow-[var(--trine-shadow-sm)]',
72
+ ],
73
+ },
74
+ {
75
+ variant: 'secondary',
76
+ loading: true,
77
+ class: [
78
+ '[&[aria-busy=true]]:border-[var(--trine-color-border)]',
79
+ '[&[aria-busy=true]]:bg-[var(--trine-color-neutral-subtle)]',
80
+ '[&[aria-busy=true]]:text-[var(--trine-color-text)]',
81
+ ],
82
+ },
83
+ {
84
+ variant: 'ghost',
85
+ loading: true,
86
+ class: [
87
+ '[&[aria-busy=true]]:border-transparent',
88
+ '[&[aria-busy=true]]:bg-[var(--trine-color-primary-subtle)]',
89
+ '[&[aria-busy=true]]:text-[var(--trine-color-primary-default)]',
90
+ ],
91
+ },
92
+ {
93
+ variant: 'danger',
94
+ loading: true,
95
+ class: [
96
+ '[&[aria-busy=true]]:border-[var(--trine-color-danger-default)]',
97
+ '[&[aria-busy=true]]:bg-[var(--trine-color-danger-default)]',
98
+ '[&[aria-busy=true]]:text-[var(--trine-color-danger-fg)]',
99
+ '[&[aria-busy=true]]:shadow-[var(--trine-shadow-sm)]',
100
+ ],
101
+ },
102
+ ],
103
+ defaultVariants: {
104
+ variant: 'primary',
105
+ size: 'md',
106
+ loading: false,
107
+ },
108
+ },
109
+ );
110
+
111
+ export const buttonSpinnerSkin = cva(
112
+ [
113
+ 'inline-block shrink-0 rounded-full border-current border-solid border-r-transparent',
114
+ 'animate-spin motion-reduce:animate-none',
115
+ ],
116
+ {
117
+ variants: {
118
+ size: {
119
+ sm: ['h-3 w-3 border-2'],
120
+ md: ['h-3.5 w-3.5 border-2'],
121
+ lg: ['h-4 w-4 border-2'],
122
+ },
123
+ },
124
+ defaultVariants: {
125
+ size: 'md',
126
+ },
127
+ },
128
+ );
@@ -0,0 +1,39 @@
1
+ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
2
+ import { buttonSkin, buttonSpinnerSkin } from './button.skin';
3
+
4
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
5
+ export type ButtonSize = 'sm' | 'md' | 'lg';
6
+ export type ButtonType = 'button' | 'submit' | 'reset';
7
+
8
+ @Component({
9
+ selector: 'trine-button',
10
+ templateUrl: './button.html',
11
+ changeDetection: ChangeDetectionStrategy.OnPush,
12
+ host: {
13
+ '[attr.data-variant]': 'variant()',
14
+ '[attr.data-size]': 'size()',
15
+ },
16
+ })
17
+ export class ButtonComponent {
18
+ readonly variant = input<ButtonVariant>('primary');
19
+ readonly size = input<ButtonSize>('md');
20
+ readonly disabled = input<boolean>(false);
21
+ readonly loading = input<boolean>(false);
22
+ readonly type = input<ButtonType>('button');
23
+
24
+ protected readonly isDisabled = computed(() => this.disabled() || this.loading());
25
+
26
+ protected readonly classes = computed(() =>
27
+ buttonSkin({
28
+ variant: this.variant(),
29
+ size: this.size(),
30
+ loading: this.loading(),
31
+ }),
32
+ );
33
+
34
+ protected readonly spinnerClasses = computed(() =>
35
+ buttonSpinnerSkin({
36
+ size: this.size(),
37
+ }),
38
+ );
39
+ }
@@ -0,0 +1,102 @@
1
+ :root {
2
+ --trine-color-primary-default: oklch(0.6204 0.195 253.83);
3
+ --trine-color-primary-hover: oklch(0.5804 0.195 253.83);
4
+ --trine-color-primary-active: oklch(0.5404 0.195 253.83);
5
+ --trine-color-primary-subtle: oklch(0.9524 0.02 253.83);
6
+ --trine-color-primary-fg: oklch(0.9911 0 0);
7
+
8
+ --trine-color-danger-default: oklch(0.6532 0.2335 25.74);
9
+ --trine-color-danger-hover: oklch(0.6132 0.2335 25.74);
10
+ --trine-color-danger-active: oklch(0.5732 0.2335 25.74);
11
+ --trine-color-danger-subtle: oklch(0.9524 0.025 25.74);
12
+ --trine-color-danger-fg: oklch(0.9911 0 0);
13
+
14
+ --trine-color-success-default: oklch(0.7329 0.1941 150.81);
15
+ --trine-color-success-hover: oklch(0.6929 0.1941 150.81);
16
+ --trine-color-success-active: oklch(0.6529 0.1941 150.81);
17
+ --trine-color-success-subtle: oklch(0.9524 0.02 150.81);
18
+ --trine-color-success-fg: oklch(0.2103 0.0059 150.81);
19
+
20
+ --trine-color-warning-default: oklch(0.7819 0.159 72.33);
21
+ --trine-color-warning-hover: oklch(0.7419 0.159 72.33);
22
+ --trine-color-warning-active: oklch(0.7019 0.159 72.33);
23
+ --trine-color-warning-subtle: oklch(0.9524 0.02 72.33);
24
+ --trine-color-warning-fg: oklch(0.2103 0.0059 72.33);
25
+
26
+ --trine-color-neutral-default: oklch(0.5517 0.003 253.83);
27
+ --trine-color-neutral-hover: oklch(0.9373 0.0012 253.83);
28
+ --trine-color-neutral-active: oklch(0.92 0.0015 253.83);
29
+ --trine-color-neutral-subtle: oklch(0.94 0.0015 253.83);
30
+ --trine-color-neutral-fg: oklch(0.2103 0.0059 253.83);
31
+
32
+ --trine-color-bg: oklch(0.9702 0.0015 253.83);
33
+ --trine-color-bg-elevated: oklch(1 0.0008 253.83);
34
+ --trine-color-bg-overlay: oklch(1 0.0004 253.83 / 0.82);
35
+ --trine-color-border: oklch(0.9 0.0015 253.83);
36
+ --trine-color-border-strong: oklch(0.871 0.0015 253.83);
37
+ --trine-color-ring: oklch(0.6204 0.195 253.83);
38
+ --trine-color-text: oklch(0.2103 0.0015 253.83);
39
+ --trine-color-text-subtle: oklch(0.5517 0.003 253.83);
40
+ --trine-color-text-disabled: oklch(0.5517 0.003 253.83);
41
+
42
+ --trine-radius-none: 0;
43
+ --trine-radius-sm: 0.375rem;
44
+ --trine-radius-md: 0.5rem;
45
+ --trine-radius-lg: 0.75rem;
46
+ --trine-radius-xl: 1rem;
47
+ --trine-radius-2xl: 1.25rem;
48
+ --trine-radius-full: 9999px;
49
+
50
+ --trine-shadow-sm: 0 1px 2px 0 oklch(0.16 0.01 258 / 0.12);
51
+ --trine-shadow-md: 0 10px 20px -10px oklch(0.16 0.01 258 / 0.22);
52
+ --trine-shadow-lg: 0 20px 40px -20px oklch(0.16 0.01 258 / 0.28);
53
+
54
+ --trine-duration-fast: 120ms;
55
+ --trine-duration-base: 180ms;
56
+ --trine-duration-slow: 280ms;
57
+ --trine-ease-default: cubic-bezier(0.2, 0, 0, 1);
58
+ --trine-ease-in: cubic-bezier(0.4, 0, 1, 1);
59
+ --trine-ease-out: cubic-bezier(0, 0, 0.2, 1);
60
+ }
61
+
62
+ html.dark {
63
+ --trine-color-primary-default: oklch(0.6204 0.195 253.83);
64
+ --trine-color-primary-hover: oklch(0.6704 0.195 253.83);
65
+ --trine-color-primary-active: oklch(0.5804 0.195 253.83);
66
+ --trine-color-primary-subtle: oklch(0.274 0.02 253.83);
67
+ --trine-color-primary-fg: oklch(0.9911 0 0);
68
+
69
+ --trine-color-danger-default: oklch(0.594 0.1973 24.63);
70
+ --trine-color-danger-hover: oklch(0.634 0.1973 24.63);
71
+ --trine-color-danger-active: oklch(0.554 0.1973 24.63);
72
+ --trine-color-danger-subtle: oklch(0.257 0.02 24.63);
73
+ --trine-color-danger-fg: oklch(0.9911 0 0);
74
+
75
+ --trine-color-success-default: oklch(0.7329 0.1941 150.81);
76
+ --trine-color-success-hover: oklch(0.7729 0.1941 150.81);
77
+ --trine-color-success-active: oklch(0.6929 0.1941 150.81);
78
+ --trine-color-success-subtle: oklch(0.257 0.02 150.81);
79
+ --trine-color-success-fg: oklch(0.2103 0.0059 150.81);
80
+
81
+ --trine-color-warning-default: oklch(0.8203 0.1392 76.34);
82
+ --trine-color-warning-hover: oklch(0.8603 0.1392 76.34);
83
+ --trine-color-warning-active: oklch(0.7803 0.1392 76.34);
84
+ --trine-color-warning-subtle: oklch(0.2721 0.02 76.34);
85
+ --trine-color-warning-fg: oklch(0.2103 0.0059 76.34);
86
+
87
+ --trine-color-neutral-default: oklch(0.705 0.003 253.83);
88
+ --trine-color-neutral-hover: oklch(0.3964 0.0015 253.83);
89
+ --trine-color-neutral-active: oklch(0.257 0.0023 253.83);
90
+ --trine-color-neutral-subtle: oklch(0.274 0.0015 253.83);
91
+ --trine-color-neutral-fg: oklch(0.9911 0.0015 253.83);
92
+
93
+ --trine-color-bg: oklch(0.12 0.0015 253.83);
94
+ --trine-color-bg-elevated: oklch(0.2103 0.003 253.83);
95
+ --trine-color-bg-overlay: oklch(0.2103 0.003 253.83 / 0.82);
96
+ --trine-color-border: oklch(0.28 0.0015 253.83);
97
+ --trine-color-border-strong: oklch(0.3964 0.0015 253.83);
98
+ --trine-color-ring: oklch(0.6204 0.195 253.83);
99
+ --trine-color-text: oklch(0.9911 0.0015 253.83);
100
+ --trine-color-text-subtle: oklch(0.705 0.003 253.83);
101
+ --trine-color-text-disabled: oklch(0.705 0.003 253.83);
102
+ }
@@ -0,0 +1,58 @@
1
+ /* Consumer-safe styling/bootstrap baseline for copy-paste ownership delivery. */
2
+ @import './tokens.css';
3
+ @import 'tailwindcss';
4
+
5
+ @source '../app';
6
+
7
+ @theme {
8
+ --color-trine-primary: var(--trine-color-primary-default);
9
+ --color-trine-primary-hover: var(--trine-color-primary-hover);
10
+ --color-trine-primary-active: var(--trine-color-primary-active);
11
+ --color-trine-primary-subtle: var(--trine-color-primary-subtle);
12
+ --color-trine-primary-fg: var(--trine-color-primary-fg);
13
+
14
+ --color-trine-danger: var(--trine-color-danger-default);
15
+ --color-trine-danger-hover: var(--trine-color-danger-hover);
16
+ --color-trine-danger-active: var(--trine-color-danger-active);
17
+ --color-trine-danger-subtle: var(--trine-color-danger-subtle);
18
+ --color-trine-danger-fg: var(--trine-color-danger-fg);
19
+
20
+ --color-trine-success: var(--trine-color-success-default);
21
+ --color-trine-success-hover: var(--trine-color-success-hover);
22
+ --color-trine-success-active: var(--trine-color-success-active);
23
+ --color-trine-success-subtle: var(--trine-color-success-subtle);
24
+ --color-trine-success-fg: var(--trine-color-success-fg);
25
+
26
+ --color-trine-warning: var(--trine-color-warning-default);
27
+ --color-trine-warning-hover: var(--trine-color-warning-hover);
28
+ --color-trine-warning-active: var(--trine-color-warning-active);
29
+ --color-trine-warning-subtle: var(--trine-color-warning-subtle);
30
+ --color-trine-warning-fg: var(--trine-color-warning-fg);
31
+
32
+ --color-trine-neutral: var(--trine-color-neutral-default);
33
+ --color-trine-neutral-hover: var(--trine-color-neutral-hover);
34
+ --color-trine-neutral-active: var(--trine-color-neutral-active);
35
+ --color-trine-neutral-subtle: var(--trine-color-neutral-subtle);
36
+ --color-trine-neutral-fg: var(--trine-color-neutral-fg);
37
+
38
+ --color-trine-bg: var(--trine-color-bg);
39
+ --color-trine-bg-elevated: var(--trine-color-bg-elevated);
40
+ --color-trine-bg-overlay: var(--trine-color-bg-overlay);
41
+ --color-trine-border: var(--trine-color-border);
42
+ --color-trine-border-strong: var(--trine-color-border-strong);
43
+ --color-trine-text: var(--trine-color-text);
44
+ --color-trine-text-subtle: var(--trine-color-text-subtle);
45
+ --color-trine-text-disabled: var(--trine-color-text-disabled);
46
+
47
+ --radius-trine-none: var(--trine-radius-none);
48
+ --radius-trine-sm: var(--trine-radius-sm);
49
+ --radius-trine-md: var(--trine-radius-md);
50
+ --radius-trine-lg: var(--trine-radius-lg);
51
+ --radius-trine-xl: var(--trine-radius-xl);
52
+ --radius-trine-2xl: var(--trine-radius-2xl);
53
+ --radius-trine-full: var(--trine-radius-full);
54
+
55
+ --shadow-trine-sm: var(--trine-shadow-sm);
56
+ --shadow-trine-md: var(--trine-shadow-md);
57
+ --shadow-trine-lg: var(--trine-shadow-lg);
58
+ }
package/CHANGELOG.md DELETED
@@ -1,30 +0,0 @@
1
- # @trine/cli Changelog
2
-
3
- ## 0.1.0-beta.1 (2026-03-22)
4
-
5
- Initial beta release.
6
-
7
- ### Commands
8
-
9
- - `trine add <component>` — Generate blueprint + skin files from templates
10
- - `trine diff <component>` — Show local and upstream status (read-only)
11
- - `trine sync <component>` — Auto-overwrite untouched blueprint with upstream changes
12
- - `trine sync <component> --interactive` — 3-way merge TUI for reviewing modified blueprints
13
- - `trine eject <component>` — Merge blueprint + skin into standalone component, one-way exit from lifecycle
14
- - `trine init` — Reconstruct manifest.json from existing blueprint files
15
-
16
- ### Sync Model
17
-
18
- - Normalized text hash v0.1 for change detection
19
- - manifest.json (`.trine/manifest.json`) is source of truth for sync state
20
- - Gate G1 validated: Prettier reformat does not trigger false-positive dirty signal
21
-
22
- ### Installation
23
-
24
- ```bash
25
- npm install -g @trine/cli@beta
26
- ```
27
-
28
- ### Requirements
29
-
30
- - Node.js >=18.0.0
@@ -1,101 +0,0 @@
1
- import { Command } from 'commander';
2
- import {
3
- writeFileSync,
4
- existsSync,
5
- mkdirSync,
6
- } from 'fs';
7
- import { resolve, join } from 'path';
8
- import { computeNormalizedHash } from '../utils/hash.js';
9
- import { readManifest, writeManifest } from '../utils/manifest.js';
10
- import { renderTemplate } from '../utils/template.js';
11
- import type { ComponentEntry, TrineManifest } from '../types/manifest.js';
12
-
13
- const ENGINE_VERSION = '0.1.0';
14
- const BLUEPRINT_SCHEMA_VERSION = '1.0.0';
15
-
16
- export const addCommand = new Command('add')
17
- .argument('<component>', 'Component name (e.g. button)')
18
- .option('--force', 'Overwrite existing blueprint (skin is NEVER overwritten)')
19
- .option(
20
- '--dir <path>',
21
- 'Output directory relative to project root',
22
- 'src/components/ui'
23
- )
24
- .description('Add a component blueprint + skin to your project')
25
- .action(async (component: string, options: { force?: boolean; dir: string }) => {
26
- const projectRoot = process.cwd();
27
- const componentDir = resolve(projectRoot, options.dir, component);
28
- const blueprintPath = join(componentDir, `${component}.blueprint.ts`);
29
- const skinPath = join(componentDir, `${component}.skin.ts`);
30
-
31
- if (!isSupportedComponent(component)) {
32
- console.error(`✗ Unknown component: "${component}"`);
33
- console.error(` Supported: button, input, dialog, checkbox, select`);
34
- process.exit(1);
35
- }
36
-
37
- if (existsSync(blueprintPath) && !options.force) {
38
- console.error(`✗ ${component}.blueprint.ts already exists`);
39
- console.error('');
40
- console.error(` To overwrite blueprint (skin will NOT be touched):`);
41
- console.error(` trine add ${component} --force`);
42
- process.exit(1);
43
- }
44
-
45
- const ctx = {
46
- engineVersion: ENGINE_VERSION,
47
- blueprintSchemaVersion: BLUEPRINT_SCHEMA_VERSION,
48
- };
49
-
50
- const blueprintContent = renderTemplate(`${component}.blueprint.ts.hbs`, ctx);
51
- const skinContent = renderTemplate(`${component}.skin.ts.hbs`, ctx);
52
-
53
- mkdirSync(componentDir, { recursive: true });
54
-
55
- writeFileSync(blueprintPath, blueprintContent, 'utf-8');
56
- console.log(` ✓ ${options.dir}/${component}/${component}.blueprint.ts`);
57
-
58
- if (!existsSync(skinPath)) {
59
- writeFileSync(skinPath, skinContent, 'utf-8');
60
- console.log(` ✓ ${options.dir}/${component}/${component}.skin.ts`);
61
- } else {
62
- console.log(` ~ ${options.dir}/${component}/${component}.skin.ts (kept — user-owned)`);
63
- }
64
-
65
- const existingManifest = readManifest(projectRoot);
66
- const manifest: TrineManifest = existingManifest ?? {
67
- 'trine-spec': '1.0',
68
- components: {},
69
- };
70
-
71
- const blueprintHash = computeNormalizedHash(blueprintContent);
72
-
73
- const entry: ComponentEntry = {
74
- 'engine-version': ENGINE_VERSION,
75
- 'blueprint-schema-version': BLUEPRINT_SCHEMA_VERSION,
76
- 'blueprint-hash': blueprintHash,
77
- 'blueprint-modified': false,
78
- 'synced-at': new Date().toISOString(),
79
- 'sync-model': 'normalized-text-v0.1',
80
- 'output-dir': options.dir,
81
- };
82
-
83
- manifest.components[component] = entry;
84
- writeManifest(manifest, projectRoot);
85
- console.log(` ✓ .trine/manifest.json`);
86
-
87
- console.log('');
88
- console.log(`✓ trine add ${component} complete`);
89
- console.log('');
90
- console.log(' Next steps:');
91
- console.log(` Import ${capitalize(component)}Component in your module/component`);
92
- console.log(` Run: trine diff ${component} — check for upstream changes`);
93
- });
94
-
95
- function capitalize(s: string): string {
96
- return s.charAt(0).toUpperCase() + s.slice(1);
97
- }
98
-
99
- function isSupportedComponent(name: string): boolean {
100
- return ['button', 'input', 'dialog', 'checkbox', 'select'].includes(name);
101
- }
@@ -1,55 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { computeNormalizedHash } from '../utils/hash.js';
3
-
4
- describe('hash normalization (Gate G1)', () => {
5
- it('same content → same hash', () => {
6
- const content = `import { Component } from '@angular/core';
7
- @Component({ selector: 'ui-button' })`;
8
- expect(computeNormalizedHash(content)).toBe(computeNormalizedHash(content));
9
- });
10
-
11
- it('prettier-formatted single quotes → double quotes → same hash', () => {
12
- const original = `import { Component } from '@angular/core';`;
13
- const prettified = `import { Component } from "@angular/core";`;
14
- expect(computeNormalizedHash(original)).toBe(computeNormalizedHash(prettified));
15
- });
16
-
17
- it('prettier multi-line → single line → same hash', () => {
18
- const original = `@Component({
19
- selector: 'ui-button',
20
- standalone: true
21
- })`;
22
- const prettified = `@Component({ selector: 'ui-button', standalone: true })`;
23
- expect(computeNormalizedHash(original)).toBe(computeNormalizedHash(prettified));
24
- });
25
-
26
- it('prettier arrays with/without trailing comma → same hash', () => {
27
- const noTrailing = `@Component({ inputs: ['a', 'b'] })`;
28
- const withTrailing = `@Component({ inputs: ['a', 'b',] })`;
29
- expect(computeNormalizedHash(noTrailing)).toBe(computeNormalizedHash(withTrailing));
30
- });
31
-
32
- it('prettier object spacing variations → same hash', () => {
33
- const tight = `@Component({selector:'ui-button',standalone:true})`;
34
- const spaced = `@Component({ selector: 'ui-button', standalone: true })`;
35
- const multiline = `@Component({
36
- selector: 'ui-button',
37
- standalone: true
38
- })`;
39
- const h1 = computeNormalizedHash(tight);
40
- expect(computeNormalizedHash(spaced)).toBe(h1);
41
- expect(computeNormalizedHash(multiline)).toBe(h1);
42
- });
43
-
44
- it('structural change → different hash', () => {
45
- const v1 = `@Component({ selector: 'ui-button' })`;
46
- const v2 = `@Component({ selector: 'ui-btn' })`;
47
- expect(computeNormalizedHash(v1)).not.toBe(computeNormalizedHash(v2));
48
- });
49
-
50
- it('import order different → same hash (sorted)', () => {
51
- const order1 = `import { A } from 'x';import { B } from 'y';`;
52
- const order2 = `import { B } from 'y';import { A } from 'x';`;
53
- expect(computeNormalizedHash(order1)).toBe(computeNormalizedHash(order2));
54
- });
55
- });