ticketlens 0.38.12 → 0.38.13
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/bin/ticketlens.mjs +14 -0
- package/package.json +1 -1
- package/skills/jtb/scripts/lib/help.mjs +8 -0
- package/skills/jtb/scripts/lib/login-flow.mjs +8 -3
- package/skills/jtb/scripts/lib/note-command.mjs +4 -0
- package/skills/jtb/scripts/lib/profile-resolver.mjs +15 -0
- package/skills/jtb/scripts/lib/profile-set-team.mjs +41 -0
- package/skills/jtb/scripts/lib/recall-command.mjs +7 -2
- package/skills/jtb/scripts/lib/recall-sync.mjs +69 -22
- package/skills/jtb/scripts/lib/sync.mjs +2 -0
package/bin/ticketlens.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { run as runFetch } from '../skills/jtb/scripts/fetch-ticket.mjs';
|
|
|
13
13
|
import { run as runTriage } from '../skills/jtb/scripts/fetch-my-tickets.mjs';
|
|
14
14
|
import { run as runInit } from '../skills/jtb/scripts/lib/init-wizard.mjs';
|
|
15
15
|
import { runSwitch } from '../skills/jtb/scripts/lib/profile-switcher.mjs';
|
|
16
|
+
import { runProfilesSetTeam } from '../skills/jtb/scripts/lib/profile-set-team.mjs';
|
|
16
17
|
import { run as runConfig } from '../skills/jtb/scripts/lib/config-wizard.mjs';
|
|
17
18
|
import { activateLicense, checkLicense, revalidateIfStale, isLicensed, showUpgradePrompt, readLicense } from '../skills/jtb/scripts/lib/license.mjs';
|
|
18
19
|
import { deleteProfile, loadProfiles, saveCredentialKey } from '../skills/jtb/scripts/lib/profile-resolver.mjs';
|
|
@@ -360,6 +361,19 @@ switch (command) {
|
|
|
360
361
|
|
|
361
362
|
case 'profiles': {
|
|
362
363
|
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printProfilesHelp(); break; }
|
|
364
|
+
|
|
365
|
+
if (cmdArgs[0] === 'set-team') {
|
|
366
|
+
const [, profileName, ...teamNameParts] = cmdArgs;
|
|
367
|
+
const teamName = teamNameParts.join(' ');
|
|
368
|
+
if (!profileName || !teamName) {
|
|
369
|
+
process.stderr.write('Usage: ticketlens profiles set-team <profile> <team name>\n');
|
|
370
|
+
process.exitCode = 1;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
runProfilesSetTeam(profileName, teamName);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
363
377
|
const plain = cmdArgs.includes('--plain');
|
|
364
378
|
const config = loadProfiles();
|
|
365
379
|
printProfiles({ config, plain });
|
package/package.json
CHANGED
|
@@ -469,6 +469,13 @@ export function printProfilesHelp({ stream = process.stdout } = {}) {
|
|
|
469
469
|
` List all locally configured Jira profiles and their active status.`,
|
|
470
470
|
` Also available as ${s.cyan('ticketlens ls')}.`,
|
|
471
471
|
'',
|
|
472
|
+
` ${s.bold('SUBCOMMANDS')}`,
|
|
473
|
+
'',
|
|
474
|
+
` ${s.brand('set-team')} ${s.dim('<profile> <team name>')}`,
|
|
475
|
+
` Route this profile's Recall notes to a specific Console team —`,
|
|
476
|
+
` for accounts that belong to more than one team. Validated against`,
|
|
477
|
+
` teams synced by ${s.cyan('ticketlens login')}/${s.cyan('ticketlens sync')}, but never blocked by a stale list.`,
|
|
478
|
+
'',
|
|
472
479
|
` ${s.bold('OPTIONS')}`,
|
|
473
480
|
'',
|
|
474
481
|
` ${s.brand('--plain')} Tab-separated output ${s.dim('(for scripting)')}`,
|
|
@@ -479,6 +486,7 @@ export function printProfilesHelp({ stream = process.stdout } = {}) {
|
|
|
479
486
|
` ${s.dim('$')} ticketlens profiles`,
|
|
480
487
|
` ${s.dim('$')} ticketlens ls`,
|
|
481
488
|
` ${s.dim('$')} ticketlens profiles --plain`,
|
|
489
|
+
` ${s.dim('$')} ticketlens profiles set-team advent "Team Manager's Team"`,
|
|
482
490
|
'',
|
|
483
491
|
];
|
|
484
492
|
stream.write(lines.join('\n') + '\n');
|
|
@@ -10,6 +10,7 @@ import { browserLogin } from './browser-login.mjs';
|
|
|
10
10
|
import { saveCliToken } from './cli-auth.mjs';
|
|
11
11
|
import { getApiBase, getConsoleBase } from './sync.mjs';
|
|
12
12
|
import { applyTeamConfigOnLogin } from './team-jira-sync.mjs';
|
|
13
|
+
import { saveTeams } from './profile-resolver.mjs';
|
|
13
14
|
|
|
14
15
|
export async function runLogin({
|
|
15
16
|
manual = false,
|
|
@@ -19,6 +20,7 @@ export async function runLogin({
|
|
|
19
20
|
fetchFn = globalThis.fetch,
|
|
20
21
|
promptSecretFn = promptSecret,
|
|
21
22
|
saveCliTokenFn = saveCliToken,
|
|
23
|
+
saveTeamsFn = saveTeams,
|
|
22
24
|
applyTeamConfigOnLoginFn = applyTeamConfigOnLogin,
|
|
23
25
|
} = {}) {
|
|
24
26
|
const s = createStyler({ isTTY: stream.isTTY });
|
|
@@ -83,12 +85,15 @@ export async function runLogin({
|
|
|
83
85
|
return;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
let tier;
|
|
88
|
+
let tier, teams;
|
|
87
89
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
tier = body?.tier;
|
|
92
|
+
teams = body?.teams;
|
|
93
|
+
} catch { /* tier/teams are best-effort — token is still valid */ }
|
|
90
94
|
|
|
91
95
|
saveCliTokenFn(token, undefined, tier);
|
|
96
|
+
saveTeamsFn(Array.isArray(teams) ? teams : []);
|
|
92
97
|
stream.write(`\x1b[A\r\x1b[2K ${s.green('✔')} Logged in.\n`);
|
|
93
98
|
|
|
94
99
|
// Flow 1: pull team Jira config for Pro/Team members (silently skipped for Free)
|
|
@@ -13,6 +13,7 @@ import { scanForSecrets } from './secret-scanner.mjs';
|
|
|
13
13
|
import { checkNoteStructure } from './note-structural-check.mjs';
|
|
14
14
|
import { writeNote, patchNoteBody, deleteNote, deleteNoteAnyPrefix, rebuildIndex } from './recall-vault.mjs';
|
|
15
15
|
import { readCliToken } from './cli-auth.mjs';
|
|
16
|
+
import { resolveProfile } from './profile-resolver.mjs';
|
|
16
17
|
import { pushNote } from './recall-sync.mjs';
|
|
17
18
|
import { enqueueNote, isRetryableFailure, maybeAutoFlush } from './recall-queue.mjs';
|
|
18
19
|
import { incrementDraftKept, incrementDraftDeleted } from './activity-counter.mjs';
|
|
@@ -73,6 +74,7 @@ export async function runNoteAdd(cmdArgs, {
|
|
|
73
74
|
scanForSecretsFn = scanForSecrets,
|
|
74
75
|
writeNoteFn = writeNote,
|
|
75
76
|
readCliTokenFn = readCliToken,
|
|
77
|
+
resolveProfileFn = resolveProfile,
|
|
76
78
|
pushNoteFn = pushNote,
|
|
77
79
|
enqueueNoteFn = enqueueNote,
|
|
78
80
|
isRetryableFailureFn = isRetryableFailure,
|
|
@@ -139,7 +141,9 @@ export async function runNoteAdd(cmdArgs, {
|
|
|
139
141
|
const warn = (s) => stream.write(s);
|
|
140
142
|
// Field names match PushRequest's validation rules (external_id, tickets) —
|
|
141
143
|
// the backend wire contract, not the local vault's internal camelCase shape.
|
|
144
|
+
const profile = resolveProfileFn(ticketKey || null, { configDir, cwd: process.cwd() });
|
|
142
145
|
const payload = { external_id: id, title, tickets: ticketKeys, tags, author, sources: [], body };
|
|
146
|
+
if (profile?.recallTeam) payload.group = profile.recallTeam;
|
|
143
147
|
const result = await pushNoteFn(payload, { cliToken, configDir, warn });
|
|
144
148
|
if (isRetryableFailureFn(result)) {
|
|
145
149
|
await enqueueNoteFn(payload, { cliToken, configDir, warn });
|
|
@@ -55,6 +55,21 @@ export function saveDefault(name, configDir = DEFAULT_CONFIG_DIR) {
|
|
|
55
55
|
invalidateProfilesCache(configDir);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// Team list synced from /v1/profiles — used to validate `recallTeam` on a
|
|
59
|
+
// profile without a live network call at push/pull time. Not credentials.
|
|
60
|
+
export function saveTeams(teams, configDir = DEFAULT_CONFIG_DIR) {
|
|
61
|
+
mkdirSync(configDir, { recursive: true });
|
|
62
|
+
const profilesPath = join(configDir, 'profiles.json');
|
|
63
|
+
const config = loadProfiles(configDir) || { profiles: {} };
|
|
64
|
+
config.teams = teams;
|
|
65
|
+
writeFileSync(profilesPath, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
66
|
+
invalidateProfilesCache(configDir);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadTeams(configDir = DEFAULT_CONFIG_DIR) {
|
|
70
|
+
return loadProfiles(configDir)?.teams ?? [];
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
export function saveProfile(name, profileData, credData, configDir = DEFAULT_CONFIG_DIR) {
|
|
59
74
|
mkdirSync(configDir, { recursive: true });
|
|
60
75
|
const profilesPath = join(configDir, 'profiles.json');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ticketlens profiles set-team — maps a local Jira profile to the Console
|
|
3
|
+
* team its Recall notes should push/pull against. Validates the name
|
|
4
|
+
* against the locally synced team list (no live network call — that list
|
|
5
|
+
* is refreshed by `ticketlens login`/`ticketlens sync`), but never blocks
|
|
6
|
+
* the write on a stale/missing list or a name mismatch: the hard check is
|
|
7
|
+
* server-side at push/pull time (RecallTeamResolver), this is a local
|
|
8
|
+
* convenience check only.
|
|
9
|
+
*/
|
|
10
|
+
import { createStyler } from './ansi.mjs';
|
|
11
|
+
import { loadProfiles, loadTeams, saveProfile } from './profile-resolver.mjs';
|
|
12
|
+
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
13
|
+
|
|
14
|
+
export function runProfilesSetTeam(profileName, teamName, {
|
|
15
|
+
configDir = DEFAULT_CONFIG_DIR,
|
|
16
|
+
stream = process.stderr,
|
|
17
|
+
loadProfilesFn = loadProfiles,
|
|
18
|
+
loadTeamsFn = loadTeams,
|
|
19
|
+
saveProfileFn = saveProfile,
|
|
20
|
+
} = {}) {
|
|
21
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
22
|
+
const config = loadProfilesFn(configDir);
|
|
23
|
+
const profile = config?.profiles?.[profileName];
|
|
24
|
+
|
|
25
|
+
if (!profile) {
|
|
26
|
+
stream.write(` ${s.red('✖')} No profile named "${profileName}". Run ${s.cyan('ticketlens profiles')} to see available profiles.\n`);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const teams = loadTeamsFn(configDir);
|
|
32
|
+
if (teams.length === 0) {
|
|
33
|
+
stream.write(` ${s.yellow('⚠')} No synced teams found — run ${s.cyan('ticketlens sync')} first to validate this name.\n`);
|
|
34
|
+
} else if (!teams.some(t => t.name === teamName)) {
|
|
35
|
+
stream.write(` ${s.yellow('⚠')} "${teamName}" isn't one of your synced teams — check spelling, or run ${s.cyan('ticketlens sync')} to refresh.\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Must merge, not replace — saveProfile() overwrites the whole profile object.
|
|
39
|
+
saveProfileFn(profileName, { ...profile, recallTeam: teamName }, null, configDir);
|
|
40
|
+
stream.write(` ${s.green('✔')} Recall notes captured under ${s.bold(s.cyan(profileName))} will target ${s.bold(s.cyan(teamName))}.\n`);
|
|
41
|
+
}
|
|
@@ -15,6 +15,7 @@ import { TICKET_KEY_PATTERN } from './cli.mjs';
|
|
|
15
15
|
import { isLicensed, showUpgradePrompt } from './license.mjs';
|
|
16
16
|
import { listNotes } from './recall-vault.mjs';
|
|
17
17
|
import { readCliToken } from './cli-auth.mjs';
|
|
18
|
+
import { resolveProfile } from './profile-resolver.mjs';
|
|
18
19
|
import { pullNotes } from './recall-sync.mjs';
|
|
19
20
|
import { maybeAutoFlush, flushQueue, readQueue } from './recall-queue.mjs';
|
|
20
21
|
import { getEffectiveRecallSettingsWithSource } from './recall-settings-sync.mjs';
|
|
@@ -31,6 +32,7 @@ export async function runRecall(cmdArgs, {
|
|
|
31
32
|
isLicensedFn = isLicensed,
|
|
32
33
|
listNotesFn = listNotes,
|
|
33
34
|
readCliTokenFn = readCliToken,
|
|
35
|
+
resolveProfileFn = resolveProfile,
|
|
34
36
|
pullNotesFn = pullNotes,
|
|
35
37
|
maybeAutoFlushFn = maybeAutoFlush,
|
|
36
38
|
} = {}) {
|
|
@@ -45,13 +47,16 @@ export async function runRecall(cmdArgs, {
|
|
|
45
47
|
return { ok: false };
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
const isTicketKey = TICKET_KEY_PATTERN.test(arg);
|
|
51
|
+
|
|
48
52
|
const cliToken = readCliTokenFn(configDir);
|
|
49
53
|
if (cliToken) {
|
|
50
|
-
|
|
54
|
+
const profile = resolveProfileFn(isTicketKey ? arg : null, { configDir, cwd: process.cwd() });
|
|
55
|
+
await pullNotesFn({ cliToken, configDir, ttlMs: 0, group: profile?.recallTeam });
|
|
51
56
|
await maybeAutoFlushFn({ cliToken, configDir });
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
const filter =
|
|
59
|
+
const filter = isTicketKey ? { ticketKey: arg } : { query: arg };
|
|
55
60
|
const results = listNotesFn(filter, { configDir });
|
|
56
61
|
|
|
57
62
|
const styled = !cmdArgs.includes('--plain') && !!stream.isTTY;
|
|
@@ -78,6 +78,20 @@ function writeEntitlementCheckedAt(configDir, isoTimestamp, cliToken) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Shared by pushNote's and pullNotes' 422 handling — a profile's configured
|
|
82
|
+
// recallTeam that doesn't match any of this account's Console teams
|
|
83
|
+
// (renamed/removed/typo) must never silently drop a note or return zero
|
|
84
|
+
// pull results; both retry once against the account's default team instead.
|
|
85
|
+
async function isUnknownTeamRejection(res) {
|
|
86
|
+
let reason;
|
|
87
|
+
try { reason = (await res.json())?.error; } catch { /* not this case — fall through */ }
|
|
88
|
+
return reason === 'Unknown team';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function warnUnknownTeam(warn, groupName, fallbackAction) {
|
|
92
|
+
warn(` ${yellow('⚠')} "${groupName}" isn't a team on your account — ${fallbackAction} instead.\n ${dim('→ Run')} ${cyan('ticketlens profiles set-team')} ${dim('to fix this profile\'s team mapping.')}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
/**
|
|
82
96
|
* POSTs one locally-authored note to the team backend.
|
|
83
97
|
*
|
|
@@ -116,23 +130,25 @@ export async function pushNote(note, {
|
|
|
116
130
|
|
|
117
131
|
warnIfInsecure(apiBase(), warn);
|
|
118
132
|
|
|
133
|
+
const post = (payload) => fetcher(`${apiBase()}${PUSH_PATH}`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Accept': 'application/json',
|
|
138
|
+
'Authorization': `Bearer ${cliToken}`,
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify(payload),
|
|
141
|
+
// Without Accept: application/json, a Laravel validation failure 302-redirects
|
|
142
|
+
// instead of returning JSON, and fetch's default redirect:'follow' silently
|
|
143
|
+
// turns that into a followed 200 — a false "pushed" the user never sees fail.
|
|
144
|
+
// 'manual' makes any unexpected redirect surface as a non-ok response instead.
|
|
145
|
+
redirect: 'manual',
|
|
146
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
147
|
+
});
|
|
148
|
+
|
|
119
149
|
let res;
|
|
120
150
|
try {
|
|
121
|
-
res = await
|
|
122
|
-
method: 'POST',
|
|
123
|
-
headers: {
|
|
124
|
-
'Content-Type': 'application/json',
|
|
125
|
-
'Accept': 'application/json',
|
|
126
|
-
'Authorization': `Bearer ${cliToken}`,
|
|
127
|
-
},
|
|
128
|
-
body: JSON.stringify(note),
|
|
129
|
-
// Without Accept: application/json, a Laravel validation failure 302-redirects
|
|
130
|
-
// instead of returning JSON, and fetch's default redirect:'follow' silently
|
|
131
|
-
// turns that into a followed 200 — a false "pushed" the user never sees fail.
|
|
132
|
-
// 'manual' makes any unexpected redirect surface as a non-ok response instead.
|
|
133
|
-
redirect: 'manual',
|
|
134
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
135
|
-
});
|
|
151
|
+
res = await post(note);
|
|
136
152
|
} catch {
|
|
137
153
|
warn(` ${yellow('⚠')} Could not sync note to your team (network error) — saved locally and queued to retry automatically.\n`);
|
|
138
154
|
return { ok: false };
|
|
@@ -166,6 +182,19 @@ export async function pushNote(note, {
|
|
|
166
182
|
return { ok: false, status: res.status };
|
|
167
183
|
}
|
|
168
184
|
|
|
185
|
+
if (res.status === 422 && note.group !== undefined && await isUnknownTeamRejection(res)) {
|
|
186
|
+
warnUnknownTeam(warn, note.group, 'saved to your default team');
|
|
187
|
+
const { group: _omit, ...withoutGroup } = note;
|
|
188
|
+
let retryRes;
|
|
189
|
+
try {
|
|
190
|
+
retryRes = await post(withoutGroup);
|
|
191
|
+
} catch {
|
|
192
|
+
warn(` ${yellow('⚠')} Could not sync note to your team (network error) — saved locally and queued to retry automatically.\n`);
|
|
193
|
+
return { ok: false };
|
|
194
|
+
}
|
|
195
|
+
return retryRes.ok ? { ok: true, status: retryRes.status } : { ok: false, status: retryRes.status };
|
|
196
|
+
}
|
|
197
|
+
|
|
169
198
|
warn(` ${yellow('⚠')} Could not sync note to your team (${res.status}) — saved locally only.\n`);
|
|
170
199
|
return { ok: false, status: res.status };
|
|
171
200
|
}
|
|
@@ -183,7 +212,9 @@ export async function pushNote(note, {
|
|
|
183
212
|
* @param {string} [opts.configDir]
|
|
184
213
|
* @param {number} [opts.ttlMs] - skip the network call if the last pull is more recent than this
|
|
185
214
|
* @param {number} [opts.timeoutMs] - request timeout; short for a passive/background pull, long for an explicit one
|
|
215
|
+
* @param {string} [opts.group] - explicit target team name, for accounts on more than one team
|
|
186
216
|
* @param {Function} [opts.fetcher]
|
|
217
|
+
* @param {Function} [opts.warn] - failure output (default: process.stderr.write)
|
|
187
218
|
* @param {Function} [opts.upsertPulledNoteFn]
|
|
188
219
|
* @param {Function} [opts.rebuildIndexFn]
|
|
189
220
|
* @param {() => number} [opts.now]
|
|
@@ -194,7 +225,9 @@ export async function pullNotes({
|
|
|
194
225
|
configDir = DEFAULT_CONFIG_DIR,
|
|
195
226
|
ttlMs = RECALL_PULL_TTL_MS,
|
|
196
227
|
timeoutMs = 15_000,
|
|
228
|
+
group,
|
|
197
229
|
fetcher = globalThis.fetch,
|
|
230
|
+
warn = (s) => process.stderr.write(s),
|
|
198
231
|
upsertPulledNoteFn = upsertPulledNote,
|
|
199
232
|
rebuildIndexFn = rebuildIndex,
|
|
200
233
|
deleteNoteFn = deleteNote,
|
|
@@ -209,20 +242,34 @@ export async function pullNotes({
|
|
|
209
242
|
return { ok: true, skipped: true, count: 0 };
|
|
210
243
|
}
|
|
211
244
|
|
|
212
|
-
const
|
|
213
|
-
|
|
245
|
+
const buildUrl = (targetGroup) => {
|
|
246
|
+
const url = new URL(`${apiBase()}${PULL_PATH}`);
|
|
247
|
+
if (lastPulledAt) url.searchParams.set('since', lastPulledAt);
|
|
248
|
+
if (targetGroup) url.searchParams.set('group', targetGroup);
|
|
249
|
+
return url.toString();
|
|
250
|
+
};
|
|
251
|
+
const get = (targetGroup) => fetcher(buildUrl(targetGroup), {
|
|
252
|
+
headers: { 'Authorization': `Bearer ${cliToken}`, 'Accept': 'application/json' },
|
|
253
|
+
redirect: 'manual',
|
|
254
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
255
|
+
});
|
|
214
256
|
|
|
215
257
|
let res;
|
|
216
258
|
try {
|
|
217
|
-
res = await
|
|
218
|
-
headers: { 'Authorization': `Bearer ${cliToken}`, 'Accept': 'application/json' },
|
|
219
|
-
redirect: 'manual',
|
|
220
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
221
|
-
});
|
|
259
|
+
res = await get(group);
|
|
222
260
|
} catch {
|
|
223
261
|
return { ok: false, count: 0 };
|
|
224
262
|
}
|
|
225
263
|
|
|
264
|
+
if (!res.ok && res.status === 422 && group !== undefined && await isUnknownTeamRejection(res)) {
|
|
265
|
+
warnUnknownTeam(warn, group, 'pulling from your default team');
|
|
266
|
+
try {
|
|
267
|
+
res = await get(undefined);
|
|
268
|
+
} catch {
|
|
269
|
+
return { ok: false, count: 0 };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
226
273
|
if (!res.ok) {
|
|
227
274
|
return { ok: false, status: res.status, count: 0 };
|
|
228
275
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
loadProfiles,
|
|
11
11
|
loadCredentials,
|
|
12
12
|
saveProfile,
|
|
13
|
+
saveTeams,
|
|
13
14
|
invalidateProfilesCache,
|
|
14
15
|
} from './profile-resolver.mjs';
|
|
15
16
|
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
@@ -89,6 +90,7 @@ export async function syncProfiles({
|
|
|
89
90
|
try { json = await res.json(); } catch { return { error: 'invalid-json' }; }
|
|
90
91
|
|
|
91
92
|
if (typeof json?.tier === 'string') saveCliTokenTier(json.tier, configDir);
|
|
93
|
+
saveTeams(Array.isArray(json?.teams) ? json.teams : [], configDir);
|
|
92
94
|
|
|
93
95
|
const remoteProfiles = Array.isArray(json?.profiles) ? json.profiles : [];
|
|
94
96
|
const localConfig = loadProfiles(configDir) || { profiles: {} };
|