@quonfig/cli 0.0.29 → 0.0.31
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 +35 -35
- package/dist/codegen/fetch-workspace-snapshot.js +3 -3
- package/dist/commands/delete.js +2 -1
- package/dist/commands/login.js +0 -1
- package/dist/commands/migrate/doctor.js +3 -1
- package/dist/commands/migrate.js +10 -13
- package/dist/commands/override.js +1 -1
- package/dist/commands/pull.js +12 -12
- package/dist/commands/push.d.ts +1 -1
- package/dist/commands/push.js +5 -5
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +6 -13
- package/dist/commands/whoami.js +65 -6
- package/dist/commands/workspace/bootstrap.js +8 -12
- package/dist/commands/workspace/create.d.ts +5 -1
- package/dist/commands/workspace/create.js +27 -10
- package/dist/commands/workspace/switch.d.ts +7 -1
- package/dist/commands/workspace/switch.js +45 -27
- package/dist/commands/workspace.d.ts +5 -0
- package/dist/commands/workspace.js +28 -9
- package/dist/migrate/sources/launch/translate.js +6 -3
- package/dist/migrate/sources/launch/zod-to-json-schema.js +4 -0
- package/dist/push/config-push-client.d.ts +1 -1
- package/dist/push/config-push-client.js +3 -5
- package/dist/util/get-client.js +12 -12
- package/dist/util/get-valid-token.d.ts +7 -7
- package/dist/util/get-valid-token.js +19 -13
- package/dist/util/gitea-api.d.ts +2 -2
- package/dist/util/gitea-api.js +5 -7
- package/dist/util/resolve-workspace.d.ts +14 -4
- package/dist/util/resolve-workspace.js +32 -20
- package/dist/util/token-storage.d.ts +10 -5
- package/dist/util/token-storage.js +14 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/dist/commands/whoami.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseCommand } from '../index.js';
|
|
2
2
|
import { getApiUrl } from '../util/domain-urls.js';
|
|
3
|
-
import { getValidAccessToken
|
|
3
|
+
import { getValidAccessToken } from '../util/get-valid-token.js';
|
|
4
4
|
import { decodeJWT } from '../util/oauth-client.js';
|
|
5
5
|
import { tryParseWorkspacePin } from '../util/quonfig-json.js';
|
|
6
6
|
import { findOrgIdBySlug, loadTokens } from '../util/token-storage.js';
|
|
@@ -31,6 +31,7 @@ export default class Whoami extends BaseCommand {
|
|
|
31
31
|
slug: t.org_slug ?? orgId,
|
|
32
32
|
name: t.org_name ?? t.org_slug ?? orgId,
|
|
33
33
|
role: '—',
|
|
34
|
+
sessionStatus: sessionStatusFor(t),
|
|
34
35
|
}));
|
|
35
36
|
const merged = mergeMemberships(localOrgs, memberships);
|
|
36
37
|
this.log(`Logged in as: ${userEmail || 'Unknown'}`);
|
|
@@ -44,7 +45,13 @@ export default class Whoami extends BaseCommand {
|
|
|
44
45
|
const marker = org.workosOrgId === activeOrgId ? '*' : ' ';
|
|
45
46
|
const label = `${org.slug}${org.workosOrgId === activeOrgId ? ' (active)' : ''}`;
|
|
46
47
|
const padded = label.padEnd(labelWidth, ' ');
|
|
47
|
-
|
|
48
|
+
const statusSuffix = org.sessionStatus ? ` (${org.sessionStatus.description})` : '';
|
|
49
|
+
this.log(` ${marker} ${padded}— ${org.role}${statusSuffix}`);
|
|
50
|
+
}
|
|
51
|
+
const expiredCount = merged.filter((o) => o.sessionStatus?.expired).length;
|
|
52
|
+
if (expiredCount > 0) {
|
|
53
|
+
this.log('');
|
|
54
|
+
this.log(`${expiredCount} of ${merged.length} session(s) expired. Run \`qfg login\` to refresh.`);
|
|
48
55
|
}
|
|
49
56
|
}
|
|
50
57
|
return {
|
|
@@ -58,6 +65,7 @@ export default class Whoami extends BaseCommand {
|
|
|
58
65
|
name: o.name,
|
|
59
66
|
role: o.role,
|
|
60
67
|
active: o.workosOrgId === activeOrgId,
|
|
68
|
+
sessionExpired: o.sessionStatus?.expired ?? null,
|
|
61
69
|
})),
|
|
62
70
|
};
|
|
63
71
|
}
|
|
@@ -82,7 +90,11 @@ export default class Whoami extends BaseCommand {
|
|
|
82
90
|
}
|
|
83
91
|
if (defaultOrgId)
|
|
84
92
|
return defaultOrgId;
|
|
85
|
-
|
|
93
|
+
// Fall back to the first token entry — gives a stable marker rather than nothing.
|
|
94
|
+
const store = await loadTokens();
|
|
95
|
+
if (!store)
|
|
96
|
+
return undefined;
|
|
97
|
+
return Object.keys(store.tokensByOrg)[0];
|
|
86
98
|
}
|
|
87
99
|
/**
|
|
88
100
|
* Best-effort fetch of `me.organizations` for role enrichment. Failure
|
|
@@ -92,7 +104,10 @@ export default class Whoami extends BaseCommand {
|
|
|
92
104
|
async fetchOrganizations() {
|
|
93
105
|
let jwt;
|
|
94
106
|
try {
|
|
95
|
-
const
|
|
107
|
+
const store = await loadTokens();
|
|
108
|
+
if (!store)
|
|
109
|
+
return [];
|
|
110
|
+
const orgId = pickAnyOrgId(store);
|
|
96
111
|
if (!orgId)
|
|
97
112
|
return [];
|
|
98
113
|
jwt = await getValidAccessToken(orgId, this.verboseLog);
|
|
@@ -132,7 +147,51 @@ function mergeMemberships(local, server) {
|
|
|
132
147
|
const merged = new Map();
|
|
133
148
|
for (const o of local)
|
|
134
149
|
merged.set(o.workosOrgId, o);
|
|
135
|
-
for (const o of server)
|
|
136
|
-
|
|
150
|
+
for (const o of server) {
|
|
151
|
+
// Server data wins for slug/name/role but lacks sessionStatus — that's
|
|
152
|
+
// a property of the locally-cached token, so preserve it.
|
|
153
|
+
const existing = merged.get(o.workosOrgId);
|
|
154
|
+
merged.set(o.workosOrgId, { ...o, sessionStatus: existing?.sessionStatus });
|
|
155
|
+
}
|
|
137
156
|
return [...merged.values()].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
138
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Read-only check of a TokenSet's expiry. Prefers the JWT's `exp` claim
|
|
160
|
+
* (matches getValidAccessToken's logic), falls back to the stored
|
|
161
|
+
* `expires_at` timestamp if the JWT can't be decoded. No network calls —
|
|
162
|
+
* whoami stays fast even with many cached orgs.
|
|
163
|
+
*/
|
|
164
|
+
function sessionStatusFor(tokens) {
|
|
165
|
+
let expMs = tokens.expires_at;
|
|
166
|
+
try {
|
|
167
|
+
const payload = decodeJWT(tokens.access_token);
|
|
168
|
+
if (typeof payload.exp === 'number')
|
|
169
|
+
expMs = payload.exp * 1000;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
/* keep stored expires_at */
|
|
173
|
+
}
|
|
174
|
+
const delta = expMs - Date.now();
|
|
175
|
+
if (delta <= 0)
|
|
176
|
+
return { expired: true, description: 'expired — run `qfg login`' };
|
|
177
|
+
return { expired: false, description: `expires in ${formatRelative(delta)}` };
|
|
178
|
+
}
|
|
179
|
+
function formatRelative(deltaMs) {
|
|
180
|
+
const minutes = Math.round(deltaMs / 60_000);
|
|
181
|
+
if (minutes < 1)
|
|
182
|
+
return '<1m';
|
|
183
|
+
if (minutes < 60)
|
|
184
|
+
return `${minutes}m`;
|
|
185
|
+
const hours = Math.floor(minutes / 60);
|
|
186
|
+
const remMin = minutes % 60;
|
|
187
|
+
if (hours < 24)
|
|
188
|
+
return remMin > 0 ? `${hours}h ${remMin}m` : `${hours}h`;
|
|
189
|
+
const days = Math.floor(hours / 24);
|
|
190
|
+
const remHours = hours % 24;
|
|
191
|
+
return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`;
|
|
192
|
+
}
|
|
193
|
+
function pickAnyOrgId(store) {
|
|
194
|
+
if (store.defaultOrgId && store.tokensByOrg[store.defaultOrgId])
|
|
195
|
+
return store.defaultOrgId;
|
|
196
|
+
return Object.keys(store.tokensByOrg)[0];
|
|
197
|
+
}
|
|
@@ -5,6 +5,7 @@ import { BaseCommand } from '../../index.js';
|
|
|
5
5
|
import { getActiveProfile, loadAuthConfig } from '../../util/token-storage.js';
|
|
6
6
|
import { mintGiteaToken } from '../../util/gitea-api.js';
|
|
7
7
|
import { readWorkspaceSlug, tryParseWorkspacePin, writeWorkspaceSlug } from '../../util/quonfig-json.js';
|
|
8
|
+
import { resolveWorkspaceUuid } from '../../util/resolve-workspace.js';
|
|
8
9
|
import { isGitRepo, hasAtLeastOneCommit, gitSetRemote, gitPushForceLease, gitPushForce, getRemoteUrl, displayUrl, runGit, } from '../../util/git-ops.js';
|
|
9
10
|
export default class WorkspaceBootstrap extends BaseCommand {
|
|
10
11
|
static description = "Push a local git repo to Gitea as this workspace's config repository";
|
|
@@ -32,19 +33,14 @@ export default class WorkspaceBootstrap extends BaseCommand {
|
|
|
32
33
|
// Resolve target directory
|
|
33
34
|
const dir = flags.dir || process.env.QUONFIG_DIR || process.cwd();
|
|
34
35
|
const resolvedDir = path.resolve(dir);
|
|
35
|
-
|
|
36
|
+
const { workspaceId, orgSlug } = await resolveWorkspaceUuid(this);
|
|
37
|
+
// The display name is best-effort; we still want it for the confirmation
|
|
38
|
+
// prompt. Fall back through saved profile → workspaceId UUID.
|
|
36
39
|
const authConfig = await loadAuthConfig();
|
|
37
|
-
if (!authConfig) {
|
|
38
|
-
return this.err('Not logged in. Run `qfg login` first.');
|
|
39
|
-
}
|
|
40
40
|
const activeProfile = getActiveProfile();
|
|
41
|
-
const profile = authConfig
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
const workspaceId = profile.workspace;
|
|
46
|
-
const workspaceName = profile.workspaceSlug || profile.workspaceName || workspaceId;
|
|
47
|
-
this.verboseLog('WorkspaceBootstrap', { workspaceId, dir: resolvedDir });
|
|
41
|
+
const profile = authConfig?.profiles[activeProfile] || authConfig?.profiles[authConfig?.defaultProfile || 'default'];
|
|
42
|
+
const workspaceName = profile?.workspaceSlug || profile?.workspaceName || workspaceId;
|
|
43
|
+
this.verboseLog('WorkspaceBootstrap', { workspaceId, orgSlug, dir: resolvedDir });
|
|
48
44
|
// Validate: must be a git repo with at least one commit
|
|
49
45
|
const isRepo = await isGitRepo(resolvedDir);
|
|
50
46
|
if (!isRepo) {
|
|
@@ -98,7 +94,7 @@ export default class WorkspaceBootstrap extends BaseCommand {
|
|
|
98
94
|
this.log('Connecting to Gitea...');
|
|
99
95
|
let tokenData;
|
|
100
96
|
try {
|
|
101
|
-
tokenData = await mintGiteaToken(workspaceId, 'write', 'bootstrap');
|
|
97
|
+
tokenData = await mintGiteaToken(workspaceId, orgSlug, 'write', 'bootstrap');
|
|
102
98
|
}
|
|
103
99
|
catch (error) {
|
|
104
100
|
return this.err(`Could not get Gitea credentials: ${String(error)}\n\nMake sure the workspace is fully provisioned in the Quonfig app before bootstrapping.`);
|
|
@@ -15,11 +15,15 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
15
15
|
* Pick the org for the new workspace from the per-org token store.
|
|
16
16
|
*
|
|
17
17
|
* - 0 orgs → "No orgs found. Run `qfg login` first."
|
|
18
|
-
* - 1 org → auto-select that org
|
|
18
|
+
* - 1 org → auto-select that org.
|
|
19
19
|
* - 2+ orgs → require `--org <slug-or-uuid>`. Slug is matched against
|
|
20
20
|
* `org_slug` in the token store; UUID is matched against the token
|
|
21
21
|
* store key. Either way the org must exist locally — we never invent
|
|
22
22
|
* an orgId we don't have a token for.
|
|
23
|
+
*
|
|
24
|
+
* Always returns BOTH the WorkOS org id (needed to mint the JWT) and the
|
|
25
|
+
* org slug (needed for the create-workspace request body). If the token
|
|
26
|
+
* lacks `org_slug`, errors with a `qfg login` hint.
|
|
23
27
|
*/
|
|
24
28
|
private resolveOrgFromTokens;
|
|
25
29
|
/**
|
|
@@ -55,18 +55,25 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
55
55
|
const { workosOrgId, orgSlug } = orgResolution;
|
|
56
56
|
let accessToken;
|
|
57
57
|
try {
|
|
58
|
-
accessToken = await getValidAccessToken(workosOrgId);
|
|
58
|
+
accessToken = await getValidAccessToken(workosOrgId, this.verboseLog);
|
|
59
59
|
}
|
|
60
|
-
catch {
|
|
61
|
-
|
|
60
|
+
catch (error) {
|
|
61
|
+
// Surface the underlying token error (e.g. "Token refresh failed for
|
|
62
|
+
// org X: invalid_grant — Refresh token already exchanged") so users
|
|
63
|
+
// and agents can distinguish "never logged in" from "session went
|
|
64
|
+
// stale and refresh was rejected" — both used to read "Not logged in".
|
|
65
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
66
|
+
const hint = detail.includes('qfg login') ? '' : ' Run `qfg login` to re-authenticate.';
|
|
67
|
+
return this.err(`${detail}${hint}`);
|
|
62
68
|
}
|
|
63
69
|
const apiUrl = getApiUrl();
|
|
64
|
-
|
|
70
|
+
// The server expects organizationSlug (it resolves to the local org
|
|
71
|
+
// UUID) — not the WorkOS org id, which is what we have keyed in the
|
|
72
|
+
// token store. resolveOrgFromTokens guarantees orgSlug is populated.
|
|
73
|
+
const body = { slug, organizationSlug: orgSlug };
|
|
65
74
|
if (flags.name)
|
|
66
75
|
body.name = flags.name;
|
|
67
|
-
|
|
68
|
-
this.log(`Creating workspace in org: ${orgSlug}`);
|
|
69
|
-
}
|
|
76
|
+
this.log(`Creating workspace in org: ${orgSlug}`);
|
|
70
77
|
this.verboseLog('WorkspaceCreate', { apiUrl, body });
|
|
71
78
|
let res;
|
|
72
79
|
try {
|
|
@@ -135,11 +142,15 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
135
142
|
* Pick the org for the new workspace from the per-org token store.
|
|
136
143
|
*
|
|
137
144
|
* - 0 orgs → "No orgs found. Run `qfg login` first."
|
|
138
|
-
* - 1 org → auto-select that org
|
|
145
|
+
* - 1 org → auto-select that org.
|
|
139
146
|
* - 2+ orgs → require `--org <slug-or-uuid>`. Slug is matched against
|
|
140
147
|
* `org_slug` in the token store; UUID is matched against the token
|
|
141
148
|
* store key. Either way the org must exist locally — we never invent
|
|
142
149
|
* an orgId we don't have a token for.
|
|
150
|
+
*
|
|
151
|
+
* Always returns BOTH the WorkOS org id (needed to mint the JWT) and the
|
|
152
|
+
* org slug (needed for the create-workspace request body). If the token
|
|
153
|
+
* lacks `org_slug`, errors with a `qfg login` hint.
|
|
143
154
|
*/
|
|
144
155
|
async resolveOrgFromTokens(flagOrg) {
|
|
145
156
|
const store = await loadTokens();
|
|
@@ -147,13 +158,19 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
147
158
|
return { error: 'No orgs found. Run `qfg login` first.' };
|
|
148
159
|
}
|
|
149
160
|
const entries = Object.entries(store.tokensByOrg);
|
|
161
|
+
const requireSlug = (workosOrgId, orgSlug) => {
|
|
162
|
+
if (!orgSlug) {
|
|
163
|
+
return { error: `Org \`${workosOrgId}\` is missing its slug locally. Run \`qfg login\` to refresh your org list.` };
|
|
164
|
+
}
|
|
165
|
+
return { workosOrgId, orgSlug };
|
|
166
|
+
};
|
|
150
167
|
if (flagOrg) {
|
|
151
168
|
if (UUID_PATTERN.test(flagOrg)) {
|
|
152
169
|
const tokens = store.tokensByOrg[flagOrg];
|
|
153
170
|
if (!tokens) {
|
|
154
171
|
return { error: `Org \`${flagOrg}\` not found in your token store. Run \`qfg login\` to refresh your org list.` };
|
|
155
172
|
}
|
|
156
|
-
return
|
|
173
|
+
return requireSlug(flagOrg, tokens.org_slug);
|
|
157
174
|
}
|
|
158
175
|
const matchedId = findOrgIdBySlug(store, flagOrg);
|
|
159
176
|
if (!matchedId) {
|
|
@@ -163,7 +180,7 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
163
180
|
}
|
|
164
181
|
if (entries.length === 1) {
|
|
165
182
|
const [orgId, tokens] = entries[0];
|
|
166
|
-
return
|
|
183
|
+
return requireSlug(orgId, tokens.org_slug);
|
|
167
184
|
}
|
|
168
185
|
const slugList = entries
|
|
169
186
|
.map(([, tokens]) => tokens.org_slug)
|
|
@@ -16,5 +16,11 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
16
16
|
* migration message used elsewhere in the CLI.
|
|
17
17
|
*/
|
|
18
18
|
private matchByArg;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Iterate every cached org and merge its workspace list. Each org has its
|
|
21
|
+
* own access token; one token only sees one org's workspaces. We tolerate
|
|
22
|
+
* a single org's failure (verbose-log it) so a stale token in one org
|
|
23
|
+
* doesn't block switching into a working one.
|
|
24
|
+
*/
|
|
25
|
+
private fetchAllOrgsWorkspaces;
|
|
20
26
|
}
|
|
@@ -21,16 +21,10 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
21
21
|
if (!store || Object.keys(store.tokensByOrg).length === 0) {
|
|
22
22
|
return this.err('Not logged in. Run `qfg login` first.');
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
accessToken = await getValidAccessToken(firstOrgId, this.verboseLog);
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
return this.err('Not logged in. Run `qfg login` first.');
|
|
32
|
-
}
|
|
33
|
-
const candidates = await this.fetchAllWorkspaces(accessToken);
|
|
24
|
+
// /userWorkspaces/list is per-token-org-scoped — a single org's JWT
|
|
25
|
+
// only sees that org's workspaces. To support multi-org pickers we
|
|
26
|
+
// must iterate every cached org and merge.
|
|
27
|
+
const candidates = await this.fetchAllOrgsWorkspaces(store);
|
|
34
28
|
if (candidates.length === 0) {
|
|
35
29
|
return this.err('No workspaces found for your account.');
|
|
36
30
|
}
|
|
@@ -70,7 +64,6 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
70
64
|
workspaceSlug: match.workspaceSlug,
|
|
71
65
|
organizationName: match.organizationName,
|
|
72
66
|
organizationSlug: orgSlug,
|
|
73
|
-
workosOrgId: match.workosOrgId,
|
|
74
67
|
},
|
|
75
68
|
},
|
|
76
69
|
});
|
|
@@ -83,7 +76,6 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
83
76
|
activeWorkspace: pin,
|
|
84
77
|
organizationName: match.organizationName,
|
|
85
78
|
organizationSlug: orgSlug,
|
|
86
|
-
workosOrgId: match.workosOrgId,
|
|
87
79
|
workspaceId: match.workspaceId,
|
|
88
80
|
workspaceSlug: match.workspaceSlug,
|
|
89
81
|
};
|
|
@@ -124,23 +116,49 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
124
116
|
}
|
|
125
117
|
return { match: found };
|
|
126
118
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Iterate every cached org and merge its workspace list. Each org has its
|
|
121
|
+
* own access token; one token only sees one org's workspaces. We tolerate
|
|
122
|
+
* a single org's failure (verbose-log it) so a stale token in one org
|
|
123
|
+
* doesn't block switching into a working one.
|
|
124
|
+
*/
|
|
125
|
+
async fetchAllOrgsWorkspaces(store) {
|
|
126
|
+
const merged = new Map();
|
|
127
|
+
for (const orgId of Object.keys(store.tokensByOrg)) {
|
|
128
|
+
let token;
|
|
129
|
+
try {
|
|
130
|
+
// eslint-disable-next-line no-await-in-loop
|
|
131
|
+
token = await getValidAccessToken(orgId, this.verboseLog);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.verboseLog('workspace switch: token refresh failed', { orgId, error: String(error) });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
// eslint-disable-next-line no-await-in-loop
|
|
139
|
+
const res = await fetch(`${getApiUrl()}/api/v1/userWorkspaces/list`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
142
|
+
body: JSON.stringify({ json: {} }),
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
this.verboseLog('workspace switch: list returned non-OK', { orgId, status: res.status });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// eslint-disable-next-line no-await-in-loop
|
|
149
|
+
const body = (await res.json());
|
|
150
|
+
const list = (body.json ?? body);
|
|
151
|
+
if (!Array.isArray(list))
|
|
152
|
+
continue;
|
|
153
|
+
for (const w of list) {
|
|
154
|
+
merged.set(w.workspaceId, w);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.verboseLog('workspace switch: fetch failed', { orgId, error: String(error) });
|
|
136
159
|
}
|
|
137
|
-
const body = (await res.json());
|
|
138
|
-
const all = (body.json ?? body);
|
|
139
|
-
return Array.isArray(all) ? all : [];
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
throw new Error(`Failed to fetch workspaces: ${error instanceof Error ? error.message : String(error)}`);
|
|
143
160
|
}
|
|
161
|
+
return [...merged.values()];
|
|
144
162
|
}
|
|
145
163
|
}
|
|
146
164
|
function pinFor(w) {
|
|
@@ -18,5 +18,10 @@ export default class Workspace extends BaseCommand {
|
|
|
18
18
|
*/
|
|
19
19
|
private activePin;
|
|
20
20
|
private fetchWorkspaces;
|
|
21
|
+
/**
|
|
22
|
+
* Fan out across every cached org's token and merge the results. One org's
|
|
23
|
+
* stale token doesn't block the others — we verbose-log and continue.
|
|
24
|
+
*/
|
|
25
|
+
private fetchAllOrgsWorkspaces;
|
|
21
26
|
private runApiKeyMode;
|
|
22
27
|
}
|
|
@@ -20,15 +20,10 @@ export default class Workspace extends BaseCommand {
|
|
|
20
20
|
const orgSlugsWithTokens = orgEntries
|
|
21
21
|
.map(([orgId, t]) => t.org_slug ?? orgId)
|
|
22
22
|
.sort((a, b) => a.localeCompare(b));
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
return this.err(error instanceof Error ? error.message : String(error));
|
|
30
|
-
}
|
|
31
|
-
const allWorkspaces = await this.fetchWorkspaces(jwt);
|
|
23
|
+
// /userWorkspaces/list is per-token-org-scoped — a single org's JWT
|
|
24
|
+
// only sees that org's workspaces. Iterate every cached org so the
|
|
25
|
+
// listing is comprehensive in multi-org accounts.
|
|
26
|
+
const allWorkspaces = await this.fetchAllOrgsWorkspaces(orgEntries);
|
|
32
27
|
const defaultProfilePin = this.profilePin(authConfig, allWorkspaces);
|
|
33
28
|
const activePin = this.activePin(store, authConfig, allWorkspaces, defaultProfilePin);
|
|
34
29
|
this.log(`Active workspace: ${activePin ?? '(none)'}`);
|
|
@@ -135,6 +130,30 @@ export default class Workspace extends BaseCommand {
|
|
|
135
130
|
return [];
|
|
136
131
|
}
|
|
137
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Fan out across every cached org's token and merge the results. One org's
|
|
135
|
+
* stale token doesn't block the others — we verbose-log and continue.
|
|
136
|
+
*/
|
|
137
|
+
async fetchAllOrgsWorkspaces(orgEntries) {
|
|
138
|
+
const merged = new Map();
|
|
139
|
+
for (const [orgId] of orgEntries) {
|
|
140
|
+
let jwt;
|
|
141
|
+
try {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
jwt = await getValidAccessToken(orgId, this.verboseLog);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
this.verboseLog('workspace: token refresh failed for org', { orgId, error: String(error) });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// eslint-disable-next-line no-await-in-loop
|
|
150
|
+
const list = await this.fetchWorkspaces(jwt);
|
|
151
|
+
for (const w of list) {
|
|
152
|
+
merged.set(w.workspaceId, w);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...merged.values()];
|
|
156
|
+
}
|
|
138
157
|
async runApiKeyMode() {
|
|
139
158
|
const override = process.env.QUONFIG_WORKSPACE;
|
|
140
159
|
if (!override) {
|
|
@@ -144,17 +144,20 @@ export function transformConfig(config, envIdMap, onDroppedEnv) {
|
|
|
144
144
|
const defaultSection = out.default;
|
|
145
145
|
const firstRule = defaultSection?.rules?.[0];
|
|
146
146
|
const innerValue = firstRule?.value?.value;
|
|
147
|
+
const key = String(out.key ?? 'unknown');
|
|
147
148
|
if (!innerValue || innerValue.schemaType !== 'ZOD' || typeof innerValue.schema !== 'string') {
|
|
148
|
-
|
|
149
|
+
// qfg-p74d: surface as a per-config skip so one bad source config doesn't
|
|
150
|
+
// abort the whole push. Surfaces in MIGRATION_REPORT.md "Skipped invalid
|
|
151
|
+
// configs" alongside variant mismatches.
|
|
152
|
+
throw new InvalidSourceConfigError(`Unsupported schema payload for "${key}"`);
|
|
149
153
|
}
|
|
150
|
-
const key = String(out.key ?? 'unknown');
|
|
151
154
|
let converted;
|
|
152
155
|
try {
|
|
153
156
|
converted = zodToJsonSchema(innerValue.schema);
|
|
154
157
|
}
|
|
155
158
|
catch (error) {
|
|
156
159
|
const msg = error instanceof Error ? error.message : String(error);
|
|
157
|
-
throw new
|
|
160
|
+
throw new InvalidSourceConfigError(`Schema conversion failed for "${key}": ${msg}\n--- Zod source ---\n${innerValue.schema}\n--- end ---`);
|
|
158
161
|
}
|
|
159
162
|
return converted.schema;
|
|
160
163
|
}
|
|
@@ -352,10 +352,14 @@ function convertObjectShape(objectLiteral, ctx) {
|
|
|
352
352
|
required.push(key);
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
+
// qfg-mrab: Quonfig's authoring validator requires additionalProperties:false on
|
|
356
|
+
// every object with properties; default zod is open, so we close it here. Users
|
|
357
|
+
// who genuinely want an open object can edit the emitted schema post-migrate.
|
|
355
358
|
return {
|
|
356
359
|
type: 'object',
|
|
357
360
|
properties,
|
|
358
361
|
required,
|
|
362
|
+
additionalProperties: false,
|
|
359
363
|
};
|
|
360
364
|
}
|
|
361
365
|
function convertBaseZodCall(method, args, ctx) {
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* `QUONFIG_API_KEY` is set in the environment); see `getValidAccessToken`.
|
|
16
16
|
*/
|
|
17
17
|
import type { ConfigPushInput, ConfigPushResult } from './run-push.js';
|
|
18
|
-
export declare const callConfigsPush: (input: ConfigPushInput) => Promise<ConfigPushResult>;
|
|
18
|
+
export declare const callConfigsPush: (input: ConfigPushInput, orgSlug: string) => Promise<ConfigPushResult>;
|
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
* `QUONFIG_API_KEY` is set in the environment); see `getValidAccessToken`.
|
|
16
16
|
*/
|
|
17
17
|
import { getApiUrl } from '../util/domain-urls.js';
|
|
18
|
-
import {
|
|
19
|
-
export const callConfigsPush = async (input) => {
|
|
20
|
-
|
|
21
|
-
const orgId = await resolveDefaultOrgId();
|
|
22
|
-
const accessToken = await getValidAccessToken(orgId);
|
|
18
|
+
import { getValidAccessTokenForOrgSlug } from '../util/get-valid-token.js';
|
|
19
|
+
export const callConfigsPush = async (input, orgSlug) => {
|
|
20
|
+
const accessToken = await getValidAccessTokenForOrgSlug(orgSlug);
|
|
23
21
|
const apiUrl = getApiUrl();
|
|
24
22
|
const res = await fetch(`${apiUrl}/api/v1/configs/push`, {
|
|
25
23
|
method: 'POST',
|
package/dist/util/get-client.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getApiUrl } from '../util/domain-urls.js';
|
|
|
3
3
|
import jsonMaybe from '../util/json-maybe.js';
|
|
4
4
|
import { tryParseWorkspacePin } from '../util/quonfig-json.js';
|
|
5
5
|
import { findOrgIdBySlug, getActiveProfile, loadAuthConfig, loadTokens } from '../util/token-storage.js';
|
|
6
|
-
import { getValidAccessToken,
|
|
6
|
+
import { getValidAccessToken, getValidAccessTokenForOrgSlug } from '../util/get-valid-token.js';
|
|
7
7
|
import version from '../version.js';
|
|
8
8
|
const BARE_SLUG_ENV_MIGRATION_MESSAGE = 'QUONFIG_WORKSPACE must be in org/workspace form (e.g. acme/foo). ' +
|
|
9
9
|
'Bare workspace slugs are no longer accepted. Update your .env and run `qfg login` if you have not yet migrated.';
|
|
@@ -133,15 +133,6 @@ const getClient = async (command, sdkKey, profile) => {
|
|
|
133
133
|
command.error('No authentication found. Please run `qfg login`.', { exit: 401 });
|
|
134
134
|
}
|
|
135
135
|
command.verboseLog('OAuth auth', { hasAuthConfig: true });
|
|
136
|
-
try {
|
|
137
|
-
const orgId = await resolveDefaultOrgId();
|
|
138
|
-
jwt = await getValidAccessToken(orgId, command.verboseLog);
|
|
139
|
-
command.verboseLog('Token valid (or refreshed)');
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
143
|
-
command.error(`Session expired. Please run \`qfg login\` to re-authenticate. (${detail})`, { exit: 401 });
|
|
144
|
-
}
|
|
145
136
|
const activeProfile = getActiveProfile(profileNameOverride);
|
|
146
137
|
const profileData = authConfig.profiles[activeProfile] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
147
138
|
command.verboseLog('Workspace lookup', {
|
|
@@ -150,9 +141,18 @@ const getClient = async (command, sdkKey, profile) => {
|
|
|
150
141
|
hasProfileData: Boolean(profileData),
|
|
151
142
|
workspaceId: profileData?.workspace,
|
|
152
143
|
});
|
|
153
|
-
if (profileData) {
|
|
154
|
-
|
|
144
|
+
if (!profileData?.organizationSlug) {
|
|
145
|
+
command.error('Saved profile is missing organization_slug. Run `qfg login` to refresh, or `qfg workspace switch <org>/<ws>`.', { exit: 1 });
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
jwt = await getValidAccessTokenForOrgSlug(profileData.organizationSlug, command.verboseLog);
|
|
149
|
+
command.verboseLog('Token valid (or refreshed)');
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
153
|
+
command.error(`Session expired. Please run \`qfg login\` to re-authenticate. (${detail})`, { exit: 401 });
|
|
155
154
|
}
|
|
155
|
+
workspaceId = profileData.workspace;
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
// Store workspaceId on the command for use in building URLs and cache it
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
type Logger = (msg: string, data?: unknown) => void;
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Slug-based wrapper around getValidAccessToken. Commands talk in
|
|
14
|
+
* `<org-slug>` (the user-vocabulary form); the slug → workosOrgId
|
|
15
|
+
* mapping is contained here so the rest of the CLI doesn't see WorkOS
|
|
16
|
+
* identifiers.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* is the stopgap until each caller's resolution lands in its own bead.
|
|
18
|
+
* The `QUONFIG_API_KEY` short-circuit fires before any disk lookup, so
|
|
19
|
+
* an empty/unknown slug is fine in CI mode.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function getValidAccessTokenForOrgSlug(orgSlug: string, log?: Logger): Promise<string>;
|
|
22
22
|
export declare function getValidAccessToken(workosOrgId: string, log?: Logger): Promise<string>;
|
|
23
23
|
export {};
|
|
@@ -9,24 +9,30 @@
|
|
|
9
9
|
* ignored because API keys are workspace-scoped.
|
|
10
10
|
*/
|
|
11
11
|
import { authenticateWithOrg, decodeJWT } from './oauth-client.js';
|
|
12
|
-
import { getTokenForOrg, loadTokens, saveTokens } from './token-storage.js';
|
|
12
|
+
import { findOrgIdBySlug, getTokenForOrg, loadTokens, saveTokens } from './token-storage.js';
|
|
13
13
|
const noopLog = () => { };
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Slug-based wrapper around getValidAccessToken. Commands talk in
|
|
16
|
+
* `<org-slug>` (the user-vocabulary form); the slug → workosOrgId
|
|
17
|
+
* mapping is contained here so the rest of the CLI doesn't see WorkOS
|
|
18
|
+
* identifiers.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* is the stopgap until each caller's resolution lands in its own bead.
|
|
20
|
+
* The `QUONFIG_API_KEY` short-circuit fires before any disk lookup, so
|
|
21
|
+
* an empty/unknown slug is fine in CI mode.
|
|
22
22
|
*/
|
|
23
|
-
export async function
|
|
23
|
+
export async function getValidAccessTokenForOrgSlug(orgSlug, log = noopLog) {
|
|
24
|
+
const envKey = process.env.QUONFIG_API_KEY;
|
|
25
|
+
if (envKey && envKey.length > 0)
|
|
26
|
+
return getValidAccessToken('', log);
|
|
24
27
|
const store = await loadTokens();
|
|
25
|
-
if (!store)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
if (!store) {
|
|
29
|
+
throw new Error('Not logged in. Run `qfg login` first.');
|
|
30
|
+
}
|
|
31
|
+
const workosOrgId = findOrgIdBySlug(store, orgSlug);
|
|
32
|
+
if (!workosOrgId) {
|
|
33
|
+
throw new Error(`No token for org \`${orgSlug}\`. Run \`qfg login\` to add this org.`);
|
|
34
|
+
}
|
|
35
|
+
return getValidAccessToken(workosOrgId, log);
|
|
30
36
|
}
|
|
31
37
|
export async function getValidAccessToken(workosOrgId, log = noopLog) {
|
|
32
38
|
const envKey = process.env.QUONFIG_API_KEY;
|
package/dist/util/gitea-api.d.ts
CHANGED
|
@@ -18,8 +18,8 @@ export interface GiteaTokenResponse {
|
|
|
18
18
|
*/
|
|
19
19
|
workspaceSlug: string;
|
|
20
20
|
}
|
|
21
|
-
export declare const mintGiteaToken: (workspaceId: string, scope: "read" | "write", purpose: "pull" | "bootstrap" | "push") => Promise<GiteaTokenResponse>;
|
|
21
|
+
export declare const mintGiteaToken: (workspaceId: string, orgSlug: string, scope: "read" | "write", purpose: "pull" | "bootstrap" | "push") => Promise<GiteaTokenResponse>;
|
|
22
22
|
/**
|
|
23
23
|
* Mint a Gitea read token, store it, and return the entry.
|
|
24
24
|
*/
|
|
25
|
-
export declare const mintAndStoreGiteaReadToken: (workspaceId: string) => Promise<GiteaTokenEntry>;
|
|
25
|
+
export declare const mintAndStoreGiteaReadToken: (workspaceId: string, orgSlug: string) => Promise<GiteaTokenEntry>;
|