project-interview-skill 1.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.
- package/LICENSE +35 -0
- package/README.md +188 -0
- package/SKILL.md +208 -0
- package/bin/project-interview-skill.js +470 -0
- package/package.json +33 -0
- package/references/interview-rubric.md +41 -0
- package/references/oral-and-resume-patterns.md +26 -0
- package/references/oral-style-samples.md +59 -0
- package/references/output-templates.md +77 -0
- package/references/star-framework.md +29 -0
- package/scripts/build_prompt.py +202 -0
- package/scripts/check_inputs.py +179 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = 'project-interview-skill';
|
|
9
|
+
const SKILL_DIR_NAME = 'project-interview-skill';
|
|
10
|
+
const SUPPORTED_PRODUCTS = ['agents', 'trae', 'cursor', 'vscode', 'claude-code', 'codex'];
|
|
11
|
+
const COLORS = {
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
bold: '\x1b[1m',
|
|
14
|
+
dim: '\x1b[2m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
blue: '\x1b[34m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
};
|
|
19
|
+
const BUNDLE_ENTRIES = [
|
|
20
|
+
'SKILL.md',
|
|
21
|
+
'references',
|
|
22
|
+
'scripts',
|
|
23
|
+
'README.md',
|
|
24
|
+
'LICENSE',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const command = args[0] && !args[0].startsWith('-') ? args.shift() : 'install';
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
33
|
+
printHelp();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (command === 'install') {
|
|
38
|
+
install(parseArgs(args));
|
|
39
|
+
} else if (command === 'doctor') {
|
|
40
|
+
doctor(parseArgs(args));
|
|
41
|
+
} else if (command === 'help' || command === '--help' || command === '-h') {
|
|
42
|
+
printHelp();
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error(`Unknown command: ${command}`);
|
|
45
|
+
}
|
|
46
|
+
} catch (error) {
|
|
47
|
+
errorLog(error.message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseArgs(args) {
|
|
53
|
+
const options = {
|
|
54
|
+
product: '',
|
|
55
|
+
project: false,
|
|
56
|
+
dryRun: false,
|
|
57
|
+
installAll: false,
|
|
58
|
+
customPath: '',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
62
|
+
const arg = args[index];
|
|
63
|
+
|
|
64
|
+
if (arg === '--project') {
|
|
65
|
+
options.project = true;
|
|
66
|
+
} else if (arg === '--dry-run') {
|
|
67
|
+
options.dryRun = true;
|
|
68
|
+
} else if (arg === '--all') {
|
|
69
|
+
options.installAll = true;
|
|
70
|
+
} else if (arg === '--path') {
|
|
71
|
+
options.customPath = requireValue(args, index, '--path');
|
|
72
|
+
index += 1;
|
|
73
|
+
} else if (arg === '--platform' || arg === '--product') {
|
|
74
|
+
options.product = normalizeProduct(requireValue(args, index, arg));
|
|
75
|
+
index += 1;
|
|
76
|
+
} else if (arg.startsWith('--')) {
|
|
77
|
+
const product = normalizeProduct(arg.slice(2));
|
|
78
|
+
if (SUPPORTED_PRODUCTS.includes(product)) {
|
|
79
|
+
options.product = product;
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (options.installAll && options.product) {
|
|
89
|
+
throw new Error('Use either --all or one product flag, not both.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return options;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function requireValue(args, index, flag) {
|
|
96
|
+
const value = args[index + 1];
|
|
97
|
+
if (!value || value.startsWith('-')) {
|
|
98
|
+
throw new Error(`Missing value for ${flag}`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeProduct(value) {
|
|
104
|
+
const normalized = value.toLowerCase();
|
|
105
|
+
const aliases = {
|
|
106
|
+
agent: 'agents',
|
|
107
|
+
universal: 'agents',
|
|
108
|
+
claude: 'claude-code',
|
|
109
|
+
claudecode: 'claude-code',
|
|
110
|
+
'claude_code': 'claude-code',
|
|
111
|
+
vs: 'vscode',
|
|
112
|
+
'vs-code': 'vscode',
|
|
113
|
+
code: 'vscode',
|
|
114
|
+
};
|
|
115
|
+
return aliases[normalized] || normalized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function install(options) {
|
|
119
|
+
validatePackage();
|
|
120
|
+
|
|
121
|
+
const products = options.installAll
|
|
122
|
+
? SUPPORTED_PRODUCTS
|
|
123
|
+
: [options.product || 'agents'];
|
|
124
|
+
const installedTargets = new Set();
|
|
125
|
+
const installedProducts = [];
|
|
126
|
+
|
|
127
|
+
for (const product of products) {
|
|
128
|
+
validateProduct(product);
|
|
129
|
+
const target = resolveTarget(product, options);
|
|
130
|
+
if (installedTargets.has(target.displayPath)) {
|
|
131
|
+
info(`Skipping ${product}; target already handled: ${target.displayPath}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
installedTargets.add(target.displayPath);
|
|
135
|
+
installProduct(product, target, options);
|
|
136
|
+
installedProducts.push(product);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!options.dryRun && installedProducts.length > 0) {
|
|
140
|
+
printNextSteps(installedProducts, options);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function doctor(options) {
|
|
145
|
+
const cwd = process.cwd();
|
|
146
|
+
const home = os.homedir();
|
|
147
|
+
|
|
148
|
+
info(`Package root: ${packageRoot()}`);
|
|
149
|
+
info(`Current project: ${cwd}`);
|
|
150
|
+
info(`Home: ${home}`);
|
|
151
|
+
console.log('');
|
|
152
|
+
console.log('Product targets:');
|
|
153
|
+
|
|
154
|
+
for (const product of SUPPORTED_PRODUCTS) {
|
|
155
|
+
const userTarget = resolveTarget(product, { ...options, project: false });
|
|
156
|
+
const projectTarget = resolveTarget(product, { ...options, project: true });
|
|
157
|
+
console.log(`- ${product}`);
|
|
158
|
+
console.log(` user: ${userTarget.displayPath}`);
|
|
159
|
+
console.log(` project: ${projectTarget.displayPath}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateProduct(product) {
|
|
164
|
+
if (!SUPPORTED_PRODUCTS.includes(product)) {
|
|
165
|
+
throw new Error(`Unsupported product "${product}". Supported: ${SUPPORTED_PRODUCTS.join(', ')}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolveTarget(product, options) {
|
|
170
|
+
if (options.customPath) {
|
|
171
|
+
return {
|
|
172
|
+
type: 'bundle',
|
|
173
|
+
product,
|
|
174
|
+
root: path.resolve(options.customPath),
|
|
175
|
+
displayPath: path.resolve(options.customPath),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const base = options.project ? process.cwd() : os.homedir();
|
|
180
|
+
|
|
181
|
+
if (product === 'agents' || product === 'codex') {
|
|
182
|
+
const root = path.join(base, '.agents', 'skills', SKILL_DIR_NAME);
|
|
183
|
+
return { type: 'bundle', product, root, displayPath: root };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (product === 'claude-code') {
|
|
187
|
+
const root = path.join(base, '.claude', 'skills', SKILL_DIR_NAME);
|
|
188
|
+
return { type: 'bundle', product, root, displayPath: root };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (product === 'trae') {
|
|
192
|
+
const root = options.project
|
|
193
|
+
? path.join(base, '.trae', 'rules', SKILL_DIR_NAME)
|
|
194
|
+
: path.join(base, '.trae', 'skills', SKILL_DIR_NAME);
|
|
195
|
+
return { type: 'bundle-with-plain-rule', product, root, displayPath: root };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (product === 'cursor') {
|
|
199
|
+
const root = path.join(base, '.cursor', 'rules', SKILL_DIR_NAME);
|
|
200
|
+
return { type: 'bundle-with-cursor-rule', product, root, displayPath: root };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const instructionsFile = options.project
|
|
204
|
+
? path.join(base, '.github', 'instructions', `${SKILL_DIR_NAME}.instructions.md`)
|
|
205
|
+
: path.join(base, '.copilot', 'instructions', `${SKILL_DIR_NAME}.instructions.md`);
|
|
206
|
+
return {
|
|
207
|
+
type: 'vscode-instructions',
|
|
208
|
+
product,
|
|
209
|
+
root: instructionsFile,
|
|
210
|
+
displayPath: instructionsFile,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function installProduct(product, target, options) {
|
|
215
|
+
if (options.dryRun) {
|
|
216
|
+
info(`[dry-run] ${product}: ${target.displayPath}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (target.type === 'vscode-instructions') {
|
|
221
|
+
writeVsCodeInstructions(target.root);
|
|
222
|
+
ok(`Installed for ${productLabel(product)}`);
|
|
223
|
+
console.log(` ${dim(target.root)}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
replaceDirectory(target.root);
|
|
228
|
+
copyBundle(target.root);
|
|
229
|
+
|
|
230
|
+
if (target.type === 'bundle-with-cursor-rule') {
|
|
231
|
+
writeCursorRule(path.join(target.root, `${SKILL_DIR_NAME}.mdc`));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (target.type === 'bundle-with-plain-rule') {
|
|
235
|
+
writePlainRule(path.join(target.root, `${SKILL_DIR_NAME}.md`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
ok(`Installed for ${productLabel(product)}`);
|
|
239
|
+
console.log(` ${dim(target.root)}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function validatePackage() {
|
|
243
|
+
const skillPath = path.join(packageRoot(), 'SKILL.md');
|
|
244
|
+
if (!fs.existsSync(skillPath)) {
|
|
245
|
+
throw new Error(`Missing SKILL.md at ${skillPath}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const firstLine = fs.readFileSync(skillPath, 'utf8').split(/\r?\n/, 1)[0];
|
|
249
|
+
if (firstLine !== '---') {
|
|
250
|
+
throw new Error('SKILL.md must start with YAML frontmatter (---).');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function copyBundle(destination) {
|
|
255
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
256
|
+
|
|
257
|
+
for (const entry of BUNDLE_ENTRIES) {
|
|
258
|
+
const source = path.join(packageRoot(), entry);
|
|
259
|
+
if (!fs.existsSync(source)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const target = path.join(destination, entry);
|
|
263
|
+
copyRecursive(source, target);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function copyRecursive(source, target) {
|
|
268
|
+
const stat = fs.statSync(source);
|
|
269
|
+
if (stat.isDirectory()) {
|
|
270
|
+
fs.mkdirSync(target, { recursive: true });
|
|
271
|
+
for (const child of fs.readdirSync(source)) {
|
|
272
|
+
copyRecursive(path.join(source, child), path.join(target, child));
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
277
|
+
fs.copyFileSync(source, target);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function replaceDirectory(directory) {
|
|
281
|
+
const absolute = path.resolve(directory);
|
|
282
|
+
const home = path.resolve(os.homedir());
|
|
283
|
+
if (absolute === path.parse(absolute).root || absolute === home || absolute === process.cwd()) {
|
|
284
|
+
throw new Error(`Refusing to replace unsafe directory: ${absolute}`);
|
|
285
|
+
}
|
|
286
|
+
fs.rmSync(absolute, { recursive: true, force: true });
|
|
287
|
+
fs.mkdirSync(absolute, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function writeCursorRule(filePath) {
|
|
291
|
+
const description = extractDescription();
|
|
292
|
+
const body = skillBody();
|
|
293
|
+
const content = [
|
|
294
|
+
'---',
|
|
295
|
+
`description: ${description}`,
|
|
296
|
+
'globs:',
|
|
297
|
+
'alwaysApply: true',
|
|
298
|
+
'---',
|
|
299
|
+
body,
|
|
300
|
+
].join('\n');
|
|
301
|
+
writeFile(filePath, content);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function writePlainRule(filePath) {
|
|
305
|
+
writeFile(filePath, skillBody());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function writeVsCodeInstructions(filePath) {
|
|
309
|
+
const body = skillBody();
|
|
310
|
+
const content = [
|
|
311
|
+
'---',
|
|
312
|
+
"applyTo: '**'",
|
|
313
|
+
'---',
|
|
314
|
+
body,
|
|
315
|
+
].join('\n');
|
|
316
|
+
writeFile(filePath, content);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function writeFile(filePath, content) {
|
|
320
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
321
|
+
fs.writeFileSync(filePath, `${content.replace(/\s+$/, '')}\n`, 'utf8');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function skillBody() {
|
|
325
|
+
const content = fs.readFileSync(path.join(packageRoot(), 'SKILL.md'), 'utf8');
|
|
326
|
+
const lines = content.split(/\r?\n/);
|
|
327
|
+
let delimiterCount = 0;
|
|
328
|
+
const body = [];
|
|
329
|
+
|
|
330
|
+
for (const line of lines) {
|
|
331
|
+
if (line === '---') {
|
|
332
|
+
delimiterCount += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (delimiterCount >= 2) {
|
|
336
|
+
body.push(line);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return body.join('\n').trim();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extractDescription() {
|
|
344
|
+
const content = fs.readFileSync(path.join(packageRoot(), 'SKILL.md'), 'utf8');
|
|
345
|
+
const lines = content.split(/\r?\n/);
|
|
346
|
+
let inFrontmatter = false;
|
|
347
|
+
let inDescriptionBlock = false;
|
|
348
|
+
const description = [];
|
|
349
|
+
|
|
350
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
351
|
+
const line = lines[index];
|
|
352
|
+
if (index === 0 && line === '---') {
|
|
353
|
+
inFrontmatter = true;
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (inFrontmatter && line === '---') {
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
if (!inFrontmatter) {
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (inDescriptionBlock) {
|
|
363
|
+
if (/^\s+/.test(line)) {
|
|
364
|
+
description.push(line.trim());
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (line.startsWith('description:')) {
|
|
370
|
+
const value = line.replace(/^description:\s*/, '').trim();
|
|
371
|
+
if (value === '>-' || value === '>' || value === '') {
|
|
372
|
+
inDescriptionBlock = true;
|
|
373
|
+
} else {
|
|
374
|
+
return value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return description.join(' ') || PACKAGE_NAME;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function packageRoot() {
|
|
383
|
+
return path.resolve(__dirname, '..');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function printHelp() {
|
|
387
|
+
console.log(`${bold(PACKAGE_NAME)}
|
|
388
|
+
|
|
389
|
+
Usage:
|
|
390
|
+
npx ${PACKAGE_NAME} install [--agents|--trae|--cursor|--vscode|--claude-code|--codex]
|
|
391
|
+
npx ${PACKAGE_NAME} install --project [--trae|--cursor|--vscode|--claude-code|--codex]
|
|
392
|
+
npx ${PACKAGE_NAME} install --all [--project]
|
|
393
|
+
npx ${PACKAGE_NAME} doctor
|
|
394
|
+
|
|
395
|
+
Options:
|
|
396
|
+
--agents Install to ~/.agents/skills/ (default user-level target)
|
|
397
|
+
--trae Install for Trae
|
|
398
|
+
--cursor Install for Cursor
|
|
399
|
+
--vscode Install VS Code Copilot instructions
|
|
400
|
+
--claude-code Install for Claude Code
|
|
401
|
+
--codex Install to .agents/skills for Codex
|
|
402
|
+
--project Install under the current project instead of the user home
|
|
403
|
+
--all Install every supported target
|
|
404
|
+
--path PATH Install the full skill bundle to a custom path
|
|
405
|
+
--dry-run Print target paths without writing files
|
|
406
|
+
-h, --help Show this help
|
|
407
|
+
`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function info(message) {
|
|
411
|
+
console.log(`${color('[info]', 'blue')} ${message}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function ok(message) {
|
|
415
|
+
console.log(`${color('[ok]', 'green')} ${message}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function errorLog(message) {
|
|
419
|
+
console.error(`${color('[error]', 'red')} ${message}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function printNextSteps(products, options) {
|
|
423
|
+
const targetText = products.map(productLabel).join(', ');
|
|
424
|
+
console.log('');
|
|
425
|
+
ok(`${PACKAGE_NAME} is ready for ${targetText}.`);
|
|
426
|
+
console.log('');
|
|
427
|
+
console.log(bold('Next:'));
|
|
428
|
+
console.log(` 1. Restart ${nextStepProductText(products)}.`);
|
|
429
|
+
console.log(' 2. Open your project root.');
|
|
430
|
+
console.log(` 3. Start a new chat with ${bold('/project-interview-skill')}.`);
|
|
431
|
+
if (!options.project) {
|
|
432
|
+
console.log(` 4. For one-project-only setup, run ${dim(`npx ${PACKAGE_NAME} install --project --<product>`)} in that project.`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function nextStepProductText(products) {
|
|
437
|
+
if (products.length === 1) {
|
|
438
|
+
return productLabel(products[0]);
|
|
439
|
+
}
|
|
440
|
+
return 'the target IDE or Agent';
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function productLabel(product) {
|
|
444
|
+
const labels = {
|
|
445
|
+
agents: 'Generic Agent',
|
|
446
|
+
trae: 'Trae',
|
|
447
|
+
cursor: 'Cursor',
|
|
448
|
+
vscode: 'VS Code',
|
|
449
|
+
'claude-code': 'Claude Code',
|
|
450
|
+
codex: 'Codex',
|
|
451
|
+
};
|
|
452
|
+
return labels[product] || product;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function bold(text) {
|
|
456
|
+
return color(text, 'bold');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function dim(text) {
|
|
460
|
+
return color(text, 'dim');
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function color(text, colorName) {
|
|
464
|
+
if (process.env.NO_COLOR || !process.stdout.isTTY) {
|
|
465
|
+
return text;
|
|
466
|
+
}
|
|
467
|
+
return `${COLORS[colorName]}${text}${COLORS.reset}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project-interview-skill",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Install a project interview preparation skill for AI coding agents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "EastonJiang <jiangxu05@outlook.com>",
|
|
7
|
+
"homepage": "https://github.com/Jaxon1216/interview-analyzer-skill#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Jaxon1216/interview-analyzer-skill.git"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"project-interview-skill": "bin/project-interview-skill.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"SKILL.md",
|
|
18
|
+
"references/",
|
|
19
|
+
"scripts/",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "python3 -m unittest discover -s tests -v",
|
|
25
|
+
"pack:check": "npm pack --dry-run"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 大厂风格面试关注点(工程向)
|
|
2
|
+
|
|
3
|
+
用于生成「面经」与「追问」时的隐性评分维度。不必逐条对用户展示,但**问题与 STAR 要点应能覆盖其中多数**。
|
|
4
|
+
|
|
5
|
+
## 问题空间
|
|
6
|
+
|
|
7
|
+
- **场景**:用户量、数据量、峰值、地域、合规(如有)。
|
|
8
|
+
- **约束**:延迟 SLO、成本上限、兼容性、迁移窗口、团队规模。
|
|
9
|
+
|
|
10
|
+
## 技术深度
|
|
11
|
+
|
|
12
|
+
- **原理**:为何这样设计,替代方案为何不行或次优。
|
|
13
|
+
- **边界**:失败模式、竞态、一致性、背压、超时与重试风暴。
|
|
14
|
+
- **观测**:日志/指标/链路追踪如何证明「有效」而非「感觉」。
|
|
15
|
+
|
|
16
|
+
## 工程实践
|
|
17
|
+
|
|
18
|
+
- **质量**:测试策略(单测/契约/集成/E2E)、CI、灰度、特性开关。
|
|
19
|
+
- **协作**:接口契约、Code Review 关注点、文档与 handoff。
|
|
20
|
+
- **线上**:事故响应、回滚、数据修复、事后复盘(无责文化下的改进项)。
|
|
21
|
+
|
|
22
|
+
## 前端补充
|
|
23
|
+
|
|
24
|
+
- 性能:首屏、长列表、包体、缓存与 CDN。
|
|
25
|
+
- 状态与数据:服务端状态 vs 客户端、错误与加载边界。
|
|
26
|
+
|
|
27
|
+
## 后端补充
|
|
28
|
+
|
|
29
|
+
- 存储与索引、事务边界、幂等、消息语义(at-most-once / at-least-once / exactly-once 的表述谨慎)。
|
|
30
|
+
- 多租户、限流、熔断、降级。
|
|
31
|
+
|
|
32
|
+
## AI / ML 补充
|
|
33
|
+
|
|
34
|
+
- 数据与标签、离线 vs 在线、延迟与成本、评估指标与偏差。
|
|
35
|
+
- 安全:提示注入、PII、模型版本与回滚。
|
|
36
|
+
|
|
37
|
+
## 「空话」黑名单(出现则改写)
|
|
38
|
+
|
|
39
|
+
- 「优化了性能」→ 改为指标或**如何测量**。
|
|
40
|
+
- 「提高了用户体验」→ 改为具体交互或错误率/任务完成率。
|
|
41
|
+
- 「使用了先进架构」→ 改为具体模式与权衡。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 口播叙述与简历一句话(抽象模板)
|
|
2
|
+
|
|
3
|
+
## 口播长答(工程向)推荐结构
|
|
4
|
+
|
|
5
|
+
来自典型高质量回答的抽象,**不固定标题名**,但信息块应齐全,便于第一人称口述 ≥150 字。
|
|
6
|
+
|
|
7
|
+
1. **场景(现象)**:线上/联调/上线初期的具体现象;可带比例、范围、如何发现(DevTools、日志、监控)。术语可加英文(如 `option`、`JSON.parse`)。
|
|
8
|
+
2. **归因(根因)**:为何会发生——错把不确定输出当稳定 API、缺防御层、契约不清等;一句话点出**本质**。
|
|
9
|
+
3. **动作(方案)**:分层写清:我先……再……最后……;可用编号列表(容错提取 → Schema 校验 → sanitize → 边界兜底);每层说**手段 + 目的**。
|
|
10
|
+
4. **结果与兜底**:指标或(待测);若仍有异常路径,写降级(如 Error Boundary)与观测。
|
|
11
|
+
|
|
12
|
+
口播语气:**面经全文第一人称「我」**;导学讲解可用「你应当理解」或中性说明,但涉及「如何答面试」的示范段可用第一人称口播。
|
|
13
|
+
|
|
14
|
+
## 简历「项目简介 / 一句话」写法
|
|
15
|
+
|
|
16
|
+
用 **1~2 句** 说清:**做什么 + 关键技术/形态 + 可选一句价值**,避免形容词堆砌。
|
|
17
|
+
|
|
18
|
+
**抽象公式**:
|
|
19
|
+
|
|
20
|
+
> 基于 {核心技术栈/范式} 的 {产品形态},用户/业务侧能 {核心操作},系统支持 {关键能力 A、B},{可选:差异点如同步/异步、端侧等}。
|
|
21
|
+
|
|
22
|
+
**示例形态(勿照抄项目名)**:
|
|
23
|
+
|
|
24
|
+
> 基于 AI 大模型的智能 BI:用户上传 Excel 后,AI 自动生成 ECharts 图表与分析结论,支持同步/异步生成与拖拽看板组合多图。
|
|
25
|
+
|
|
26
|
+
面经文档中的 **项目概要** 必须接近此密度与信息量,可直接贴简历「项目描述」栏。
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 口播抽象层级样例(前端 + 后端各一份)
|
|
2
|
+
|
|
3
|
+
面经口播的默认调性:**通用工程语言为主 + 极少量内部名兜底证据**。以下两份样例都属于该调性(简称 "样例风格 B"),生成面经时应对齐该抽象层级,不得下沉到"框架内部 API 堆叠"风格。
|
|
4
|
+
|
|
5
|
+
## 判定标准
|
|
6
|
+
|
|
7
|
+
- ✅ 通用词占主导(React / SSR / Hydration / State Machine / JSBridge / Race Condition / Idempotency / P95 …)
|
|
8
|
+
- ✅ 每 150 字口播里,内部私名(私有 hook、私有函数、后端字段、私有枚举、私有打点事件、私有容器/命名空间、灰度开关键、业务代号)出现次数 ≤ 2 次
|
|
9
|
+
- ✅ 每次出现私名,后半句紧跟"本质是 ×× 模式 / 抽象上是 ××"的翻译
|
|
10
|
+
- ❌ 一整段口播里堆叠 5 个以上私名、每句一个字段名或函数名,即使内容真实也判不通过
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 样例一(前端)——首页 SSR 主链路与失败降级
|
|
15
|
+
|
|
16
|
+
**面试官问题**:你们首页是怎么做服务端渲染的?SSR 失败了怎么办?
|
|
17
|
+
|
|
18
|
+
**口播版(第一人称,≥150 字,完整 STAR,对齐样例风格 B)**:
|
|
19
|
+
|
|
20
|
+
我们首页是端内 H5 主入口,流量大、首屏敏感,所以走了 **Streaming SSR**(情境)。目标是让首屏尽量在服务端直出、同时任何异常都不能白屏(任务)。做法上我把主链路拆成三段:①**服务端预取阶段**拉首屏聚合接口,拿到数据先做**契约断言**——字段不合法就抛错触发框架降级(内部我们叫 assert,本质是**响应体 schema 校验**),宁可退化到 CSR 也不渲染错数据;②**渲染阶段**用一个 async data boundary(类似 `<Suspense for data>`),把加载、成功、失败三种边界显式建模,pending 期展示骨架屏防止布局抖动(CLS)、resolve 后走 hydration、reject 直接跳兜底页;③**客户端接管**后我用一个**运行时命中标志**判断本次是否真的走了 SSR——没命中或者数据没落地就补发一次 CSR 请求兜底,不依赖服务端一定成功(行动)。最终形成"SSR 直出 → 命中即水合 → 未命中/失败降级 CSR 或兜底页"的闭环,并用**水合成功率打点**观测效果,具体数值以监控为准(结果/兜底)。
|
|
21
|
+
|
|
22
|
+
**追问**:你怎么保证服务端和客户端渲染结果一致,不出现 Hydration mismatch?
|
|
23
|
+
|
|
24
|
+
**口播版(≥150 字)**:
|
|
25
|
+
|
|
26
|
+
Hydration mismatch 是同构渲染最经典的坑,根因是**服务端和客户端首次渲染看到的输入不一致**(情境/归因)。我的防护分三层:第一层是**运行环境隔离**——服务端预取分支绝不访问 `window`/`localStorage`/`navigator` 这类浏览器专属 API,需要的上下文统一通过请求头/query 在服务端算好,再通过一个**可序列化的 context**透传给客户端 hook 消费(行动)。这里有个序列化的坑:Fetch 标准的 `Headers` 是迭代器对象、直接 JSON 化会丢字段,所以要手动 forEach 摊平成 plain object 再传。第二层是**只在客户端渲染的内容**用 `<ClientOnly>` 或延迟到 `useEffect` 里执行,避免服务端提前渲染出和客户端不一致的 DOM。第三层是**身份态校验**——因为 SSR 直出的 HTML 里带的是服务端识别到的用户身份,如果端上当前登录用户已经变了(比如切了账号),我会在 hydration 完成后做一次身份比对,不一致就带一个 query 参数(相当于**强制 CSR 的 feature flag**)重新打开页面,避免串数据(行动/兜底)。这套做法上线后 hydration 相关的报错基本收敛到 0,具体成功率以打点监控为准(结果/待测)。
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 样例二(后端)——高并发下单接口的幂等与限流
|
|
31
|
+
|
|
32
|
+
**面试官问题**:秒杀/大促场景下单接口你们怎么设计的?高并发下怎么保证不超卖、不重复扣款?
|
|
33
|
+
|
|
34
|
+
**口播版(第一人称,≥150 字,完整 STAR,对齐样例风格 B)**:
|
|
35
|
+
|
|
36
|
+
秒杀场景对我这条链路的核心挑战有三个:**瞬时高 QPS、库存不能超卖、支付不能重复扣款**(情境/任务)。我把下单接口设计成一条**分层收敛**的链路:①**入口层**先做限流——网关用令牌桶按用户维度限流,再叠加一层**热点探测**,发现某个 SKU 突增就走单独的**熔断降级**通道,防止把下游打爆(行动);②**幂等层**是核心——每次下单前端必须带一个**客户端生成的 request id**(idempotency key),后端拿到后先查 Redis 是否已处理过,如果命中就直接返回上次结果,保证同一 key 无论重放多少次都只成功扣一次库存(行动);③**库存扣减层**用 Redis 原子操作(`DECR` + Lua 脚本)预扣,预扣成功才进 MQ 走真正的落库与支付流程,避免直接打 DB 造成行锁(行动);④**最终一致**——支付回调走事务消息,失败/超时靠对账 job 兜底,保证扣款和订单状态最终一致(行动)。整条链路的 P95 我控制在 200ms 以内,超卖率上线以来是 0,幂等命中率和限流拒绝率都有监控大盘可以看,具体数值以监控为准(结果/兜底)。
|
|
37
|
+
|
|
38
|
+
**追问**:如果 Redis 挂了、库存扣减扛不住怎么办?你怎么设计降级?
|
|
39
|
+
|
|
40
|
+
**口播版(≥150 字)**:
|
|
41
|
+
|
|
42
|
+
Redis 是这条链路的**关键依赖**,单点挂掉必须有兜底,不能让业务直接雪崩(情境/归因)。我的降级分三档:第一档是**Redis 主从切换**——运维层用 Sentinel 做故障转移,业务侧客户端配置了合理的连接超时和重试,主挂了几秒内会切到从,业务方几乎无感(行动);第二档是**Redis 整体不可用时的软降级**——预扣库存这一步用**本地内存 + 分布式限流器**做临时替代,单机维度还是能阻挡明显的超卖,虽然精度下降但至少不崩;同时前端页面通过一个 feature toggle(**灰度开关**)提示"火爆中,稍后重试",让流量削峰,避免请求全部打到 DB(行动);第三档是**兜底对账**——即便真的超卖了几单,我在异步链路里跑一个**定时对账 job**,拿实际入库量对齐订单表,超出部分自动退款并通知用户,保证账目最终正确(行动/兜底)。这套设计的底层原则是**"关键依赖不能有单点"**,同时**每一层降级都要伴随可观测**——限流拒绝率、Redis 命中率、对账差异数都有监控告警,一旦触发能第一时间人工介入,具体阈值和历史触发次数以监控为准(结果/待测)。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 反例(禁止的调性)
|
|
47
|
+
|
|
48
|
+
同一个前端 SSR 问题,如果写成下面这样就是**私名堆叠型**,面经生成时要主动改写:
|
|
49
|
+
|
|
50
|
+
> 我们首页配的是 `ssr: { mode: 'stream' }`,服务端 `prefetch.server.ts` 里先 `fetchXxxHomepage` 拿数据,`homepageAssert` 断言失败就抛错。客户端入口用 `<Await resolve={promise}>` 解析预取结果,pending 显示 `RouterSkeleton`,reject 走 `errorElement` 跳 50x。我在 `useEffect` 里用 `isSSR()` 判断,如果 `getSSRRuntimeInfo().hit` 为 false,就调 `fetchRouterData` 补发 CSR 请求。全链路水合成功打点用的是 `xxx_router_streaming_ssr_hydrate_success`。
|
|
51
|
+
|
|
52
|
+
失败原因:150 字里堆叠了 10+ 个只在本项目/本公司能搜到的私名,外部面试官既记不住、也无法追问,信息密度看似高实则接近 0。生成时应对齐样例一的抽象层级重写。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 使用方式
|
|
57
|
+
|
|
58
|
+
- 生成面经每一道主问/追问的口播时,先写通用抽象叙述,再决定是否需要 1~2 个内部名做证据;不要反过来"从私名堆到抽象"。
|
|
59
|
+
- 生成完成后,拿本文件的**判定标准**做自检:密度超标、翻译缺失、堆叠反例特征命中三项中任何一项 → 该题重写。
|