@open-rlb/ng-bootstrap 3.3.45 → 3.3.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-rlb/ng-bootstrap",
3
- "version": "3.3.45",
3
+ "version": "3.3.46",
4
4
  "peerDependencies": {
5
5
  "@angular/cdk": ">=21.0.0 <22.0.0",
6
6
  "@angular/common": ">=21.0.0 <22.0.0",
@@ -5,6 +5,11 @@
5
5
  "description": "Add @open-rlb/ng-bootstrap to an Angular project: install peer dependencies, register Bootstrap styles, wire up provideRlbBootstrap(), and scaffold a starter component.",
6
6
  "factory": "./ng-add/index#ngAdd",
7
7
  "schema": "./ng-add/schema.json"
8
+ },
9
+ "sync-skills": {
10
+ "description": "Copy the Claude skills bundled with the installed version of @open-rlb/ng-bootstrap into .claude/skills. Re-runnable: this is how consumers pick up skill updates after npm update.",
11
+ "factory": "./sync-skills/index#syncSkills",
12
+ "schema": "./sync-skills/schema.json"
8
13
  }
9
14
  }
10
15
  }
@@ -16,6 +16,8 @@ const DEPENDENCIES = [
16
16
  { name: 'bootstrap-icons', version: '^1.11.0', type: utility_1.DependencyType.Default },
17
17
  { name: '@types/bootstrap', version: '^5.2.0', type: utility_1.DependencyType.Dev },
18
18
  ];
19
+ /** Keeps `.claude/skills` in step with the installed library version on every `npm install`. */
20
+ const SYNC_SKILLS_COMMAND = 'ng g @open-rlb/ng-bootstrap:sync-skills';
19
21
  /** Global styles required for the Bootstrap look & feel. */
20
22
  const STYLE_PATHS = [
21
23
  'node_modules/bootstrap/dist/css/bootstrap.min.css',
@@ -34,8 +36,10 @@ function ngAdd(options) {
34
36
  // 4. Optionally scaffold a starter component.
35
37
  options.skipStarter ? noop : scaffoldStarter(tree, project),
36
38
  // 5. Optionally copy the bundled Claude skills into .claude/skills.
37
- options.skipSkills ? noop : copyClaudeSkills(),
38
- // 6. Print next steps.
39
+ options.skipSkills ? noop : (0, schematics_1.schematic)('sync-skills', {}),
40
+ // 6. Optionally keep them in sync on every future `npm install`.
41
+ options.skipSkills || options.skipSkillsAutoSync ? noop : addSkillsPostinstall(),
42
+ // 7. Print next steps.
39
43
  logNextSteps(project, options),
40
44
  ]);
41
45
  };
@@ -91,13 +95,34 @@ function scaffoldStarter(tree, project) {
91
95
  };
92
96
  }
93
97
  /**
94
- * Copies the Claude skills bundled with the package into the consumer's
95
- * `.claude/skills` folder. Library-authored skills are authoritative, so existing
96
- * copies are overwritten to stay in sync with the installed version.
98
+ * Adds a `postinstall` script that re-runs the sync-skills schematic, so `npm update` alone
99
+ * refreshes `.claude/skills` to match the newly installed library version.
100
+ *
101
+ * The script deliberately lives in the consumer's package.json rather than the library's: a
102
+ * library-side install script is silently skipped under `--ignore-scripts`, has to guess the
103
+ * app root via INIT_CWD, and would fire in unrelated repos on transitive installs.
97
104
  */
