@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { APICommand } from '../index.js';
|
|
2
|
+
/**
|
|
3
|
+
* `qfg run` — resolve Quonfig configs into env vars and exec a child process.
|
|
4
|
+
*
|
|
5
|
+
* Use this for tools that read env vars at module import (drizzle-kit migrate,
|
|
6
|
+
* next-auth's AUTH_SECRET, build steps, one-shot scripts) where going through
|
|
7
|
+
* `instrumentation.ts` / SDK init isn't possible.
|
|
8
|
+
*
|
|
9
|
+
* The flag separator `--` is REQUIRED between `qfg run` flags and the child
|
|
10
|
+
* command. Flags that look like child flags (e.g. `--silent`) without `--`
|
|
11
|
+
* would otherwise be eaten by oclif.
|
|
12
|
+
*
|
|
13
|
+
* Auth/env mode is binary — see resolve-run-environment.ts for the full rule.
|
|
14
|
+
*/
|
|
15
|
+
export default class Run extends APICommand {
|
|
16
|
+
static args: {
|
|
17
|
+
command: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
18
|
+
};
|
|
19
|
+
static description: string;
|
|
20
|
+
static examples: string[];
|
|
21
|
+
static flags: {
|
|
22
|
+
env: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[], import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
23
|
+
'env-file': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
24
|
+
environment: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
25
|
+
'preserve-env': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
26
|
+
};
|
|
27
|
+
static strict: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Override APICommand.init() — that base eagerly calls getClient(), which
|
|
30
|
+
* walks the OAuth path and fails before we've even validated the mode rule.
|
|
31
|
+
* For `qfg run` in Mode A (SDK key), we never need a JWT at all; for Mode B
|
|
32
|
+
* we'll mint the client on demand from run().
|
|
33
|
+
*/
|
|
34
|
+
init(): Promise<void>;
|
|
35
|
+
run(): Promise<void>;
|
|
36
|
+
private execChild;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a single RawConfigWithDependencies into a stringified env value.
|
|
39
|
+
* Mirrors the providedBy / decryptWith handling in commands/get.ts so the
|
|
40
|
+
* two paths agree on what "the value of this key on the CLI host" means.
|
|
41
|
+
*/
|
|
42
|
+
private resolveOne;
|
|
43
|
+
private resolveValues;
|
|
44
|
+
private resolveViaSdkKey;
|
|
45
|
+
private resolveViaUserAuth;
|
|
46
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { Quonfig } from '@quonfig/node';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { APICommand } from '../index.js';
|
|
6
|
+
import { decrypt } from '../util/encryption.js';
|
|
7
|
+
import { getDeliveryUrl } from '../util/domain-urls.js';
|
|
8
|
+
import getClient from '../util/get-client.js';
|
|
9
|
+
import { parseEnvFileContents, parseInlineEnvSpec } from '../util/parse-run-env-spec.js';
|
|
10
|
+
import { resolveRunEnvironmentMode } from '../util/resolve-run-environment.js';
|
|
11
|
+
/**
|
|
12
|
+
* `qfg run` — resolve Quonfig configs into env vars and exec a child process.
|
|
13
|
+
*
|
|
14
|
+
* Use this for tools that read env vars at module import (drizzle-kit migrate,
|
|
15
|
+
* next-auth's AUTH_SECRET, build steps, one-shot scripts) where going through
|
|
16
|
+
* `instrumentation.ts` / SDK init isn't possible.
|
|
17
|
+
*
|
|
18
|
+
* The flag separator `--` is REQUIRED between `qfg run` flags and the child
|
|
19
|
+
* command. Flags that look like child flags (e.g. `--silent`) without `--`
|
|
20
|
+
* would otherwise be eaten by oclif.
|
|
21
|
+
*
|
|
22
|
+
* Auth/env mode is binary — see resolve-run-environment.ts for the full rule.
|
|
23
|
+
*/
|
|
24
|
+
export default class Run extends APICommand {
|
|
25
|
+
// Hidden positional that holds the child command and its args. The user
|
|
26
|
+
// is expected to put `--` before it; oclif moves everything after `--`
|
|
27
|
+
// into argv. We document the `--` requirement in the description.
|
|
28
|
+
static args = {
|
|
29
|
+
command: Args.string({ description: "child command (after '--')", required: false }),
|
|
30
|
+
};
|
|
31
|
+
static description = `Run a child process with Quonfig configs injected as env vars.
|
|
32
|
+
|
|
33
|
+
Resolves --env VAR=key.path mappings (and --env-file lines) against your
|
|
34
|
+
workspace, merges them into the parent env, and exec's the child command.
|
|
35
|
+
Exits with the child's exit code. Missing keys → fail before spawning.
|
|
36
|
+
|
|
37
|
+
Auth / environment is BINARY:
|
|
38
|
+
Mode A — QUONFIG_BACKEND_SDK_KEY set: env is encoded in the key.
|
|
39
|
+
Setting --environment OR QUONFIG_ENVIRONMENT alongside is an error.
|
|
40
|
+
Mode B — no SDK key: pass exactly one of --environment or QUONFIG_ENVIRONMENT.
|
|
41
|
+
|
|
42
|
+
The flag separator '--' is REQUIRED between qfg flags and the child command.`;
|
|
43
|
+
static examples = [
|
|
44
|
+
'<%= config.bin %> <%= command.id %> --env DATABASE_URL=db.url --environment=staging -- env',
|
|
45
|
+
'<%= config.bin %> <%= command.id %> --env-file=.qfg.env --environment=staging -- npm run migrate',
|
|
46
|
+
'<%= config.bin %> <%= command.id %> --env DATABASE_URL=db.url -- ./bin/migrate.js # Mode A: relies on QUONFIG_BACKEND_SDK_KEY',
|
|
47
|
+
'<%= config.bin %> <%= command.id %> --env DATABASE_URL=db.url --preserve-env --environment=staging -- npm test',
|
|
48
|
+
];
|
|
49
|
+
static flags = {
|
|
50
|
+
env: Flags.string({
|
|
51
|
+
default: [],
|
|
52
|
+
description: 'Inline env mapping in VAR=key.path form. Repeatable.',
|
|
53
|
+
multiple: true,
|
|
54
|
+
}),
|
|
55
|
+
'env-file': Flags.string({
|
|
56
|
+
description: 'Path to a file with one VAR=key.path per line (# comments and blank lines OK).',
|
|
57
|
+
required: false,
|
|
58
|
+
}),
|
|
59
|
+
environment: Flags.string({
|
|
60
|
+
description: 'Quonfig environment to evaluate in (Mode B). Mutually exclusive with QUONFIG_ENVIRONMENT.',
|
|
61
|
+
required: false,
|
|
62
|
+
}),
|
|
63
|
+
'preserve-env': Flags.boolean({
|
|
64
|
+
default: false,
|
|
65
|
+
description: 'Skip vars that are already set in the parent env. Default: override.',
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
// We collect everything after `--` ourselves; oclif then leaves us alone.
|
|
69
|
+
static strict = false;
|
|
70
|
+
/**
|
|
71
|
+
* Override APICommand.init() — that base eagerly calls getClient(), which
|
|
72
|
+
* walks the OAuth path and fails before we've even validated the mode rule.
|
|
73
|
+
* For `qfg run` in Mode A (SDK key), we never need a JWT at all; for Mode B
|
|
74
|
+
* we'll mint the client on demand from run().
|
|
75
|
+
*/
|
|
76
|
+
async init() {
|
|
77
|
+
// Skip APICommand.init — call BaseCommand.init via the prototype chain.
|
|
78
|
+
// Object.getPrototypeOf(APICommand.prototype) is BaseCommand.prototype.
|
|
79
|
+
const baseInit = Object.getPrototypeOf(APICommand.prototype).init;
|
|
80
|
+
await baseInit.call(this);
|
|
81
|
+
}
|
|
82
|
+
async run() {
|
|
83
|
+
const { argv, flags } = await this.parse(Run);
|
|
84
|
+
// 1. Resolve mode (binary: Mode A SDK key, or Mode B user auth).
|
|
85
|
+
const sdkKey = process.env.QUONFIG_BACKEND_SDK_KEY;
|
|
86
|
+
const envEnv = process.env.QUONFIG_ENVIRONMENT;
|
|
87
|
+
const mode = resolveRunEnvironmentMode({
|
|
88
|
+
sdkKey,
|
|
89
|
+
envFlag: flags.environment,
|
|
90
|
+
envFromEnvironment: envEnv,
|
|
91
|
+
});
|
|
92
|
+
if (mode.mode === 'error') {
|
|
93
|
+
this.error(mode.message, { exit: 1 });
|
|
94
|
+
}
|
|
95
|
+
// 2. Collect VAR=key.path mappings from --env (inline) and --env-file.
|
|
96
|
+
const specs = [];
|
|
97
|
+
for (const inline of flags.env ?? []) {
|
|
98
|
+
try {
|
|
99
|
+
specs.push(parseInlineEnvSpec(inline));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 1 });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (flags['env-file']) {
|
|
106
|
+
let contents;
|
|
107
|
+
try {
|
|
108
|
+
contents = readFileSync(flags['env-file'], 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.error(`Failed to read --env-file ${flags['env-file']}: ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
specs.push(...parseEnvFileContents(contents));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
this.error(`In ${flags['env-file']}: ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Detect duplicate VARs early. Last-write-wins would silently swallow
|
|
121
|
+
// typos; we'd rather fail loud.
|
|
122
|
+
const seen = new Map();
|
|
123
|
+
for (const s of specs) {
|
|
124
|
+
const prev = seen.get(s.varName);
|
|
125
|
+
if (prev !== undefined && prev !== s.configKey) {
|
|
126
|
+
this.error(`Duplicate --env entry for ${s.varName} (${prev} vs ${s.configKey}). Pick one.`, { exit: 1 });
|
|
127
|
+
}
|
|
128
|
+
seen.set(s.varName, s.configKey);
|
|
129
|
+
}
|
|
130
|
+
// 3. Locate the child command. argv after `--` is what we want; the
|
|
131
|
+
// oclif parser already strips the `--` token itself. If argv is empty
|
|
132
|
+
// after parsing flags, the user forgot to specify a child.
|
|
133
|
+
const childArgs = [...argv];
|
|
134
|
+
if (childArgs.length === 0) {
|
|
135
|
+
this.error('No child command specified. Usage: qfg run [flags] -- <cmd> [args...]', { exit: 1 });
|
|
136
|
+
}
|
|
137
|
+
const [childCmd, ...childRest] = childArgs;
|
|
138
|
+
// 4. Resolve config values from Quonfig. Fail-fast on any miss.
|
|
139
|
+
const resolved = await this.resolveValues(specs, mode, flags.workspace);
|
|
140
|
+
// 5. Build the child env. Default: override. --preserve-env: skip set.
|
|
141
|
+
const childEnv = { ...process.env };
|
|
142
|
+
for (const [varName, value] of Object.entries(resolved)) {
|
|
143
|
+
if (flags['preserve-env'] && childEnv[varName] !== undefined && childEnv[varName] !== '') {
|
|
144
|
+
this.verboseLog('preserve-env: keeping parent env', { varName });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
childEnv[varName] = value;
|
|
148
|
+
}
|
|
149
|
+
// 6. Spawn. stdio:inherit so the child gets the real TTY where applicable.
|
|
150
|
+
await this.execChild(childCmd, childRest, childEnv);
|
|
151
|
+
}
|
|
152
|
+
async execChild(cmd, args, env) {
|
|
153
|
+
this.verboseLog('qfg run: spawning child', { cmd, args, varCount: Object.keys(env).length });
|
|
154
|
+
const exitCode = await new Promise((resolve) => {
|
|
155
|
+
const child = spawn(cmd, args, { env, stdio: 'inherit' });
|
|
156
|
+
child.on('error', (error) => {
|
|
157
|
+
// ENOENT etc. — fires before 'exit'. 127 is the conventional shell
|
|
158
|
+
// exit code for "command not found".
|
|
159
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
160
|
+
process.stderr.write(`qfg run: failed to spawn '${cmd}': ${msg}\n`);
|
|
161
|
+
resolve(127);
|
|
162
|
+
});
|
|
163
|
+
child.on('exit', (code, signal) => {
|
|
164
|
+
if (signal) {
|
|
165
|
+
// Mirror the shell convention: signal-killed → 128 + signal number.
|
|
166
|
+
const signalNumbers = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1, SIGKILL: 9 };
|
|
167
|
+
resolve(128 + (signalNumbers[signal] ?? 0));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
resolve(code ?? 0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// process.exit so the child's exit code becomes our exit code, even if
|
|
174
|
+
// oclif's outer catch handler would otherwise mask non-zero codes.
|
|
175
|
+
// In NODE_ENV=test we throw a typed error instead so the test runner
|
|
176
|
+
// can inspect it without process.exit killing the harness.
|
|
177
|
+
if (process.env.NODE_ENV === 'test') {
|
|
178
|
+
const err = new Error(`__test_child_exit__:${exitCode}`);
|
|
179
|
+
err.childExitCode = exitCode;
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
// qfg run: propagating the child's exit code is the whole feature; we
|
|
183
|
+
// can't `throw` here because oclif's outer catch handler masks the
|
|
184
|
+
// numeric code with 1.
|
|
185
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
186
|
+
process.exit(exitCode);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a single RawConfigWithDependencies into a stringified env value.
|
|
190
|
+
* Mirrors the providedBy / decryptWith handling in commands/get.ts so the
|
|
191
|
+
* two paths agree on what "the value of this key on the CLI host" means.
|
|
192
|
+
*/
|
|
193
|
+
resolveOne(key, config) {
|
|
194
|
+
let value = config.value;
|
|
195
|
+
const providedBy = config.dependencies?.find((d) => d.dependencyType === 'providedBy');
|
|
196
|
+
if (providedBy) {
|
|
197
|
+
const envVarName = providedBy.source;
|
|
198
|
+
const envValue = process.env[envVarName];
|
|
199
|
+
if (envValue === undefined) {
|
|
200
|
+
throw new Error(`provided by env var '${envVarName}' which is not set`);
|
|
201
|
+
}
|
|
202
|
+
value = envValue;
|
|
203
|
+
}
|
|
204
|
+
const decryptWith = config.dependencies?.find((d) => d.dependencyType === 'decryptWith');
|
|
205
|
+
if (decryptWith?.config) {
|
|
206
|
+
const encKeyConfig = decryptWith.config;
|
|
207
|
+
const keyProvided = encKeyConfig.dependencies?.find((d) => d.dependencyType === 'providedBy');
|
|
208
|
+
if (keyProvided) {
|
|
209
|
+
const envVarName = keyProvided.source;
|
|
210
|
+
const encryptionKey = process.env[envVarName];
|
|
211
|
+
if (encryptionKey === undefined) {
|
|
212
|
+
throw new Error(`decryptWith key '${encKeyConfig.key}' is provided by env var '${envVarName}' which is not set`);
|
|
213
|
+
}
|
|
214
|
+
if (typeof value !== 'string') {
|
|
215
|
+
throw new TypeError(`marked decryptWith but value is not a string`);
|
|
216
|
+
}
|
|
217
|
+
value = decrypt(value, encryptionKey);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (value === null || value === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
if (typeof value === 'string')
|
|
223
|
+
return value;
|
|
224
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
225
|
+
return String(value);
|
|
226
|
+
return JSON.stringify(value);
|
|
227
|
+
}
|
|
228
|
+
async resolveValues(specs, mode, workspaceFlag) {
|
|
229
|
+
if (specs.length === 0) {
|
|
230
|
+
this.warn('qfg run: no --env or --env-file mappings provided. The child will run with the parent env unchanged.');
|
|
231
|
+
return {};
|
|
232
|
+
}
|
|
233
|
+
if (mode.mode === 'sdk-key') {
|
|
234
|
+
return this.resolveViaSdkKey(specs, mode.sdkKey);
|
|
235
|
+
}
|
|
236
|
+
return this.resolveViaUserAuth(specs, mode.environmentName, workspaceFlag);
|
|
237
|
+
}
|
|
238
|
+
async resolveViaSdkKey(specs, sdkKey) {
|
|
239
|
+
// Mode A — talk to api-delivery via the @quonfig/node Quonfig SDK so we
|
|
240
|
+
// get the same evaluation path production apps use.
|
|
241
|
+
const quonfig = new Quonfig({
|
|
242
|
+
sdkKey,
|
|
243
|
+
collectEvaluationSummaries: false,
|
|
244
|
+
collectLoggerCounts: false,
|
|
245
|
+
contextUploadMode: 'none',
|
|
246
|
+
enableSSE: false,
|
|
247
|
+
apiUrls: [getDeliveryUrl()],
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
await quonfig.init();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
this.error(`qfg run: failed to initialize Quonfig with QUONFIG_BACKEND_SDK_KEY: ${error instanceof Error ? error.message : String(error)}`, { exit: 1 });
|
|
254
|
+
}
|
|
255
|
+
const resolved = {};
|
|
256
|
+
const missing = [];
|
|
257
|
+
for (const spec of specs) {
|
|
258
|
+
const value = quonfig.get(spec.configKey);
|
|
259
|
+
if (value === undefined || value === null) {
|
|
260
|
+
missing.push(spec.configKey);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
resolved[spec.varName] = String(value);
|
|
264
|
+
}
|
|
265
|
+
if (missing.length > 0) {
|
|
266
|
+
this.error(`qfg run: missing config key(s) in workspace (mode=sdk-key): ${missing.join(', ')}. Child not spawned.`, { exit: 1 });
|
|
267
|
+
}
|
|
268
|
+
return resolved;
|
|
269
|
+
}
|
|
270
|
+
async resolveViaUserAuth(specs, environmentName, workspaceFlag) {
|
|
271
|
+
// Mode B — same path as `qfg get`: POST /api/v1/evaluations/evaluate
|
|
272
|
+
// with workspaceId + environmentName, then resolve providedBy /
|
|
273
|
+
// decryptWith chains locally on the CLI host (qfg-c7d).
|
|
274
|
+
this.rawApiClient = await getClient(this, undefined, workspaceFlag);
|
|
275
|
+
const metadataRequest = await this.apiClient.post('/api/v1/metadata/list', { workspaceId: this.workspaceId });
|
|
276
|
+
if (!metadataRequest.ok) {
|
|
277
|
+
const detail = metadataRequest.error?.error ?? `HTTP ${metadataRequest.status}`;
|
|
278
|
+
this.error(`qfg run: failed to fetch configs: ${detail}`, { exit: 1 });
|
|
279
|
+
}
|
|
280
|
+
const metaJson = metadataRequest.json;
|
|
281
|
+
const allKeys = new Set((metaJson.configs ?? []).map((c) => c.key));
|
|
282
|
+
const missingMeta = specs.filter((s) => !allKeys.has(s.configKey)).map((s) => s.configKey);
|
|
283
|
+
if (missingMeta.length > 0) {
|
|
284
|
+
this.error(`qfg run: missing config key(s) in workspace: ${missingMeta.join(', ')}. Child not spawned.`, {
|
|
285
|
+
exit: 1,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const evalRequest = await this.apiClient.post('/api/v1/evaluations/evaluate', {
|
|
289
|
+
workspaceId: this.workspaceId,
|
|
290
|
+
environmentName,
|
|
291
|
+
context: {},
|
|
292
|
+
});
|
|
293
|
+
if (!evalRequest.ok) {
|
|
294
|
+
const detail = evalRequest.error?.error ?? `HTTP ${evalRequest.status}`;
|
|
295
|
+
this.error(`qfg run: failed to evaluate configs: ${detail}`, { exit: 1 });
|
|
296
|
+
}
|
|
297
|
+
const results = (Array.isArray(evalRequest.json) ? evalRequest.json : []);
|
|
298
|
+
const byKey = new Map(results.map((r) => [r.key, r]));
|
|
299
|
+
const resolved = {};
|
|
300
|
+
const errors = [];
|
|
301
|
+
for (const spec of specs) {
|
|
302
|
+
const config = byKey.get(spec.configKey);
|
|
303
|
+
if (!config) {
|
|
304
|
+
errors.push(`${spec.configKey}: not evaluable in environment "${environmentName}"`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const value = this.resolveOne(spec.configKey, config);
|
|
309
|
+
if (value === undefined) {
|
|
310
|
+
errors.push(`${spec.configKey}: produced no value`);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
resolved[spec.varName] = value;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
errors.push(`${spec.configKey}: ${error instanceof Error ? error.message : String(error)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (errors.length > 0) {
|
|
320
|
+
this.error(`qfg run: failed to resolve ${errors.length} config(s). Child not spawned.\n - ${errors.join('\n - ')}`, { exit: 1 });
|
|
321
|
+
}
|
|
322
|
+
return resolved;
|
|
323
|
+
}
|
|
324
|
+
}
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export default class Sync extends BaseCommand {
|
|
|
8
8
|
interval: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
9
|
watch: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
10
|
};
|
|
11
|
+
private orgSlug;
|
|
11
12
|
private resolvedDir;
|
|
12
13
|
private workspaceId;
|
|
13
|
-
private orgSlug;
|
|
14
14
|
run(): Promise<JsonObj | void>;
|
|
15
15
|
private ensureRemoteUrl;
|
|
16
16
|
private looksLike401;
|
package/dist/commands/sync.js
CHANGED
|
@@ -4,6 +4,12 @@ export default class Whoami extends BaseCommand {
|
|
|
4
4
|
static description: string;
|
|
5
5
|
static examples: string[];
|
|
6
6
|
run(): Promise<JsonObj | void>;
|
|
7
|
+
/**
|
|
8
|
+
* Best-effort fetch of `me.organizations` for role enrichment. Failure
|
|
9
|
+
* (network, unauthenticated) is non-fatal — whoami still works from the
|
|
10
|
+
* local store, just without role labels.
|
|
11
|
+
*/
|
|
12
|
+
private fetchOrganizations;
|
|
7
13
|
/**
|
|
8
14
|
* Pick the "active" org for display. Order:
|
|
9
15
|
* 1. QUONFIG_WORKSPACE in `org/ws` form → look up org by slug in the store.
|
|
@@ -11,10 +17,4 @@ export default class Whoami extends BaseCommand {
|
|
|
11
17
|
* 3. The first token entry — gives a stable marker rather than nothing.
|
|
12
18
|
*/
|
|
13
19
|
private resolveActiveOrgId;
|
|
14
|
-
/**
|
|
15
|
-
* Best-effort fetch of `me.organizations` for role enrichment. Failure
|
|
16
|
-
* (network, unauthenticated) is non-fatal — whoami still works from the
|
|
17
|
-
* local store, just without role labels.
|
|
18
|
-
*/
|
|
19
|
-
private fetchOrganizations;
|
|
20
20
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -64,33 +64,6 @@ export default class Whoami extends BaseCommand {
|
|
|
64
64
|
})),
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
68
|
-
* Pick the "active" org for display. Order:
|
|
69
|
-
* 1. QUONFIG_WORKSPACE in `org/ws` form → look up org by slug in the store.
|
|
70
|
-
* 2. The token store's defaultOrgId, if it points at an entry we have.
|
|
71
|
-
* 3. The first token entry — gives a stable marker rather than nothing.
|
|
72
|
-
*/
|
|
73
|
-
async resolveActiveOrgId(defaultOrgId) {
|
|
74
|
-
const env = process.env.QUONFIG_WORKSPACE;
|
|
75
|
-
if (env) {
|
|
76
|
-
const pin = tryParseWorkspacePin(env);
|
|
77
|
-
if (pin) {
|
|
78
|
-
const store = await loadTokens();
|
|
79
|
-
if (store) {
|
|
80
|
-
const matched = findOrgIdBySlug(store, pin.orgSlug);
|
|
81
|
-
if (matched)
|
|
82
|
-
return matched;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (defaultOrgId)
|
|
87
|
-
return defaultOrgId;
|
|
88
|
-
// Fall back to the first token entry — gives a stable marker rather than nothing.
|
|
89
|
-
const store = await loadTokens();
|
|
90
|
-
if (!store)
|
|
91
|
-
return undefined;
|
|
92
|
-
return Object.keys(store.tokensByOrg)[0];
|
|
93
|
-
}
|
|
94
67
|
/**
|
|
95
68
|
* Best-effort fetch of `me.organizations` for role enrichment. Failure
|
|
96
69
|
* (network, unauthenticated) is non-fatal — whoami still works from the
|
|
@@ -130,6 +103,33 @@ export default class Whoami extends BaseCommand {
|
|
|
130
103
|
return [];
|
|
131
104
|
}
|
|
132
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Pick the "active" org for display. Order:
|
|
108
|
+
* 1. QUONFIG_WORKSPACE in `org/ws` form → look up org by slug in the store.
|
|
109
|
+
* 2. The token store's defaultOrgId, if it points at an entry we have.
|
|
110
|
+
* 3. The first token entry — gives a stable marker rather than nothing.
|
|
111
|
+
*/
|
|
112
|
+
async resolveActiveOrgId(defaultOrgId) {
|
|
113
|
+
const env = process.env.QUONFIG_WORKSPACE;
|
|
114
|
+
if (env) {
|
|
115
|
+
const pin = tryParseWorkspacePin(env);
|
|
116
|
+
if (pin) {
|
|
117
|
+
const store = await loadTokens();
|
|
118
|
+
if (store) {
|
|
119
|
+
const matched = findOrgIdBySlug(store, pin.orgSlug);
|
|
120
|
+
if (matched)
|
|
121
|
+
return matched;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (defaultOrgId)
|
|
126
|
+
return defaultOrgId;
|
|
127
|
+
// Fall back to the first token entry — gives a stable marker rather than nothing.
|
|
128
|
+
const store = await loadTokens();
|
|
129
|
+
if (!store)
|
|
130
|
+
return undefined;
|
|
131
|
+
return Object.keys(store.tokensByOrg)[0];
|
|
132
|
+
}
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
135
|
* Combine the locally-cached orgs (one entry per token) with the server's
|
|
@@ -11,6 +11,12 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
11
11
|
org: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
12
|
};
|
|
13
13
|
run(): Promise<JsonObj | void>;
|
|
14
|
+
/**
|
|
15
|
+
* oRPC surfaces structured errors as {json: {code, message, ...}} or
|
|
16
|
+
* {error: {...}}. Extract the message for display; fall back to raw
|
|
17
|
+
* text when the body isn't JSON.
|
|
18
|
+
*/
|
|
19
|
+
private extractErrorMessage;
|
|
14
20
|
/**
|
|
15
21
|
* Pick the org for the new workspace from the per-org token store.
|
|
16
22
|
*
|
|
@@ -26,10 +32,4 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
26
32
|
* lacks `org_slug`, errors with a `qfg login` hint.
|
|
27
33
|
*/
|
|
28
34
|
private resolveOrgFromTokens;
|
|
29
|
-
/**
|
|
30
|
-
* oRPC surfaces structured errors as {json: {code, message, ...}} or
|
|
31
|
-
* {error: {...}}. Extract the message for display; fall back to raw
|
|
32
|
-
* text when the body isn't JSON.
|
|
33
|
-
*/
|
|
34
|
-
private extractErrorMessage;
|
|
35
35
|
}
|
|
@@ -17,6 +17,14 @@ function slugify(value) {
|
|
|
17
17
|
.replaceAll(/[^\da-z]+/g, '-')
|
|
18
18
|
.replaceAll(/^-|-$/g, '');
|
|
19
19
|
}
|
|
20
|
+
function requireSlug(workosOrgId, orgSlug) {
|
|
21
|
+
if (!orgSlug) {
|
|
22
|
+
return {
|
|
23
|
+
error: `Org \`${workosOrgId}\` is missing its slug locally. Run \`qfg login\` to refresh your org list.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { workosOrgId, orgSlug };
|
|
27
|
+
}
|
|
20
28
|
export default class WorkspaceCreate extends BaseCommand {
|
|
21
29
|
static args = {
|
|
22
30
|
slug: Args.string({
|
|
@@ -138,6 +146,25 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
138
146
|
environments: created.environments,
|
|
139
147
|
};
|
|
140
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* oRPC surfaces structured errors as {json: {code, message, ...}} or
|
|
151
|
+
* {error: {...}}. Extract the message for display; fall back to raw
|
|
152
|
+
* text when the body isn't JSON.
|
|
153
|
+
*/
|
|
154
|
+
async extractErrorMessage(res) {
|
|
155
|
+
try {
|
|
156
|
+
const body = (await res.clone().json());
|
|
157
|
+
return body.json?.message || body.error?.message || body.message || '';
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
try {
|
|
161
|
+
return await res.text();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
141
168
|
/**
|
|
142
169
|
* Pick the org for the new workspace from the per-org token store.
|
|
143
170
|
*
|
|
@@ -158,17 +185,13 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
158
185
|
return { error: 'No orgs found. Run `qfg login` first.' };
|
|
159
186
|
}
|
|
160
187
|
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
|
-
};
|
|
167
188
|
if (flagOrg) {
|
|
168
189
|
if (UUID_PATTERN.test(flagOrg)) {
|
|
169
190
|
const tokens = store.tokensByOrg[flagOrg];
|
|
170
191
|
if (!tokens) {
|
|
171
|
-
return {
|
|
192
|
+
return {
|
|
193
|
+
error: `Org \`${flagOrg}\` not found in your token store. Run \`qfg login\` to refresh your org list.`,
|
|
194
|
+
};
|
|
172
195
|
}
|
|
173
196
|
return requireSlug(flagOrg, tokens.org_slug);
|
|
174
197
|
}
|
|
@@ -184,30 +207,11 @@ export default class WorkspaceCreate extends BaseCommand {
|
|
|
184
207
|
}
|
|
185
208
|
const slugList = entries
|
|
186
209
|
.map(([, tokens]) => tokens.org_slug)
|
|
187
|
-
.filter(
|
|
210
|
+
.filter(Boolean)
|
|
188
211
|
.join(', ');
|
|
189
212
|
const orgList = slugList || entries.map(([orgId]) => orgId).join(', ');
|
|
190
213
|
return {
|
|
191
214
|
error: `You are a member of multiple orgs. Specify --org <org-slug> to indicate which org to create the workspace in. Your orgs: ${orgList}.`,
|
|
192
215
|
};
|
|
193
216
|
}
|
|
194
|
-
/**
|
|
195
|
-
* oRPC surfaces structured errors as {json: {code, message, ...}} or
|
|
196
|
-
* {error: {...}}. Extract the message for display; fall back to raw
|
|
197
|
-
* text when the body isn't JSON.
|
|
198
|
-
*/
|
|
199
|
-
async extractErrorMessage(res) {
|
|
200
|
-
try {
|
|
201
|
-
const body = (await res.clone().json());
|
|
202
|
-
return body.json?.message || body.error?.message || body.message || '';
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
try {
|
|
206
|
-
return await res.text();
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
return '';
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
217
|
}
|
|
@@ -7,6 +7,13 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
7
7
|
static description: string;
|
|
8
8
|
static examples: string[];
|
|
9
9
|
run(): Promise<JsonObj | void>;
|
|
10
|
+
/**
|
|
11
|
+
* Iterate every cached org and merge its workspace list. Each org has its
|
|
12
|
+
* own access token; one token only sees one org's workspaces. We tolerate
|
|
13
|
+
* a single org's failure (verbose-log it) so a stale token in one org
|
|
14
|
+
* doesn't block switching into a working one.
|
|
15
|
+
*/
|
|
16
|
+
private fetchAllOrgsWorkspaces;
|
|
10
17
|
/**
|
|
11
18
|
* Resolve the user's positional arg against the candidate list.
|
|
12
19
|
*
|
|
@@ -16,11 +23,4 @@ export default class WorkspaceSwitch extends BaseCommand {
|
|
|
16
23
|
* migration message used elsewhere in the CLI.
|
|
17
24
|
*/
|
|
18
25
|
private matchByArg;
|
|
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;
|
|
26
26
|
}
|