@hs-x/cli 0.2.7 → 0.3.6

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.
Files changed (285) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +25 -8
  3. package/dist/audit-interactive.d.ts +29 -0
  4. package/dist/audit-interactive.d.ts.map +1 -0
  5. package/dist/audit-interactive.js +156 -0
  6. package/dist/audit-interactive.js.map +1 -0
  7. package/dist/bin/hs-x.js.map +1 -1
  8. package/dist/checkpoint-interactive.d.ts +24 -0
  9. package/dist/checkpoint-interactive.d.ts.map +1 -0
  10. package/dist/checkpoint-interactive.js +65 -0
  11. package/dist/checkpoint-interactive.js.map +1 -0
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +266 -39
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/kit.d.ts +1 -1
  16. package/dist/cli/kit.d.ts.map +1 -1
  17. package/dist/cli/kit.js +19 -3
  18. package/dist/cli/kit.js.map +1 -1
  19. package/dist/cloudflare-auth.d.ts +8 -0
  20. package/dist/cloudflare-auth.d.ts.map +1 -1
  21. package/dist/cloudflare-auth.js +12 -2
  22. package/dist/cloudflare-auth.js.map +1 -1
  23. package/dist/cloudflare-oauth-store.d.ts +9 -1
  24. package/dist/cloudflare-oauth-store.d.ts.map +1 -1
  25. package/dist/cloudflare-oauth-store.js +23 -4
  26. package/dist/cloudflare-oauth-store.js.map +1 -1
  27. package/dist/cloudflare-oauth.d.ts +1 -0
  28. package/dist/cloudflare-oauth.d.ts.map +1 -1
  29. package/dist/cloudflare-oauth.js +6 -2
  30. package/dist/cloudflare-oauth.js.map +1 -1
  31. package/dist/commands/account.d.ts +28 -13
  32. package/dist/commands/account.d.ts.map +1 -1
  33. package/dist/commands/account.js +219 -69
  34. package/dist/commands/account.js.map +1 -1
  35. package/dist/commands/api.d.ts.map +1 -1
  36. package/dist/commands/api.js +7 -11
  37. package/dist/commands/api.js.map +1 -1
  38. package/dist/commands/completion.d.ts.map +1 -1
  39. package/dist/commands/completion.js +13 -7
  40. package/dist/commands/completion.js.map +1 -1
  41. package/dist/commands/connect.d.ts.map +1 -1
  42. package/dist/commands/connect.js +239 -125
  43. package/dist/commands/connect.js.map +1 -1
  44. package/dist/commands/control-plane-read.d.ts +71 -10
  45. package/dist/commands/control-plane-read.d.ts.map +1 -1
  46. package/dist/commands/control-plane-read.js +1035 -58
  47. package/dist/commands/control-plane-read.js.map +1 -1
  48. package/dist/commands/deploy-promote.d.ts +25 -3
  49. package/dist/commands/deploy-promote.d.ts.map +1 -1
  50. package/dist/commands/deploy-promote.js +314 -38
  51. package/dist/commands/deploy-promote.js.map +1 -1
  52. package/dist/commands/deploy.d.ts +69 -4
  53. package/dist/commands/deploy.d.ts.map +1 -1
  54. package/dist/commands/deploy.js +1512 -276
  55. package/dist/commands/deploy.js.map +1 -1
  56. package/dist/commands/dev.d.ts +5 -1
  57. package/dist/commands/dev.d.ts.map +1 -1
  58. package/dist/commands/dev.js +502 -107
  59. package/dist/commands/dev.js.map +1 -1
  60. package/dist/commands/doctor.d.ts +14 -0
  61. package/dist/commands/doctor.d.ts.map +1 -1
  62. package/dist/commands/doctor.js +131 -20
  63. package/dist/commands/doctor.js.map +1 -1
  64. package/dist/commands/flags.d.ts +108 -6
  65. package/dist/commands/flags.d.ts.map +1 -1
  66. package/dist/commands/flags.js +996 -75
  67. package/dist/commands/flags.js.map +1 -1
  68. package/dist/commands/help-command.d.ts +14 -0
  69. package/dist/commands/help-command.d.ts.map +1 -1
  70. package/dist/commands/help-command.js +144 -44
  71. package/dist/commands/help-command.js.map +1 -1
  72. package/dist/commands/history.js +14 -10
  73. package/dist/commands/history.js.map +1 -1
  74. package/dist/commands/init.d.ts +4 -2
  75. package/dist/commands/init.d.ts.map +1 -1
  76. package/dist/commands/init.js +98 -17
  77. package/dist/commands/init.js.map +1 -1
  78. package/dist/commands/link.d.ts +2 -3
  79. package/dist/commands/link.d.ts.map +1 -1
  80. package/dist/commands/link.js +201 -101
  81. package/dist/commands/link.js.map +1 -1
  82. package/dist/commands/login.d.ts.map +1 -1
  83. package/dist/commands/login.js +22 -5
  84. package/dist/commands/login.js.map +1 -1
  85. package/dist/commands/migrate.d.ts +37 -17
  86. package/dist/commands/migrate.d.ts.map +1 -1
  87. package/dist/commands/migrate.js +500 -73
  88. package/dist/commands/migrate.js.map +1 -1
  89. package/dist/commands/rollback.d.ts +42 -2
  90. package/dist/commands/rollback.d.ts.map +1 -1
  91. package/dist/commands/rollback.js +422 -58
  92. package/dist/commands/rollback.js.map +1 -1
  93. package/dist/commands/secrets.d.ts +76 -3
  94. package/dist/commands/secrets.d.ts.map +1 -1
  95. package/dist/commands/secrets.js +328 -41
  96. package/dist/commands/secrets.js.map +1 -1
  97. package/dist/commands/status.d.ts +13 -2
  98. package/dist/commands/status.d.ts.map +1 -1
  99. package/dist/commands/status.js +154 -17
  100. package/dist/commands/status.js.map +1 -1
  101. package/dist/commands/unlink.d.ts +2 -2
  102. package/dist/commands/unlink.d.ts.map +1 -1
  103. package/dist/commands/unlink.js +2 -2
  104. package/dist/commands/unlink.js.map +1 -1
  105. package/dist/commands/update.d.ts.map +1 -1
  106. package/dist/commands/update.js +4 -3
  107. package/dist/commands/update.js.map +1 -1
  108. package/dist/commands/validate.d.ts +12 -3
  109. package/dist/commands/validate.d.ts.map +1 -1
  110. package/dist/commands/validate.js +70 -8
  111. package/dist/commands/validate.js.map +1 -1
  112. package/dist/constants.d.ts +1 -1
  113. package/dist/constants.js +1 -1
  114. package/dist/control-plane-fetch.d.ts.map +1 -1
  115. package/dist/control-plane-fetch.js +5 -4
  116. package/dist/control-plane-fetch.js.map +1 -1
  117. package/dist/deploy-revision-capture.d.ts +48 -0
  118. package/dist/deploy-revision-capture.d.ts.map +1 -0
  119. package/dist/deploy-revision-capture.js +125 -0
  120. package/dist/deploy-revision-capture.js.map +1 -0
  121. package/dist/dev/event-bus.d.ts +5 -2
  122. package/dist/dev/event-bus.d.ts.map +1 -1
  123. package/dist/dev/event-bus.js +4 -1
  124. package/dist/dev/event-bus.js.map +1 -1
  125. package/dist/dev/hubspot-logs-poller.d.ts +35 -0
  126. package/dist/dev/hubspot-logs-poller.d.ts.map +1 -0
  127. package/dist/dev/hubspot-logs-poller.js +94 -0
  128. package/dist/dev/hubspot-logs-poller.js.map +1 -0
  129. package/dist/dev/invoke.d.ts +9 -1
  130. package/dist/dev/invoke.d.ts.map +1 -1
  131. package/dist/dev/invoke.js +69 -20
  132. package/dist/dev/invoke.js.map +1 -1
  133. package/dist/dev/keyboard.d.ts +31 -0
  134. package/dist/dev/keyboard.d.ts.map +1 -0
  135. package/dist/dev/keyboard.js +143 -0
  136. package/dist/dev/keyboard.js.map +1 -0
  137. package/dist/dev/log-server.d.ts +7 -0
  138. package/dist/dev/log-server.d.ts.map +1 -1
  139. package/dist/dev/log-server.js +68 -31
  140. package/dist/dev/log-server.js.map +1 -1
  141. package/dist/dev/migrated-card.d.ts +17 -0
  142. package/dist/dev/migrated-card.d.ts.map +1 -0
  143. package/dist/dev/migrated-card.js +114 -0
  144. package/dist/dev/migrated-card.js.map +1 -0
  145. package/dist/dev/stream-filter.d.ts +40 -0
  146. package/dist/dev/stream-filter.d.ts.map +1 -0
  147. package/dist/dev/stream-filter.js +93 -0
  148. package/dist/dev/stream-filter.js.map +1 -0
  149. package/dist/dev/stream-renderer.d.ts +34 -0
  150. package/dist/dev/stream-renderer.d.ts.map +1 -1
  151. package/dist/dev/stream-renderer.js +222 -8
  152. package/dist/dev/stream-renderer.js.map +1 -1
  153. package/dist/errors-registry.d.ts.map +1 -1
  154. package/dist/errors-registry.js +280 -36
  155. package/dist/errors-registry.js.map +1 -1
  156. package/dist/errors.d.ts.map +1 -1
  157. package/dist/errors.js.map +1 -1
  158. package/dist/help.d.ts.map +1 -1
  159. package/dist/help.js +8 -0
  160. package/dist/help.js.map +1 -1
  161. package/dist/hubspot-app-logs.d.ts +76 -0
  162. package/dist/hubspot-app-logs.d.ts.map +1 -0
  163. package/dist/hubspot-app-logs.js +323 -0
  164. package/dist/hubspot-app-logs.js.map +1 -0
  165. package/dist/hubspot-auth.d.ts +8 -2
  166. package/dist/hubspot-auth.d.ts.map +1 -1
  167. package/dist/hubspot-auth.js +9 -9
  168. package/dist/hubspot-auth.js.map +1 -1
  169. package/dist/hubspot-developer-client.js +119 -11
  170. package/dist/hubspot-developer-client.js.map +1 -1
  171. package/dist/index.d.ts +1 -0
  172. package/dist/index.d.ts.map +1 -1
  173. package/dist/index.js +1 -0
  174. package/dist/index.js.map +1 -1
  175. package/dist/init/templates.d.ts +11 -1
  176. package/dist/init/templates.d.ts.map +1 -1
  177. package/dist/init/templates.js +228 -15
  178. package/dist/init/templates.js.map +1 -1
  179. package/dist/link-migration-journal.d.ts +131 -0
  180. package/dist/link-migration-journal.d.ts.map +1 -0
  181. package/dist/link-migration-journal.js +395 -0
  182. package/dist/link-migration-journal.js.map +1 -0
  183. package/dist/link-preflight.d.ts +78 -0
  184. package/dist/link-preflight.d.ts.map +1 -0
  185. package/dist/link-preflight.js +231 -0
  186. package/dist/link-preflight.js.map +1 -0
  187. package/dist/local-project-picker.d.ts +34 -0
  188. package/dist/local-project-picker.d.ts.map +1 -0
  189. package/dist/local-project-picker.js +121 -0
  190. package/dist/local-project-picker.js.map +1 -0
  191. package/dist/logs-interactive.d.ts +37 -0
  192. package/dist/logs-interactive.d.ts.map +1 -0
  193. package/dist/logs-interactive.js +110 -0
  194. package/dist/logs-interactive.js.map +1 -0
  195. package/dist/output-context.d.ts +13 -0
  196. package/dist/output-context.d.ts.map +1 -0
  197. package/dist/output-context.js +46 -0
  198. package/dist/output-context.js.map +1 -0
  199. package/dist/project-context.d.ts +55 -0
  200. package/dist/project-context.d.ts.map +1 -0
  201. package/dist/project-context.js +115 -0
  202. package/dist/project-context.js.map +1 -0
  203. package/dist/project-picker.d.ts +65 -0
  204. package/dist/project-picker.d.ts.map +1 -0
  205. package/dist/project-picker.js +227 -0
  206. package/dist/project-picker.js.map +1 -0
  207. package/dist/prompt.d.ts +8 -0
  208. package/dist/prompt.d.ts.map +1 -1
  209. package/dist/prompt.js +81 -13
  210. package/dist/prompt.js.map +1 -1
  211. package/dist/reporter/human.d.ts +10 -2
  212. package/dist/reporter/human.d.ts.map +1 -1
  213. package/dist/reporter/human.js +82 -14
  214. package/dist/reporter/human.js.map +1 -1
  215. package/dist/reporter/index.d.ts.map +1 -1
  216. package/dist/reporter/index.js +7 -5
  217. package/dist/reporter/index.js.map +1 -1
  218. package/dist/reporter/json.d.ts +2 -1
  219. package/dist/reporter/json.d.ts.map +1 -1
  220. package/dist/reporter/json.js +10 -7
  221. package/dist/reporter/json.js.map +1 -1
  222. package/dist/reporter/style.d.ts +19 -0
  223. package/dist/reporter/style.d.ts.map +1 -1
  224. package/dist/reporter/style.js +126 -6
  225. package/dist/reporter/style.js.map +1 -1
  226. package/dist/reporter/types.d.ts +34 -4
  227. package/dist/reporter/types.d.ts.map +1 -1
  228. package/dist/reporter/types.js.map +1 -1
  229. package/dist/services/account-store.d.ts +3 -1
  230. package/dist/services/account-store.d.ts.map +1 -1
  231. package/dist/services/account-store.js +2 -1
  232. package/dist/services/account-store.js.map +1 -1
  233. package/dist/services/app-paths.d.ts.map +1 -1
  234. package/dist/services/app-paths.js +3 -1
  235. package/dist/services/app-paths.js.map +1 -1
  236. package/dist/services/cloudflare-auth.d.ts +1 -1
  237. package/dist/services/cloudflare-auth.d.ts.map +1 -1
  238. package/dist/services/cloudflare-auth.js +2 -2
  239. package/dist/services/cloudflare-auth.js.map +1 -1
  240. package/dist/services/cloudflare-kv.d.ts.map +1 -1
  241. package/dist/services/cloudflare-kv.js.map +1 -1
  242. package/dist/services/command-history.d.ts.map +1 -1
  243. package/dist/services/command-history.js.map +1 -1
  244. package/dist/services/config.d.ts.map +1 -1
  245. package/dist/services/config.js +1 -1
  246. package/dist/services/config.js.map +1 -1
  247. package/dist/services/control-plane.d.ts.map +1 -1
  248. package/dist/services/control-plane.js +3 -3
  249. package/dist/services/control-plane.js.map +1 -1
  250. package/dist/services/env-loader.d.ts.map +1 -1
  251. package/dist/services/env-loader.js +1 -3
  252. package/dist/services/env-loader.js.map +1 -1
  253. package/dist/services/http.d.ts.map +1 -1
  254. package/dist/services/http.js.map +1 -1
  255. package/dist/services/hubspot-dev-client.d.ts.map +1 -1
  256. package/dist/services/hubspot-dev-client.js +30 -0
  257. package/dist/services/hubspot-dev-client.js.map +1 -1
  258. package/dist/services/live.d.ts +2 -2
  259. package/dist/services/live.d.ts.map +1 -1
  260. package/dist/services/live.js +1 -1
  261. package/dist/services/live.js.map +1 -1
  262. package/dist/services/machine-id.d.ts.map +1 -1
  263. package/dist/services/machine-id.js.map +1 -1
  264. package/dist/services/reporter.d.ts +8 -5
  265. package/dist/services/reporter.d.ts.map +1 -1
  266. package/dist/services/reporter.js +6 -4
  267. package/dist/services/reporter.js.map +1 -1
  268. package/dist/services/tenant-state.d.ts.map +1 -1
  269. package/dist/services/tenant-state.js.map +1 -1
  270. package/dist/state-store.d.ts +4 -0
  271. package/dist/state-store.d.ts.map +1 -1
  272. package/dist/state-store.js +15 -0
  273. package/dist/state-store.js.map +1 -1
  274. package/dist/status-drilldown.d.ts +24 -0
  275. package/dist/status-drilldown.d.ts.map +1 -0
  276. package/dist/status-drilldown.js +50 -0
  277. package/dist/status-drilldown.js.map +1 -0
  278. package/dist/tenant-state.d.ts.map +1 -1
  279. package/dist/tenant-state.js +3 -2
  280. package/dist/tenant-state.js.map +1 -1
  281. package/dist/token-key-custody.d.ts +91 -0
  282. package/dist/token-key-custody.d.ts.map +1 -0
  283. package/dist/token-key-custody.js +375 -0
  284. package/dist/token-key-custody.js.map +1 -0
  285. package/package.json +14 -15