98
- function copyClaudeSkills() {
99
- const skills = (0, schematics_1.apply)((0, schematics_1.url)('./claude-skills'), [(0, schematics_1.move)('.claude/skills')]);
100
- return (0, schematics_1.mergeWith)(skills, schematics_1.MergeStrategy.Overwrite);
105
+ function addSkillsPostinstall() {
106
+ return (tree, context) => {
107
+ const raw = tree.read('/package.json');
108
+ if (!raw) {
109
+ return tree;
110
+ }
111
+ const pkg = JSON.parse(raw.toString('utf-8'));
112
+ const existing = pkg.scripts?.['postinstall'];
113
+ if (existing?.includes(SYNC_SKILLS_COMMAND)) {
114
+ return tree;
115
+ }
116
+ // Never rewrite a postinstall the consumer already relies on — tell them what to append.
117
+ if (existing) {
118
+ context.logger.warn(`⚠ A "postinstall" script already exists. To keep the Claude skills up to date, append:\n` +
119
+ ` && ${SYNC_SKILLS_COMMAND}`);
120
+ return tree;
121
+ }
122
+ pkg.scripts = { ...pkg.scripts, postinstall: SYNC_SKILLS_COMMAND };
123
+ tree.overwrite('/package.json', JSON.stringify(pkg, null, 2) + '\n');
124
+ return tree;
125
+ };
101
126
  }
102
127
  function logNextSteps(project, options) {
103
128
  return (_tree, context) => {
@@ -113,6 +138,11 @@ function logNextSteps(project, options) {
113
138
  }
114
139
  if (!options.skipSkills) {
115
140
  log.info(' • Claude skills copied to .claude/skills/ (date-tz, rlb-* component guides)');
141
+ if (!options.skipSkillsAutoSync) {
142
+ log.info(` • "postinstall": "${SYNC_SKILLS_COMMAND}" added to package.json`);
143
+ log.info(' Skills refresh on every `npm install`. Note `npm update <pkg>` skips');
144
+ log.info(' root lifecycle scripts — follow it with a bare `npm install`.');
145
+ }
116
146
  }
117
147
  log.info('');
118
148
  };
@@ -20,6 +20,11 @@
20
20
  "type": "boolean",
21
21
  "default": false,
22
22
  "description": "Do not copy the bundled Claude skills into .claude/skills."
23
+ },
24
+ "skipSkillsAutoSync": {
25
+ "type": "boolean",
26
+ "default": false,
27
+ "description": "Do not add a postinstall script that refreshes .claude/skills on future installs."
23
28
  }
24
29
  },
