shakers-cli-beta 0.8.0-beta.5 → 0.10.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/README.md +39 -9
- package/bin/ai-usage.js +37 -19
- package/bin/login.js +1 -1
- package/bin/mcp.js +25 -21
- package/bin/mcpb.js +5 -0
- package/package.json +3 -2
- package/src/auth-session-store.js +1 -0
- package/src/authorship.js +7 -6
- package/src/backend-request.js +2 -2
- package/src/certify-dimension-client.js +0 -54
- package/src/certify-dimension-flow.js +20 -81
- package/src/config.js +29 -8
- package/src/cv-file.js +62 -0
- package/src/device-login.js +5 -3
- package/src/find-projects-client.js +16 -6
- package/src/git-activity.js +8 -7
- package/src/git-log-prefetch.js +53 -0
- package/src/i18n.js +33 -58
- package/src/mcp-ai-usage-tool.js +150 -65
- package/src/mcp-auth-form.js +213 -44
- package/src/mcp-certify-dimension-tools.js +2 -2
- package/src/mcp-choice.js +47 -0
- package/src/mcp-install.js +32 -3
- package/src/mcp-instructions.js +27 -0
- package/src/mcp-onboarding-tools.js +3 -3
- package/src/mcp-prompts.js +73 -0
- package/src/mcp-rate-tools.js +2 -2
- package/src/mcp-register-tools.js +89 -313
- package/src/mcp-role-tools.js +177 -16
- package/src/mcp-server.js +108 -4
- package/src/mcp-signup-client.js +118 -0
- package/src/mcp-signup-tools.js +818 -0
- package/src/onboarding-client.js +28 -52
- package/src/onboarding-flow.js +3 -147
- package/src/onboarding-status-client.js +16 -1
- package/src/register-context.js +1 -17
- package/src/roles-client.js +3 -1
- package/src/scanner.js +13 -1
- package/src/session-normalizers.js +174 -0
- package/src/session-scan.js +149 -10
- package/src/signup-client.js +33 -21
- package/src/signup-copy.js +338 -0
- package/src/signup-language.js +21 -0
- package/src/usage-run-steps.js +3 -2
- package/src/work-streams.js +7 -2
package/src/cv-file.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { expandHome } = require('./env-paths');
|
|
6
|
+
|
|
7
|
+
// The talent names a file on their own machine. Read it only when the resolved file (symlinks
|
|
8
|
+
// followed) really is a CV: an allowed extension, a regular file, under the cap, and starting
|
|
9
|
+
// with that format's signature. Anything else is refused before it is loaded or sent.
|
|
10
|
+
const CV_MAX_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
const CV_FORMATS = {
|
|
13
|
+
'.pdf': { contentType: 'application/pdf', signature: Buffer.from('%PDF-') },
|
|
14
|
+
'.doc': { contentType: 'application/msword', signature: Buffer.from([0xd0, 0xcf, 0x11, 0xe0]) },
|
|
15
|
+
'.docx': {
|
|
16
|
+
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
17
|
+
signature: Buffer.from([0x50, 0x4b, 0x03, 0x04]),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function readLocalCv(input, { extensions, badFormat }) {
|
|
22
|
+
if (typeof input !== 'string' || !input.trim()) return { ok: false, reason: 'cv-not-found' };
|
|
23
|
+
const given = expandHome(input.trim());
|
|
24
|
+
const ext = path.extname(given).toLowerCase();
|
|
25
|
+
if (!extensions.includes(ext)) return { ok: false, reason: badFormat };
|
|
26
|
+
const format = CV_FORMATS[ext];
|
|
27
|
+
let real;
|
|
28
|
+
try {
|
|
29
|
+
real = fs.realpathSync(given);
|
|
30
|
+
} catch {
|
|
31
|
+
return { ok: false, reason: 'cv-not-found' };
|
|
32
|
+
}
|
|
33
|
+
if (path.extname(real).toLowerCase() !== ext) return { ok: false, reason: badFormat };
|
|
34
|
+
|
|
35
|
+
let fd;
|
|
36
|
+
try {
|
|
37
|
+
fd = fs.openSync(real, 'r');
|
|
38
|
+
} catch {
|
|
39
|
+
return { ok: false, reason: 'cv-not-found' };
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const stat = fs.fstatSync(fd);
|
|
43
|
+
if (!stat.isFile()) return { ok: false, reason: 'cv-not-found' };
|
|
44
|
+
if (stat.size > CV_MAX_BYTES) return { ok: false, reason: 'cv-too-large' };
|
|
45
|
+
const data = Buffer.alloc(stat.size);
|
|
46
|
+
let read = 0;
|
|
47
|
+
while (read < stat.size) {
|
|
48
|
+
const n = fs.readSync(fd, data, read, stat.size - read, read);
|
|
49
|
+
if (n === 0) break;
|
|
50
|
+
read += n;
|
|
51
|
+
}
|
|
52
|
+
const content = data.subarray(0, read);
|
|
53
|
+
if (!content.subarray(0, format.signature.length).equals(format.signature)) return { ok: false, reason: badFormat };
|
|
54
|
+
return { ok: true, file: { filename: path.basename(given), contentType: format.contentType, data: content } };
|
|
55
|
+
} catch {
|
|
56
|
+
return { ok: false, reason: 'cv-not-found' };
|
|
57
|
+
} finally {
|
|
58
|
+
fs.closeSync(fd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { readLocalCv, CV_MAX_BYTES };
|
package/src/device-login.js
CHANGED
|
@@ -32,7 +32,7 @@ async function pollDeviceToken({ deviceCode, tokenEndpoint, clientId, timeoutMs
|
|
|
32
32
|
|
|
33
33
|
// ORDER: a NEW user needs complete-registration BEFORE GET /auth/token (else the Hub 500s: no domain user).
|
|
34
34
|
async function finalizeDeviceSession(
|
|
35
|
-
{ sessionToken, isNewUser, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, timeoutMs } = {},
|
|
35
|
+
{ sessionToken, isNewUser, authTokenEndpoint, completeRegistrationEndpoint = null, registrationContext = null, claimCode = null, timeoutMs } = {},
|
|
36
36
|
{
|
|
37
37
|
requestCompleteRegistration = require('./signup-client').requestCompleteRegistration,
|
|
38
38
|
requestApiToken: apiToken = requestApiToken,
|
|
@@ -40,14 +40,16 @@ async function finalizeDeviceSession(
|
|
|
40
40
|
) {
|
|
41
41
|
if (!authTokenEndpoint) return { ok: false, reason: 'no-endpoint' };
|
|
42
42
|
|
|
43
|
+
let claimed = false;
|
|
43
44
|
if (isNewUser && completeRegistrationEndpoint) {
|
|
44
|
-
const done = await requestCompleteRegistration(registrationContext || {}, { endpoint: completeRegistrationEndpoint, bearerToken: sessionToken, timeoutMs });
|
|
45
|
+
const done = await requestCompleteRegistration(registrationContext || {}, { endpoint: completeRegistrationEndpoint, bearerToken: sessionToken, claimCode, timeoutMs });
|
|
45
46
|
if (!done.ok) return { ok: false, reason: done.reason || 'complete-registration-failed' };
|
|
47
|
+
claimed = done.claimed === true;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
const jwt = await apiToken({ accessToken: sessionToken }, { endpoint: authTokenEndpoint, timeoutMs });
|
|
49
51
|
if (!jwt.ok) return { ok: false, reason: 'token-exchange-failed' };
|
|
50
|
-
return { ok: true, token: jwt.token };
|
|
52
|
+
return claimCode ? { ok: true, token: jwt.token, claimed } : { ok: true, token: jwt.token };
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
async function runDeviceLogin(
|
|
@@ -47,12 +47,13 @@ function strArray(v) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
// Only fields the readview's criteria map supports server-side: savedByMe,
|
|
50
|
-
// attendance, country (confirmed in FindPositionsService + its prisma readview).
|
|
50
|
+
// attendance, country, clusterIds (confirmed in FindPositionsService + its prisma readview).
|
|
51
51
|
// languages and skills are output-only (no filter field) → not built here.
|
|
52
|
-
function buildCriteria(tab, { limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null } = {}) {
|
|
52
|
+
function buildCriteria(tab, { limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null, clusterId = null } = {}) {
|
|
53
53
|
const criteria = { limit, offset };
|
|
54
54
|
const conditions = [];
|
|
55
55
|
if (tab === 'saved') conditions.push({ field: 'savedByMe', operator: 'EQUALS', value: true });
|
|
56
|
+
if (clusterId) conditions.push({ field: 'clusterIds', operator: 'IN', value: [clusterId] });
|
|
56
57
|
if (attendance) conditions.push({ field: 'attendance', operator: 'IN', value: [attendance] });
|
|
57
58
|
if (country) conditions.push({ field: 'country', operator: 'IN', value: [country] });
|
|
58
59
|
if (conditions.length) criteria.filter = { conjunction: 'AND', conditions };
|
|
@@ -93,12 +94,12 @@ function normalizeItem(raw) {
|
|
|
93
94
|
|
|
94
95
|
async function requestFindProjects(
|
|
95
96
|
{ hubAccessToken } = {},
|
|
96
|
-
{ tab = 'all', endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null } = {},
|
|
97
|
+
{ tab = 'all', endpoint, timeoutMs = DEFAULT_TIMEOUT_MS, limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null, clusterId = null } = {},
|
|
97
98
|
) {
|
|
98
99
|
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
99
100
|
if (!hubAccessToken) return { ok: false, reason: 'no-hub-token' };
|
|
100
101
|
|
|
101
|
-
const criteria = buildCriteria(tab, { limit, offset, attendance, country });
|
|
102
|
+
const criteria = buildCriteria(tab, { limit, offset, attendance, country, clusterId });
|
|
102
103
|
const url = `${endpoint}${endpoint.includes('?') ? '&' : '?'}criteria=${encodeURIComponent(JSON.stringify(criteria))}`;
|
|
103
104
|
let res;
|
|
104
105
|
try {
|
|
@@ -117,18 +118,27 @@ async function requestFindProjects(
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
// CLI+MCP entry: resolve the endpoint from config, then fetch one page.
|
|
120
|
-
async function fetchFindProjects(deps = {}, { hubAccessToken, tab = 'all', timeoutMs = DEFAULT_TIMEOUT_MS, limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null } = {}) {
|
|
121
|
+
async function fetchFindProjects(deps = {}, { hubAccessToken, tab = 'all', timeoutMs = DEFAULT_TIMEOUT_MS, limit = DEFAULT_LIMIT, offset = 0, attendance = null, country = null, clusterId = null } = {}) {
|
|
121
122
|
const {
|
|
122
123
|
getFindPositionsEndpoint = require('./config').getFindPositionsEndpoint,
|
|
123
124
|
requestFindProjects: request = requestFindProjects,
|
|
124
125
|
} = deps;
|
|
125
126
|
const endpoint = getFindPositionsEndpoint();
|
|
126
127
|
if (!endpoint) return { ok: false, reason: 'no-endpoint' };
|
|
127
|
-
return request({ hubAccessToken }, { tab, endpoint, timeoutMs, limit, offset, attendance, country });
|
|
128
|
+
return request({ hubAccessToken }, { tab, endpoint, timeoutMs, limit, offset, attendance, country, clusterId });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Open positions visible to the talent that look for one role (cluster): the list's own total, one row fetched.
|
|
132
|
+
async function countOpenPositions(deps = {}, { hubAccessToken, clusterId, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
133
|
+
const res = await fetchFindProjects(deps, { hubAccessToken, timeoutMs, limit: 1, clusterId });
|
|
134
|
+
if (!res.ok) return res;
|
|
135
|
+
const total = res.meta && Number.isInteger(res.meta.total) ? res.meta.total : null;
|
|
136
|
+
return total === null ? { ok: false, reason: 'no-total' } : { ok: true, total };
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
module.exports = {
|
|
131
140
|
TABS,
|
|
141
|
+
countOpenPositions,
|
|
132
142
|
ATTENDANCE,
|
|
133
143
|
DEFAULT_LIMIT,
|
|
134
144
|
buildCriteria,
|
package/src/git-activity.js
CHANGED
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
|
+
const { takePrefetchedGitLog } = require('./git-log-prefetch');
|
|
7
|
+
|
|
6
8
|
const GIT_TIMEOUT_MS = 15000;
|
|
7
9
|
const MAX_FILE_TYPES = 50;
|
|
8
10
|
|
|
11
|
+
// The one history read behind the activity numbers (\x01 = commit marker, \x1f = field sep).
|
|
12
|
+
const GIT_ACTIVITY_LOG_ARGS = ['log', '--no-merges', '--numstat', '--pretty=format:\x01%ae\x1f%at'];
|
|
13
|
+
|
|
9
14
|
function runGit(root, args) {
|
|
10
15
|
try {
|
|
11
16
|
return execFileSync('git', ['-C', root, ...args], {
|
|
@@ -56,12 +61,8 @@ function collectGitActivity(root, verifiedEmail) {
|
|
|
56
61
|
|
|
57
62
|
const sep = String.fromCharCode(1);
|
|
58
63
|
const unit = String.fromCharCode(31);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
'--no-merges',
|
|
62
|
-
'--numstat',
|
|
63
|
-
`--pretty=format:${sep}%ae${unit}%at`,
|
|
64
|
-
]);
|
|
64
|
+
const prefetchedLog = takePrefetchedGitLog(root, GIT_ACTIVITY_LOG_ARGS);
|
|
65
|
+
const log = prefetchedLog !== undefined ? prefetchedLog : runGit(root, GIT_ACTIVITY_LOG_ARGS);
|
|
65
66
|
if (log === null) return null;
|
|
66
67
|
|
|
67
68
|
const target = String(verifiedEmail || '')
|
|
@@ -120,4 +121,4 @@ function collectGitActivity(root, verifiedEmail) {
|
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
module.exports = { collectGitActivity, extensionOf, isGitRepo };
|
|
124
|
+
module.exports = { collectGitActivity, extensionOf, isGitRepo, GIT_ACTIVITY_LOG_ARGS };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('child_process');
|
|
4
|
+
|
|
5
|
+
// Runs the aggregation's per-repo git logs concurrently with the collectors' exact args; each result is taken once and expires.
|
|
6
|
+
|
|
7
|
+
const GIT_TIMEOUT_MS = 15000;
|
|
8
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
9
|
+
const MAX_CONCURRENT = 8;
|
|
10
|
+
const TTL_MS = 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const prefetched = new Map();
|
|
13
|
+
|
|
14
|
+
const keyOf = (root, args) => JSON.stringify([root, args]);
|
|
15
|
+
|
|
16
|
+
function runAsync(root, args) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
execFile(
|
|
19
|
+
'git',
|
|
20
|
+
['-C', root, ...args],
|
|
21
|
+
{ encoding: 'utf8', timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER },
|
|
22
|
+
(err, stdout) => resolve(err ? null : stdout),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Runs every (root, args) pair, at most MAX_CONCURRENT at a time. Never throws.
|
|
28
|
+
async function prefetchGitLogs(roots, argSets) {
|
|
29
|
+
const jobs = [];
|
|
30
|
+
for (const root of roots || []) {
|
|
31
|
+
for (const args of argSets) jobs.push({ root, args });
|
|
32
|
+
}
|
|
33
|
+
let next = 0;
|
|
34
|
+
const worker = async () => {
|
|
35
|
+
while (next < jobs.length) {
|
|
36
|
+
const { root, args } = jobs[next++];
|
|
37
|
+
const out = await runAsync(root, args);
|
|
38
|
+
if (out !== null) prefetched.set(keyOf(root, args), { out, at: Date.now() });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT, jobs.length) }, worker));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The prefetched stdout for exactly this command, once; `undefined` on a miss.
|
|
45
|
+
function takePrefetchedGitLog(root, args) {
|
|
46
|
+
const key = keyOf(root, args);
|
|
47
|
+
const hit = prefetched.get(key);
|
|
48
|
+
if (!hit) return undefined;
|
|
49
|
+
prefetched.delete(key);
|
|
50
|
+
return Date.now() - hit.at <= TTL_MS ? hit.out : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { prefetchGitLogs, takePrefetchedGitLog };
|
package/src/i18n.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { detectLangCode } = require('./locale');
|
|
4
4
|
const { toLowerLang } = require('./lang-codes');
|
|
5
5
|
const { CATEGORIES } = require('./detectors');
|
|
6
|
+
const { LEGAL } = require('./signup-copy');
|
|
6
7
|
|
|
7
8
|
// Localization of the report (HTML + terminal) and of the CLI notices tied to the report.
|
|
8
9
|
|
|
@@ -38,7 +39,7 @@ const catalogs = {
|
|
|
38
39
|
askFreelanceOpinion: 'Cual es tu opinion sobre el freelance?',
|
|
39
40
|
freelanceOpinionLabels: {
|
|
40
41
|
WAS_FREELANCE_BEFORE: 'Ya he sido freelance',
|
|
41
|
-
OPEN_TO_FREELANCE: 'Me
|
|
42
|
+
OPEN_TO_FREELANCE: 'Me plantearía ser freelance',
|
|
42
43
|
NOT_INTERESTED: 'No me interesa',
|
|
43
44
|
},
|
|
44
45
|
askChangeMotivators: 'Que te motiva del freelance o de Shakers?',
|
|
@@ -50,7 +51,7 @@ const catalogs = {
|
|
|
50
51
|
COMMUNITY: 'Pertenecer a una comunidad',
|
|
51
52
|
LIFESTYLE_CHANGE: 'Cambiar de estilo de vida',
|
|
52
53
|
},
|
|
53
|
-
professionalSaved: '
|
|
54
|
+
professionalSaved: 'Situación laboral guardada.',
|
|
54
55
|
chooseMethodHeading: '¿Cómo quieres crear tu cuenta?',
|
|
55
56
|
signupTitle: 'Crea tu cuenta de Shakers',
|
|
56
57
|
askName: 'Nombre:',
|
|
@@ -87,7 +88,7 @@ const catalogs = {
|
|
|
87
88
|
availableAsk: 'Estas disponible para nuevos proyectos? (s/n):',
|
|
88
89
|
monthlyHoursAsk: 'Cuantas horas al mes?',
|
|
89
90
|
workModesAsk: 'En que modalidad quieres trabajar? (marca una o varias)',
|
|
90
|
-
workModeLabels: { REMOTE: 'Remoto', HYBRID: '
|
|
91
|
+
workModeLabels: { REMOTE: 'Remoto', HYBRID: 'Híbrido', IN_PERSON: 'Presencial' },
|
|
91
92
|
workModesHint: 'Espacio para marcar/desmarcar · Enter para confirmar',
|
|
92
93
|
onlyRemoteNote: 'Has marcado solo Remoto: no veras proyectos hibridos ni presenciales.',
|
|
93
94
|
countryAsk: 'Pais (codigo ISO-2 de 2 letras, p.ej. ES):',
|
|
@@ -97,7 +98,7 @@ const catalogs = {
|
|
|
97
98
|
longFullTimeAsk: 'Abierto a proyectos largos a tiempo completo? (s/n):',
|
|
98
99
|
phonePrefixAsk: 'Prefijo telefonico (opcional, p.ej. +34, Enter para omitir):',
|
|
99
100
|
phoneNumberAsk: 'Numero de telefono (opcional, Enter para omitir):',
|
|
100
|
-
phoneSaved: '
|
|
101
|
+
phoneSaved: 'Teléfono guardado.',
|
|
101
102
|
phoneUnavailable: 'No pudimos guardar tu telefono ahora. Puedes actualizarlo mas tarde.',
|
|
102
103
|
availabilitySaved: 'Disponibilidad guardada.',
|
|
103
104
|
availabilityUnavailable: 'No pudimos guardar tu disponibilidad ahora. Puedes actualizarla mas tarde.',
|
|
@@ -106,35 +107,35 @@ const catalogs = {
|
|
|
106
107
|
languagesAsk: 'Quieres indicar los idiomas que hablas? (s/n):',
|
|
107
108
|
askLanguageCode: 'Idioma:',
|
|
108
109
|
languageNames: {
|
|
109
|
-
es: '
|
|
110
|
-
en: '
|
|
111
|
-
fr: '
|
|
112
|
-
ca: '
|
|
113
|
-
de: '
|
|
110
|
+
es: 'Español',
|
|
111
|
+
en: 'Inglés',
|
|
112
|
+
fr: 'Francés',
|
|
113
|
+
ca: 'Catalán',
|
|
114
|
+
de: 'Alemán',
|
|
114
115
|
eu: 'Euskera',
|
|
115
116
|
it: 'Italiano',
|
|
116
|
-
pt: '
|
|
117
|
+
pt: 'Portugués',
|
|
117
118
|
},
|
|
118
119
|
askLanguageLevel: 'Tu nivel en ese idioma:',
|
|
119
120
|
languageLevelLabels: {
|
|
120
|
-
NATIVE: 'Nativo o
|
|
121
|
-
ADVANCED: 'Fluido en el trabajo y el
|
|
122
|
-
INTERMEDIATE: 'Puedo trabajar en
|
|
123
|
-
INTERMEDIATE_WRITTEN: 'Puedo trabajar en
|
|
121
|
+
NATIVE: 'Nativo o bilingüe',
|
|
122
|
+
ADVANCED: 'Fluido en el trabajo y el día a día',
|
|
123
|
+
INTERMEDIATE: 'Puedo trabajar en él (hablado y escrito)',
|
|
124
|
+
INTERMEDIATE_WRITTEN: 'Puedo trabajar en él, prefiero comunicación escrita',
|
|
124
125
|
},
|
|
125
126
|
askAnotherLanguage: 'Quieres anadir otro idioma? (s/n):',
|
|
126
127
|
languagesSaved: 'Idiomas guardados.',
|
|
127
128
|
languagesUnavailable: 'No pudimos guardar tus idiomas ahora. Puedes anadirlos mas tarde.',
|
|
128
129
|
languagesSkipped: 'De acuerdo, puedes indicar tus idiomas mas tarde.',
|
|
129
130
|
usageTitle: 'Evaluacion de tu uso de IA (opcional)',
|
|
130
|
-
usageInfoAccessed:
|
|
131
|
-
usageGoalDuration:
|
|
131
|
+
usageInfoAccessed: LEGAL.es.usageInfoAccessed,
|
|
132
|
+
usageGoalDuration: LEGAL.es.usageGoalDuration,
|
|
132
133
|
usageAccept: 'Quieres evaluar tu uso de IA ahora? (s/n):',
|
|
133
134
|
usageSkippedEvidence: 'Ya tienes evidencia de uso de IA en Shakers; nos saltamos este paso.',
|
|
134
135
|
interviewTitle: 'Entrevista de onboarding (opcional)',
|
|
135
136
|
interviewOnlyTitle: 'Entrevista de onboarding',
|
|
136
|
-
interviewInfoAccessed:
|
|
137
|
-
interviewGoalDuration:
|
|
137
|
+
interviewInfoAccessed: LEGAL.es.interviewInfoAccessed,
|
|
138
|
+
interviewGoalDuration: LEGAL.es.interviewGoalDuration,
|
|
138
139
|
interviewAccept: 'Quieres hacer la entrevista ahora? (s/n):',
|
|
139
140
|
interviewComplete: 'Entrevista completada. Gracias!',
|
|
140
141
|
interviewAlreadyDone: 'Ya completaste la entrevista de onboarding; no se puede repetir desde la CLI. Puedes seguir ajustando tu perfil.',
|
|
@@ -702,11 +703,11 @@ const catalogs = {
|
|
|
702
703
|
// language the Talent may not read. Vocabulario CONTEXT: Talent, Skill.
|
|
703
704
|
certifyDimension: {
|
|
704
705
|
help:
|
|
705
|
-
'certify
|
|
706
|
+
'certify - certifica una DIMENSIÓN de tu rol. La entrevista se hace en la web.\n\n'
|
|
706
707
|
+ 'Uso:\n'
|
|
707
708
|
+ ' certify [--lang es|en] [--dimension <clave|slug|n>]\n\n'
|
|
708
709
|
+ 'Requiere una sesión activa (ejecuta antes: shakers login).\n'
|
|
709
|
-
+ '
|
|
710
|
+
+ 'Eliges la dimensión aquí y te damos el enlace para certificarla en la web.\n',
|
|
710
711
|
title: 'Certificar una dimensión',
|
|
711
712
|
loginRequired: '"certify" requiere una sesión activa.\n Falta: iniciar sesión. Ejecuta primero: shakers login',
|
|
712
713
|
discovering: 'Buscando las dimensiones que puedes certificar…',
|
|
@@ -722,22 +723,9 @@ const catalogs = {
|
|
|
722
723
|
stateExpired: 'caducada',
|
|
723
724
|
dimensionInvalid: 'Esa dimensión no está entre las certificables ahora mismo.',
|
|
724
725
|
dimensionUsing: (slug) => `Dimensión elegida: ${slug}`,
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
? `Ya hiciste esta dimensión hace poco. Podrás repetirla a partir del ${day}.`
|
|
729
|
-
: 'Ya hiciste esta dimensión hace poco y todavía no se puede repetir.'),
|
|
730
|
-
needsWeb: 'Esta dimensión lleva un ejercicio que por ahora solo se hace en la web, no por terminal.',
|
|
731
|
-
interviewUnavailable: (reason) => `La entrevista no pudo completarse (${reason}).`,
|
|
732
|
-
reportPolling: 'Calculando tu veredicto…',
|
|
733
|
-
reportTimeout: 'El veredicto todavía se está calculando. Consúltalo en tu perfil en unos minutos.',
|
|
734
|
-
reportFailed: (reason) => `No se pudo obtener el veredicto (${reason}). Consúltalo en tu perfil.`,
|
|
735
|
-
verdictTitle: 'Veredicto',
|
|
736
|
-
verdictDimension: (dim) => `Dimensión: ${dim}`,
|
|
737
|
-
verdictBand: (band) => `Nivel: ${band}`,
|
|
738
|
-
verdictCertified: 'Resultado: certificada',
|
|
739
|
-
verdictNotCertified: 'Resultado: no certificada esta vez',
|
|
740
|
-
done: 'Certificación completada.',
|
|
726
|
+
webHandoff: 'La entrevista de certificación se hace en la web.',
|
|
727
|
+
webHandoffLink: (url) => `Ábrela aquí para certificar esta dimensión:\n ${url}`,
|
|
728
|
+
webHandoffNoLink: 'Entra en tu perfil de Shakers en la web y ve a Certificaciones para hacerla.',
|
|
741
729
|
},
|
|
742
730
|
findProjects: {
|
|
743
731
|
help:
|
|
@@ -1743,14 +1731,14 @@ const catalogs = {
|
|
|
1743
1731
|
languagesUnavailable: 'We could not save your languages right now. You can add them later.',
|
|
1744
1732
|
languagesSkipped: 'OK, you can add your languages later.',
|
|
1745
1733
|
usageTitle: 'Evaluate your AI usage (optional)',
|
|
1746
|
-
usageInfoAccessed:
|
|
1747
|
-
usageGoalDuration:
|
|
1734
|
+
usageInfoAccessed: LEGAL.en.usageInfoAccessed,
|
|
1735
|
+
usageGoalDuration: LEGAL.en.usageGoalDuration,
|
|
1748
1736
|
usageAccept: 'Do you want to evaluate your AI usage now? (y/n):',
|
|
1749
1737
|
usageSkippedEvidence: 'You already have AI-usage evidence on Shakers; skipping this step.',
|
|
1750
1738
|
interviewTitle: 'Onboarding interview (optional)',
|
|
1751
1739
|
interviewOnlyTitle: 'Onboarding interview',
|
|
1752
|
-
interviewInfoAccessed:
|
|
1753
|
-
interviewGoalDuration:
|
|
1740
|
+
interviewInfoAccessed: LEGAL.en.interviewInfoAccessed,
|
|
1741
|
+
interviewGoalDuration: LEGAL.en.interviewGoalDuration,
|
|
1754
1742
|
interviewAccept: 'Do you want to take the interview now? (y/n):',
|
|
1755
1743
|
interviewComplete: 'Interview complete. Thank you!',
|
|
1756
1744
|
interviewAlreadyDone: 'You already completed the onboarding interview; it cannot be repeated from the CLI. You can keep refining your profile.',
|
|
@@ -2310,11 +2298,11 @@ const catalogs = {
|
|
|
2310
2298
|
// English mirror of the `certify` catalog. Same content/invariants.
|
|
2311
2299
|
certifyDimension: {
|
|
2312
2300
|
help:
|
|
2313
|
-
'certify
|
|
2301
|
+
'certify - certify a DIMENSION of your role. The interview is done on the web.\n\n'
|
|
2314
2302
|
+ 'Usage:\n'
|
|
2315
2303
|
+ ' certify [--lang es|en] [--dimension <key|slug|n>]\n\n'
|
|
2316
2304
|
+ 'Requires an active session (run `shakers login` first).\n'
|
|
2317
|
-
+ '
|
|
2305
|
+
+ 'You pick the dimension here and we hand you the link to certify it on the web.\n',
|
|
2318
2306
|
title: 'Certify a dimension',
|
|
2319
2307
|
loginRequired: '"certify" requires an active session.\n Missing: sign-in. Run `shakers login` first.',
|
|
2320
2308
|
discovering: 'Looking up the dimensions you can certify…',
|
|
@@ -2330,22 +2318,9 @@ const catalogs = {
|
|
|
2330
2318
|
stateExpired: 'expired',
|
|
2331
2319
|
dimensionInvalid: 'That dimension is not among the certifiable ones right now.',
|
|
2332
2320
|
dimensionUsing: (slug) => `Chosen dimension: ${slug}`,
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
? `You took this dimension recently. You can retake it from ${day}.`
|
|
2337
|
-
: 'You took this dimension recently and cannot retake it yet.'),
|
|
2338
|
-
needsWeb: 'This dimension has an exercise that for now is only taken on the web, not in the terminal.',
|
|
2339
|
-
interviewUnavailable: (reason) => `The interview could not be completed (${reason}).`,
|
|
2340
|
-
reportPolling: 'Computing your verdict…',
|
|
2341
|
-
reportTimeout: 'The verdict is still being computed. Check your profile in a few minutes.',
|
|
2342
|
-
reportFailed: (reason) => `Could not fetch the verdict (${reason}). Check your profile.`,
|
|
2343
|
-
verdictTitle: 'Verdict',
|
|
2344
|
-
verdictDimension: (dim) => `Dimension: ${dim}`,
|
|
2345
|
-
verdictBand: (band) => `Level: ${band}`,
|
|
2346
|
-
verdictCertified: 'Result: certified',
|
|
2347
|
-
verdictNotCertified: 'Result: not certified this time',
|
|
2348
|
-
done: 'Certification complete.',
|
|
2321
|
+
webHandoff: 'The certification interview is done on the web.',
|
|
2322
|
+
webHandoffLink: (url) => `Open it here to certify this dimension:\n ${url}`,
|
|
2323
|
+
webHandoffNoLink: 'Open your Shakers profile on the web and go to Certifications to take it.',
|
|
2349
2324
|
},
|
|
2350
2325
|
findProjects: {
|
|
2351
2326
|
help:
|