@@ -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 { loadTenantStateStore } from '../tenant-state.js';
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` the leaveable, direct-to-tenant flag authoring CLI.
36
- * It reads the per-install grant secret + InstallationKey scope a linked deploy
37
- * stashed in local state, then signs and POSTs to the deployed Worker's
38
- * `/_hsx/flags/define` and `/_hsx/flags/list` via {@link createFlagAuthoringClient}.
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 projectId = resolveFlag(argv, '--project-id') ?? process.env.HSX_PROJECT_ID;
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 appIdRaw = resolveFlag(argv, '--app-id') ?? process.env.HSX_APP_ID;
60
- const hubSpotAppId = appIdRaw ? Number(appIdRaw) : undefined;
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
- async function runSetState(input) {
174
- const key = resolveFlag(input.argv, '--key');
175
- if (!key)
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
- const existing = (await input.client.listFlags()).find((flag) => flag.key === key);
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 = (json) => Effect.sync(() => createReporter({ command: 'flags', argv: json ? ['--json'] : [] }));
988
+ const syncReporter = Effect.gen(function* () {
989
+ const reporter = yield* Reporter;
990
+ return reporter.implementation;
991
+ });
251
992
  /**
252
- * Resolve the {@link FlagAuthoringClient} from options + env (mirrors the legacy
253
- * preamble). Reuses `loadTenantStateStore`/`createFlagAuthoringClient`; identity
254
- * env fallbacks are read through `Config`. Cloudflare creds resolve from
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 resolveClient(opts) {
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').pipe(Config.withDefault('')).pipe(Effect.orDie);
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 projectId = Option.getOrElse(opts.projectId, () => (envProjectId.length > 0 ? envProjectId : undefined));
269
- const environment = Option.getOrElse(opts.environment, () => Option.getOrElse(opts.env, () => 'production'));
270
- const appIdRaw = Option.match(opts.appId, {
271
- onNone: () => (envAppId.length > 0 ? Number(envAppId) : undefined),
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. Deploy this project first with `hs-x deploy`.');
1028
+ return yield* failEff(json, 'No .hs-x/cloudflare.json found. Run `hs-x deploy` in this checkout first.');
296
1029
  }
297
- const binding = yield* Effect.tryPromise({
298
- try: () => context.stateStore.getBinding({
299
- projectId,
300
- environment,
301
- hubSpotAppId: hubSpotAppId,
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) => failEff(json, cause instanceof Error ? cause.message : String(cause))));
305
- if (!binding?.syncGrantSecretValue || !binding.tenantScopeAccountId) {
306
- return yield* failEff(json, `No flag-authoring credentials for ${projectId}/${environment} (app ${hubSpotAppId}). Re-deploy as a linked project to provision the tenant data plane.`);
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(opts.json);
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 ? ['flags', 'list', '--limit', String(limit)] : ['flags', 'list'],
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
- return yield* failEff(opts.json, 'Require --file <flag-definition.json> for `flags create`.');
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
- const reporter = yield* syncReporter(opts.json);
363
- yield* withClientErrors(opts.json, runCreateFromSource({ source: source.right, file, json: opts.json, client, scope, reporter }));
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.asVoid));
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
- export const flagsCmd = Command.make('flags').pipe(Command.withSubcommands([
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,