@quonfig/cli 0.0.39 → 0.0.40
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 +190 -43
- package/dist/commands/login.js +1 -1
- package/dist/commands/migrate/my-code.js +2 -5
- package/dist/commands/pull.d.ts +11 -11
- package/dist/commands/pull.js +20 -19
- package/dist/commands/push.js +2 -2
- package/dist/commands/run.d.ts +46 -0
- package/dist/commands/run.js +324 -0
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/whoami.d.ts +6 -6
- package/dist/commands/whoami.js +27 -27
- package/dist/commands/workspace/create.d.ts +6 -6
- package/dist/commands/workspace/create.js +31 -27
- package/dist/commands/workspace/switch.d.ts +7 -7
- package/dist/commands/workspace/switch.js +36 -36
- package/dist/commands/workspace.d.ts +7 -7
- package/dist/commands/workspace.js +41 -43
- package/dist/push/run-push.d.ts +16 -16
- package/dist/util/git-ops.js +5 -10
- package/dist/util/open-browser.js +1 -1
- package/dist/util/parse-run-env-spec.d.ts +23 -0
- package/dist/util/parse-run-env-spec.js +56 -0
- package/dist/util/quonfig-json.js +2 -2
- package/dist/util/resolve-run-environment.d.ts +46 -0
- package/dist/util/resolve-run-environment.js +52 -0
- package/dist/util/resolve-workspace.d.ts +1 -1
- package/dist/util/token-storage.d.ts +6 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +114 -1
- package/package.json +1 -1
|
@@ -80,42 +80,6 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
80
80
|
workspaceSlug: match.workspaceSlug,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Resolve the user's positional arg against the candidate list.
|
|
85
|
-
*
|
|
86
|
-
* - `org/ws` form (preferred) → match by both org slug and workspace slug.
|
|
87
|
-
* - UUID → workspaceId match (kept for back-compat with scripts that
|
|
88
|
-
* pass IDs straight through). Bare slug is rejected with the same
|
|
89
|
-
* migration message used elsewhere in the CLI.
|
|
90
|
-
*/
|
|
91
|
-
matchByArg(arg, candidates, store) {
|
|
92
|
-
if (UUID_PATTERN.test(arg)) {
|
|
93
|
-
const found = candidates.find((w) => w.workspaceId === arg);
|
|
94
|
-
if (!found)
|
|
95
|
-
return { error: `Workspace UUID "${arg}" not found among your accessible workspaces.` };
|
|
96
|
-
return { match: found };
|
|
97
|
-
}
|
|
98
|
-
const pin = tryParseWorkspacePin(arg);
|
|
99
|
-
if (!pin) {
|
|
100
|
-
return {
|
|
101
|
-
error: 'Workspace argument must be in `<org-slug>/<workspace-slug>` form (e.g. acme/production). ' +
|
|
102
|
-
'Bare workspace slugs are no longer accepted.',
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
const orgId = findOrgIdBySlug(store, pin.orgSlug);
|
|
106
|
-
if (!orgId) {
|
|
107
|
-
return { error: `No token found for org \`${pin.orgSlug}\`. Run \`qfg login\` to add this org.` };
|
|
108
|
-
}
|
|
109
|
-
const found = candidates.find((w) => w.workosOrgId === orgId && w.workspaceSlug === pin.workspaceSlug);
|
|
110
|
-
if (!found) {
|
|
111
|
-
const inOrg = candidates.filter((w) => w.workosOrgId === orgId).map((w) => w.workspaceSlug);
|
|
112
|
-
const available = inOrg.length > 0 ? inOrg.join(', ') : '(none)';
|
|
113
|
-
return {
|
|
114
|
-
error: `Workspace "${pin.workspaceSlug}" not found in org "${pin.orgSlug}". Available: ${available}.`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
return { match: found };
|
|
118
|
-
}
|
|
119
83
|
/**
|
|
120
84
|
* Iterate every cached org and merge its workspace list. Each org has its
|
|
121
85
|
* own access token; one token only sees one org's workspaces. We tolerate
|
|
@@ -160,6 +124,42 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
160
124
|
}
|
|
161
125
|
return [...merged.values()];
|
|
162
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the user's positional arg against the candidate list.
|
|
129
|
+
*
|
|
130
|
+
* - `org/ws` form (preferred) → match by both org slug and workspace slug.
|
|
131
|
+
* - UUID → workspaceId match (kept for back-compat with scripts that
|
|
132
|
+
* pass IDs straight through). Bare slug is rejected with the same
|
|
133
|
+
* migration message used elsewhere in the CLI.
|
|
134
|
+
*/
|
|
135
|
+
matchByArg(arg, candidates, store) {
|
|
136
|
+
if (UUID_PATTERN.test(arg)) {
|
|
137
|
+
const found = candidates.find((w) => w.workspaceId === arg);
|
|
138
|
+
if (!found)
|
|
139
|
+
return { error: `Workspace UUID "${arg}" not found among your accessible workspaces.` };
|
|
140
|
+
return { match: found };
|
|
141
|
+
}
|
|
142
|
+
const pin = tryParseWorkspacePin(arg);
|
|
143
|
+
if (!pin) {
|
|
144
|
+
return {
|
|
145
|
+
error: 'Workspace argument must be in `<org-slug>/<workspace-slug>` form (e.g. acme/production). ' +
|
|
146
|
+
'Bare workspace slugs are no longer accepted.',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const orgId = findOrgIdBySlug(store, pin.orgSlug);
|
|
150
|
+
if (!orgId) {
|
|
151
|
+
return { error: `No token found for org \`${pin.orgSlug}\`. Run \`qfg login\` to add this org.` };
|
|
152
|
+
}
|
|
153
|
+
const found = candidates.find((w) => w.workosOrgId === orgId && w.workspaceSlug === pin.workspaceSlug);
|
|
154
|
+
if (!found) {
|
|
155
|
+
const inOrg = candidates.filter((w) => w.workosOrgId === orgId).map((w) => w.workspaceSlug);
|
|
156
|
+
const available = inOrg.length > 0 ? inOrg.join(', ') : '(none)';
|
|
157
|
+
return {
|
|
158
|
+
error: `Workspace "${pin.workspaceSlug}" not found in org "${pin.orgSlug}". Available: ${available}.`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return { match: found };
|
|
162
|
+
}
|
|
163
163
|
}
|
|
164
164
|
function pinFor(w) {
|
|
165
165
|
const orgSlug = w.organizationSlug ?? slugFromName(w.organizationName) ?? w.workosOrgId;
|
|
@@ -4,12 +4,6 @@ export default class Workspace extends BaseCommand {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
run(): Promise<JsonObj | void>;
|
|
7
|
-
/**
|
|
8
|
-
* Pin the default-profile workspaceId to org/ws form by looking it up in
|
|
9
|
-
* the workspaces list. Returns undefined if the auth config is empty or
|
|
10
|
-
* the saved workspace doesn't appear in the listing.
|
|
11
|
-
*/
|
|
12
|
-
private profilePin;
|
|
13
7
|
/**
|
|
14
8
|
* QUONFIG_WORKSPACE wins over the default profile (matches the resolution
|
|
15
9
|
* order in get-client.ts/resolve-workspace.ts). Bare slugs are ignored
|
|
@@ -17,11 +11,17 @@ export default class Workspace extends BaseCommand {
|
|
|
17
11
|
* shows the default profile instead.
|
|
18
12
|
*/
|
|
19
13
|
private activePin;
|
|
20
|
-
private fetchWorkspaces;
|
|
21
14
|
/**
|
|
22
15
|
* Fan out across every cached org's token and merge the results. One org's
|
|
23
16
|
* stale token doesn't block the others — we verbose-log and continue.
|
|
24
17
|
*/
|
|
25
18
|
private fetchAllOrgsWorkspaces;
|
|
19
|
+
private fetchWorkspaces;
|
|
20
|
+
/**
|
|
21
|
+
* Pin the default-profile workspaceId to org/ws form by looking it up in
|
|
22
|
+
* the workspaces list. Returns undefined if the auth config is empty or
|
|
23
|
+
* the saved workspace doesn't appear in the listing.
|
|
24
|
+
*/
|
|
25
|
+
private profilePin;
|
|
26
26
|
private runApiKeyMode;
|
|
27
27
|
}
|
|
@@ -17,9 +17,7 @@ export default class Workspace extends BaseCommand {
|
|
|
17
17
|
}
|
|
18
18
|
const authConfig = await loadAuthConfig();
|
|
19
19
|
const orgEntries = Object.entries(store.tokensByOrg);
|
|
20
|
-
const orgSlugsWithTokens = orgEntries
|
|
21
|
-
.map(([orgId, t]) => t.org_slug ?? orgId)
|
|
22
|
-
.sort((a, b) => a.localeCompare(b));
|
|
20
|
+
const orgSlugsWithTokens = orgEntries.map(([orgId, t]) => t.org_slug ?? orgId).sort((a, b) => a.localeCompare(b));
|
|
23
21
|
// /userWorkspaces/list is per-token-org-scoped — a single org's JWT
|
|
24
22
|
// only sees that org's workspaces. Iterate every cached org so the
|
|
25
23
|
// listing is comprehensive in multi-org accounts.
|
|
@@ -69,26 +67,6 @@ export default class Workspace extends BaseCommand {
|
|
|
69
67
|
})),
|
|
70
68
|
};
|
|
71
69
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Pin the default-profile workspaceId to org/ws form by looking it up in
|
|
74
|
-
* the workspaces list. Returns undefined if the auth config is empty or
|
|
75
|
-
* the saved workspace doesn't appear in the listing.
|
|
76
|
-
*/
|
|
77
|
-
profilePin(authConfig, allWorkspaces) {
|
|
78
|
-
if (!authConfig)
|
|
79
|
-
return undefined;
|
|
80
|
-
const activeProfileName = getActiveProfile();
|
|
81
|
-
const profile = authConfig.profiles[activeProfileName] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
82
|
-
if (!profile)
|
|
83
|
-
return undefined;
|
|
84
|
-
const match = allWorkspaces.find((w) => w.workspaceId === profile.workspace);
|
|
85
|
-
if (!match)
|
|
86
|
-
return undefined;
|
|
87
|
-
const orgSlug = match.organizationSlug ?? slugFromName(match.organizationName);
|
|
88
|
-
if (!orgSlug)
|
|
89
|
-
return undefined;
|
|
90
|
-
return `${orgSlug}/${match.workspaceSlug}`;
|
|
91
|
-
}
|
|
92
70
|
/**
|
|
93
71
|
* QUONFIG_WORKSPACE wins over the default profile (matches the resolution
|
|
94
72
|
* order in get-client.ts/resolve-workspace.ts). Bare slugs are ignored
|
|
@@ -110,26 +88,6 @@ export default class Workspace extends BaseCommand {
|
|
|
110
88
|
}
|
|
111
89
|
return fallback;
|
|
112
90
|
}
|
|
113
|
-
async fetchWorkspaces(jwt) {
|
|
114
|
-
try {
|
|
115
|
-
const res = await fetch(`${getApiUrl()}/api/v1/userWorkspaces/list`, {
|
|
116
|
-
method: 'POST',
|
|
117
|
-
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
118
|
-
body: JSON.stringify({ json: {} }),
|
|
119
|
-
});
|
|
120
|
-
if (!res.ok) {
|
|
121
|
-
this.verboseLog('workspace: userWorkspaces/list non-OK', { status: res.status });
|
|
122
|
-
return [];
|
|
123
|
-
}
|
|
124
|
-
const body = (await res.json());
|
|
125
|
-
const list = (body.json ?? body);
|
|
126
|
-
return Array.isArray(list) ? list : [];
|
|
127
|
-
}
|
|
128
|
-
catch (error) {
|
|
129
|
-
this.verboseLog('workspace: userWorkspaces/list failed', { error: String(error) });
|
|
130
|
-
return [];
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
91
|
/**
|
|
134
92
|
* Fan out across every cached org's token and merge the results. One org's
|
|
135
93
|
* stale token doesn't block the others — we verbose-log and continue.
|
|
@@ -154,6 +112,46 @@ export default class Workspace extends BaseCommand {
|
|
|
154
112
|
}
|
|
155
113
|
return [...merged.values()];
|
|
156
114
|
}
|
|
115
|
+
async fetchWorkspaces(jwt) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(`${getApiUrl()}/api/v1/userWorkspaces/list`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({ json: {} }),
|
|
121
|
+
});
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
this.verboseLog('workspace: userWorkspaces/list non-OK', { status: res.status });
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
const body = (await res.json());
|
|
127
|
+
const list = (body.json ?? body);
|
|
128
|
+
return Array.isArray(list) ? list : [];
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.verboseLog('workspace: userWorkspaces/list failed', { error: String(error) });
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Pin the default-profile workspaceId to org/ws form by looking it up in
|
|
137
|
+
* the workspaces list. Returns undefined if the auth config is empty or
|
|
138
|
+
* the saved workspace doesn't appear in the listing.
|
|
139
|
+
*/
|
|
140
|
+
profilePin(authConfig, allWorkspaces) {
|
|
141
|
+
if (!authConfig)
|
|
142
|
+
return undefined;
|
|
143
|
+
const activeProfileName = getActiveProfile();
|
|
144
|
+
const profile = authConfig.profiles[activeProfileName] || authConfig.profiles[authConfig.defaultProfile || 'default'];
|
|
145
|
+
if (!profile)
|
|
146
|
+
return undefined;
|
|
147
|
+
const match = allWorkspaces.find((w) => w.workspaceId === profile.workspace);
|
|
148
|
+
if (!match)
|
|
149
|
+
return undefined;
|
|
150
|
+
const orgSlug = match.organizationSlug ?? slugFromName(match.organizationName);
|
|
151
|
+
if (!orgSlug)
|
|
152
|
+
return undefined;
|
|
153
|
+
return `${orgSlug}/${match.workspaceSlug}`;
|
|
154
|
+
}
|
|
157
155
|
async runApiKeyMode() {
|
|
158
156
|
const override = process.env.QUONFIG_WORKSPACE;
|
|
159
157
|
if (!override) {
|
package/dist/push/run-push.d.ts
CHANGED
|
@@ -36,22 +36,6 @@ export interface GitOps {
|
|
|
36
36
|
* HEAD vs origin/main; for the bare path it's local vs probe-clone.
|
|
37
37
|
*/
|
|
38
38
|
diffHeadVsOrigin(dir: string): Promise<FileDelta[]>;
|
|
39
|
-
/** `git fetch origin` in the given dir. */
|
|
40
|
-
fetch(dir: string): Promise<void>;
|
|
41
|
-
/** Returns the `remote.origin.url` for the repo, or undefined if unset / not a repo. */
|
|
42
|
-
getRemoteOriginUrl(dir: string): Promise<string | undefined>;
|
|
43
|
-
/** Returns true if the dir has a `.git/` (worktree or repo). */
|
|
44
|
-
isGitRepo(dir: string): Promise<boolean>;
|
|
45
|
-
/**
|
|
46
|
-
* Returns true if origin/main has commits the local HEAD does not have
|
|
47
|
-
* (local strictly behind, or diverged). False when local is up-to-date
|
|
48
|
-
* or strictly ahead of origin/main, or when origin/main is unknown.
|
|
49
|
-
*
|
|
50
|
-
* The clone-path stale-HEAD guard (qfg-fboj) refuses to push in either
|
|
51
|
-
* "behind" or "diverged" state because both produce a diff that ships
|
|
52
|
-
* REVERSAL deltas to the server, silently undoing remote-newer commits.
|
|
53
|
-
*/
|
|
54
|
-
isLocalBehindRemote(dir: string): Promise<boolean>;
|
|
55
39
|
/**
|
|
56
40
|
* Tracked files (relative paths) with working-tree or staged changes.
|
|
57
41
|
* Used by the clone-path dirty-tree warning to surface uncommitted
|
|
@@ -59,6 +43,8 @@ export interface GitOps {
|
|
|
59
43
|
* files are excluded — same rule as `dirtyTrackedFiles` in git-ops.ts.
|
|
60
44
|
*/
|
|
61
45
|
dirtyTrackedFiles(dir: string): Promise<string[]>;
|
|
46
|
+
/** `git fetch origin` in the given dir. */
|
|
47
|
+
fetch(dir: string): Promise<void>;
|
|
62
48
|
/**
|
|
63
49
|
* Returns the local SHA of `origin/main` after fetch — the remote tip
|
|
64
50
|
* the diff was computed against — or undefined if no `origin/main` ref
|
|
@@ -69,6 +55,20 @@ export interface GitOps {
|
|
|
69
55
|
* origin moved between fetch and push.
|
|
70
56
|
*/
|
|
71
57
|
getOriginMainSha(dir: string): Promise<string | undefined>;
|
|
58
|
+
/** Returns the `remote.origin.url` for the repo, or undefined if unset / not a repo. */
|
|
59
|
+
getRemoteOriginUrl(dir: string): Promise<string | undefined>;
|
|
60
|
+
/** Returns true if the dir has a `.git/` (worktree or repo). */
|
|
61
|
+
isGitRepo(dir: string): Promise<boolean>;
|
|
62
|
+
/**
|
|
63
|
+
* Returns true if origin/main has commits the local HEAD does not have
|
|
64
|
+
* (local strictly behind, or diverged). False when local is up-to-date
|
|
65
|
+
* or strictly ahead of origin/main, or when origin/main is unknown.
|
|
66
|
+
*
|
|
67
|
+
* The clone-path stale-HEAD guard (qfg-fboj) refuses to push in either
|
|
68
|
+
* "behind" or "diverged" state because both produce a diff that ships
|
|
69
|
+
* REVERSAL deltas to the server, silently undoing remote-newer commits.
|
|
70
|
+
*/
|
|
71
|
+
isLocalBehindRemote(dir: string): Promise<boolean>;
|
|
72
72
|
/** Set origin to `url` (add if missing, set-url if present). */
|
|
73
73
|
setRemoteOrigin(dir: string, url: string): Promise<void>;
|
|
74
74
|
}
|
package/dist/util/git-ops.js
CHANGED
|
@@ -182,7 +182,8 @@ export const commitPinFixIfPinOnly = async (dir, file, expectedSlug) => {
|
|
|
182
182
|
// Compare working tree vs HEAD ignoring `workspace`. If anything else
|
|
183
183
|
// differs, the user has additional uncommitted edits — leave alone.
|
|
184
184
|
const stripped = (o) => {
|
|
185
|
-
const
|
|
185
|
+
const rest = { ...o };
|
|
186
|
+
delete rest.workspace;
|
|
186
187
|
return rest;
|
|
187
188
|
};
|
|
188
189
|
const a = JSON.stringify(stripped(workingParsed), Object.keys(stripped(workingParsed)).sort());
|
|
@@ -296,13 +297,7 @@ export const gitMergeFfOnly = async (dir) => {
|
|
|
296
297
|
let newCommits = [];
|
|
297
298
|
try {
|
|
298
299
|
const { stdout: localSha } = await runGit(['-C', dir, 'rev-parse', 'HEAD']);
|
|
299
|
-
const { stdout: log } = await runGit([
|
|
300
|
-
'-C',
|
|
301
|
-
dir,
|
|
302
|
-
'log',
|
|
303
|
-
'--pretty=format:%s',
|
|
304
|
-
`${localSha.trim()}..origin/main`,
|
|
305
|
-
]);
|
|
300
|
+
const { stdout: log } = await runGit(['-C', dir, 'log', '--pretty=format:%s', `${localSha.trim()}..origin/main`]);
|
|
306
301
|
newCommits = log
|
|
307
302
|
.split('\n')
|
|
308
303
|
.map((s) => s.trim())
|
|
@@ -371,7 +366,7 @@ export const gitPullRebase = async (dir) => {
|
|
|
371
366
|
await runGit(['-C', dir, 'pull', '--rebase', 'origin', 'main']);
|
|
372
367
|
return { kind: 'clean', commitsRebased };
|
|
373
368
|
}
|
|
374
|
-
catch (
|
|
369
|
+
catch (error) {
|
|
375
370
|
// Distinguish "rebase paused on conflicts" from "rebase never started".
|
|
376
371
|
// Conflicts: `diff --diff-filter=U` lists the unmerged paths and
|
|
377
372
|
// `.git/rebase-merge/` is present.
|
|
@@ -388,7 +383,7 @@ export const gitPullRebase = async (dir) => {
|
|
|
388
383
|
catch {
|
|
389
384
|
// Fall through to 'failed' — diff itself errored.
|
|
390
385
|
}
|
|
391
|
-
const reason =
|
|
386
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
392
387
|
return { kind: 'failed', reason };
|
|
393
388
|
}
|
|
394
389
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for `qfg run` env-var → config-key mappings.
|
|
3
|
+
*
|
|
4
|
+
* Two callers, one grammar:
|
|
5
|
+
* - `--env VAR=key.path` (inline)
|
|
6
|
+
* - `--env-file=PATH` (one VAR=key.path per line)
|
|
7
|
+
*
|
|
8
|
+
* Grammar choices that are intentionally narrow:
|
|
9
|
+
* - Separator is `=`, not `:`. Matches `docker -e VAR=value` muscle memory.
|
|
10
|
+
* A line like `VAR:key.path` is rejected, not silently re-interpreted.
|
|
11
|
+
* - Split on the FIRST `=` only, so future config keys can theoretically
|
|
12
|
+
* contain `=` (rare, but cheap to support).
|
|
13
|
+
* - VAR side trimmed; key side trimmed once (whitespace inside the key
|
|
14
|
+
* is preserved if it's somehow legal — but we don't go out of our way).
|
|
15
|
+
* - Env files: blank lines and `#` comments are skipped. Errors include
|
|
16
|
+
* the 1-based line number so users can `vim +N file.env` to the spot.
|
|
17
|
+
*/
|
|
18
|
+
export interface RunEnvSpec {
|
|
19
|
+
configKey: string;
|
|
20
|
+
varName: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const parseInlineEnvSpec: (raw: string) => RunEnvSpec;
|
|
23
|
+
export declare const parseEnvFileContents: (contents: string) => RunEnvSpec[];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsers for `qfg run` env-var → config-key mappings.
|
|
3
|
+
*
|
|
4
|
+
* Two callers, one grammar:
|
|
5
|
+
* - `--env VAR=key.path` (inline)
|
|
6
|
+
* - `--env-file=PATH` (one VAR=key.path per line)
|
|
7
|
+
*
|
|
8
|
+
* Grammar choices that are intentionally narrow:
|
|
9
|
+
* - Separator is `=`, not `:`. Matches `docker -e VAR=value` muscle memory.
|
|
10
|
+
* A line like `VAR:key.path` is rejected, not silently re-interpreted.
|
|
11
|
+
* - Split on the FIRST `=` only, so future config keys can theoretically
|
|
12
|
+
* contain `=` (rare, but cheap to support).
|
|
13
|
+
* - VAR side trimmed; key side trimmed once (whitespace inside the key
|
|
14
|
+
* is preserved if it's somehow legal — but we don't go out of our way).
|
|
15
|
+
* - Env files: blank lines and `#` comments are skipped. Errors include
|
|
16
|
+
* the 1-based line number so users can `vim +N file.env` to the spot.
|
|
17
|
+
*/
|
|
18
|
+
const splitOnFirstEquals = (raw) => {
|
|
19
|
+
const idx = raw.indexOf('=');
|
|
20
|
+
if (idx === -1)
|
|
21
|
+
return undefined;
|
|
22
|
+
return [raw.slice(0, idx), raw.slice(idx + 1)];
|
|
23
|
+
};
|
|
24
|
+
export const parseInlineEnvSpec = (raw) => {
|
|
25
|
+
const split = splitOnFirstEquals(raw);
|
|
26
|
+
if (!split) {
|
|
27
|
+
throw new Error(`Invalid --env value "${raw}": expected VAR=key.path`);
|
|
28
|
+
}
|
|
29
|
+
const varName = split[0].trim();
|
|
30
|
+
const configKey = split[1].trim();
|
|
31
|
+
if (varName === '') {
|
|
32
|
+
throw new Error(`Invalid --env value "${raw}": empty VAR name (left of =)`);
|
|
33
|
+
}
|
|
34
|
+
if (configKey === '') {
|
|
35
|
+
throw new Error(`Invalid --env value "${raw}": empty config key (right of =)`);
|
|
36
|
+
}
|
|
37
|
+
return { varName, configKey };
|
|
38
|
+
};
|
|
39
|
+
export const parseEnvFileContents = (contents) => {
|
|
40
|
+
const out = [];
|
|
41
|
+
let lineNumber = 0;
|
|
42
|
+
for (const raw of contents.split('\n')) {
|
|
43
|
+
lineNumber += 1;
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
if (trimmed === '' || trimmed.startsWith('#'))
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
out.push(parseInlineEnvSpec(trimmed));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
52
|
+
throw new Error(`${detail} (line ${lineNumber})`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
};
|
|
@@ -177,12 +177,12 @@ export function upsertWorkspaceKey(raw, slug) {
|
|
|
177
177
|
const after = raw.slice(endOfLastValue + 1);
|
|
178
178
|
const hasOtherKeys = Object.keys(parsed).length > 0;
|
|
179
179
|
// Detect indent from the first nested key. Default to two spaces.
|
|
180
|
-
const indentMatch = raw.match(/\n([
|
|
180
|
+
const indentMatch = raw.match(/\n([\t ]+)"/);
|
|
181
181
|
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
182
182
|
// If the original used a multi-line object (closing brace on its own
|
|
183
183
|
// line), keep that shape. Otherwise the original was a single-line
|
|
184
184
|
// object like `{}` or `{"k":v}` — emit on one line.
|
|
185
|
-
const isMultiline = /\n\s
|
|
185
|
+
const isMultiline = /\n\s*}\s*$/.test(raw) || /\n/.test(raw.slice(0, closingIdx));
|
|
186
186
|
let insertion;
|
|
187
187
|
if (isMultiline) {
|
|
188
188
|
insertion = hasOtherKeys ? `,\n${indent}"workspace": ${slugJson}` : `\n${indent}"workspace": ${slugJson}`;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the auth/environment mode for `qfg run`.
|
|
3
|
+
*
|
|
4
|
+
* The rule is **binary and mutually exclusive** — never resolve in
|
|
5
|
+
* precedence order, because a "match-only" exception drifts silently and
|
|
6
|
+
* masks misconfiguration in CI.
|
|
7
|
+
*
|
|
8
|
+
* Mode A — `QUONFIG_BACKEND_SDK_KEY` set:
|
|
9
|
+
* The SDK key encodes both project and environment. If `--environment`
|
|
10
|
+
* OR `QUONFIG_ENVIRONMENT` is also present, return an `error` — even
|
|
11
|
+
* if they would agree, because tolerating that case lets a stale env
|
|
12
|
+
* var quietly drift past us until the day it disagrees.
|
|
13
|
+
*
|
|
14
|
+
* Mode B — no SDK key:
|
|
15
|
+
* Require EXACTLY ONE of `--environment` flag or `QUONFIG_ENVIRONMENT`
|
|
16
|
+
* env var. Both → error. Neither → error. There is no interactive
|
|
17
|
+
* prompt — `qfg run` is scripted, and prompting mid-`&&`-chain is
|
|
18
|
+
* worse UX than failing loud.
|
|
19
|
+
*
|
|
20
|
+
* Empty strings are treated as "unset" because shells routinely emit
|
|
21
|
+
* `EXPORT_FOO=` when a var is declared but has no value, and silently
|
|
22
|
+
* picking Mode A with an empty key would surface as a confusing 401 from
|
|
23
|
+
* api-delivery much later.
|
|
24
|
+
*/
|
|
25
|
+
export interface ResolveRunEnvironmentInput {
|
|
26
|
+
/** Value of the `--environment` flag (or undefined). */
|
|
27
|
+
envFlag: string | undefined;
|
|
28
|
+
/** Value of `QUONFIG_ENVIRONMENT` env var (or undefined). */
|
|
29
|
+
envFromEnvironment: string | undefined;
|
|
30
|
+
/** Value of `QUONFIG_BACKEND_SDK_KEY` (or undefined). */
|
|
31
|
+
sdkKey: string | undefined;
|
|
32
|
+
}
|
|
33
|
+
export type ResolveRunEnvironmentResult = {
|
|
34
|
+
mode: 'sdk-key';
|
|
35
|
+
sdkKey: string;
|
|
36
|
+
} | {
|
|
37
|
+
mode: 'user';
|
|
38
|
+
environmentName: string;
|
|
39
|
+
} | {
|
|
40
|
+
mode: 'error';
|
|
41
|
+
message: string;
|
|
42
|
+
};
|
|
43
|
+
export declare const RUN_MODE_AMBIGUOUS_ERROR: string;
|
|
44
|
+
export declare const RUN_MODE_NO_ENV_ERROR: string;
|
|
45
|
+
export declare const RUN_MODE_BOTH_ENV_ERROR = "qfg run: pass exactly one of --environment or QUONFIG_ENVIRONMENT (both are set).";
|
|
46
|
+
export declare const resolveRunEnvironmentMode: (input: ResolveRunEnvironmentInput) => ResolveRunEnvironmentResult;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the auth/environment mode for `qfg run`.
|
|
3
|
+
*
|
|
4
|
+
* The rule is **binary and mutually exclusive** — never resolve in
|
|
5
|
+
* precedence order, because a "match-only" exception drifts silently and
|
|
6
|
+
* masks misconfiguration in CI.
|
|
7
|
+
*
|
|
8
|
+
* Mode A — `QUONFIG_BACKEND_SDK_KEY` set:
|
|
9
|
+
* The SDK key encodes both project and environment. If `--environment`
|
|
10
|
+
* OR `QUONFIG_ENVIRONMENT` is also present, return an `error` — even
|
|
11
|
+
* if they would agree, because tolerating that case lets a stale env
|
|
12
|
+
* var quietly drift past us until the day it disagrees.
|
|
13
|
+
*
|
|
14
|
+
* Mode B — no SDK key:
|
|
15
|
+
* Require EXACTLY ONE of `--environment` flag or `QUONFIG_ENVIRONMENT`
|
|
16
|
+
* env var. Both → error. Neither → error. There is no interactive
|
|
17
|
+
* prompt — `qfg run` is scripted, and prompting mid-`&&`-chain is
|
|
18
|
+
* worse UX than failing loud.
|
|
19
|
+
*
|
|
20
|
+
* Empty strings are treated as "unset" because shells routinely emit
|
|
21
|
+
* `EXPORT_FOO=` when a var is declared but has no value, and silently
|
|
22
|
+
* picking Mode A with an empty key would surface as a confusing 401 from
|
|
23
|
+
* api-delivery much later.
|
|
24
|
+
*/
|
|
25
|
+
export const RUN_MODE_AMBIGUOUS_ERROR = 'qfg run: QUONFIG_BACKEND_SDK_KEY is set, which encodes the environment.\n' +
|
|
26
|
+
'Remove --environment and unset QUONFIG_ENVIRONMENT, or remove the SDK key.';
|
|
27
|
+
export const RUN_MODE_NO_ENV_ERROR = 'qfg run: no environment specified.\n' +
|
|
28
|
+
'Either set QUONFIG_BACKEND_SDK_KEY (which encodes env) or\n' +
|
|
29
|
+
'set QUONFIG_ENVIRONMENT / pass --environment after `qfg login`.';
|
|
30
|
+
export const RUN_MODE_BOTH_ENV_ERROR = 'qfg run: pass exactly one of --environment or QUONFIG_ENVIRONMENT (both are set).';
|
|
31
|
+
const blank = (s) => s === undefined || s === '';
|
|
32
|
+
export const resolveRunEnvironmentMode = (input) => {
|
|
33
|
+
const hasSdkKey = !blank(input.sdkKey);
|
|
34
|
+
const hasFlag = !blank(input.envFlag);
|
|
35
|
+
const hasEnvVar = !blank(input.envFromEnvironment);
|
|
36
|
+
if (hasSdkKey) {
|
|
37
|
+
if (hasFlag || hasEnvVar) {
|
|
38
|
+
return { mode: 'error', message: RUN_MODE_AMBIGUOUS_ERROR };
|
|
39
|
+
}
|
|
40
|
+
return { mode: 'sdk-key', sdkKey: input.sdkKey };
|
|
41
|
+
}
|
|
42
|
+
if (hasFlag && hasEnvVar) {
|
|
43
|
+
return { mode: 'error', message: RUN_MODE_BOTH_ENV_ERROR };
|
|
44
|
+
}
|
|
45
|
+
if (hasFlag) {
|
|
46
|
+
return { mode: 'user', environmentName: input.envFlag };
|
|
47
|
+
}
|
|
48
|
+
if (hasEnvVar) {
|
|
49
|
+
return { mode: 'user', environmentName: input.envFromEnvironment };
|
|
50
|
+
}
|
|
51
|
+
return { mode: 'error', message: RUN_MODE_NO_ENV_ERROR };
|
|
52
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { BaseCommand } from '../index.js';
|
|
2
2
|
export interface ResolvedWorkspace {
|
|
3
|
-
workspaceId: string;
|
|
4
3
|
/**
|
|
5
4
|
* Org slug that owns workspaceId. Empty string in the QUONFIG_API_KEY
|
|
6
5
|
* path because API keys are workspace-scoped and the slug is never
|
|
@@ -8,6 +7,7 @@ export interface ResolvedWorkspace {
|
|
|
8
7
|
* on the env key before consulting it.
|
|
9
8
|
*/
|
|
10
9
|
orgSlug: string;
|
|
10
|
+
workspaceId: string;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Resolve the target workspace UUID + owning org slug for commands that
|
|
@@ -4,9 +4,8 @@ export interface TokenStorageOptions {
|
|
|
4
4
|
export interface TokenSet {
|
|
5
5
|
access_token: string;
|
|
6
6
|
expires_at: number;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
user_id?: string;
|
|
7
|
+
/** Org display name for `qfg whoami` / `qfg workspace`. */
|
|
8
|
+
org_name?: string;
|
|
10
9
|
/**
|
|
11
10
|
* Human-readable org slug (e.g. "acme") — populated by the per-org login
|
|
12
11
|
* flow so the CLI can resolve `acme/foo`-form workspace addresses against
|
|
@@ -14,8 +13,9 @@ export interface TokenSet {
|
|
|
14
13
|
* project/plans/multi-org-cli-auth.md (qfg-kr7).
|
|
15
14
|
*/
|
|
16
15
|
org_slug?: string;
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
refresh_token: string;
|
|
17
|
+
user_email?: string;
|
|
18
|
+
user_id?: string;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Schema version of tokens.json. Stamped on every saveTokens write so a
|
|
@@ -24,11 +24,11 @@ export interface TokenSet {
|
|
|
24
24
|
*/
|
|
25
25
|
export declare const TOKEN_STORE_VERSION = 2;
|
|
26
26
|
export interface TokenStore {
|
|
27
|
-
version?: number;
|
|
28
27
|
defaultOrgId?: string;
|
|
29
28
|
tokensByOrg: {
|
|
30
29
|
[workosOrgId: string]: TokenSet;
|
|
31
30
|
};
|
|
31
|
+
version?: number;
|
|
32
32
|
}
|
|
33
33
|
export declare const getTokenForOrg: (store: TokenStore, workosOrgId: string) => TokenSet | undefined;
|
|
34
34
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.40";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.0.
|
|
2
|
+
export default '0.0.40';
|