25
30
  "required": []
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.syncSkills = syncSkills;
4
+ const schematics_1 = require("@angular-devkit/schematics");
5
+ const rxjs_1 = require("rxjs");
6
+ /** Where the consumer's Claude skills live. */
7
+ const SKILLS_ROOT = '/.claude/skills';
8
+ /**
9
+ * Records which skill folders this library owns, so a later sync can delete the ones it no
10
+ * longer ships without touching skills the consumer wrote themselves.
11
+ */
12
+ const MANIFEST_PATH = `${SKILLS_ROOT}/.rlb-skills.json`;
13
+ /** Set this in CI if a pipeline asserts a clean working tree after `npm install`. */
14
+ const SKIP_ENV_VAR = 'RLB_SKIP_SKILL_SYNC';
15
+ const PACKAGE_NAME = '@open-rlb/ng-bootstrap';
16
+ /**
17
+ * Copies the Claude skills bundled with the *installed* version of the library into the
18
+ * consumer's `.claude/skills`. Safe to re-run: it is how consumers pick up skill changes after
19
+ * `npm update`, typically from their own `postinstall` script.
20
+ */
21
+ function syncSkills(options) {
22
+ return async (tree, context) => {
23
+ if (process.env[SKIP_ENV_VAR]) {
24
+ context.logger.info(`• ${SKIP_ENV_VAR} is set — skipping Claude skill sync.`);
25
+ return noop;
26
+ }
27
+ const bundled = await readBundledSkills(context);
28
+ if (bundled.skills.length === 0) {
29
+ context.logger.warn(`⚠ ${PACKAGE_NAME} shipped without Claude skills — nothing to sync. ` +
30
+ 'This usually means the package was built with a bare `ng build` instead of `npm run lib:build`.');
31
+ return noop;
32
+ }
33
+ const previous = readManifest(tree);
34
+ // Only folders a previous sync claimed are ours to delete. Anything else in .claude/skills
35
+ // was authored in the consumer and must survive.
36
+ const stale = options.prune === false
37
+ ? []
38
+ : (previous?.skills ?? []).filter(name => !bundled.skills.includes(name));
39
+ const added = bundled.files.filter(file => !tree.exists(join(SKILLS_ROOT, file.path)));
40
+ const updated = bundled.files.filter(file => {
41
+ const target = join(SKILLS_ROOT, file.path);
42
+ const current = tree.read(target);
43
+ return current !== null && !current.equals(file.content);
44
+ });
45
+ return (0, schematics_1.chain)([
46
+ prune(stale),
47
+ (0, schematics_1.mergeWith)((0, schematics_1.apply)((0, schematics_1.url)('./claude-skills'), [(0, schematics_1.move)(SKILLS_ROOT)]), schematics_1.MergeStrategy.Overwrite),
48
+ writeManifest(bundled.skills),
49
+ logSummary({ skills: bundled.skills, added: added.length, updated: updated.length, stale }),
50
+ ]);
51
+ };
52
+ }
53
+ /** Reads the skills packaged alongside this schematic (see scripts/build-schematics.mjs). */
54
+ async function readBundledSkills(context) {
55
+ const source = (0, schematics_1.url)('./claude-skills')(context);
56
+ const bundledTree = (0, rxjs_1.isObservable)(source) ? await (0, rxjs_1.firstValueFrom)(source) : source;
57
+ const skills = new Set();
58
+ const files = [];
59
+ bundledTree.visit((path, entry) => {
60
+ const folder = path.replace(/^\//, '').split('/')[0];
61
+ if (folder) {
62
+ skills.add(folder);
63
+ }
64
+ if (entry) {
65
+ files.push({ path, content: entry.content });
66
+ }
67
+ });
68
+ return { skills: [...skills].sort(), files };
69
+ }
70
+ function readManifest(tree) {
71
+ const raw = tree.read(MANIFEST_PATH);
72
+ if (!raw) {
73
+ return null;
74
+ }
75
+ try {
76
+ const parsed = JSON.parse(raw.toString('utf-8'));
77
+ return Array.isArray(parsed.skills)
78
+ ? { package: PACKAGE_NAME, version: '', ...parsed, skills: parsed.skills }
79
+ : null;
80
+ }
81
+ catch {
82
+ // A hand-mangled manifest must not abort the sync — treat it as "nothing owned yet".
83
+ return null;
84
+ }
85
+ }
86
+ /** Deletes every file under the named skill folders. */
87
+ function prune(stale) {
88
+ return (tree) => {
89
+ for (const name of stale) {
90
+ const dir = tree.getDir(join(SKILLS_ROOT, name));
91
+ const paths = [];
92
+ dir.visit(path => paths.push(path));
93
+ paths.forEach(path => tree.delete(path));
94
+ }
95
+ return tree;
96
+ };
97
+ }
98
+ function writeManifest(skills) {
99
+ return (tree) => {
100
+ const manifest = { package: PACKAGE_NAME, version: installedVersion(), skills };
101
+ const content = JSON.stringify(manifest, null, 2) + '\n';
102
+ if (tree.exists(MANIFEST_PATH)) {
103
+ tree.overwrite(MANIFEST_PATH, content);
104
+ }
105
+ else {
106
+ tree.create(MANIFEST_PATH, content);
107
+ }
108
+ return tree;
109
+ };
110
+ }
111
+ /**
112
+ * The version of the library these skills came from. Resolved from the package.json two levels
113
+ * up from the compiled `schematics/sync-skills/index.js`, i.e. the installed package's own.
114
+ */
115
+ function installedVersion() {
116
+ try {
117
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
118
+ return require('../../package.json').version ?? 'unknown';
119
+ }
120
+ catch {
121
+ return 'unknown';
122
+ }
123
+ }
124
+ function logSummary(summary) {
125
+ return (_tree, context) => {
126
+ const log = context.logger;
127
+ log.info('');
128
+ log.info(`✅ Claude skills synced from ${PACKAGE_NAME}@${installedVersion()}`);
129
+ log.info(` • ${summary.skills.length} skills: ${summary.skills.join(', ')}`);
130
+ log.info(` • ${summary.added} new file(s), ${summary.updated} updated`);
131
+ if (summary.stale.length) {
132
+ log.info(` • pruned (no longer shipped): ${summary.stale.join(', ')}`);
133
+ }
134
+ log.info('');
135
+ };
136
+ }
137
+ function join(base, path) {
138
+ return `${base}/${path}`.replace(/\/+/g, '/');
139
+ }
140
+ /** A no-op rule. */
141
+ const noop = tree => tree;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "$id": "OpenRlbNgBootstrapSyncSkills",
4
+ "title": "@open-rlb/ng-bootstrap sync-skills schematic",
5
+ "type": "object",
6
+ "properties": {
7
+ "prune": {
8
+ "type": "boolean",
9
+ "default": true,
10
+ "description": "Delete skills the library previously shipped but no longer does. Skills authored in the consumer are never touched."
11
+ }
12
+ },
13
+ "required": []
14
+ }