@hs-x/cli 0.3.0 → 0.3.7
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 +6 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +20 -49
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-auth.d.ts +19 -0
- package/dist/cloudflare-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +83 -2
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +18 -19
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-scoped-resource.d.ts +41 -0
- package/dist/cloudflare-scoped-resource.d.ts.map +1 -0
- package/dist/cloudflare-scoped-resource.js +74 -0
- package/dist/cloudflare-scoped-resource.js.map +1 -0
- package/dist/command-catalog.d.ts +13 -0
- package/dist/command-catalog.d.ts.map +1 -0
- package/dist/command-catalog.js +49 -0
- package/dist/command-catalog.js.map +1 -0
- package/dist/commands/account.d.ts +6 -6
- package/dist/commands/api.js +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +22 -34
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +97 -18
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +8 -8
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +2 -9
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +1 -1
- package/dist/commands/deploy-retry.d.ts +20 -0
- package/dist/commands/deploy-retry.d.ts.map +1 -0
- package/dist/commands/deploy-retry.js +106 -0
- package/dist/commands/deploy-retry.js.map +1 -0
- package/dist/commands/deploy.d.ts +14 -9
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +603 -294
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +43 -5
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/flags.d.ts +10 -5
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +208 -80
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +41 -60
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +34 -5
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +74 -15
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/migrate.d.ts +1 -1
- package/dist/commands/rollback.d.ts +1 -1
- package/dist/commands/secrets.d.ts +2 -2
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/validate.d.ts +2 -2
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/control-plane-tenant-provisioning.d.ts +24 -0
- package/dist/control-plane-tenant-provisioning.d.ts.map +1 -0
- package/dist/control-plane-tenant-provisioning.js +85 -0
- package/dist/control-plane-tenant-provisioning.js.map +1 -0
- package/dist/deploy-targets.d.ts +24 -0
- package/dist/deploy-targets.d.ts.map +1 -0
- package/dist/deploy-targets.js +38 -0
- package/dist/deploy-targets.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +3 -3
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +28 -4
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/invoke.d.ts +3 -4
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +9 -4
- package/dist/dev/invoke.js.map +1 -1
- package/dist/dev/loopback-http-listener.d.ts +18 -0
- package/dist/dev/loopback-http-listener.d.ts.map +1 -0
- package/dist/dev/loopback-http-listener.js +91 -0
- package/dist/dev/loopback-http-listener.js.map +1 -0
- package/dist/dev/session-manager.d.ts.map +1 -1
- package/dist/dev/session-manager.js +3 -2
- package/dist/dev/session-manager.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +33 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +20 -3
- package/dist/help.js.map +1 -1
- package/dist/init/templates.d.ts +1 -1
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +71 -1
- package/dist/init/templates.js.map +1 -1
- package/dist/machine-artifact.d.ts +16 -0
- package/dist/machine-artifact.d.ts.map +1 -0
- package/dist/machine-artifact.js +18 -0
- package/dist/machine-artifact.js.map +1 -0
- package/dist/machine-id.d.ts +15 -0
- package/dist/machine-id.d.ts.map +1 -1
- package/dist/machine-id.js +53 -0
- package/dist/machine-id.js.map +1 -1
- package/dist/mutation-consent.d.ts +20 -0
- package/dist/mutation-consent.d.ts.map +1 -0
- package/dist/mutation-consent.js +9 -0
- package/dist/mutation-consent.js.map +1 -0
- package/dist/output-context.d.ts +3 -0
- package/dist/output-context.d.ts.map +1 -1
- package/dist/output-context.js +6 -0
- package/dist/output-context.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +123 -48
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/index.js +1 -1
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +5 -0
- package/dist/reporter/style.js.map +1 -1
- package/dist/services/live.d.ts +1 -1
- package/dist/services/telemetry.d.ts +1 -2
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/telemetry.js +1 -10
- package/dist/services/telemetry.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +1 -7
- package/dist/telemetry.js.map +1 -1
- package/dist/tenant-data-plane-provisioning.d.ts +35 -0
- package/dist/tenant-data-plane-provisioning.d.ts.map +1 -0
- package/dist/tenant-data-plane-provisioning.js +38 -0
- package/dist/tenant-data-plane-provisioning.js.map +1 -0
- package/package.json +11 -11
- package/dist/cloudflare-worker-versions.d.ts +0 -184
- package/dist/cloudflare-worker-versions.d.ts.map +0 -1
- package/dist/cloudflare-worker-versions.js +0 -360
- package/dist/cloudflare-worker-versions.js.map +0 -1
- package/dist/rollback-traffic.d.ts +0 -85
- package/dist/rollback-traffic.d.ts.map +0 -1
- package/dist/rollback-traffic.js +0 -204
- package/dist/rollback-traffic.js.map +0 -1
package/dist/commands/flags.js
CHANGED
|
@@ -9,6 +9,7 @@ import { activeSession } from '../account-store.js';
|
|
|
9
9
|
import { CliError, exitWith } from '../cli-error.js';
|
|
10
10
|
import { accountIdOption, controlPlaneUrlOption, jsonOption, runHandler, yesOption, } from '../cli/kit.js';
|
|
11
11
|
import { DEFAULT_CONTROL_PLANE_URL } from '../config.js';
|
|
12
|
+
import { requestMutationConsent } from '../mutation-consent.js';
|
|
12
13
|
import { isMachineOutput } from '../output-context.js';
|
|
13
14
|
import { canDiscoverHubSpotAppIdFromTenantBindings, readLocalProjectContext, resolveHubSpotAppId, resolveHubSpotAppIdFromTenantBindings, resolveProjectId, resolveRuntimeOrigin, } from '../project-context.js';
|
|
14
15
|
import { ProjectPickerCancelled, createControlPlaneFlagAuthoringClient, discoverProjectRuntimeBindings, discoverProjects, resolveAccountId, resolveProjectSelection, resolveRuntimeBinding, } from '../project-picker.js';
|
|
@@ -30,21 +31,56 @@ function resolveFlag(argv, flag) {
|
|
|
30
31
|
}
|
|
31
32
|
return undefined;
|
|
32
33
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
function isMachineReporter(reporter) {
|
|
35
|
+
return reporter.mode === 'json' || reporter.mode === 'ndjson';
|
|
36
|
+
}
|
|
37
|
+
function fail(reporter, _json, message, code = 'HSX_E_INPUT_INVALID') {
|
|
38
|
+
if (isMachineReporter(reporter)) {
|
|
39
|
+
reporter.error(code, message);
|
|
40
|
+
reporter.done(undefined, 1);
|
|
36
41
|
}
|
|
37
42
|
else {
|
|
38
43
|
reporter.rawErr(`Error: ${message}\n`);
|
|
39
44
|
}
|
|
40
45
|
return { exitCode: 1 };
|
|
41
46
|
}
|
|
47
|
+
function succeed(reporter, data, human) {
|
|
48
|
+
if (isMachineReporter(reporter)) {
|
|
49
|
+
reporter.data(data);
|
|
50
|
+
reporter.done();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
human();
|
|
54
|
+
}
|
|
55
|
+
function flagTransportErrorCode(error) {
|
|
56
|
+
if (!(error instanceof RuntimeFlagAuthoringError))
|
|
57
|
+
return 'HSX_E_INTERNAL';
|
|
58
|
+
if (error.status === 401 || error.status === 403)
|
|
59
|
+
return 'HSX_E_API_UNAUTHORIZED';
|
|
60
|
+
if (error.status >= 500)
|
|
61
|
+
return 'HSX_E_API_SERVER';
|
|
62
|
+
return 'HSX_E_API_BAD_REQUEST';
|
|
63
|
+
}
|
|
42
64
|
const liveFlagsPrompts = {
|
|
43
65
|
isInteractive,
|
|
44
66
|
select: promptSelect,
|
|
45
67
|
text: promptText,
|
|
46
68
|
confirm: promptConfirm,
|
|
47
69
|
};
|
|
70
|
+
const liveFlagConsentPrompts = {
|
|
71
|
+
isInteractive,
|
|
72
|
+
confirm: promptConfirm,
|
|
73
|
+
};
|
|
74
|
+
const FLAG_DEFINITION_CONSENT_ERROR = 'Creating or updating a flag requires explicit consent in machine or non-interactive mode. Re-run with --yes.';
|
|
75
|
+
function flagStateConsentError(state) {
|
|
76
|
+
return `Changing a flag to ${state} requires explicit consent in machine or non-interactive mode. Re-run with --yes.`;
|
|
77
|
+
}
|
|
78
|
+
function hasExplicitYes(argv) {
|
|
79
|
+
return argv.some((arg) => arg === '--yes' || arg === '-y');
|
|
80
|
+
}
|
|
81
|
+
function flagDefinitionPlan(definition, options) {
|
|
82
|
+
return `\nFlag definition plan\n${JSON.stringify(definition, null, 2)}\n\n${options.wizard ? 'Percentage rollouts are available through --file only.\n\n' : ''}`;
|
|
83
|
+
}
|
|
48
84
|
class FlagsPromptCancelled extends Error {
|
|
49
85
|
constructor() {
|
|
50
86
|
super('Cancelled.');
|
|
@@ -237,10 +273,45 @@ export async function flagsCommand(input) {
|
|
|
237
273
|
const { argv, json } = input;
|
|
238
274
|
const root = input.root ?? process.cwd();
|
|
239
275
|
const subcommand = argv[1];
|
|
240
|
-
const reporter = createReporter({
|
|
276
|
+
const reporter = createReporter({
|
|
277
|
+
command: 'flags',
|
|
278
|
+
argv: json && !argv.includes('--json') ? [...argv, '--json'] : argv,
|
|
279
|
+
});
|
|
241
280
|
if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
|
|
242
281
|
return fail(reporter, json, `Usage: hs-x flags <${[...SUBCOMMANDS].join('|')}> [options]`);
|
|
243
282
|
}
|
|
283
|
+
const machineOutput = json || isMachineOutput();
|
|
284
|
+
const interactive = isInteractive();
|
|
285
|
+
const yes = hasExplicitYes(argv);
|
|
286
|
+
if ((subcommand === 'create' || subcommand === 'define') && !resolveFlag(argv, '--file')) {
|
|
287
|
+
if (machineOutput || !interactive) {
|
|
288
|
+
return fail(reporter, json, 'Require --file <flag-definition.json> for `flags create` outside an interactive terminal.');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if ((subcommand === 'enable' || subcommand === 'disable' || subcommand === 'archive') &&
|
|
292
|
+
!resolveFlag(argv, '--key') &&
|
|
293
|
+
(machineOutput || !interactive)) {
|
|
294
|
+
const state = subcommand === 'enable' ? 'enabled' : subcommand === 'disable' ? 'disabled' : 'archived';
|
|
295
|
+
return fail(reporter, json, `Require --key <flagKey> to ${state} a flag.`);
|
|
296
|
+
}
|
|
297
|
+
if (subcommand !== 'list' && (machineOutput || !interactive)) {
|
|
298
|
+
const consent = await requestMutationConsent({
|
|
299
|
+
yes,
|
|
300
|
+
machineOutput,
|
|
301
|
+
interactive,
|
|
302
|
+
message: 'Apply this flag mutation?',
|
|
303
|
+
confirm: promptConfirm,
|
|
304
|
+
});
|
|
305
|
+
if (consent === 'require-yes') {
|
|
306
|
+
return fail(reporter, json, subcommand === 'create' || subcommand === 'define'
|
|
307
|
+
? FLAG_DEFINITION_CONSENT_ERROR
|
|
308
|
+
: flagStateConsentError(subcommand === 'enable'
|
|
309
|
+
? 'enabled'
|
|
310
|
+
: subcommand === 'disable'
|
|
311
|
+
? 'disabled'
|
|
312
|
+
: 'archived'));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
244
315
|
const local = await readLocalProjectContext(root);
|
|
245
316
|
const projectId = resolveProjectId({
|
|
246
317
|
explicit: resolveFlag(argv, '--project-id'),
|
|
@@ -312,7 +383,7 @@ export async function flagsCommand(input) {
|
|
|
312
383
|
return await runList({ argv, json, client, reporter });
|
|
313
384
|
case 'create':
|
|
314
385
|
case 'define':
|
|
315
|
-
return await runCreate({ argv, json, client, scope, reporter });
|
|
386
|
+
return await runCreate({ argv, json, client, scope, reporter, yes });
|
|
316
387
|
case 'enable':
|
|
317
388
|
return await runSetState({ argv, json, client, state: 'enabled', reporter });
|
|
318
389
|
case 'disable':
|
|
@@ -325,29 +396,31 @@ export async function flagsCommand(input) {
|
|
|
325
396
|
}
|
|
326
397
|
catch (error) {
|
|
327
398
|
if (error instanceof RuntimeFlagAuthoringError) {
|
|
328
|
-
return fail(reporter, json, `Tenant Worker rejected the request (${error.status}): ${error.message}
|
|
399
|
+
return fail(reporter, json, `Tenant Worker rejected the request (${error.status}): ${error.message}`, flagTransportErrorCode(error));
|
|
329
400
|
}
|
|
330
|
-
return fail(reporter, json, error instanceof Error ? error.message : String(error));
|
|
401
|
+
return fail(reporter, json, error instanceof Error ? error.message : String(error), flagTransportErrorCode(error));
|
|
331
402
|
}
|
|
332
403
|
}
|
|
333
404
|
async function runList(input) {
|
|
334
405
|
const limitRaw = resolveFlag(input.argv, '--limit');
|
|
335
406
|
const flags = await input.client.listFlags(limitRaw !== undefined ? { limit: Number(limitRaw) } : undefined);
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
407
|
+
succeed(input.reporter, { flags }, () => {
|
|
408
|
+
if (flags.length === 0) {
|
|
409
|
+
input.reporter.raw('No flags defined.\n');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
343
412
|
for (const flag of flags) {
|
|
344
413
|
const label = flag.label ? ` — ${flag.label}` : '';
|
|
345
414
|
input.reporter.raw(`${flag.key} [${flag.type}, ${flag.state}, v${flag.version}]${label}\n`);
|
|
346
415
|
}
|
|
347
|
-
}
|
|
416
|
+
});
|
|
348
417
|
return { exitCode: 0 };
|
|
349
418
|
}
|
|
350
419
|
export async function runCreate(input) {
|
|
420
|
+
const prompts = input.prompts ?? liveFlagConsentPrompts;
|
|
421
|
+
const yes = input.yes ?? hasExplicitYes(input.argv);
|
|
422
|
+
const machineOutput = input.json || isMachineOutput();
|
|
423
|
+
const interactive = prompts.isInteractive();
|
|
351
424
|
const file = resolveFlag(input.argv, '--file');
|
|
352
425
|
if (!file)
|
|
353
426
|
return fail(input.reporter, input.json, 'Require --file <flag-definition.json> for `flags create`.');
|
|
@@ -377,13 +450,25 @@ export async function runCreate(input) {
|
|
|
377
450
|
if (decoded._tag === 'Left') {
|
|
378
451
|
return fail(input.reporter, input.json, `Invalid flag definition: ${String(decoded.left)}`);
|
|
379
452
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
input.reporter.raw(`${JSON.stringify({ ok: true, flag: result.flag, syncedSnapshots: result.syncedSnapshots }, null, 2)}\n`);
|
|
453
|
+
if (!machineOutput && interactive) {
|
|
454
|
+
input.reporter.raw(flagDefinitionPlan(decoded.right, { wizard: false }));
|
|
383
455
|
}
|
|
384
|
-
|
|
385
|
-
|
|
456
|
+
const consent = await requestMutationConsent({
|
|
457
|
+
yes,
|
|
458
|
+
machineOutput,
|
|
459
|
+
interactive,
|
|
460
|
+
message: `Create or update flag "${decoded.right.key}"?`,
|
|
461
|
+
confirm: prompts.confirm,
|
|
462
|
+
});
|
|
463
|
+
if (consent === 'require-yes') {
|
|
464
|
+
return fail(input.reporter, input.json, FLAG_DEFINITION_CONSENT_ERROR);
|
|
386
465
|
}
|
|
466
|
+
if (consent === 'cancelled')
|
|
467
|
+
return cancelled(input.reporter);
|
|
468
|
+
const result = await input.client.defineFlag(decoded.right);
|
|
469
|
+
succeed(input.reporter, { flag: result.flag, syncedSnapshots: result.syncedSnapshots }, () => {
|
|
470
|
+
input.reporter.raw(`Defined "${result.flag.key}" [${result.flag.type}, ${result.flag.state}, v${result.flag.version}] — synced ${result.syncedSnapshots} snapshot(s).\n`);
|
|
471
|
+
});
|
|
387
472
|
return { exitCode: 0 };
|
|
388
473
|
}
|
|
389
474
|
/** Dependency-injected action seam used by the bare `hs-x flags` TTY flow. */
|
|
@@ -815,22 +900,28 @@ export async function runCreateWizard(input) {
|
|
|
815
900
|
const referenceError = validateWizardReferences(decoded.right);
|
|
816
901
|
if (referenceError)
|
|
817
902
|
return fail(input.reporter, false, referenceError);
|
|
818
|
-
input.reporter.raw(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
903
|
+
input.reporter.raw(flagDefinitionPlan(decoded.right, { wizard: true }));
|
|
904
|
+
let requestedBack = false;
|
|
905
|
+
const consent = await requestMutationConsent({
|
|
906
|
+
yes: input.yes ?? false,
|
|
907
|
+
machineOutput: false,
|
|
908
|
+
interactive: true,
|
|
824
909
|
message: `Create or update flag "${decoded.right.key}"?`,
|
|
825
|
-
|
|
826
|
-
|
|
910
|
+
confirm: async (options) => {
|
|
911
|
+
const answer = await prompts.confirm({ ...options, allowBack: true });
|
|
912
|
+
if (answer === BACK) {
|
|
913
|
+
requestedBack = true;
|
|
914
|
+
return undefined;
|
|
915
|
+
}
|
|
916
|
+
return answer;
|
|
917
|
+
},
|
|
827
918
|
});
|
|
828
|
-
if (
|
|
829
|
-
return cancelled(input.reporter);
|
|
830
|
-
if (confirmed === true) {
|
|
919
|
+
if (consent === 'proceed') {
|
|
831
920
|
definition = decoded.right;
|
|
832
921
|
break;
|
|
833
922
|
}
|
|
923
|
+
if (!requestedBack)
|
|
924
|
+
return cancelled(input.reporter);
|
|
834
925
|
// Esc from the final confirmation returns to the immediately preceding
|
|
835
926
|
// targeting-rule step without discarding the rest of the draft.
|
|
836
927
|
const revisedRules = await buildRules(prompts, variations ?? []);
|
|
@@ -844,7 +935,13 @@ export async function runCreateWizard(input) {
|
|
|
844
935
|
return { exitCode: 0 };
|
|
845
936
|
}
|
|
846
937
|
function cancelled(reporter) {
|
|
847
|
-
reporter
|
|
938
|
+
if (isMachineReporter(reporter)) {
|
|
939
|
+
reporter.error('HSX_E_INPUT_INVALID', 'Cancelled.');
|
|
940
|
+
reporter.done(undefined, 130);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
reporter.rawErr('Cancelled.\n');
|
|
944
|
+
}
|
|
848
945
|
return { exitCode: 130 };
|
|
849
946
|
}
|
|
850
947
|
const liveFlagMutationPrompts = {
|
|
@@ -854,13 +951,21 @@ const liveFlagMutationPrompts = {
|
|
|
854
951
|
};
|
|
855
952
|
export async function runSetState(input) {
|
|
856
953
|
const prompts = input.prompts ?? liveFlagMutationPrompts;
|
|
954
|
+
const yes = input.yes ?? hasExplicitYes(input.argv);
|
|
955
|
+
const machineOutput = input.json || isMachineOutput();
|
|
956
|
+
const interactive = prompts.isInteractive();
|
|
857
957
|
let key = resolveFlag(input.argv, '--key');
|
|
858
|
-
const canPrompt = !
|
|
958
|
+
const canPrompt = !machineOutput && interactive;
|
|
859
959
|
// Deterministic machine/non-interactive validation must not touch the tenant
|
|
860
960
|
// transport merely to discover that a required mutation key is absent.
|
|
861
961
|
if (!key && !canPrompt) {
|
|
862
962
|
return fail(input.reporter, input.json, `Require --key <flagKey> to ${input.state} a flag.`);
|
|
863
963
|
}
|
|
964
|
+
// Machine and non-interactive mutations require consent before even the
|
|
965
|
+
// read-only flag lookup, so a missing --yes can never reach tenant transport.
|
|
966
|
+
if (key && !yes && (machineOutput || !interactive)) {
|
|
967
|
+
return fail(input.reporter, input.json, flagStateConsentError(input.state));
|
|
968
|
+
}
|
|
864
969
|
const flags = await input.client.listFlags();
|
|
865
970
|
const candidates = flags.filter((flag) => flag.state !== 'archived' && flag.state !== input.state);
|
|
866
971
|
if (!key && canPrompt) {
|
|
@@ -892,19 +997,21 @@ export async function runSetState(input) {
|
|
|
892
997
|
if (existing.state === input.state) {
|
|
893
998
|
return fail(input.reporter, input.json, `Flag "${key}" is already ${input.state}.`);
|
|
894
999
|
}
|
|
895
|
-
if (
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1000
|
+
if (!machineOutput && interactive) {
|
|
1001
|
+
input.reporter.raw(`\nFlag state-change plan\nKey: ${key}\nCurrent state: ${existing.state}\nCurrent version: ${existing.version}\nNew state: ${input.state}\nNew version: ${existing.version + 1}\n\n`);
|
|
1002
|
+
}
|
|
1003
|
+
const consent = await requestMutationConsent({
|
|
1004
|
+
yes,
|
|
1005
|
+
machineOutput,
|
|
1006
|
+
interactive,
|
|
1007
|
+
message: `Set flag "${key}" to ${input.state}?`,
|
|
1008
|
+
confirm: prompts.confirm,
|
|
1009
|
+
});
|
|
1010
|
+
if (consent === 'require-yes') {
|
|
1011
|
+
return fail(input.reporter, input.json, flagStateConsentError(input.state));
|
|
1012
|
+
}
|
|
1013
|
+
if (consent === 'cancelled') {
|
|
1014
|
+
return cancelled(input.reporter);
|
|
908
1015
|
}
|
|
909
1016
|
const updated = {
|
|
910
1017
|
...existing,
|
|
@@ -913,12 +1020,9 @@ export async function runSetState(input) {
|
|
|
913
1020
|
updatedAt: new Date().toISOString(),
|
|
914
1021
|
};
|
|
915
1022
|
const result = await input.client.defineFlag(updated);
|
|
916
|
-
|
|
917
|
-
input.reporter.raw(`${JSON.stringify({ ok: true, key, state: input.state, flag: result.flag }, null, 2)}\n`);
|
|
918
|
-
}
|
|
919
|
-
else {
|
|
1023
|
+
succeed(input.reporter, { key, state: input.state, flag: result.flag }, () => {
|
|
920
1024
|
input.reporter.raw(`Flag "${key}" is now ${input.state} (v${result.flag.version}).\n`);
|
|
921
|
-
}
|
|
1025
|
+
});
|
|
922
1026
|
return { exitCode: 0 };
|
|
923
1027
|
}
|
|
924
1028
|
// ─── Native @effect/cli command (Phase 4) ───────────────────────────────────
|
|
@@ -929,15 +1033,9 @@ export async function runSetState(input) {
|
|
|
929
1033
|
// `runList`/`runCreate`/`runSetState` bodies and the `createFlagAuthoringClient`
|
|
930
1034
|
// transport are reused verbatim from the legacy path.
|
|
931
1035
|
//
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
// `{schema_version, command, ok, data}` envelope. Errors in human mode print
|
|
936
|
-
// `Error: <msg>` to stderr. To preserve the exact bytes the subprocess tests
|
|
937
|
-
// assert, the bodies emit through the legacy `fail()` / `reporter.raw` writers
|
|
938
|
-
// and the handler fails with a silent `exitWith(1)` so `runHandler` does not
|
|
939
|
-
// re-render. TODO(phase-5): reconcile these payloads with the standard envelope
|
|
940
|
-
// once the dispatch swap lets the fixtures move in lockstep.
|
|
1036
|
+
// Machine output uses the shared reporter contract: JSON emits one
|
|
1037
|
+
// `{schema_version, command, ok, data|error}` envelope and JSON-stream emits
|
|
1038
|
+
// start/data-or-error/done events. Human output retains the established text.
|
|
941
1039
|
const projectIdOption = Options.text('project-id').pipe(Options.withAlias('project'), Options.optional);
|
|
942
1040
|
const appIdOption = Options.integer('app-id').pipe(Options.optional);
|
|
943
1041
|
const environmentOption = Options.text('environment').pipe(Options.optional);
|
|
@@ -958,12 +1056,13 @@ const identityOptions = {
|
|
|
958
1056
|
runtimeOrigin: runtimeOriginOption,
|
|
959
1057
|
cloudflareApiToken: cloudflareApiTokenOption,
|
|
960
1058
|
};
|
|
961
|
-
/** Effect twin of `fail()` for the native path
|
|
962
|
-
function failEff(
|
|
1059
|
+
/** Effect twin of `fail()` for the native path, rendered once before a silent exit. */
|
|
1060
|
+
function failEff(_json, message, code = 'HSX_E_INPUT_INVALID') {
|
|
963
1061
|
return Effect.gen(function* () {
|
|
964
1062
|
const reporter = yield* Reporter;
|
|
965
|
-
if (json) {
|
|
966
|
-
yield* reporter.
|
|
1063
|
+
if (reporter.mode === 'json' || reporter.mode === 'ndjson') {
|
|
1064
|
+
yield* reporter.error(code, message);
|
|
1065
|
+
yield* reporter.done(undefined, 1);
|
|
967
1066
|
}
|
|
968
1067
|
else {
|
|
969
1068
|
yield* reporter.rawErr(`Error: ${message}\n`);
|
|
@@ -974,16 +1073,20 @@ function failEff(json, message) {
|
|
|
974
1073
|
function cancelEff() {
|
|
975
1074
|
return Effect.gen(function* () {
|
|
976
1075
|
const reporter = yield* Reporter;
|
|
977
|
-
|
|
1076
|
+
if (reporter.mode === 'json' || reporter.mode === 'ndjson') {
|
|
1077
|
+
yield* reporter.error('HSX_E_INPUT_INVALID', 'Cancelled.');
|
|
1078
|
+
yield* reporter.done(undefined, 130);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
yield* reporter.rawErr('Cancelled.\n');
|
|
1082
|
+
}
|
|
978
1083
|
return yield* Effect.fail(exitWith(130));
|
|
979
1084
|
});
|
|
980
1085
|
}
|
|
981
1086
|
/**
|
|
982
1087
|
* Synchronous reporter sink for the shared `runList`/`runCreate`/`runSetState`
|
|
983
|
-
* bodies on the native path.
|
|
984
|
-
*
|
|
985
|
-
* sanctioned I/O edge, instead of `process.std*`. Mode is derived solely from
|
|
986
|
-
* `--json` so the produced bytes match the legacy path exactly.
|
|
1088
|
+
* bodies on the native path. Machine modes use data/error/done; human mode
|
|
1089
|
+
* retains the established raw text through the sanctioned reporter edge.
|
|
987
1090
|
*/
|
|
988
1091
|
const syncReporter = Effect.gen(function* () {
|
|
989
1092
|
const reporter = yield* Reporter;
|
|
@@ -1191,16 +1294,15 @@ function resolveClient(opts, config = {}) {
|
|
|
1191
1294
|
});
|
|
1192
1295
|
}
|
|
1193
1296
|
/**
|
|
1194
|
-
* Map a thrown `RuntimeFlagAuthoringError`/`Error` to the
|
|
1195
|
-
* failure
|
|
1196
|
-
* anything else is rendered like the legacy `try/catch` around the dispatch.
|
|
1297
|
+
* Map a thrown `RuntimeFlagAuthoringError`/`Error` to the standard machine
|
|
1298
|
+
* failure contract while preserving the legacy human message.
|
|
1197
1299
|
*/
|
|
1198
1300
|
function withClientErrors(json, body) {
|
|
1199
1301
|
return body.pipe(Effect.catchAll((error) => error instanceof CliError
|
|
1200
1302
|
? Effect.fail(error)
|
|
1201
1303
|
: error instanceof RuntimeFlagAuthoringError
|
|
1202
|
-
? failEff(json, `Tenant Worker rejected the request (${error.status}): ${error.message}
|
|
1203
|
-
: failEff(json, error instanceof Error ? error.message : String(error))));
|
|
1304
|
+
? failEff(json, `Tenant Worker rejected the request (${error.status}): ${error.message}`, flagTransportErrorCode(error))
|
|
1305
|
+
: failEff(json, error instanceof Error ? error.message : String(error), flagTransportErrorCode(error))));
|
|
1204
1306
|
}
|
|
1205
1307
|
const flagsListCmd = Command.make('list', { ...identityOptions, limit: limitOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
1206
1308
|
const { client } = yield* resolveClient(opts);
|
|
@@ -1225,6 +1327,11 @@ function makeCreateCmd(name) {
|
|
|
1225
1327
|
if (!file && !wizardAllowed) {
|
|
1226
1328
|
return yield* failEff(opts.json, 'Require --file <flag-definition.json> for `flags create` outside an interactive terminal.');
|
|
1227
1329
|
}
|
|
1330
|
+
const machineOutput = opts.json || isMachineOutput();
|
|
1331
|
+
const interactive = isInteractive();
|
|
1332
|
+
if (file && !opts.yes && (machineOutput || !interactive)) {
|
|
1333
|
+
return yield* failEff(opts.json, FLAG_DEFINITION_CONSENT_ERROR);
|
|
1334
|
+
}
|
|
1228
1335
|
// A definition file is authoritative and never opens context/wizard
|
|
1229
1336
|
// pickers. If its scope is ambiguous, report the required identity flag.
|
|
1230
1337
|
const { client, scope } = yield* resolveClient(opts, { allowPrompt: !file });
|
|
@@ -1250,6 +1357,7 @@ function makeCreateCmd(name) {
|
|
|
1250
1357
|
client,
|
|
1251
1358
|
scope,
|
|
1252
1359
|
reporter,
|
|
1360
|
+
yes: opts.yes,
|
|
1253
1361
|
}));
|
|
1254
1362
|
})));
|
|
1255
1363
|
}
|
|
@@ -1258,9 +1366,14 @@ const flagsDefineCmd = makeCreateCmd('define');
|
|
|
1258
1366
|
function makeSetStateCmd(name, state) {
|
|
1259
1367
|
return Command.make(name, { ...identityOptions, key: keyOption, yes: yesOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
1260
1368
|
const key = Option.getOrUndefined(opts.key);
|
|
1261
|
-
if (key === undefined && (isMachineOutput() || !isInteractive())) {
|
|
1369
|
+
if (key === undefined && (opts.json || isMachineOutput() || !isInteractive())) {
|
|
1262
1370
|
return yield* failEff(opts.json, `Require --key <flagKey> to ${state} a flag.`);
|
|
1263
1371
|
}
|
|
1372
|
+
if (key !== undefined &&
|
|
1373
|
+
!opts.yes &&
|
|
1374
|
+
(opts.json || isMachineOutput() || !isInteractive())) {
|
|
1375
|
+
return yield* failEff(opts.json, flagStateConsentError(state));
|
|
1376
|
+
}
|
|
1264
1377
|
const { client } = yield* resolveClient(opts);
|
|
1265
1378
|
const reporter = yield* syncReporter;
|
|
1266
1379
|
yield* withClientErrors(opts.json, Effect.tryPromise({
|
|
@@ -1321,6 +1434,9 @@ export const flagsCmd = bareFlagsCmd.pipe(Command.withSubcommands([
|
|
|
1321
1434
|
*/
|
|
1322
1435
|
function runCreateFromSource(input) {
|
|
1323
1436
|
return Effect.gen(function* () {
|
|
1437
|
+
const prompts = input.prompts ?? liveFlagConsentPrompts;
|
|
1438
|
+
const machineOutput = input.json || isMachineOutput();
|
|
1439
|
+
const interactive = prompts.isInteractive();
|
|
1324
1440
|
let parsed;
|
|
1325
1441
|
try {
|
|
1326
1442
|
parsed = JSON.parse(input.source);
|
|
@@ -1344,16 +1460,28 @@ function runCreateFromSource(input) {
|
|
|
1344
1460
|
if (decoded._tag === 'Left') {
|
|
1345
1461
|
return yield* failEff(input.json, `Invalid flag definition: ${String(decoded.left)}`);
|
|
1346
1462
|
}
|
|
1463
|
+
if (!machineOutput && interactive) {
|
|
1464
|
+
input.reporter.raw(flagDefinitionPlan(decoded.right, { wizard: false }));
|
|
1465
|
+
}
|
|
1466
|
+
const consent = yield* Effect.promise(() => requestMutationConsent({
|
|
1467
|
+
yes: input.yes,
|
|
1468
|
+
machineOutput,
|
|
1469
|
+
interactive,
|
|
1470
|
+
message: `Create or update flag "${decoded.right.key}"?`,
|
|
1471
|
+
confirm: prompts.confirm,
|
|
1472
|
+
}));
|
|
1473
|
+
if (consent === 'require-yes') {
|
|
1474
|
+
return yield* failEff(input.json, FLAG_DEFINITION_CONSENT_ERROR);
|
|
1475
|
+
}
|
|
1476
|
+
if (consent === 'cancelled')
|
|
1477
|
+
return yield* cancelEff();
|
|
1347
1478
|
const result = yield* Effect.tryPromise({
|
|
1348
1479
|
try: () => input.client.defineFlag(decoded.right),
|
|
1349
1480
|
catch: (cause) => cause,
|
|
1350
1481
|
});
|
|
1351
|
-
|
|
1352
|
-
input.reporter.raw(`${JSON.stringify({ ok: true, flag: result.flag, syncedSnapshots: result.syncedSnapshots }, null, 2)}\n`);
|
|
1353
|
-
}
|
|
1354
|
-
else {
|
|
1482
|
+
succeed(input.reporter, { flag: result.flag, syncedSnapshots: result.syncedSnapshots }, () => {
|
|
1355
1483
|
input.reporter.raw(`Defined "${result.flag.key}" [${result.flag.type}, ${result.flag.state}, v${result.flag.version}] — synced ${result.syncedSnapshots} snapshot(s).\n`);
|
|
1356
|
-
}
|
|
1484
|
+
});
|
|
1357
1485
|
});
|
|
1358
1486
|
}
|
|
1359
1487
|
//# sourceMappingURL=flags.js.map
|