@elliemae/pui-cli 9.0.0-alpha.3 → 9.0.0-alpha.5
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/README.md +10 -0
- package/dist/cjs/cli.js +2 -0
- package/dist/cjs/commands/skills.js +207 -0
- package/dist/cjs/lint-config/eslint/flat/common.mjs +6 -0
- package/dist/cjs/lint-config/eslint/flat/index.mjs +1 -0
- package/dist/cjs/lint-config/eslint/flat/presets.mjs +15 -0
- package/dist/cjs/lint-config/eslint/flat/rules.mjs +16 -0
- package/dist/cjs/skills/migrate-to-pui-cli-9/SKILL.md +140 -0
- package/dist/esm/cli.js +2 -0
- package/dist/esm/commands/skills.js +176 -0
- package/dist/esm/lint-config/eslint/flat/common.mjs +6 -0
- package/dist/esm/lint-config/eslint/flat/index.mjs +1 -0
- package/dist/esm/lint-config/eslint/flat/presets.mjs +15 -0
- package/dist/esm/lint-config/eslint/flat/rules.mjs +16 -0
- package/dist/esm/skills/migrate-to-pui-cli-9/SKILL.md +140 -0
- package/dist/types/lib/commands/skills.d.ts +11 -0
- package/dist/types/lib/lint-config/eslint/flat/index.d.mts +1 -1
- package/dist/types/lib/lint-config/eslint/flat/presets.d.mts +6 -0
- package/dist/types/lib/lint-config/eslint/flat/rules.d.mts +15 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/lib/lint-config/eslint/flat/common.mjs +6 -0
- package/lib/lint-config/eslint/flat/index.mjs +1 -0
- package/lib/lint-config/eslint/flat/presets.mjs +15 -0
- package/lib/lint-config/eslint/flat/rules.mjs +16 -0
- package/lib/skills/migrate-to-pui-cli-9/SKILL.md +140 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
## Migration Guide
|
|
10
10
|
|
|
11
|
+
### pui-cli 9 (Node 24, pnpm 11, ESLint 9)
|
|
12
|
+
|
|
13
|
+
Team upgrade guide: [pui-cli 9 migration guide](docs/pui-cli-9-migration.md).
|
|
14
|
+
|
|
15
|
+
Install the bundled Cursor skill in a consumer repo:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm exec pui-cli skills install migrate-to-pui-cli-9 --target all
|
|
19
|
+
```
|
|
20
|
+
|
|
11
21
|
### ESLint 9 flat config (alpha)
|
|
12
22
|
|
|
13
23
|
`pui-cli` now ships ESLint 9 with `typescript-eslint` v8 flat configs. Replace legacy `.eslintrc.cjs` with:
|
package/dist/cjs/cli.js
CHANGED
|
@@ -40,6 +40,7 @@ var import_vitest = require("./commands/vitest.js");
|
|
|
40
40
|
var import_version = require("./commands/version.js");
|
|
41
41
|
var import_tscheck = require("./commands/tscheck.js");
|
|
42
42
|
var import_buildcdn = require("./commands/buildcdn.js");
|
|
43
|
+
var import_skills = require("./commands/skills.js");
|
|
43
44
|
const import_meta = {};
|
|
44
45
|
const __dirname = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
45
46
|
(0, import_dotenv.config)();
|
|
@@ -57,5 +58,6 @@ process.env.PATH += import_node_path.default.delimiter + import_node_path.defaul
|
|
|
57
58
|
await (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).command(import_version.versionCmd).help().argv;
|
|
58
59
|
await (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).command(import_tscheck.tscheckCmd).help().argv;
|
|
59
60
|
await (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).command(import_buildcdn.buildCDNCmd).help().argv;
|
|
61
|
+
await (0, import_yargs.default)((0, import_helpers.hideBin)(process.argv)).command(import_skills.skillsCmd).help().argv;
|
|
60
62
|
await (0, import_update_notifier.notifyUpdates)();
|
|
61
63
|
})();
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var skills_exports = {};
|
|
30
|
+
__export(skills_exports, {
|
|
31
|
+
skillsCmd: () => skillsCmd
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(skills_exports);
|
|
34
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
35
|
+
var import_node_url = require("node:url");
|
|
36
|
+
var import_node_module = require("node:module");
|
|
37
|
+
var import_promises = require("node:fs/promises");
|
|
38
|
+
var import_node_fs = require("node:fs");
|
|
39
|
+
var import_yargs = __toESM(require("yargs"), 1);
|
|
40
|
+
var import_utils = require("./utils.js");
|
|
41
|
+
const import_meta = {};
|
|
42
|
+
const SKILL_TARGETS = ["cursor", "claude", "copilot", "all"];
|
|
43
|
+
const TARGET_RELATIVE_DIRS = {
|
|
44
|
+
cursor: [".cursor", "skills"],
|
|
45
|
+
claude: [".claude", "skills"],
|
|
46
|
+
copilot: [".github", "skills"]
|
|
47
|
+
};
|
|
48
|
+
const __dirname = import_node_path.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
49
|
+
const getPackageRoot = () => {
|
|
50
|
+
const require2 = (0, import_node_module.createRequire)(import_meta.url);
|
|
51
|
+
try {
|
|
52
|
+
return import_node_path.default.dirname(require2.resolve("@elliemae/pui-cli/package.json"));
|
|
53
|
+
} catch {
|
|
54
|
+
return import_node_path.default.resolve(__dirname, "..", "..");
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const getBundledSkillsDir = () => import_node_path.default.join(getPackageRoot(), "lib", "skills");
|
|
58
|
+
const getTargetSkillsDir = (target) => import_node_path.default.join(process.cwd(), ...TARGET_RELATIVE_DIRS[target]);
|
|
59
|
+
const resolveTargets = (target) => {
|
|
60
|
+
const selected = target ? Array.isArray(target) ? target : [target] : ["cursor"];
|
|
61
|
+
if (selected.includes("all")) {
|
|
62
|
+
return ["cursor", "claude", "copilot"];
|
|
63
|
+
}
|
|
64
|
+
return selected;
|
|
65
|
+
};
|
|
66
|
+
const pathExists = async (targetPath) => {
|
|
67
|
+
try {
|
|
68
|
+
await (0, import_promises.access)(targetPath, import_node_fs.constants.F_OK);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const listSkillNames = async (skillsDir) => {
|
|
75
|
+
const entries = await (0, import_promises.readdir)(skillsDir, { withFileTypes: true });
|
|
76
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
77
|
+
};
|
|
78
|
+
const copySkillDir = async (src, dest) => {
|
|
79
|
+
await (0, import_promises.mkdir)(dest, { recursive: true });
|
|
80
|
+
const entries = await (0, import_promises.readdir)(src, { withFileTypes: true });
|
|
81
|
+
await Promise.all(
|
|
82
|
+
entries.map(async (entry) => {
|
|
83
|
+
const srcPath = import_node_path.default.join(src, entry.name);
|
|
84
|
+
const destPath = import_node_path.default.join(dest, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
return copySkillDir(srcPath, destPath);
|
|
87
|
+
}
|
|
88
|
+
return (0, import_promises.copyFile)(srcPath, destPath);
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
const installSkillToTarget = async (skillName, target, force) => {
|
|
93
|
+
const bundledDir = getBundledSkillsDir();
|
|
94
|
+
const src = import_node_path.default.join(bundledDir, skillName);
|
|
95
|
+
const dest = import_node_path.default.join(getTargetSkillsDir(target), skillName);
|
|
96
|
+
if (!await pathExists(src)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Skill "${skillName}" not found. Run "pui-cli skills list" for available skills.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const srcStat = await (0, import_promises.stat)(src);
|
|
102
|
+
if (!srcStat.isDirectory()) {
|
|
103
|
+
throw new Error(`Skill "${skillName}" is not a valid skill directory.`);
|
|
104
|
+
}
|
|
105
|
+
if (await pathExists(dest) && !force) {
|
|
106
|
+
(0, import_utils.logWarning)(
|
|
107
|
+
`Skipped "${skillName}" for ${target} \u2014 already exists at ${dest}. Use --force to overwrite.`
|
|
108
|
+
);
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
await copySkillDir(src, dest);
|
|
112
|
+
(0, import_utils.logSuccess)(`Installed skill "${skillName}" to ${dest}`);
|
|
113
|
+
return true;
|
|
114
|
+
};
|
|
115
|
+
const installSkill = async (skillName, targets, force) => {
|
|
116
|
+
let installed = 0;
|
|
117
|
+
for (const target of targets) {
|
|
118
|
+
await (0, import_promises.mkdir)(getTargetSkillsDir(target), { recursive: true });
|
|
119
|
+
if (await installSkillToTarget(skillName, target, force)) {
|
|
120
|
+
installed += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return installed > 0;
|
|
124
|
+
};
|
|
125
|
+
const runList = async () => {
|
|
126
|
+
const bundledDir = getBundledSkillsDir();
|
|
127
|
+
if (!await pathExists(bundledDir)) {
|
|
128
|
+
throw new Error(`Bundled skills directory not found: ${bundledDir}`);
|
|
129
|
+
}
|
|
130
|
+
const skills = await listSkillNames(bundledDir);
|
|
131
|
+
if (!skills.length) {
|
|
132
|
+
(0, import_utils.logInfo)("No bundled skills found.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
(0, import_utils.logInfo)("Available skills:");
|
|
136
|
+
skills.forEach((name) => (0, import_utils.logInfo)(` - ${name}`));
|
|
137
|
+
};
|
|
138
|
+
const logReloadHint = (targets) => {
|
|
139
|
+
const hints = [];
|
|
140
|
+
if (targets.includes("cursor")) {
|
|
141
|
+
hints.push("Cursor (.cursor/skills/)");
|
|
142
|
+
}
|
|
143
|
+
if (targets.includes("claude")) {
|
|
144
|
+
hints.push("Claude Code (.claude/skills/)");
|
|
145
|
+
}
|
|
146
|
+
if (targets.includes("copilot")) {
|
|
147
|
+
hints.push("GitHub Copilot (.github/skills/)");
|
|
148
|
+
}
|
|
149
|
+
if (hints.length) {
|
|
150
|
+
(0, import_utils.logInfo)(
|
|
151
|
+
`Restart or reload your agent to pick up skills in: ${hints.join(", ")}.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const runInstall = async (name, force, target) => {
|
|
156
|
+
const bundledDir = getBundledSkillsDir();
|
|
157
|
+
if (!await pathExists(bundledDir)) {
|
|
158
|
+
throw new Error(`Bundled skills directory not found: ${bundledDir}`);
|
|
159
|
+
}
|
|
160
|
+
const targets = resolveTargets(target);
|
|
161
|
+
const skills = name ? [name] : await listSkillNames(bundledDir);
|
|
162
|
+
if (!skills.length) {
|
|
163
|
+
(0, import_utils.logInfo)("No bundled skills to install.");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
let installed = 0;
|
|
167
|
+
for (const skillName of skills) {
|
|
168
|
+
if (await installSkill(skillName, targets, force)) {
|
|
169
|
+
installed += 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (installed) {
|
|
173
|
+
logReloadHint(targets);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const skillsCmd = {
|
|
177
|
+
command: "skills <action> [name]",
|
|
178
|
+
describe: "Install bundled agent skills for Cursor, Claude Code, and GitHub Copilot",
|
|
179
|
+
builder: (yargsRef) => yargsRef.positional("action", {
|
|
180
|
+
describe: "List bundled skills or install them into the current repo",
|
|
181
|
+
choices: ["list", "install"],
|
|
182
|
+
demandOption: true
|
|
183
|
+
}).positional("name", {
|
|
184
|
+
describe: "Skill folder name (install all when omitted)",
|
|
185
|
+
type: "string"
|
|
186
|
+
}).option("target", {
|
|
187
|
+
describe: "Install destination: cursor (.cursor/skills), claude (.claude/skills), copilot (.github/skills), or all",
|
|
188
|
+
choices: SKILL_TARGETS,
|
|
189
|
+
default: "cursor"
|
|
190
|
+
}).option("force", {
|
|
191
|
+
describe: "Overwrite an existing skill directory",
|
|
192
|
+
type: "boolean",
|
|
193
|
+
default: false
|
|
194
|
+
}).help(),
|
|
195
|
+
handler: async (argv) => {
|
|
196
|
+
try {
|
|
197
|
+
if (argv.action === "list") {
|
|
198
|
+
await runList();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await runInstall(argv.name, Boolean(argv.force), argv.target);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
(0, import_utils.logError)(err.message);
|
|
204
|
+
(0, import_yargs.default)().exit(-1, err);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
jestRecommendedRules,
|
|
13
13
|
sharedCoreRules,
|
|
14
14
|
testFiles,
|
|
15
|
+
testFixtureFiles,
|
|
15
16
|
testJsxFiles,
|
|
16
17
|
testingLibraryDomRules,
|
|
17
18
|
testingLibraryReactRules,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
jsRules,
|
|
25
26
|
typescriptRules,
|
|
26
27
|
typescriptStrictRules,
|
|
28
|
+
testFixtureRelaxedRules,
|
|
27
29
|
typescriptTestRelaxedRules,
|
|
28
30
|
} from './rules.mjs';
|
|
29
31
|
|
|
@@ -113,6 +115,10 @@ export function createBaseFlatConfigs(tsRules) {
|
|
|
113
115
|
'testing-library/no-node-access': 'off',
|
|
114
116
|
},
|
|
115
117
|
},
|
|
118
|
+
{
|
|
119
|
+
files: testFixtureFiles,
|
|
120
|
+
rules: testFixtureRelaxedRules,
|
|
121
|
+
},
|
|
116
122
|
{
|
|
117
123
|
files: wdioSpecFiles,
|
|
118
124
|
plugins: { wdio, jest },
|
|
@@ -12,11 +12,26 @@ import storybook from 'eslint-plugin-storybook';
|
|
|
12
12
|
import testingLibrary from 'eslint-plugin-testing-library';
|
|
13
13
|
import wdio from 'eslint-plugin-wdio';
|
|
14
14
|
|
|
15
|
+
/** Application and integration tests (Jest + relaxed type-checked rules). */
|
|
15
16
|
export const testFiles = [
|
|
16
17
|
'**/*.{test,spec}.{js,jsx,ts,tsx}',
|
|
17
18
|
'**/__tests__/**',
|
|
18
19
|
'**/lib/testing/**',
|
|
19
20
|
'**/mocks/**',
|
|
21
|
+
// PUI libs/apps co-locate tests under lib/ (e.g. pui-app-sdk, pui-app-loader).
|
|
22
|
+
'lib/**/tests/**',
|
|
23
|
+
'app/**/tests/**',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generated or vendored JS under test trees (checksum bundles, MSW endpoints, pinned assets).
|
|
28
|
+
* Not maintained source — relax structural rules only.
|
|
29
|
+
*/
|
|
30
|
+
export const testFixtureFiles = [
|
|
31
|
+
'**/*.checksum*.js',
|
|
32
|
+
'**/*.endpoint.js',
|
|
33
|
+
'**/tests/**/latest/*.{js,cjs,mjs}',
|
|
34
|
+
'**/tests/**/[0-9]*.[0-9]*/*.{js,cjs,mjs}',
|
|
20
35
|
];
|
|
21
36
|
|
|
22
37
|
export const testJsxFiles = ['**/*.{test,spec}.{jsx,tsx}'];
|
|
@@ -49,6 +49,22 @@ export const typescriptTestRelaxedRules = {
|
|
|
49
49
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
50
50
|
'@typescript-eslint/no-unsafe-return': 'off',
|
|
51
51
|
'@typescript-eslint/unbound-method': 'off',
|
|
52
|
+
'@typescript-eslint/no-unsafe-declaration-merging': 'off',
|
|
53
|
+
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
|
|
54
|
+
'@typescript-eslint/await-thenable': 'off',
|
|
55
|
+
'@typescript-eslint/prefer-promise-reject-errors': 'off',
|
|
56
|
+
'no-constant-binary-expression': 'off',
|
|
57
|
+
'valid-typeof': 'off',
|
|
58
|
+
'prefer-const': 'off',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Relaxed core rules for generated test fixture JS (checksum mocks, etc.). */
|
|
62
|
+
export const testFixtureRelaxedRules = {
|
|
63
|
+
'no-unused-vars': 'off',
|
|
64
|
+
'no-console': 'off',
|
|
65
|
+
'max-lines': 'off',
|
|
66
|
+
'max-statements': 'off',
|
|
67
|
+
complexity: 'off',
|
|
52
68
|
};
|
|
53
69
|
|
|
54
70
|
export const jsRules = {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: migrate-to-pui-cli-9
|
|
3
|
+
description: >-
|
|
4
|
+
Migrate a PUI app or library to pui-cli 9 (Node 24, pnpm 11, ESLint 9 flat config).
|
|
5
|
+
Use when upgrading @elliemae/pui-cli, migrating from .eslintrc.cjs to eslint.config.mjs,
|
|
6
|
+
fixing ESLint 9 lint failures, or adopting the shared flat config from pui-cli.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Migrate to pui-cli 9
|
|
10
|
+
|
|
11
|
+
Upgrades a PUI repo from pui-cli 8 (ESLint 8 + `.eslintrc.cjs`) to pui-cli 9
|
|
12
|
+
(ESLint 9 + `eslint.config.mjs` + Node 24 + pnpm 11).
|
|
13
|
+
|
|
14
|
+
## Pre-flight: Toolchain
|
|
15
|
+
|
|
16
|
+
| Requirement | Version |
|
|
17
|
+
| ------------------- | --------------------------------- |
|
|
18
|
+
| Node.js | **24** (see repo `.node-version`) |
|
|
19
|
+
| pnpm | **11** |
|
|
20
|
+
| `@elliemae/pui-cli` | **9.x** (alpha/beta until GA) |
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
fnm use 24 # or nvm/volta equivalent
|
|
24
|
+
corepack enable
|
|
25
|
+
pnpm install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Migration steps (phased PRs recommended)
|
|
29
|
+
|
|
30
|
+
### Phase 1 — Toolchain only
|
|
31
|
+
|
|
32
|
+
1. Update `.node-version` to `24` if needed.
|
|
33
|
+
2. Ensure CI Jenkins/docker images use Node 24.
|
|
34
|
+
3. Upgrade pnpm to 11 (`packageManager` field in root `package.json`).
|
|
35
|
+
4. Run `pnpm install`, `pnpm test`, `pnpm run build` — fix any Node/pnpm breakages only.
|
|
36
|
+
|
|
37
|
+
### Phase 2 — Bump pui-cli
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm add -D @elliemae/pui-cli@9
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Re-run install and smoke-test build/test without ESLint changes yet if the bump is large.
|
|
44
|
+
|
|
45
|
+
### Phase 3 — ESLint 9 flat config
|
|
46
|
+
|
|
47
|
+
**React apps and libraries:**
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
// eslint.config.mjs
|
|
51
|
+
import { eslintFlatConfig } from '@elliemae/pui-cli/eslint';
|
|
52
|
+
|
|
53
|
+
export default eslintFlatConfig;
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Node / TS services (non-React):**
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// eslint.config.mjs
|
|
60
|
+
import { eslintFlatBaseConfig } from '@elliemae/pui-cli/eslint';
|
|
61
|
+
|
|
62
|
+
export default eslintFlatBaseConfig;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then:
|
|
66
|
+
|
|
67
|
+
1. Delete `.eslintrc.cjs` and `.eslintignore` (ignores are in the shared config).
|
|
68
|
+
2. Run `pnpm exec pui-cli lint --fix`.
|
|
69
|
+
3. Run `pnpm exec pui-cli lint` until **zero errors** (warnings may remain).
|
|
70
|
+
|
|
71
|
+
**Strict mode** (only after default config is clean):
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import { eslintFlatConfigStrict } from '@elliemae/pui-cli/eslint';
|
|
75
|
+
export default eslintFlatConfigStrict;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Phase 4 — Repo-specific overrides (if needed)
|
|
79
|
+
|
|
80
|
+
Add overrides **after** importing the shared config only when lint debt blocks the migration:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
import { eslintFlatConfig } from '@elliemae/pui-cli/eslint';
|
|
84
|
+
|
|
85
|
+
export default [
|
|
86
|
+
...eslintFlatConfig,
|
|
87
|
+
{
|
|
88
|
+
rules: {
|
|
89
|
+
// Temporary: legacy had ~100 no-explicit-any warnings via @typescript-eslint v5
|
|
90
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Remove overrides in a follow-up debt PR. Do not copy Airbnb or legacy `.eslintrc` rules.
|
|
97
|
+
|
|
98
|
+
### Phase 5 — Verify
|
|
99
|
+
|
|
100
|
+
- [ ] `pnpm exec pui-cli lint` — 0 errors
|
|
101
|
+
- [ ] `pnpm exec pui-cli tscheck --files`
|
|
102
|
+
- [ ] `pnpm test`
|
|
103
|
+
- [ ] `pnpm run build` (or `pui-cli build`)
|
|
104
|
+
- [ ] Pre-commit / lint-staged passes
|
|
105
|
+
- [ ] CI green on the target branch
|
|
106
|
+
|
|
107
|
+
## Common lint fixes after upgrade
|
|
108
|
+
|
|
109
|
+
| Symptom | Fix |
|
|
110
|
+
| ------------------------------------ | ---------------------------------------------------------------------- |
|
|
111
|
+
| `import-x/no-unresolved` | Use `import type` for type-only imports |
|
|
112
|
+
| `@typescript-eslint/no-explicit-any` | Type the value or add targeted override (warn) during transition |
|
|
113
|
+
| `@typescript-eslint/no-unused-vars` | Remove or prefix with `_` |
|
|
114
|
+
| Stale `eslint-disable` comments | Remove disables for rules no longer in config |
|
|
115
|
+
| Test/fixture files flagged | Shared config includes `lib/**/tests/**` globs — ensure pui-cli 9.0.0+ |
|
|
116
|
+
| `.d.ts` files | pui-cli turns off `no-explicit-any` for `**/*.d.ts` |
|
|
117
|
+
|
|
118
|
+
Full rule comparison: [eslint-rules-migration.md](https://docs.pui.mortgagetech.q1.ice.com/cli/eslint-rules-migration) (also at `docs/eslint-rules-migration.md` in pui-cli).
|
|
119
|
+
|
|
120
|
+
## Install this skill in Cursor
|
|
121
|
+
|
|
122
|
+
From the consumer repo (after `@elliemae/pui-cli` is installed):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm exec pui-cli skills install migrate-to-pui-cli-9 --target all
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Skills are copied to `.cursor/skills/`, `.claude/skills/`, and `.github/skills/` for Cursor, Claude Code, and GitHub Copilot.
|
|
129
|
+
|
|
130
|
+
## What NOT to change
|
|
131
|
+
|
|
132
|
+
- Application business logic — migration is tooling/config only
|
|
133
|
+
- Webpack/babel config unless pui-cli 9 release notes require it
|
|
134
|
+
- Prettier / Stylelint / commitlint configs unless pui-cli 9 bumps those presets
|
|
135
|
+
|
|
136
|
+
## Additional resources
|
|
137
|
+
|
|
138
|
+
- [pui-cli 9 migration guide](https://docs.pui.mortgagetech.q1.ice.com/cli/pui-cli-9-migration)
|
|
139
|
+
- [ESLint rules migration guide](https://docs.pui.mortgagetech.q1.ice.com/cli/eslint-rules-migration)
|
|
140
|
+
- Reference one-liner: `pui-react-boilerplate/eslint.config.mjs`
|
package/dist/esm/cli.js
CHANGED
|
@@ -17,6 +17,7 @@ import { vitestCmd } from "./commands/vitest.js";
|
|
|
17
17
|
import { versionCmd } from "./commands/version.js";
|
|
18
18
|
import { tscheckCmd } from "./commands/tscheck.js";
|
|
19
19
|
import { buildCDNCmd } from "./commands/buildcdn.js";
|
|
20
|
+
import { skillsCmd } from "./commands/skills.js";
|
|
20
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
envConfig();
|
|
22
23
|
process.env.PATH += path.delimiter + path.join(__dirname, "..", "node_modules", ".bin");
|
|
@@ -33,5 +34,6 @@ process.env.PATH += path.delimiter + path.join(__dirname, "..", "node_modules",
|
|
|
33
34
|
await yargs(hideBin(process.argv)).command(versionCmd).help().argv;
|
|
34
35
|
await yargs(hideBin(process.argv)).command(tscheckCmd).help().argv;
|
|
35
36
|
await yargs(hideBin(process.argv)).command(buildCDNCmd).help().argv;
|
|
37
|
+
await yargs(hideBin(process.argv)).command(skillsCmd).help().argv;
|
|
36
38
|
await notifyUpdates();
|
|
37
39
|
})();
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { access, copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import yargs from "yargs";
|
|
7
|
+
import { logError, logInfo, logSuccess, logWarning } from "./utils.js";
|
|
8
|
+
const SKILL_TARGETS = ["cursor", "claude", "copilot", "all"];
|
|
9
|
+
const TARGET_RELATIVE_DIRS = {
|
|
10
|
+
cursor: [".cursor", "skills"],
|
|
11
|
+
claude: [".claude", "skills"],
|
|
12
|
+
copilot: [".github", "skills"]
|
|
13
|
+
};
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const getPackageRoot = () => {
|
|
16
|
+
const require2 = createRequire(import.meta.url);
|
|
17
|
+
try {
|
|
18
|
+
return path.dirname(require2.resolve("@elliemae/pui-cli/package.json"));
|
|
19
|
+
} catch {
|
|
20
|
+
return path.resolve(__dirname, "..", "..");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const getBundledSkillsDir = () => path.join(getPackageRoot(), "lib", "skills");
|
|
24
|
+
const getTargetSkillsDir = (target) => path.join(process.cwd(), ...TARGET_RELATIVE_DIRS[target]);
|
|
25
|
+
const resolveTargets = (target) => {
|
|
26
|
+
const selected = target ? Array.isArray(target) ? target : [target] : ["cursor"];
|
|
27
|
+
if (selected.includes("all")) {
|
|
28
|
+
return ["cursor", "claude", "copilot"];
|
|
29
|
+
}
|
|
30
|
+
return selected;
|
|
31
|
+
};
|
|
32
|
+
const pathExists = async (targetPath) => {
|
|
33
|
+
try {
|
|
34
|
+
await access(targetPath, constants.F_OK);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const listSkillNames = async (skillsDir) => {
|
|
41
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
42
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
43
|
+
};
|
|
44
|
+
const copySkillDir = async (src, dest) => {
|
|
45
|
+
await mkdir(dest, { recursive: true });
|
|
46
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
47
|
+
await Promise.all(
|
|
48
|
+
entries.map(async (entry) => {
|
|
49
|
+
const srcPath = path.join(src, entry.name);
|
|
50
|
+
const destPath = path.join(dest, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
return copySkillDir(srcPath, destPath);
|
|
53
|
+
}
|
|
54
|
+
return copyFile(srcPath, destPath);
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
const installSkillToTarget = async (skillName, target, force) => {
|
|
59
|
+
const bundledDir = getBundledSkillsDir();
|
|
60
|
+
const src = path.join(bundledDir, skillName);
|
|
61
|
+
const dest = path.join(getTargetSkillsDir(target), skillName);
|
|
62
|
+
if (!await pathExists(src)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Skill "${skillName}" not found. Run "pui-cli skills list" for available skills.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const srcStat = await stat(src);
|
|
68
|
+
if (!srcStat.isDirectory()) {
|
|
69
|
+
throw new Error(`Skill "${skillName}" is not a valid skill directory.`);
|
|
70
|
+
}
|
|
71
|
+
if (await pathExists(dest) && !force) {
|
|
72
|
+
logWarning(
|
|
73
|
+
`Skipped "${skillName}" for ${target} \u2014 already exists at ${dest}. Use --force to overwrite.`
|
|
74
|
+
);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
await copySkillDir(src, dest);
|
|
78
|
+
logSuccess(`Installed skill "${skillName}" to ${dest}`);
|
|
79
|
+
return true;
|
|
80
|
+
};
|
|
81
|
+
const installSkill = async (skillName, targets, force) => {
|
|
82
|
+
let installed = 0;
|
|
83
|
+
for (const target of targets) {
|
|
84
|
+
await mkdir(getTargetSkillsDir(target), { recursive: true });
|
|
85
|
+
if (await installSkillToTarget(skillName, target, force)) {
|
|
86
|
+
installed += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return installed > 0;
|
|
90
|
+
};
|
|
91
|
+
const runList = async () => {
|
|
92
|
+
const bundledDir = getBundledSkillsDir();
|
|
93
|
+
if (!await pathExists(bundledDir)) {
|
|
94
|
+
throw new Error(`Bundled skills directory not found: ${bundledDir}`);
|
|
95
|
+
}
|
|
96
|
+
const skills = await listSkillNames(bundledDir);
|
|
97
|
+
if (!skills.length) {
|
|
98
|
+
logInfo("No bundled skills found.");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
logInfo("Available skills:");
|
|
102
|
+
skills.forEach((name) => logInfo(` - ${name}`));
|
|
103
|
+
};
|
|
104
|
+
const logReloadHint = (targets) => {
|
|
105
|
+
const hints = [];
|
|
106
|
+
if (targets.includes("cursor")) {
|
|
107
|
+
hints.push("Cursor (.cursor/skills/)");
|
|
108
|
+
}
|
|
109
|
+
if (targets.includes("claude")) {
|
|
110
|
+
hints.push("Claude Code (.claude/skills/)");
|
|
111
|
+
}
|
|
112
|
+
if (targets.includes("copilot")) {
|
|
113
|
+
hints.push("GitHub Copilot (.github/skills/)");
|
|
114
|
+
}
|
|
115
|
+
if (hints.length) {
|
|
116
|
+
logInfo(
|
|
117
|
+
`Restart or reload your agent to pick up skills in: ${hints.join(", ")}.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const runInstall = async (name, force, target) => {
|
|
122
|
+
const bundledDir = getBundledSkillsDir();
|
|
123
|
+
if (!await pathExists(bundledDir)) {
|
|
124
|
+
throw new Error(`Bundled skills directory not found: ${bundledDir}`);
|
|
125
|
+
}
|
|
126
|
+
const targets = resolveTargets(target);
|
|
127
|
+
const skills = name ? [name] : await listSkillNames(bundledDir);
|
|
128
|
+
if (!skills.length) {
|
|
129
|
+
logInfo("No bundled skills to install.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let installed = 0;
|
|
133
|
+
for (const skillName of skills) {
|
|
134
|
+
if (await installSkill(skillName, targets, force)) {
|
|
135
|
+
installed += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (installed) {
|
|
139
|
+
logReloadHint(targets);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const skillsCmd = {
|
|
143
|
+
command: "skills <action> [name]",
|
|
144
|
+
describe: "Install bundled agent skills for Cursor, Claude Code, and GitHub Copilot",
|
|
145
|
+
builder: (yargsRef) => yargsRef.positional("action", {
|
|
146
|
+
describe: "List bundled skills or install them into the current repo",
|
|
147
|
+
choices: ["list", "install"],
|
|
148
|
+
demandOption: true
|
|
149
|
+
}).positional("name", {
|
|
150
|
+
describe: "Skill folder name (install all when omitted)",
|
|
151
|
+
type: "string"
|
|
152
|
+
}).option("target", {
|
|
153
|
+
describe: "Install destination: cursor (.cursor/skills), claude (.claude/skills), copilot (.github/skills), or all",
|
|
154
|
+
choices: SKILL_TARGETS,
|
|
155
|
+
default: "cursor"
|
|
156
|
+
}).option("force", {
|
|
157
|
+
describe: "Overwrite an existing skill directory",
|
|
158
|
+
type: "boolean",
|
|
159
|
+
default: false
|
|
160
|
+
}).help(),
|
|
161
|
+
handler: async (argv) => {
|
|
162
|
+
try {
|
|
163
|
+
if (argv.action === "list") {
|
|
164
|
+
await runList();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
await runInstall(argv.name, Boolean(argv.force), argv.target);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
logError(err.message);
|
|
170
|
+
yargs().exit(-1, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
export {
|
|
175
|
+
skillsCmd
|
|
176
|
+
};
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
jestRecommendedRules,
|
|
13
13
|
sharedCoreRules,
|
|
14
14
|
testFiles,
|
|
15
|
+
testFixtureFiles,
|
|
15
16
|
testJsxFiles,
|
|
16
17
|
testingLibraryDomRules,
|
|
17
18
|
testingLibraryReactRules,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
jsRules,
|
|
25
26
|
typescriptRules,
|
|
26
27
|
typescriptStrictRules,
|
|
28
|
+
testFixtureRelaxedRules,
|
|
27
29
|
typescriptTestRelaxedRules,
|
|
28
30
|
} from './rules.mjs';
|
|
29
31
|
|
|
@@ -113,6 +115,10 @@ export function createBaseFlatConfigs(tsRules) {
|
|
|
113
115
|
'testing-library/no-node-access': 'off',
|
|
114
116
|
},
|
|
115
117
|
},
|
|
118
|
+
{
|
|
119
|
+
files: testFixtureFiles,
|
|
120
|
+
rules: testFixtureRelaxedRules,
|
|
121
|
+
},
|
|
116
122
|
{
|
|
117
123
|
files: wdioSpecFiles,
|
|
118
124
|
plugins: { wdio, jest },
|