@hs-x/cli 0.2.6 → 0.3.0
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/LICENSE +202 -0
- package/README.md +25 -8
- package/dist/audit-interactive.d.ts +29 -0
- package/dist/audit-interactive.d.ts.map +1 -0
- package/dist/audit-interactive.js +156 -0
- package/dist/audit-interactive.js.map +1 -0
- package/dist/bin/hs-x.js.map +1 -1
- package/dist/checkpoint-interactive.d.ts +24 -0
- package/dist/checkpoint-interactive.d.ts.map +1 -0
- package/dist/checkpoint-interactive.js +65 -0
- package/dist/checkpoint-interactive.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +266 -39
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +19 -3
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-auth.d.ts +8 -0
- package/dist/cloudflare-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +12 -2
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth-store.d.ts +9 -1
- package/dist/cloudflare-oauth-store.d.ts.map +1 -1
- package/dist/cloudflare-oauth-store.js +23 -4
- package/dist/cloudflare-oauth-store.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts +1 -0
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +6 -2
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-worker-versions.d.ts +184 -0
- package/dist/cloudflare-worker-versions.d.ts.map +1 -0
- package/dist/cloudflare-worker-versions.js +360 -0
- package/dist/cloudflare-worker-versions.js.map +1 -0
- package/dist/commands/account.d.ts +28 -13
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +219 -69
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/api.d.ts +1 -1
- package/dist/commands/api.d.ts.map +1 -1
- package/dist/commands/api.js +6 -10
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +13 -7
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +239 -125
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +72 -11
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +1035 -58
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +25 -3
- package/dist/commands/deploy-promote.d.ts.map +1 -1
- package/dist/commands/deploy-promote.js +314 -38
- package/dist/commands/deploy-promote.js.map +1 -1
- package/dist/commands/deploy.d.ts +67 -4
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1333 -255
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +502 -107
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +131 -20
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +108 -6
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +996 -75
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts +14 -0
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +144 -44
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/history.js +14 -10
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +69 -12
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +2 -3
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +201 -101
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -5
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrate.d.ts +23 -3
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +500 -73
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/rollback.d.ts +42 -2
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +422 -58
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/secrets.d.ts +76 -3
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +328 -41
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/status.d.ts +13 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +154 -17
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +2 -2
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +2 -2
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +4 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts +12 -3
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +70 -8
- package/dist/commands/validate.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/control-plane-fetch.d.ts.map +1 -1
- package/dist/control-plane-fetch.js +5 -4
- package/dist/control-plane-fetch.js.map +1 -1
- package/dist/deploy-revision-capture.d.ts +48 -0
- package/dist/deploy-revision-capture.d.ts.map +1 -0
- package/dist/deploy-revision-capture.js +125 -0
- package/dist/deploy-revision-capture.js.map +1 -0
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +10 -35
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/event-bus.d.ts +5 -2
- package/dist/dev/event-bus.d.ts.map +1 -1
- package/dist/dev/event-bus.js +4 -1
- package/dist/dev/event-bus.js.map +1 -1
- package/dist/dev/hubspot-logs-poller.d.ts +35 -0
- package/dist/dev/hubspot-logs-poller.d.ts.map +1 -0
- package/dist/dev/hubspot-logs-poller.js +94 -0
- package/dist/dev/hubspot-logs-poller.js.map +1 -0
- package/dist/dev/invoke.d.ts +9 -1
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +69 -20
- package/dist/dev/invoke.js.map +1 -1
- package/dist/dev/keyboard.d.ts +31 -0
- package/dist/dev/keyboard.d.ts.map +1 -0
- package/dist/dev/keyboard.js +143 -0
- package/dist/dev/keyboard.js.map +1 -0
- package/dist/dev/log-server.d.ts +7 -0
- package/dist/dev/log-server.d.ts.map +1 -1
- package/dist/dev/log-server.js +68 -31
- package/dist/dev/log-server.js.map +1 -1
- package/dist/dev/migrated-card.d.ts +17 -0
- package/dist/dev/migrated-card.d.ts.map +1 -0
- package/dist/dev/migrated-card.js +114 -0
- package/dist/dev/migrated-card.js.map +1 -0
- package/dist/dev/stream-filter.d.ts +40 -0
- package/dist/dev/stream-filter.d.ts.map +1 -0
- package/dist/dev/stream-filter.js +93 -0
- package/dist/dev/stream-filter.js.map +1 -0
- package/dist/dev/stream-renderer.d.ts +34 -0
- package/dist/dev/stream-renderer.d.ts.map +1 -1
- package/dist/dev/stream-renderer.js +222 -8
- package/dist/dev/stream-renderer.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +280 -36
- package/dist/errors-registry.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +8 -0
- package/dist/help.js.map +1 -1
- package/dist/hubspot-app-logs.d.ts +76 -0
- package/dist/hubspot-app-logs.d.ts.map +1 -0
- package/dist/hubspot-app-logs.js +323 -0
- package/dist/hubspot-app-logs.js.map +1 -0
- package/dist/hubspot-auth.d.ts +8 -2
- package/dist/hubspot-auth.d.ts.map +1 -1
- package/dist/hubspot-auth.js +36 -12
- package/dist/hubspot-auth.js.map +1 -1
- package/dist/hubspot-developer-client.js +119 -11
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init/templates.d.ts +10 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +157 -14
- package/dist/init/templates.js.map +1 -1
- package/dist/link-migration-journal.d.ts +131 -0
- package/dist/link-migration-journal.d.ts.map +1 -0
- package/dist/link-migration-journal.js +395 -0
- package/dist/link-migration-journal.js.map +1 -0
- package/dist/link-preflight.d.ts +78 -0
- package/dist/link-preflight.d.ts.map +1 -0
- package/dist/link-preflight.js +231 -0
- package/dist/link-preflight.js.map +1 -0
- package/dist/local-project-picker.d.ts +34 -0
- package/dist/local-project-picker.d.ts.map +1 -0
- package/dist/local-project-picker.js +121 -0
- package/dist/local-project-picker.js.map +1 -0
- package/dist/logs-interactive.d.ts +37 -0
- package/dist/logs-interactive.d.ts.map +1 -0
- package/dist/logs-interactive.js +110 -0
- package/dist/logs-interactive.js.map +1 -0
- package/dist/output-context.d.ts +13 -0
- package/dist/output-context.d.ts.map +1 -0
- package/dist/output-context.js +46 -0
- package/dist/output-context.js.map +1 -0
- package/dist/project-context.d.ts +55 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +115 -0
- package/dist/project-context.js.map +1 -0
- package/dist/project-picker.d.ts +65 -0
- package/dist/project-picker.d.ts.map +1 -0
- package/dist/project-picker.js +227 -0
- package/dist/project-picker.js.map +1 -0
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +81 -13
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/human.d.ts +10 -2
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +82 -14
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +7 -5
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/json.d.ts +2 -1
- package/dist/reporter/json.d.ts.map +1 -1
- package/dist/reporter/json.js +10 -7
- package/dist/reporter/json.js.map +1 -1
- package/dist/reporter/style.d.ts +19 -0
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +126 -6
- package/dist/reporter/style.js.map +1 -1
- package/dist/reporter/types.d.ts +34 -4
- package/dist/reporter/types.d.ts.map +1 -1
- package/dist/reporter/types.js.map +1 -1
- package/dist/rollback-traffic.d.ts +85 -0
- package/dist/rollback-traffic.d.ts.map +1 -0
- package/dist/rollback-traffic.js +204 -0
- package/dist/rollback-traffic.js.map +1 -0
- package/dist/services/account-store.d.ts +3 -1
- package/dist/services/account-store.d.ts.map +1 -1
- package/dist/services/account-store.js +2 -1
- package/dist/services/account-store.js.map +1 -1
- package/dist/services/app-paths.d.ts.map +1 -1
- package/dist/services/app-paths.js +3 -1
- package/dist/services/app-paths.js.map +1 -1
- package/dist/services/cloudflare-auth.d.ts +1 -1
- package/dist/services/cloudflare-auth.d.ts.map +1 -1
- package/dist/services/cloudflare-auth.js +2 -2
- package/dist/services/cloudflare-auth.js.map +1 -1
- package/dist/services/cloudflare-kv.d.ts.map +1 -1
- package/dist/services/cloudflare-kv.js.map +1 -1
- package/dist/services/command-history.d.ts.map +1 -1
- package/dist/services/command-history.js.map +1 -1
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +1 -1
- package/dist/services/config.js.map +1 -1
- package/dist/services/control-plane.d.ts.map +1 -1
- package/dist/services/control-plane.js +3 -3
- package/dist/services/control-plane.js.map +1 -1
- package/dist/services/env-loader.d.ts.map +1 -1
- package/dist/services/env-loader.js +1 -3
- package/dist/services/env-loader.js.map +1 -1
- package/dist/services/http.d.ts.map +1 -1
- package/dist/services/http.js.map +1 -1
- package/dist/services/hubspot-dev-client.d.ts.map +1 -1
- package/dist/services/hubspot-dev-client.js +30 -0
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/dist/services/live.d.ts +2 -2
- package/dist/services/live.d.ts.map +1 -1
- package/dist/services/live.js +1 -1
- package/dist/services/live.js.map +1 -1
- package/dist/services/machine-id.d.ts.map +1 -1
- package/dist/services/machine-id.js.map +1 -1
- package/dist/services/reporter.d.ts +8 -5
- package/dist/services/reporter.d.ts.map +1 -1
- package/dist/services/reporter.js +6 -4
- package/dist/services/reporter.js.map +1 -1
- package/dist/services/tenant-state.d.ts.map +1 -1
- package/dist/services/tenant-state.js.map +1 -1
- package/dist/state-store.d.ts +4 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +15 -0
- package/dist/state-store.js.map +1 -1
- package/dist/status-drilldown.d.ts +24 -0
- package/dist/status-drilldown.d.ts.map +1 -0
- package/dist/status-drilldown.js +50 -0
- package/dist/status-drilldown.js.map +1 -0
- package/dist/tenant-state.d.ts.map +1 -1
- package/dist/tenant-state.js +3 -2
- package/dist/tenant-state.js.map +1 -1
- package/dist/token-key-custody.d.ts +91 -0
- package/dist/token-key-custody.d.ts.map +1 -0
- package/dist/token-key-custody.js +375 -0
- package/dist/token-key-custody.js.map +1 -0
- package/package.json +13 -14
package/dist/commands/flags.js
CHANGED
|
@@ -2,15 +2,23 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import * as Command from '@effect/cli/Command';
|
|
3
3
|
import * as Options from '@effect/cli/Options';
|
|
4
4
|
import { FileSystem } from '@effect/platform';
|
|
5
|
-
import { Schema, schemas } from '@hs-x/types';
|
|
6
5
|
import { RuntimeFlagAuthoringError, createFlagAuthoringClient, } from '@hs-x/runtime';
|
|
6
|
+
import { Schema, schemas } from '@hs-x/types';
|
|
7
7
|
import { Config, Effect, Option } from 'effect';
|
|
8
|
+
import { activeSession } from '../account-store.js';
|
|
8
9
|
import { CliError, exitWith } from '../cli-error.js';
|
|
9
|
-
import { jsonOption, runHandler } from '../cli/kit.js';
|
|
10
|
+
import { accountIdOption, controlPlaneUrlOption, jsonOption, runHandler, yesOption, } from '../cli/kit.js';
|
|
11
|
+
import { DEFAULT_CONTROL_PLANE_URL } from '../config.js';
|
|
12
|
+
import { isMachineOutput } from '../output-context.js';
|
|
13
|
+
import { canDiscoverHubSpotAppIdFromTenantBindings, readLocalProjectContext, resolveHubSpotAppId, resolveHubSpotAppIdFromTenantBindings, resolveProjectId, resolveRuntimeOrigin, } from '../project-context.js';
|
|
14
|
+
import { ProjectPickerCancelled, createControlPlaneFlagAuthoringClient, discoverProjectRuntimeBindings, discoverProjects, resolveAccountId, resolveProjectSelection, resolveRuntimeBinding, } from '../project-picker.js';
|
|
15
|
+
import { BACK, isInteractive, promptConfirm, promptSelect, promptText, } from '../prompt.js';
|
|
10
16
|
import { createReporter } from '../reporter/index.js';
|
|
11
|
-
import {
|
|
17
|
+
import { AccountStore } from '../services/account-store.js';
|
|
18
|
+
import { ControlPlaneClient } from '../services/control-plane.js';
|
|
12
19
|
import { Cwd } from '../services/cwd.js';
|
|
13
20
|
import { Reporter } from '../services/reporter.js';
|
|
21
|
+
import { loadTenantStateStore } from '../tenant-state.js';
|
|
14
22
|
const SUBCOMMANDS = new Set(['list', 'create', 'define', 'enable', 'disable', 'archive']);
|
|
15
23
|
function resolveFlag(argv, flag) {
|
|
16
24
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -31,12 +39,191 @@ function fail(reporter, json, message) {
|
|
|
31
39
|
}
|
|
32
40
|
return { exitCode: 1 };
|
|
33
41
|
}
|
|
42
|
+
const liveFlagsPrompts = {
|
|
43
|
+
isInteractive,
|
|
44
|
+
select: promptSelect,
|
|
45
|
+
text: promptText,
|
|
46
|
+
confirm: promptConfirm,
|
|
47
|
+
};
|
|
48
|
+
class FlagsPromptCancelled extends Error {
|
|
49
|
+
constructor() {
|
|
50
|
+
super('Cancelled.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const DEPLOY_REQUIRED = 'No deployed flag context was found. Run `hs-x deploy` in this checkout first.';
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the deployed flag scope without ever asking for opaque identifiers.
|
|
56
|
+
* Explicit options win over environment, then checkout bindings, then the
|
|
57
|
+
* discoverable tenant state. Ambiguity is selectable only in an interactive
|
|
58
|
+
* invocation; automation receives a deterministic pass-the-option error.
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveFlagsContext(input) {
|
|
61
|
+
const prompts = input.prompts ?? liveFlagsPrompts;
|
|
62
|
+
const choose = async (opts) => {
|
|
63
|
+
const selected = await prompts.select(opts);
|
|
64
|
+
if (selected === undefined)
|
|
65
|
+
throw new FlagsPromptCancelled();
|
|
66
|
+
return selected;
|
|
67
|
+
};
|
|
68
|
+
let projectId = resolveProjectId({
|
|
69
|
+
explicit: input.explicitProjectId,
|
|
70
|
+
env: input.envProjectId,
|
|
71
|
+
local: input.local,
|
|
72
|
+
})?.value;
|
|
73
|
+
if (!projectId) {
|
|
74
|
+
const projects = await input.stateStore.listProjects();
|
|
75
|
+
if (projects.length === 0)
|
|
76
|
+
throw new Error(DEPLOY_REQUIRED);
|
|
77
|
+
if (projects.length === 1) {
|
|
78
|
+
projectId = projects[0];
|
|
79
|
+
}
|
|
80
|
+
else if (input.allowPrompt) {
|
|
81
|
+
const rows = await Promise.all(projects.map(async (id) => {
|
|
82
|
+
const deploys = await input.stateStore.listDeploys({ projectId: id });
|
|
83
|
+
const environments = [...new Set(deploys.map((deploy) => deploy.environment))];
|
|
84
|
+
return {
|
|
85
|
+
value: id,
|
|
86
|
+
label: id,
|
|
87
|
+
description: `${deploys.length} deploy${deploys.length === 1 ? '' : 's'}${environments.length > 0 ? ` · ${environments.join(', ')}` : ''}`,
|
|
88
|
+
};
|
|
89
|
+
}));
|
|
90
|
+
projectId = await choose({ message: 'Which deployed project?', options: rows });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw new Error(`Multiple deployed projects are available (${projects.join(', ')}). Pass --project-id to choose one.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const deploys = await input.stateStore.listDeploys({ projectId });
|
|
97
|
+
let environment = input.explicitEnvironment ?? input.envEnvironment;
|
|
98
|
+
if (!environment) {
|
|
99
|
+
const environments = [...new Set(deploys.map((deploy) => deploy.environment))].sort();
|
|
100
|
+
if (environments.length === 0)
|
|
101
|
+
throw new Error(DEPLOY_REQUIRED);
|
|
102
|
+
if (environments.length === 1) {
|
|
103
|
+
environment = environments[0];
|
|
104
|
+
}
|
|
105
|
+
else if (input.allowPrompt) {
|
|
106
|
+
environment = await choose({
|
|
107
|
+
message: 'Which environment?',
|
|
108
|
+
options: environments.map((value) => {
|
|
109
|
+
const matches = deploys.filter((deploy) => deploy.environment === value);
|
|
110
|
+
const latest = matches[0];
|
|
111
|
+
return {
|
|
112
|
+
value,
|
|
113
|
+
label: value,
|
|
114
|
+
description: `${matches.length} deploy${matches.length === 1 ? '' : 's'}${latest ? ` · latest ${latest.createdAt}` : ''}`,
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
throw new Error(`Multiple environments are deployed for ${projectId} (${environments.join(', ')}). Pass --environment to choose one.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const appInput = {
|
|
124
|
+
explicit: input.explicitAppId,
|
|
125
|
+
env: input.envAppId,
|
|
126
|
+
compatibilityEnv: input.compatibilityEnvAppId,
|
|
127
|
+
local: input.local,
|
|
128
|
+
};
|
|
129
|
+
let hubSpotAppId = resolveHubSpotAppId(appInput)?.value;
|
|
130
|
+
let bindings;
|
|
131
|
+
if (hubSpotAppId === undefined) {
|
|
132
|
+
if (input.explicitAppId !== undefined ||
|
|
133
|
+
input.envAppId ||
|
|
134
|
+
input.compatibilityEnvAppId ||
|
|
135
|
+
!canDiscoverHubSpotAppIdFromTenantBindings(appInput)) {
|
|
136
|
+
throw new Error('Require a valid integer --app-id (or HSX_APP_ID).');
|
|
137
|
+
}
|
|
138
|
+
bindings = await input.stateStore.listBindings({ projectId, environment });
|
|
139
|
+
const appIds = [
|
|
140
|
+
...new Set(bindings
|
|
141
|
+
.map((binding) => Number(binding.hubSpotAppId))
|
|
142
|
+
.filter((appId) => Number.isInteger(appId) && appId > 0)),
|
|
143
|
+
];
|
|
144
|
+
if (appIds.length === 0) {
|
|
145
|
+
throw new Error(`No HubSpot app binding is deployed for ${projectId}/${environment}. Run \`hs-x deploy\` to provision flag authoring.`);
|
|
146
|
+
}
|
|
147
|
+
if (appIds.length === 1) {
|
|
148
|
+
hubSpotAppId = appIds[0];
|
|
149
|
+
}
|
|
150
|
+
else if (input.allowPrompt) {
|
|
151
|
+
const selected = await choose({
|
|
152
|
+
message: 'Which HubSpot app?',
|
|
153
|
+
options: appIds.map((appId) => {
|
|
154
|
+
const binding = bindings?.find((candidate) => Number(candidate.hubSpotAppId) === appId);
|
|
155
|
+
const ready = Boolean(binding?.syncGrantSecretValue && binding.tenantScopeAccountId);
|
|
156
|
+
return {
|
|
157
|
+
value: String(appId),
|
|
158
|
+
label: `HubSpot app ${appId}`,
|
|
159
|
+
description: `${projectId}/${environment} · ${ready ? 'flag authoring ready' : 'needs redeploy'}`,
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
});
|
|
163
|
+
hubSpotAppId = Number(selected);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
throw new Error(`Multiple tenant bindings match ${projectId}/${environment} (apps ${appIds.join(', ')}). Pass --app-id to choose one.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const binding = bindings?.find((candidate) => Number(candidate.hubSpotAppId) === hubSpotAppId) ??
|
|
170
|
+
(await input.stateStore.getBinding({
|
|
171
|
+
projectId,
|
|
172
|
+
environment,
|
|
173
|
+
hubSpotAppId: hubSpotAppId,
|
|
174
|
+
}));
|
|
175
|
+
if (!binding?.syncGrantSecretValue || !binding.tenantScopeAccountId) {
|
|
176
|
+
throw new Error(`No flag-authoring credentials for ${projectId}/${environment} (app ${hubSpotAppId}). Re-deploy as a linked project to provision the tenant data plane.`);
|
|
177
|
+
}
|
|
178
|
+
let runtimeBaseUrl = input.explicitRuntimeOrigin ?? input.envRuntimeOrigin;
|
|
179
|
+
if (!runtimeBaseUrl) {
|
|
180
|
+
const active = await input.stateStore.getActive({ projectId, environment });
|
|
181
|
+
const activeDeploy = active
|
|
182
|
+
? await input.stateStore.getDeploy({ projectId, deployId: active.deployId })
|
|
183
|
+
: undefined;
|
|
184
|
+
const candidates = [
|
|
185
|
+
...new Map((activeDeploy
|
|
186
|
+
? [activeDeploy]
|
|
187
|
+
: deploys.filter((deploy) => deploy.environment === environment))
|
|
188
|
+
.filter((deploy) => deploy.status === 'deployed')
|
|
189
|
+
.flatMap((deploy) => deploy.workerUrls
|
|
190
|
+
.filter((url) => url.trim().length > 0)
|
|
191
|
+
.map((url) => [url, { url, deploy }]))).values(),
|
|
192
|
+
];
|
|
193
|
+
if (candidates.length === 0)
|
|
194
|
+
throw new Error(DEPLOY_REQUIRED);
|
|
195
|
+
if (candidates.length === 1) {
|
|
196
|
+
runtimeBaseUrl = candidates[0]?.url;
|
|
197
|
+
}
|
|
198
|
+
else if (input.allowPrompt) {
|
|
199
|
+
runtimeBaseUrl = await choose({
|
|
200
|
+
message: 'Which deployed runtime?',
|
|
201
|
+
options: candidates.map(({ url, deploy }) => ({
|
|
202
|
+
value: url,
|
|
203
|
+
label: url,
|
|
204
|
+
description: `${deploy.deployId} · ${deploy.createdAt}`,
|
|
205
|
+
})),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
throw new Error(`Multiple deployed runtimes match ${projectId}/${environment}. Pass --runtime-origin to choose one.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!runtimeBaseUrl)
|
|
213
|
+
throw new Error(DEPLOY_REQUIRED);
|
|
214
|
+
return {
|
|
215
|
+
projectId: projectId,
|
|
216
|
+
environment: environment,
|
|
217
|
+
hubSpotAppId: hubSpotAppId,
|
|
218
|
+
runtimeBaseUrl,
|
|
219
|
+
binding: binding,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
34
222
|
/**
|
|
35
|
-
* ADR-015 §5 `hs-x flags`
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* The control plane is never on this path.
|
|
223
|
+
* ADR-015 §5 `hs-x flags` flag authoring CLI. The native command discovers
|
|
224
|
+
* account-scoped projects and redacted runtime bindings through the control
|
|
225
|
+
* plane, then uses its flag conduit. This legacy entry point retains the
|
|
226
|
+
* direct-to-tenant local-state transport for compatibility.
|
|
40
227
|
*
|
|
41
228
|
* hs-x flags list [--limit N]
|
|
42
229
|
* hs-x flags create --file flag.json (identity is stamped from the scope)
|
|
@@ -54,23 +241,52 @@ export async function flagsCommand(input) {
|
|
|
54
241
|
if (!subcommand || !SUBCOMMANDS.has(subcommand)) {
|
|
55
242
|
return fail(reporter, json, `Usage: hs-x flags <${[...SUBCOMMANDS].join('|')}> [options]`);
|
|
56
243
|
}
|
|
57
|
-
const
|
|
244
|
+
const local = await readLocalProjectContext(root);
|
|
245
|
+
const projectId = resolveProjectId({
|
|
246
|
+
explicit: resolveFlag(argv, '--project-id'),
|
|
247
|
+
env: process.env.HSX_PROJECT_ID,
|
|
248
|
+
local,
|
|
249
|
+
})?.value;
|
|
58
250
|
const environment = resolveFlag(argv, '--environment') ?? resolveFlag(argv, '--env') ?? 'production';
|
|
59
|
-
const
|
|
60
|
-
const
|
|
251
|
+
const explicitAppId = resolveFlag(argv, '--app-id');
|
|
252
|
+
const appIdInput = {
|
|
253
|
+
...(explicitAppId === undefined ? {} : { explicit: Number(explicitAppId) }),
|
|
254
|
+
env: process.env.HSX_APP_ID,
|
|
255
|
+
compatibilityEnv: process.env.HSX_HUBSPOT_APP_ID,
|
|
256
|
+
local,
|
|
257
|
+
};
|
|
258
|
+
let hubSpotAppId = resolveHubSpotAppId(appIdInput)?.value;
|
|
61
259
|
if (!projectId)
|
|
62
260
|
return fail(reporter, json, 'Require --project-id (or HSX_PROJECT_ID).');
|
|
63
|
-
if (hubSpotAppId === undefined || !Number.isInteger(hubSpotAppId)) {
|
|
64
|
-
return fail(reporter, json, 'Require an integer --app-id (or HSX_APP_ID).');
|
|
65
|
-
}
|
|
66
|
-
const runtimeBaseUrl = resolveFlag(argv, '--runtime-origin') ?? process.env.HSX_RUNTIME_BASE_URL;
|
|
67
|
-
if (!runtimeBaseUrl) {
|
|
68
|
-
return fail(reporter, json, 'Require --runtime-origin (or HSX_RUNTIME_BASE_URL) — the deployed Worker URL.');
|
|
69
|
-
}
|
|
70
261
|
const context = await loadTenantStateStore({ argv, root });
|
|
71
262
|
if (!context) {
|
|
72
263
|
return fail(reporter, json, 'No .hs-x/cloudflare.json found. Deploy this project first with `hs-x deploy`.');
|
|
73
264
|
}
|
|
265
|
+
if (hubSpotAppId === undefined && canDiscoverHubSpotAppIdFromTenantBindings(appIdInput)) {
|
|
266
|
+
try {
|
|
267
|
+
hubSpotAppId = (await resolveHubSpotAppIdFromTenantBindings({
|
|
268
|
+
stateStore: context.stateStore,
|
|
269
|
+
projectId,
|
|
270
|
+
environment,
|
|
271
|
+
}))?.value;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
return fail(reporter, json, error instanceof Error ? error.message : String(error));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (hubSpotAppId === undefined) {
|
|
278
|
+
return fail(reporter, json, 'Require an integer --app-id (or HSX_APP_ID).');
|
|
279
|
+
}
|
|
280
|
+
const runtimeBaseUrl = (await resolveRuntimeOrigin({
|
|
281
|
+
explicit: resolveFlag(argv, '--runtime-origin'),
|
|
282
|
+
env: process.env.HSX_RUNTIME_BASE_URL,
|
|
283
|
+
stateStore: context.stateStore,
|
|
284
|
+
projectId,
|
|
285
|
+
environment,
|
|
286
|
+
}))?.value;
|
|
287
|
+
if (!runtimeBaseUrl) {
|
|
288
|
+
return fail(reporter, json, 'Require --runtime-origin (or HSX_RUNTIME_BASE_URL) — no active deploy Worker URL was found.');
|
|
289
|
+
}
|
|
74
290
|
const binding = await context.stateStore.getBinding({
|
|
75
291
|
projectId,
|
|
76
292
|
environment,
|
|
@@ -131,7 +347,7 @@ async function runList(input) {
|
|
|
131
347
|
}
|
|
132
348
|
return { exitCode: 0 };
|
|
133
349
|
}
|
|
134
|
-
async function runCreate(input) {
|
|
350
|
+
export async function runCreate(input) {
|
|
135
351
|
const file = resolveFlag(input.argv, '--file');
|
|
136
352
|
if (!file)
|
|
137
353
|
return fail(input.reporter, input.json, 'Require --file <flag-definition.json> for `flags create`.');
|
|
@@ -170,13 +386,526 @@ async function runCreate(input) {
|
|
|
170
386
|
}
|
|
171
387
|
return { exitCode: 0 };
|
|
172
388
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
389
|
+
/** Dependency-injected action seam used by the bare `hs-x flags` TTY flow. */
|
|
390
|
+
export async function selectFlagsAction(prompts = liveFlagsPrompts) {
|
|
391
|
+
const action = await prompts.select({
|
|
392
|
+
message: 'What would you like to do?',
|
|
393
|
+
options: [
|
|
394
|
+
{ value: 'list', label: 'List flags', description: 'Show flags in this deployed runtime' },
|
|
395
|
+
{
|
|
396
|
+
value: 'create',
|
|
397
|
+
label: 'Create or update a flag',
|
|
398
|
+
description: 'Guided typed flag definition with exact-match targeting',
|
|
399
|
+
},
|
|
400
|
+
{ value: 'enable', label: 'Enable a flag', description: 'Choose a legal transition' },
|
|
401
|
+
{ value: 'disable', label: 'Disable a flag', description: 'Choose a legal transition' },
|
|
402
|
+
{ value: 'archive', label: 'Archive a flag', description: 'Permanent lifecycle transition' },
|
|
403
|
+
{ value: 'cancel', label: 'Cancel' },
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
return action === undefined || action === 'cancel' ? undefined : action;
|
|
407
|
+
}
|
|
408
|
+
const MAX_GUIDED_VARIATIONS = 20;
|
|
409
|
+
const MAX_GUIDED_RULES = 10;
|
|
410
|
+
async function wizardText(prompts, opts) {
|
|
411
|
+
for (;;) {
|
|
412
|
+
const answer = await prompts.text(opts);
|
|
413
|
+
if (answer === undefined || answer === BACK)
|
|
414
|
+
return answer;
|
|
415
|
+
const value = answer.trim();
|
|
416
|
+
if (!opts.validate || opts.validate(value) === undefined)
|
|
417
|
+
return value;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function variationValueText(type) {
|
|
421
|
+
switch (type) {
|
|
422
|
+
case 'string':
|
|
423
|
+
return 'String value';
|
|
424
|
+
case 'number':
|
|
425
|
+
return 'Number value';
|
|
426
|
+
case 'json':
|
|
427
|
+
return 'JSON value';
|
|
428
|
+
case 'boolean':
|
|
429
|
+
return 'Boolean value';
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function parseVariationValue(type, source) {
|
|
433
|
+
switch (type) {
|
|
434
|
+
case 'string':
|
|
435
|
+
return { type, value: source };
|
|
436
|
+
case 'number': {
|
|
437
|
+
const value = Number(source);
|
|
438
|
+
return source.length > 0 && Number.isFinite(value) ? { type, value } : undefined;
|
|
439
|
+
}
|
|
440
|
+
case 'json':
|
|
441
|
+
try {
|
|
442
|
+
return { type, value: JSON.parse(source) };
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function buildVariations(prompts, type) {
|
|
450
|
+
if (type === 'boolean') {
|
|
451
|
+
return [
|
|
452
|
+
{ name: 'off', label: 'Off', value: { type: 'boolean', value: false } },
|
|
453
|
+
{ name: 'on', label: 'On', value: { type: 'boolean', value: true } },
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
const variations = [];
|
|
457
|
+
while (variations.length < MAX_GUIDED_VARIATIONS) {
|
|
458
|
+
const name = await wizardText(prompts, {
|
|
459
|
+
message: `Variation ${variations.length + 1} key`,
|
|
460
|
+
allowBack: true,
|
|
461
|
+
validate: (value) => {
|
|
462
|
+
if (value.length === 0)
|
|
463
|
+
return 'Enter a variation key.';
|
|
464
|
+
if (variations.some((variation) => variation.name === value)) {
|
|
465
|
+
return 'Variation keys must be unique.';
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
if (name === undefined)
|
|
471
|
+
return undefined;
|
|
472
|
+
if (name === BACK) {
|
|
473
|
+
if (variations.length === 0)
|
|
474
|
+
return BACK;
|
|
475
|
+
variations.pop();
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const label = await wizardText(prompts, {
|
|
479
|
+
message: `Label for variation "${name}"`,
|
|
480
|
+
default: name,
|
|
481
|
+
allowBack: true,
|
|
482
|
+
validate: (value) => (value.length === 0 ? 'Enter a variation label.' : undefined),
|
|
483
|
+
});
|
|
484
|
+
if (label === undefined)
|
|
485
|
+
return undefined;
|
|
486
|
+
if (label === BACK)
|
|
487
|
+
continue;
|
|
488
|
+
let value;
|
|
489
|
+
for (;;) {
|
|
490
|
+
const source = await wizardText(prompts, {
|
|
491
|
+
message: variationValueText(type),
|
|
492
|
+
allowBack: true,
|
|
493
|
+
validate: (raw) => parseVariationValue(type, raw) === undefined
|
|
494
|
+
? type === 'number'
|
|
495
|
+
? 'Enter a finite number.'
|
|
496
|
+
: 'Enter valid JSON.'
|
|
497
|
+
: undefined,
|
|
498
|
+
});
|
|
499
|
+
if (source === undefined)
|
|
500
|
+
return undefined;
|
|
501
|
+
if (source === BACK)
|
|
502
|
+
break;
|
|
503
|
+
value = parseVariationValue(type, source);
|
|
504
|
+
if (value !== undefined)
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (value === undefined)
|
|
508
|
+
continue;
|
|
509
|
+
variations.push({ name, label, value });
|
|
510
|
+
if (variations.length === MAX_GUIDED_VARIATIONS)
|
|
511
|
+
break;
|
|
512
|
+
const another = await prompts.confirm({
|
|
513
|
+
message: 'Add another variation?',
|
|
514
|
+
default: variations.length < 2,
|
|
515
|
+
allowBack: true,
|
|
516
|
+
});
|
|
517
|
+
if (another === undefined)
|
|
518
|
+
return undefined;
|
|
519
|
+
if (another === BACK) {
|
|
520
|
+
variations.pop();
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (!another)
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
return variations;
|
|
527
|
+
}
|
|
528
|
+
async function buildRules(prompts, variations) {
|
|
529
|
+
const addFirst = await prompts.confirm({
|
|
530
|
+
message: 'Add a targeting rule?',
|
|
531
|
+
default: false,
|
|
532
|
+
allowBack: true,
|
|
533
|
+
});
|
|
534
|
+
if (addFirst === undefined || addFirst === BACK)
|
|
535
|
+
return addFirst;
|
|
536
|
+
if (!addFirst)
|
|
537
|
+
return [];
|
|
538
|
+
const rules = [];
|
|
539
|
+
while (rules.length < MAX_GUIDED_RULES) {
|
|
540
|
+
const level = await prompts.select({
|
|
541
|
+
message: `Rule ${rules.length + 1} property`,
|
|
542
|
+
options: [
|
|
543
|
+
{
|
|
544
|
+
value: 'installed_portal',
|
|
545
|
+
label: 'Installed portal ID',
|
|
546
|
+
description: 'Match portalId or installId',
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
value: 'company',
|
|
550
|
+
label: 'Company portal/install ID',
|
|
551
|
+
description: 'Company allow-lists resolve to portal identity',
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
value: 'contact',
|
|
555
|
+
label: 'Contact identity',
|
|
556
|
+
description: 'Match email or HubSpot user ID',
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
value: 'actor',
|
|
560
|
+
label: 'Actor identity',
|
|
561
|
+
description: 'Match targeting key, email, or HubSpot user ID',
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
allowBack: true,
|
|
565
|
+
});
|
|
566
|
+
if (level === undefined)
|
|
567
|
+
return undefined;
|
|
568
|
+
if (level === BACK) {
|
|
569
|
+
if (rules.length === 0)
|
|
570
|
+
return BACK;
|
|
571
|
+
rules.pop();
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const operator = await prompts.select({
|
|
575
|
+
message: 'Targeting operator',
|
|
576
|
+
options: [
|
|
577
|
+
{
|
|
578
|
+
value: 'is',
|
|
579
|
+
label: 'is',
|
|
580
|
+
description: 'Exact identity match (the bounded schema-supported operator)',
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
default: 'is',
|
|
584
|
+
allowBack: true,
|
|
585
|
+
});
|
|
586
|
+
if (operator === undefined)
|
|
587
|
+
return undefined;
|
|
588
|
+
if (operator === BACK)
|
|
589
|
+
continue;
|
|
590
|
+
const matchKey = await wizardText(prompts, {
|
|
591
|
+
message: 'Target value',
|
|
592
|
+
allowBack: true,
|
|
593
|
+
validate: (value) => (value.length === 0 ? 'Enter a target value.' : undefined),
|
|
594
|
+
});
|
|
595
|
+
if (matchKey === undefined)
|
|
596
|
+
return undefined;
|
|
597
|
+
if (matchKey === BACK)
|
|
598
|
+
continue;
|
|
599
|
+
const served = await prompts.select({
|
|
600
|
+
message: 'Served variation',
|
|
601
|
+
options: variations.map((variation) => ({
|
|
602
|
+
value: variation.name,
|
|
603
|
+
label: `${variation.label} (${variation.name})`,
|
|
604
|
+
})),
|
|
605
|
+
allowBack: true,
|
|
606
|
+
});
|
|
607
|
+
if (served === undefined)
|
|
608
|
+
return undefined;
|
|
609
|
+
if (served === BACK)
|
|
610
|
+
continue;
|
|
611
|
+
rules.push({
|
|
612
|
+
id: `rule-${rules.length + 1}`,
|
|
613
|
+
level,
|
|
614
|
+
matchKeys: [matchKey],
|
|
615
|
+
variation: served,
|
|
616
|
+
});
|
|
617
|
+
if (rules.length === MAX_GUIDED_RULES)
|
|
618
|
+
break;
|
|
619
|
+
const another = await prompts.confirm({
|
|
620
|
+
message: 'Add another targeting rule?',
|
|
621
|
+
default: false,
|
|
622
|
+
allowBack: true,
|
|
623
|
+
});
|
|
624
|
+
if (another === undefined)
|
|
625
|
+
return undefined;
|
|
626
|
+
if (another === BACK) {
|
|
627
|
+
rules.pop();
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (!another)
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
return rules;
|
|
634
|
+
}
|
|
635
|
+
function validateWizardReferences(definition) {
|
|
636
|
+
const names = definition.variations.map((variation) => variation.name);
|
|
637
|
+
if (names.length === 0)
|
|
638
|
+
return 'Define at least one variation.';
|
|
639
|
+
if (new Set(names).size !== names.length)
|
|
640
|
+
return 'Variation keys must be unique.';
|
|
641
|
+
if (!names.includes(definition.defaultVariation)) {
|
|
642
|
+
return 'The default variation must reference a defined variation.';
|
|
643
|
+
}
|
|
644
|
+
for (const rule of definition.rules) {
|
|
645
|
+
if (!rule.matchKeys || rule.matchKeys.length === 0)
|
|
646
|
+
return `Rule ${rule.id} needs a target value.`;
|
|
647
|
+
if (!rule.variation || !names.includes(rule.variation)) {
|
|
648
|
+
return `Rule ${rule.id} must reference a defined variation.`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
/** Build and submit a bounded typed definition using only schema-supported exact-match rules. */
|
|
654
|
+
export async function runCreateWizard(input) {
|
|
655
|
+
const prompts = input.prompts ?? liveFlagsPrompts;
|
|
656
|
+
if (!prompts.isInteractive() || isMachineOutput()) {
|
|
657
|
+
return fail(input.reporter, false, 'Require --file <flag-definition.json> outside an interactive terminal.');
|
|
658
|
+
}
|
|
659
|
+
let key;
|
|
660
|
+
let label;
|
|
661
|
+
let type;
|
|
662
|
+
let state;
|
|
663
|
+
let variations;
|
|
664
|
+
let defaultVariation;
|
|
665
|
+
let rules;
|
|
666
|
+
let step = 0;
|
|
667
|
+
while (step < 7) {
|
|
668
|
+
if (step === 0) {
|
|
669
|
+
const answer = await wizardText(prompts, {
|
|
670
|
+
message: 'Flag key',
|
|
671
|
+
validate: (value) => /^[a-z0-9][a-z0-9-_]*$/.test(value)
|
|
672
|
+
? undefined
|
|
673
|
+
: 'Use lowercase letters, numbers, hyphens, or underscores.',
|
|
674
|
+
});
|
|
675
|
+
if (answer === undefined || answer === BACK)
|
|
676
|
+
return cancelled(input.reporter);
|
|
677
|
+
key = answer;
|
|
678
|
+
step += 1;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (step === 1) {
|
|
682
|
+
const answer = await wizardText(prompts, {
|
|
683
|
+
message: 'Flag label',
|
|
684
|
+
default: key
|
|
685
|
+
.split(/[-_]/)
|
|
686
|
+
.filter(Boolean)
|
|
687
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
|
|
688
|
+
.join(' '),
|
|
689
|
+
allowBack: true,
|
|
690
|
+
validate: (value) => (value.length > 0 ? undefined : 'Enter a label.'),
|
|
691
|
+
});
|
|
692
|
+
if (answer === undefined)
|
|
693
|
+
return cancelled(input.reporter);
|
|
694
|
+
if (answer === BACK) {
|
|
695
|
+
step -= 1;
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
label = answer;
|
|
699
|
+
step += 1;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (step === 2) {
|
|
703
|
+
const answer = await prompts.select({
|
|
704
|
+
message: 'Flag type',
|
|
705
|
+
options: [
|
|
706
|
+
{ value: 'boolean', label: 'Boolean' },
|
|
707
|
+
{ value: 'string', label: 'String' },
|
|
708
|
+
{ value: 'number', label: 'Number' },
|
|
709
|
+
{ value: 'json', label: 'JSON' },
|
|
710
|
+
],
|
|
711
|
+
default: type ?? 'boolean',
|
|
712
|
+
allowBack: true,
|
|
713
|
+
});
|
|
714
|
+
if (answer === undefined)
|
|
715
|
+
return cancelled(input.reporter);
|
|
716
|
+
if (answer === BACK) {
|
|
717
|
+
step -= 1;
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (type !== answer) {
|
|
721
|
+
variations = undefined;
|
|
722
|
+
defaultVariation = undefined;
|
|
723
|
+
rules = undefined;
|
|
724
|
+
}
|
|
725
|
+
type = answer;
|
|
726
|
+
step += 1;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
if (step === 3) {
|
|
730
|
+
const answer = await prompts.select({
|
|
731
|
+
message: 'Initial state',
|
|
732
|
+
options: [
|
|
733
|
+
{ value: 'disabled', label: 'Disabled', description: 'Serve the default variation' },
|
|
734
|
+
{ value: 'enabled', label: 'Enabled', description: 'Ready for evaluation' },
|
|
735
|
+
],
|
|
736
|
+
default: state ?? 'disabled',
|
|
737
|
+
allowBack: true,
|
|
738
|
+
});
|
|
739
|
+
if (answer === undefined)
|
|
740
|
+
return cancelled(input.reporter);
|
|
741
|
+
if (answer === BACK) {
|
|
742
|
+
step -= 1;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
state = answer;
|
|
746
|
+
step += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (step === 4) {
|
|
750
|
+
const answer = await buildVariations(prompts, type);
|
|
751
|
+
if (answer === undefined)
|
|
752
|
+
return cancelled(input.reporter);
|
|
753
|
+
if (answer === BACK) {
|
|
754
|
+
step -= 1;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
variations = answer;
|
|
758
|
+
defaultVariation = undefined;
|
|
759
|
+
rules = undefined;
|
|
760
|
+
step += 1;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (step === 5) {
|
|
764
|
+
const answer = await prompts.select({
|
|
765
|
+
message: 'Default variation',
|
|
766
|
+
options: (variations ?? []).map((variation) => ({
|
|
767
|
+
value: variation.name,
|
|
768
|
+
label: `${variation.label} (${variation.name})`,
|
|
769
|
+
})),
|
|
770
|
+
default: (defaultVariation ?? variations?.[0]?.name),
|
|
771
|
+
allowBack: true,
|
|
772
|
+
});
|
|
773
|
+
if (answer === undefined)
|
|
774
|
+
return cancelled(input.reporter);
|
|
775
|
+
if (answer === BACK) {
|
|
776
|
+
step -= 1;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
defaultVariation = answer;
|
|
780
|
+
step += 1;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const answer = await buildRules(prompts, variations ?? []);
|
|
784
|
+
if (answer === undefined)
|
|
785
|
+
return cancelled(input.reporter);
|
|
786
|
+
if (answer === BACK) {
|
|
787
|
+
step -= 1;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
rules = answer;
|
|
791
|
+
step += 1;
|
|
792
|
+
}
|
|
793
|
+
const updatedAt = (input.now ?? (() => new Date()))().toISOString();
|
|
794
|
+
let definition;
|
|
795
|
+
for (;;) {
|
|
796
|
+
const candidate = {
|
|
797
|
+
accountId: input.scope.accountId,
|
|
798
|
+
projectId: input.scope.projectId,
|
|
799
|
+
environment: input.scope.environment,
|
|
800
|
+
hubSpotAppId: input.scope.hubSpotAppId,
|
|
801
|
+
key,
|
|
802
|
+
label,
|
|
803
|
+
type,
|
|
804
|
+
state,
|
|
805
|
+
variations,
|
|
806
|
+
defaultVariation,
|
|
807
|
+
rules,
|
|
808
|
+
version: 1,
|
|
809
|
+
updatedAt,
|
|
810
|
+
};
|
|
811
|
+
const decoded = Schema.decodeUnknownEither(schemas.FlagDefinition)(candidate);
|
|
812
|
+
if (decoded._tag === 'Left') {
|
|
813
|
+
return fail(input.reporter, false, `Invalid flag definition: ${String(decoded.left)}`);
|
|
814
|
+
}
|
|
815
|
+
const referenceError = validateWizardReferences(decoded.right);
|
|
816
|
+
if (referenceError)
|
|
817
|
+
return fail(input.reporter, false, referenceError);
|
|
818
|
+
input.reporter.raw(`\nFlag definition preview\n${JSON.stringify(decoded.right, null, 2)}\n\nPercentage rollouts are available through --file only.\n\n`);
|
|
819
|
+
if (input.yes) {
|
|
820
|
+
definition = decoded.right;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
const confirmed = await prompts.confirm({
|
|
824
|
+
message: `Create or update flag "${decoded.right.key}"?`,
|
|
825
|
+
default: false,
|
|
826
|
+
allowBack: true,
|
|
827
|
+
});
|
|
828
|
+
if (confirmed === undefined || confirmed === false)
|
|
829
|
+
return cancelled(input.reporter);
|
|
830
|
+
if (confirmed === true) {
|
|
831
|
+
definition = decoded.right;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
// Esc from the final confirmation returns to the immediately preceding
|
|
835
|
+
// targeting-rule step without discarding the rest of the draft.
|
|
836
|
+
const revisedRules = await buildRules(prompts, variations ?? []);
|
|
837
|
+
if (revisedRules === undefined)
|
|
838
|
+
return cancelled(input.reporter);
|
|
839
|
+
if (revisedRules !== BACK)
|
|
840
|
+
rules = revisedRules;
|
|
841
|
+
}
|
|
842
|
+
const result = await input.client.defineFlag(definition);
|
|
843
|
+
input.reporter.raw(`Defined "${result.flag.key}" [${result.flag.type}, ${result.flag.state}, v${result.flag.version}] — synced ${result.syncedSnapshots} snapshot(s).\n`);
|
|
844
|
+
return { exitCode: 0 };
|
|
845
|
+
}
|
|
846
|
+
function cancelled(reporter) {
|
|
847
|
+
reporter.rawErr('Cancelled.\n');
|
|
848
|
+
return { exitCode: 130 };
|
|
849
|
+
}
|
|
850
|
+
const liveFlagMutationPrompts = {
|
|
851
|
+
isInteractive,
|
|
852
|
+
select: promptSelect,
|
|
853
|
+
confirm: promptConfirm,
|
|
854
|
+
};
|
|
855
|
+
export async function runSetState(input) {
|
|
856
|
+
const prompts = input.prompts ?? liveFlagMutationPrompts;
|
|
857
|
+
let key = resolveFlag(input.argv, '--key');
|
|
858
|
+
const canPrompt = !input.json && !isMachineOutput() && prompts.isInteractive();
|
|
859
|
+
// Deterministic machine/non-interactive validation must not touch the tenant
|
|
860
|
+
// transport merely to discover that a required mutation key is absent.
|
|
861
|
+
if (!key && !canPrompt) {
|
|
862
|
+
return fail(input.reporter, input.json, `Require --key <flagKey> to ${input.state} a flag.`);
|
|
863
|
+
}
|
|
864
|
+
const flags = await input.client.listFlags();
|
|
865
|
+
const candidates = flags.filter((flag) => flag.state !== 'archived' && flag.state !== input.state);
|
|
866
|
+
if (!key && canPrompt) {
|
|
867
|
+
if (candidates.length === 0) {
|
|
868
|
+
return fail(input.reporter, input.json, `No flags can be ${input.state}.`);
|
|
869
|
+
}
|
|
870
|
+
key = await prompts.select({
|
|
871
|
+
message: `Which flag should be ${input.state}?`,
|
|
872
|
+
options: candidates.map((flag) => ({
|
|
873
|
+
value: flag.key,
|
|
874
|
+
label: flag.label ? `${flag.label} (${flag.key})` : flag.key,
|
|
875
|
+
description: `${flag.type} · ${flag.state} · v${flag.version}`,
|
|
876
|
+
})),
|
|
877
|
+
});
|
|
878
|
+
if (!key) {
|
|
879
|
+
input.reporter.rawErr('Cancelled.\n');
|
|
880
|
+
return { exitCode: 130 };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (!key) {
|
|
176
884
|
return fail(input.reporter, input.json, `Require --key <flagKey> to ${input.state} a flag.`);
|
|
177
|
-
|
|
885
|
+
}
|
|
886
|
+
const existing = flags.find((flag) => flag.key === key);
|
|
178
887
|
if (!existing)
|
|
179
888
|
return fail(input.reporter, input.json, `Flag "${key}" not found.`);
|
|
889
|
+
if (existing.state === 'archived') {
|
|
890
|
+
return fail(input.reporter, input.json, `Flag "${key}" is archived and cannot transition.`);
|
|
891
|
+
}
|
|
892
|
+
if (existing.state === input.state) {
|
|
893
|
+
return fail(input.reporter, input.json, `Flag "${key}" is already ${input.state}.`);
|
|
894
|
+
}
|
|
895
|
+
if (input.state === 'archived' &&
|
|
896
|
+
!input.json &&
|
|
897
|
+
!input.yes &&
|
|
898
|
+
!isMachineOutput() &&
|
|
899
|
+
prompts.isInteractive()) {
|
|
900
|
+
const confirmed = await prompts.confirm({
|
|
901
|
+
message: `Archive flag "${key}"?`,
|
|
902
|
+
default: false,
|
|
903
|
+
});
|
|
904
|
+
if (confirmed !== true) {
|
|
905
|
+
input.reporter.rawErr('Cancelled.\n');
|
|
906
|
+
return { exitCode: 130 };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
180
909
|
const updated = {
|
|
181
910
|
...existing,
|
|
182
911
|
state: input.state,
|
|
@@ -209,7 +938,7 @@ async function runSetState(input) {
|
|
|
209
938
|
// and the handler fails with a silent `exitWith(1)` so `runHandler` does not
|
|
210
939
|
// re-render. TODO(phase-5): reconcile these payloads with the standard envelope
|
|
211
940
|
// once the dispatch swap lets the fixtures move in lockstep.
|
|
212
|
-
const projectIdOption = Options.text('project-id').pipe(Options.optional);
|
|
941
|
+
const projectIdOption = Options.text('project-id').pipe(Options.withAlias('project'), Options.optional);
|
|
213
942
|
const appIdOption = Options.integer('app-id').pipe(Options.optional);
|
|
214
943
|
const environmentOption = Options.text('environment').pipe(Options.optional);
|
|
215
944
|
const envOption = Options.text('env').pipe(Options.optional);
|
|
@@ -220,6 +949,8 @@ const fileOption = Options.text('file').pipe(Options.optional);
|
|
|
220
949
|
const keyOption = Options.text('key').pipe(Options.optional);
|
|
221
950
|
const identityOptions = {
|
|
222
951
|
json: jsonOption,
|
|
952
|
+
controlPlaneUrl: controlPlaneUrlOption,
|
|
953
|
+
accountId: accountIdOption,
|
|
223
954
|
projectId: projectIdOption,
|
|
224
955
|
appId: appIdOption,
|
|
225
956
|
environment: environmentOption,
|
|
@@ -240,6 +971,13 @@ function failEff(json, message) {
|
|
|
240
971
|
return yield* Effect.fail(exitWith(1));
|
|
241
972
|
});
|
|
242
973
|
}
|
|
974
|
+
function cancelEff() {
|
|
975
|
+
return Effect.gen(function* () {
|
|
976
|
+
const reporter = yield* Reporter;
|
|
977
|
+
yield* reporter.rawErr('Cancelled.\n');
|
|
978
|
+
return yield* Effect.fail(exitWith(130));
|
|
979
|
+
});
|
|
980
|
+
}
|
|
243
981
|
/**
|
|
244
982
|
* Synchronous reporter sink for the shared `runList`/`runCreate`/`runSetState`
|
|
245
983
|
* bodies on the native path. These helpers emit verbatim bytes (the bespoke
|
|
@@ -247,40 +985,35 @@ function failEff(json, message) {
|
|
|
247
985
|
* sanctioned I/O edge, instead of `process.std*`. Mode is derived solely from
|
|
248
986
|
* `--json` so the produced bytes match the legacy path exactly.
|
|
249
987
|
*/
|
|
250
|
-
const syncReporter =
|
|
988
|
+
const syncReporter = Effect.gen(function* () {
|
|
989
|
+
const reporter = yield* Reporter;
|
|
990
|
+
return reporter.implementation;
|
|
991
|
+
});
|
|
251
992
|
/**
|
|
252
|
-
* Resolve the
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* `--cloudflare-api-token` (reconstructed into a minimal argv) or env.
|
|
993
|
+
* Resolve the compatibility direct-to-tenant client. The CP-backed resolver
|
|
994
|
+
* below uses this only when no login is available or an explicit direct-mode
|
|
995
|
+
* runtime/Cloudflare option requests the legacy path.
|
|
256
996
|
*/
|
|
257
|
-
function
|
|
997
|
+
function resolveDirectClient(opts, config = {}) {
|
|
258
998
|
return Effect.gen(function* () {
|
|
259
999
|
const json = opts.json;
|
|
260
1000
|
const root = yield* Cwd;
|
|
261
1001
|
const envProjectId = yield* Config.string('HSX_PROJECT_ID')
|
|
262
1002
|
.pipe(Config.withDefault(''))
|
|
263
1003
|
.pipe(Effect.orDie);
|
|
264
|
-
const envAppId = yield* Config.string('HSX_APP_ID')
|
|
1004
|
+
const envAppId = yield* Config.string('HSX_APP_ID')
|
|
1005
|
+
.pipe(Config.withDefault(''))
|
|
1006
|
+
.pipe(Effect.orDie);
|
|
1007
|
+
const envHubSpotAppId = yield* Config.string('HSX_HUBSPOT_APP_ID')
|
|
1008
|
+
.pipe(Config.withDefault(''))
|
|
1009
|
+
.pipe(Effect.orDie);
|
|
265
1010
|
const envRuntimeBaseUrl = yield* Config.string('HSX_RUNTIME_BASE_URL')
|
|
266
1011
|
.pipe(Config.withDefault(''))
|
|
267
1012
|
.pipe(Effect.orDie);
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
onSome: (value) => value,
|
|
273
|
-
});
|
|
274
|
-
const runtimeBaseUrl = Option.getOrElse(opts.runtimeOrigin, () => envRuntimeBaseUrl.length > 0 ? envRuntimeBaseUrl : undefined);
|
|
275
|
-
if (!projectId)
|
|
276
|
-
return yield* failEff(json, 'Require --project-id (or HSX_PROJECT_ID).');
|
|
277
|
-
if (appIdRaw === undefined || !Number.isInteger(appIdRaw)) {
|
|
278
|
-
return yield* failEff(json, 'Require an integer --app-id (or HSX_APP_ID).');
|
|
279
|
-
}
|
|
280
|
-
const hubSpotAppId = appIdRaw;
|
|
281
|
-
if (!runtimeBaseUrl) {
|
|
282
|
-
return yield* failEff(json, 'Require --runtime-origin (or HSX_RUNTIME_BASE_URL) — the deployed Worker URL.');
|
|
283
|
-
}
|
|
1013
|
+
const envEnvironment = yield* Config.string('HSX_ENVIRONMENT')
|
|
1014
|
+
.pipe(Config.withDefault(''))
|
|
1015
|
+
.pipe(Effect.orDie);
|
|
1016
|
+
const local = yield* Effect.promise(() => readLocalProjectContext(root));
|
|
284
1017
|
// loadTenantStateStore reads Cloudflare creds from argv-style flags or env;
|
|
285
1018
|
// forward an explicit --cloudflare-api-token when provided, else rely on env.
|
|
286
1019
|
const argv = Option.match(opts.cloudflareApiToken, {
|
|
@@ -292,33 +1025,171 @@ function resolveClient(opts) {
|
|
|
292
1025
|
catch: (cause) => cause,
|
|
293
1026
|
}).pipe(Effect.catchAll((cause) => failEff(json, cause instanceof Error ? cause.message : String(cause))));
|
|
294
1027
|
if (!context) {
|
|
295
|
-
return yield* failEff(json, 'No .hs-x/cloudflare.json found.
|
|
1028
|
+
return yield* failEff(json, 'No .hs-x/cloudflare.json found. Run `hs-x deploy` in this checkout first.');
|
|
296
1029
|
}
|
|
297
|
-
const
|
|
298
|
-
try: () =>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1030
|
+
const resolved = yield* Effect.tryPromise({
|
|
1031
|
+
try: () => resolveFlagsContext({
|
|
1032
|
+
stateStore: context.stateStore,
|
|
1033
|
+
local,
|
|
1034
|
+
explicitProjectId: Option.getOrUndefined(opts.projectId),
|
|
1035
|
+
envProjectId: envProjectId || undefined,
|
|
1036
|
+
explicitEnvironment: Option.getOrUndefined(opts.environment) ?? Option.getOrUndefined(opts.env),
|
|
1037
|
+
envEnvironment: envEnvironment || undefined,
|
|
1038
|
+
explicitAppId: Option.getOrUndefined(opts.appId),
|
|
1039
|
+
envAppId: envAppId || undefined,
|
|
1040
|
+
compatibilityEnvAppId: envHubSpotAppId || undefined,
|
|
1041
|
+
explicitRuntimeOrigin: Option.getOrUndefined(opts.runtimeOrigin),
|
|
1042
|
+
envRuntimeOrigin: envRuntimeBaseUrl || undefined,
|
|
1043
|
+
allowPrompt: config.allowPrompt !== false && !json && !isMachineOutput() && isInteractive(),
|
|
302
1044
|
}),
|
|
303
1045
|
catch: (cause) => cause,
|
|
304
|
-
}).pipe(Effect.catchAll((cause) =>
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
1046
|
+
}).pipe(Effect.catchAll((cause) => cause instanceof FlagsPromptCancelled
|
|
1047
|
+
? cancelEff()
|
|
1048
|
+
: failEff(json, cause instanceof Error ? cause.message : String(cause))));
|
|
308
1049
|
const scope = {
|
|
309
|
-
accountId: binding.tenantScopeAccountId,
|
|
310
|
-
projectId,
|
|
311
|
-
environment,
|
|
312
|
-
hubSpotAppId,
|
|
1050
|
+
accountId: resolved.binding.tenantScopeAccountId,
|
|
1051
|
+
projectId: resolved.projectId,
|
|
1052
|
+
environment: resolved.environment,
|
|
1053
|
+
hubSpotAppId: resolved.hubSpotAppId,
|
|
313
1054
|
};
|
|
314
1055
|
const client = createFlagAuthoringClient({
|
|
315
|
-
runtimeBaseUrl,
|
|
316
|
-
grantSecret: binding.syncGrantSecretValue,
|
|
1056
|
+
runtimeBaseUrl: resolved.runtimeBaseUrl,
|
|
1057
|
+
grantSecret: resolved.binding.syncGrantSecretValue,
|
|
317
1058
|
scope,
|
|
318
1059
|
});
|
|
319
1060
|
return { client, scope };
|
|
320
1061
|
});
|
|
321
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* CP-first context discovery. A stored login selects the authoritative account
|
|
1065
|
+
* project catalog and redacted runtime-binding collection; the returned client
|
|
1066
|
+
* writes only through the existing control-plane flag conduit.
|
|
1067
|
+
*/
|
|
1068
|
+
function resolveClient(opts, config = {}) {
|
|
1069
|
+
return Effect.gen(function* () {
|
|
1070
|
+
const root = yield* Cwd;
|
|
1071
|
+
const local = yield* Effect.promise(() => readLocalProjectContext(root));
|
|
1072
|
+
const accounts = yield* AccountStore;
|
|
1073
|
+
const stored = yield* accounts.load;
|
|
1074
|
+
const envAccountId = yield* Config.string('HSX_ACCOUNT_ID')
|
|
1075
|
+
.pipe(Config.withDefault(''))
|
|
1076
|
+
.pipe(Effect.orDie);
|
|
1077
|
+
const envProjectId = yield* Config.string('HSX_PROJECT_ID')
|
|
1078
|
+
.pipe(Config.withDefault(''))
|
|
1079
|
+
.pipe(Effect.orDie);
|
|
1080
|
+
const envEnvironment = yield* Config.string('HSX_ENVIRONMENT')
|
|
1081
|
+
.pipe(Config.withDefault(''))
|
|
1082
|
+
.pipe(Effect.orDie);
|
|
1083
|
+
const envAppId = yield* Config.string('HSX_APP_ID')
|
|
1084
|
+
.pipe(Config.withDefault(''))
|
|
1085
|
+
.pipe(Effect.orDie);
|
|
1086
|
+
const compatibilityEnvAppId = yield* Config.string('HSX_HUBSPOT_APP_ID')
|
|
1087
|
+
.pipe(Config.withDefault(''))
|
|
1088
|
+
.pipe(Effect.orDie);
|
|
1089
|
+
const envRuntimeOrigin = yield* Config.string('HSX_RUNTIME_BASE_URL')
|
|
1090
|
+
.pipe(Config.withDefault(''))
|
|
1091
|
+
.pipe(Effect.orDie);
|
|
1092
|
+
const envControlPlaneUrl = yield* Config.string('HSX_CONTROL_PLANE_URL')
|
|
1093
|
+
.pipe(Config.withDefault(''))
|
|
1094
|
+
.pipe(Effect.orDie);
|
|
1095
|
+
const forceDirect = Option.isSome(opts.runtimeOrigin) ||
|
|
1096
|
+
Option.isSome(opts.cloudflareApiToken) ||
|
|
1097
|
+
envRuntimeOrigin.length > 0;
|
|
1098
|
+
const session = activeSession(stored);
|
|
1099
|
+
if (forceDirect || !session)
|
|
1100
|
+
return yield* resolveDirectClient(opts, config);
|
|
1101
|
+
const explicitAccountId = Option.getOrUndefined(opts.accountId);
|
|
1102
|
+
const accountId = resolveAccountId({
|
|
1103
|
+
...(explicitAccountId ? { explicitAccountId } : {}),
|
|
1104
|
+
...(envAccountId ? { envAccountId } : {}),
|
|
1105
|
+
...(local.projectBinding?.accountId
|
|
1106
|
+
? { localAccountId: local.projectBinding.accountId }
|
|
1107
|
+
: {}),
|
|
1108
|
+
...(stored.defaultAccountId ? { defaultAccountId: stored.defaultAccountId } : {}),
|
|
1109
|
+
});
|
|
1110
|
+
if (!accountId) {
|
|
1111
|
+
return yield* failEff(opts.json, 'Flags require an HS-X account: run `hs-x login`, or pass --account-id / HSX_ACCOUNT_ID.');
|
|
1112
|
+
}
|
|
1113
|
+
const controlPlaneUrl = Option.getOrUndefined(opts.controlPlaneUrl) ??
|
|
1114
|
+
(envControlPlaneUrl || DEFAULT_CONTROL_PLANE_URL);
|
|
1115
|
+
const cp = yield* ControlPlaneClient;
|
|
1116
|
+
const controlPlane = {
|
|
1117
|
+
request: (request) => Effect.runPromise(cp.fetch({
|
|
1118
|
+
controlPlaneUrl,
|
|
1119
|
+
path: request.path,
|
|
1120
|
+
...(request.method ? { method: request.method } : {}),
|
|
1121
|
+
...(request.body === undefined ? {} : { body: request.body }),
|
|
1122
|
+
})),
|
|
1123
|
+
};
|
|
1124
|
+
const allowPrompt = config.allowPrompt !== false && !opts.json && !isMachineOutput() && isInteractive();
|
|
1125
|
+
const resolved = yield* Effect.tryPromise({
|
|
1126
|
+
try: async () => {
|
|
1127
|
+
const explicitProjectId = Option.getOrUndefined(opts.projectId) ?? (envProjectId || undefined);
|
|
1128
|
+
const projects = await discoverProjects({ accountId, controlPlane });
|
|
1129
|
+
const project = await resolveProjectSelection({
|
|
1130
|
+
accountId,
|
|
1131
|
+
projects,
|
|
1132
|
+
...(explicitProjectId ? { explicitProjectId } : {}),
|
|
1133
|
+
...(local.projectBinding?.projectId
|
|
1134
|
+
? { localProjectId: local.projectBinding.projectId }
|
|
1135
|
+
: {}),
|
|
1136
|
+
allowPrompt,
|
|
1137
|
+
prompts: liveFlagsPrompts,
|
|
1138
|
+
message: 'Which project do you want to manage flags for?',
|
|
1139
|
+
});
|
|
1140
|
+
const bindings = await discoverProjectRuntimeBindings({
|
|
1141
|
+
accountId,
|
|
1142
|
+
projectId: project.id,
|
|
1143
|
+
controlPlane,
|
|
1144
|
+
});
|
|
1145
|
+
const appIdRaw = envAppId || compatibilityEnvAppId || undefined;
|
|
1146
|
+
const explicitAppId = Option.getOrUndefined(opts.appId) ??
|
|
1147
|
+
(appIdRaw === undefined ? undefined : Number(appIdRaw));
|
|
1148
|
+
if (explicitAppId !== undefined &&
|
|
1149
|
+
(!Number.isInteger(explicitAppId) || explicitAppId <= 0)) {
|
|
1150
|
+
throw new Error('Require a valid integer --app-id (or HSX_APP_ID).');
|
|
1151
|
+
}
|
|
1152
|
+
const binding = await resolveRuntimeBinding({
|
|
1153
|
+
project,
|
|
1154
|
+
bindings,
|
|
1155
|
+
...((Option.getOrUndefined(opts.environment) ??
|
|
1156
|
+
Option.getOrUndefined(opts.env) ??
|
|
1157
|
+
(envEnvironment || undefined))
|
|
1158
|
+
? {
|
|
1159
|
+
explicitEnvironment: Option.getOrUndefined(opts.environment) ??
|
|
1160
|
+
Option.getOrUndefined(opts.env) ??
|
|
1161
|
+
envEnvironment,
|
|
1162
|
+
}
|
|
1163
|
+
: {}),
|
|
1164
|
+
...(explicitAppId === undefined ? {} : { explicitAppId }),
|
|
1165
|
+
...(local.hubSpotBinding?.appId ? { localAppId: local.hubSpotBinding.appId } : {}),
|
|
1166
|
+
allowPrompt,
|
|
1167
|
+
prompts: liveFlagsPrompts,
|
|
1168
|
+
});
|
|
1169
|
+
const scope = {
|
|
1170
|
+
accountId,
|
|
1171
|
+
projectId: project.id,
|
|
1172
|
+
environment: binding.environment,
|
|
1173
|
+
hubSpotAppId: binding.hubSpotAppId,
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
client: createControlPlaneFlagAuthoringClient({
|
|
1177
|
+
accountId,
|
|
1178
|
+
projectId: project.id,
|
|
1179
|
+
environment: binding.environment,
|
|
1180
|
+
hubSpotAppId: binding.hubSpotAppId,
|
|
1181
|
+
controlPlane,
|
|
1182
|
+
}),
|
|
1183
|
+
scope,
|
|
1184
|
+
};
|
|
1185
|
+
},
|
|
1186
|
+
catch: (cause) => cause,
|
|
1187
|
+
}).pipe(Effect.catchAll((cause) => cause instanceof ProjectPickerCancelled
|
|
1188
|
+
? cancelEff()
|
|
1189
|
+
: failEff(opts.json, cause instanceof Error ? cause.message : String(cause))));
|
|
1190
|
+
return resolved;
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
322
1193
|
/**
|
|
323
1194
|
* Map a thrown `RuntimeFlagAuthoringError`/`Error` to the legacy bespoke
|
|
324
1195
|
* failure. A `CliError` (an already-rendered `failEff`) is re-raised untouched;
|
|
@@ -333,11 +1204,13 @@ function withClientErrors(json, body) {
|
|
|
333
1204
|
}
|
|
334
1205
|
const flagsListCmd = Command.make('list', { ...identityOptions, limit: limitOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
335
1206
|
const { client } = yield* resolveClient(opts);
|
|
336
|
-
const reporter = yield* syncReporter
|
|
1207
|
+
const reporter = yield* syncReporter;
|
|
337
1208
|
const limit = Option.getOrUndefined(opts.limit);
|
|
338
1209
|
yield* withClientErrors(opts.json, Effect.tryPromise({
|
|
339
1210
|
try: () => runList({
|
|
340
|
-
argv: limit !== undefined
|
|
1211
|
+
argv: limit !== undefined
|
|
1212
|
+
? ['flags', 'list', '--limit', String(limit)]
|
|
1213
|
+
: ['flags', 'list'],
|
|
341
1214
|
json: opts.json,
|
|
342
1215
|
client,
|
|
343
1216
|
reporter,
|
|
@@ -346,11 +1219,22 @@ const flagsListCmd = Command.make('list', { ...identityOptions, limit: limitOpti
|
|
|
346
1219
|
}).pipe(Effect.asVoid));
|
|
347
1220
|
})));
|
|
348
1221
|
function makeCreateCmd(name) {
|
|
349
|
-
return Command.make(name, { ...identityOptions, file: fileOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
350
|
-
const { client, scope } = yield* resolveClient(opts);
|
|
1222
|
+
return Command.make(name, { ...identityOptions, file: fileOption, yes: yesOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
351
1223
|
const file = Option.getOrUndefined(opts.file);
|
|
1224
|
+
const wizardAllowed = !opts.json && !isMachineOutput() && isInteractive();
|
|
1225
|
+
if (!file && !wizardAllowed) {
|
|
1226
|
+
return yield* failEff(opts.json, 'Require --file <flag-definition.json> for `flags create` outside an interactive terminal.');
|
|
1227
|
+
}
|
|
1228
|
+
// A definition file is authoritative and never opens context/wizard
|
|
1229
|
+
// pickers. If its scope is ambiguous, report the required identity flag.
|
|
1230
|
+
const { client, scope } = yield* resolveClient(opts, { allowPrompt: !file });
|
|
1231
|
+
const reporter = yield* syncReporter;
|
|
352
1232
|
if (!file) {
|
|
353
|
-
|
|
1233
|
+
yield* withClientErrors(opts.json, Effect.tryPromise({
|
|
1234
|
+
try: () => runCreateWizard({ client, scope, reporter, yes: opts.yes }),
|
|
1235
|
+
catch: (cause) => cause,
|
|
1236
|
+
}).pipe(Effect.flatMap((result) => result.exitCode === 0 ? Effect.void : Effect.fail(exitWith(result.exitCode)))));
|
|
1237
|
+
return;
|
|
354
1238
|
}
|
|
355
1239
|
// Read via FileSystem (no node:fs); a read failure mirrors the legacy
|
|
356
1240
|
// "could not read/parse" bespoke failure.
|
|
@@ -359,17 +1243,26 @@ function makeCreateCmd(name) {
|
|
|
359
1243
|
if (source._tag === 'Left') {
|
|
360
1244
|
return yield* failEff(opts.json, `Could not read/parse ${file}: ${String(source.left.cause)}`);
|
|
361
1245
|
}
|
|
362
|
-
|
|
363
|
-
|
|
1246
|
+
yield* withClientErrors(opts.json, runCreateFromSource({
|
|
1247
|
+
source: source.right,
|
|
1248
|
+
file,
|
|
1249
|
+
json: opts.json,
|
|
1250
|
+
client,
|
|
1251
|
+
scope,
|
|
1252
|
+
reporter,
|
|
1253
|
+
}));
|
|
364
1254
|
})));
|
|
365
1255
|
}
|
|
366
1256
|
const flagsCreateCmd = makeCreateCmd('create');
|
|
367
1257
|
const flagsDefineCmd = makeCreateCmd('define');
|
|
368
1258
|
function makeSetStateCmd(name, state) {
|
|
369
|
-
return Command.make(name, { ...identityOptions, key: keyOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
370
|
-
const { client } = yield* resolveClient(opts);
|
|
371
|
-
const reporter = yield* syncReporter(opts.json);
|
|
1259
|
+
return Command.make(name, { ...identityOptions, key: keyOption, yes: yesOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
372
1260
|
const key = Option.getOrUndefined(opts.key);
|
|
1261
|
+
if (key === undefined && (isMachineOutput() || !isInteractive())) {
|
|
1262
|
+
return yield* failEff(opts.json, `Require --key <flagKey> to ${state} a flag.`);
|
|
1263
|
+
}
|
|
1264
|
+
const { client } = yield* resolveClient(opts);
|
|
1265
|
+
const reporter = yield* syncReporter;
|
|
373
1266
|
yield* withClientErrors(opts.json, Effect.tryPromise({
|
|
374
1267
|
try: () => runSetState({
|
|
375
1268
|
argv: key !== undefined ? ['flags', name, '--key', key] : ['flags', name],
|
|
@@ -377,15 +1270,43 @@ function makeSetStateCmd(name, state) {
|
|
|
377
1270
|
client,
|
|
378
1271
|
state,
|
|
379
1272
|
reporter,
|
|
1273
|
+
yes: opts.yes,
|
|
380
1274
|
}),
|
|
381
1275
|
catch: (cause) => cause,
|
|
382
|
-
}).pipe(Effect.
|
|
1276
|
+
}).pipe(Effect.flatMap((result) => result.exitCode === 0 ? Effect.void : Effect.fail(exitWith(result.exitCode)))));
|
|
383
1277
|
})));
|
|
384
1278
|
}
|
|
385
1279
|
const flagsEnableCmd = makeSetStateCmd('enable', 'enabled');
|
|
386
1280
|
const flagsDisableCmd = makeSetStateCmd('disable', 'disabled');
|
|
387
1281
|
const flagsArchiveCmd = makeSetStateCmd('archive', 'archived');
|
|
388
|
-
|
|
1282
|
+
const bareFlagsCmd = Command.make('flags', { ...identityOptions, yes: yesOption }, (opts) => runHandler('flags', { json: opts.json }, Effect.gen(function* () {
|
|
1283
|
+
if (opts.json || isMachineOutput() || !isInteractive()) {
|
|
1284
|
+
return yield* failEff(opts.json, 'Bare `hs-x flags` requires an interactive terminal. Choose list, create, enable, disable, or archive.');
|
|
1285
|
+
}
|
|
1286
|
+
const action = yield* Effect.promise(() => selectFlagsAction());
|
|
1287
|
+
if (action === undefined)
|
|
1288
|
+
return yield* cancelEff();
|
|
1289
|
+
const { client, scope } = yield* resolveClient(opts);
|
|
1290
|
+
const reporter = yield* syncReporter;
|
|
1291
|
+
const operation = action === 'list'
|
|
1292
|
+
? () => runList({ argv: ['flags', 'list'], json: false, client, reporter })
|
|
1293
|
+
: action === 'create'
|
|
1294
|
+
? () => runCreateWizard({ client, scope, reporter, yes: opts.yes })
|
|
1295
|
+
: () => runSetState({
|
|
1296
|
+
argv: ['flags', action],
|
|
1297
|
+
json: false,
|
|
1298
|
+
client,
|
|
1299
|
+
state: action === 'enable'
|
|
1300
|
+
? 'enabled'
|
|
1301
|
+
: action === 'disable'
|
|
1302
|
+
? 'disabled'
|
|
1303
|
+
: 'archived',
|
|
1304
|
+
reporter,
|
|
1305
|
+
yes: opts.yes,
|
|
1306
|
+
});
|
|
1307
|
+
yield* withClientErrors(false, Effect.tryPromise({ try: operation, catch: (cause) => cause }).pipe(Effect.flatMap((result) => result.exitCode === 0 ? Effect.void : Effect.fail(exitWith(result.exitCode)))));
|
|
1308
|
+
})));
|
|
1309
|
+
export const flagsCmd = bareFlagsCmd.pipe(Command.withSubcommands([
|
|
389
1310
|
flagsListCmd,
|
|
390
1311
|
flagsCreateCmd,
|
|
391
1312
|
flagsDefineCmd,
|