@fabrika/provider-zerops 0.3.1 → 0.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fabrika/provider-zerops",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,11 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "zod": "^3.25.76",
46
- "@fabrika/auth": "^0.3.1",
47
- "@fabrika/auth-core": "^0.3.1",
48
- "@fabrika/installation-init": "^0.3.1",
49
- "@fabrika/provider-contract": "^0.3.1",
50
- "@fabrika/proxy-core": "^0.3.1",
46
+ "@fabrika/auth": "^0.3.3",
47
+ "@fabrika/auth-core": "^0.3.3",
48
+ "@fabrika/installation-init": "^0.3.3",
49
+ "@fabrika/provider-contract": "^0.3.3",
50
+ "@fabrika/proxy-core": "^0.3.3",
51
51
  "yaml": "^2.9.1"
52
52
  }
53
53
  }
package/src/cli-args.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { describeArgument } from '@fabrika/installation-init'
1
2
  export interface ZeropsCliArgs {
2
3
  command: string | undefined
3
4
  subcommand: string | undefined
@@ -71,7 +72,7 @@ export const parseZeropsCliArgs = (argv: readonly string[]): ZeropsCliArgs => {
71
72
  } else if (subcommand === undefined && command === 'namespace') {
72
73
  subcommand = arg
73
74
  } else {
74
- throw new Error(`Unexpected argument: ${arg}`)
75
+ throw new Error(`Unexpected argument: ${describeArgument(arg)}`)
75
76
  }
76
77
  }
77
78
 
package/src/descriptor.ts CHANGED
@@ -128,8 +128,52 @@ export const selectZeropsDescriptorSetup = (parsed: ZeropsDescriptor, setup?: st
128
128
  return selected
129
129
  }
130
130
 
131
+ const inheritanceSchema = z.object({
132
+ zerops: z.array(z.intersection(
133
+ descriptorMappingSchema,
134
+ z.object({ setup: z.string(), extends: z.union([z.string(), z.array(z.string())]).optional() }),
135
+ )),
136
+ })
137
+
138
+ /**
139
+ * The setup entries a deploy of `selected` applies: the selected one and, transitively, every setup it
140
+ * `extends`. Only these reach the running service; the other setups in the file never do.
141
+ */
142
+ export const appliedZeropsSetups = (parsed: ZeropsDescriptor, selected: string): JsonValue[] => {
143
+ const document = inheritanceSchema.safeParse(parsed.document)
144
+ if (!document.success) return invalid('requires extends to name setups')
145
+ const applied: JsonValue[] = []
146
+ const visit = (name: string): void => {
147
+ const entry = document.data.zerops.find((candidate) => candidate.setup === name)
148
+ if (entry === undefined) return invalid(`extends unknown setup \`${name}\``)
149
+ if (applied.includes(entry)) return
150
+ applied.push(entry)
151
+ for (const parent of typeof entry.extends === 'string' ? [entry.extends] : entry.extends ?? []) visit(parent)
152
+ }
153
+ visit(selected)
154
+ return applied
155
+ }
156
+
157
+ /**
158
+ * Refuse a selected setup that `extends` another without its own `run`: Zerops merges `extends` per
159
+ * whole section, and the deploy writes `FABRIKA_RELEASE` into `run`, which would replace the inherited one.
160
+ */
161
+ export const assertOwnRunWhenExtending = (parsed: ZeropsDescriptor, selected: string): void => {
162
+ const [entry] = appliedZeropsSetups(parsed, selected)
163
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return
164
+ if (entry['extends'] !== undefined && entry['run'] === undefined) {
165
+ invalid(`setup \`${selected}\` extends another setup, so it must declare its own run section — the deploy writes FABRIKA_RELEASE into it`)
166
+ }
167
+ }
168
+
169
+ /** The document Zerops executes: the selected setup and what it extends, which is exactly what the ownership check read. */
170
+ export const executionZeropsDocument = (parsed: ZeropsDescriptor, selected: string): { readonly [key: string]: JsonValue } => ({
171
+ ...parsed.document,
172
+ zerops: appliedZeropsSetups(parsed, selected),
173
+ })
174
+
131
175
  export const prepareZeropsDescriptor = (contents: string, setup?: string): PreparedZeropsDescriptor => {
132
176
  const parsed = parseZeropsDescriptor(contents)
133
177
  const selected = selectZeropsDescriptorSetup(parsed, setup)
134
- return { contents: renderYaml(parsed.document), setup: selected }
178
+ return { contents: renderYaml(executionZeropsDocument(parsed, selected)), setup: selected }
135
179
  }
@@ -8,6 +8,7 @@
8
8
  // and prompts for nothing — and a missing required value is an error naming both the flag and the
9
9
  // variable that would supply it.
10
10
 
11
+ import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
11
12
  import { parsePlatformHostname, parsePlatformScheme } from './placement-options'
12
13
 
13
14
  /** Every input `runPlatformAdmin` takes from the operator. */
@@ -36,35 +37,6 @@ const FLAGS = ['--email', '--iam-host', '--scheme'] as const
36
37
  */
37
38
  export const REISSUE_FLAG = '--reissue'
38
39
 
39
- const readFlag = (argv: readonly string[], name: string): string | undefined => {
40
- const prefix = `${name}=`
41
- const matches = argv.filter((arg) => arg.startsWith(prefix))
42
- if (matches.length > 1) {
43
- throw new Error(`${name} was given more than once`)
44
- }
45
- const value = matches[0]?.slice(prefix.length)
46
- return value === undefined || value.trim() === '' ? undefined : value.trim()
47
- }
48
-
49
- const readEnv = (env: Record<string, string | undefined>, name: string): string | undefined => {
50
- const value = env[name]
51
- return value === undefined || value.trim() === '' ? undefined : value.trim()
52
- }
53
-
54
- const setting = (
55
- argv: readonly string[],
56
- env: Record<string, string | undefined>,
57
- flag: string,
58
- variable: string,
59
- ): string | undefined => readFlag(argv, flag) ?? readEnv(env, variable)
60
-
61
- const required = (value: string | undefined, flag: string, variable: string): string => {
62
- if (value === undefined) {
63
- throw new Error(`${flag}=<value> or ${variable} is required`)
64
- }
65
- return value
66
- }
67
-
68
40
  const assertKnownArguments = (argv: readonly string[]): void => {
69
41
  for (const arg of argv) {
70
42
  if (arg === REISSUE_FLAG) {
@@ -73,7 +45,7 @@ const assertKnownArguments = (argv: readonly string[]): void => {
73
45
  const name = arg.split('=')[0] ?? arg
74
46
  if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
75
47
  throw new Error(
76
- `unexpected argument \`${arg}\`. Credentials are read from the environment and have no flag; run `
48
+ `unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
77
49
  + '`fabrika platform admin --provider=zerops --help` for the surface',
78
50
  )
79
51
  }
@@ -92,7 +64,7 @@ const readScheme = (argv: readonly string[], env: Record<string, string | undefi
92
64
  * composed from it and `--scheme`, so a value carrying either would compose a second one.
93
65
  */
94
66
  const readIamHost = (argv: readonly string[], env: Record<string, string | undefined>): string => {
95
- const host = required(setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'), '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST')
67
+ const host = requiredSetting(setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'), '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST')
96
68
  return parsePlatformHostname(host)
97
69
  }
98
70
 
@@ -119,7 +91,7 @@ export const parsePlatformAdminArgs = (
119
91
  ): PlatformAdminInput => {
120
92
  assertKnownArguments(argv)
121
93
  return {
122
- email: required(setting(argv, env, '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'), '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'),
94
+ email: requiredSetting(setting(argv, env, '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'), '--email', 'FABRIKA_PLATFORM_ADMIN_EMAIL'),
123
95
  iamHost: readIamHost(argv, env),
124
96
  scheme: readScheme(argv, env),
125
97
  provisioningKey: provisioningKey(env),
@@ -12,6 +12,7 @@
12
12
  // The command is unattended by construction: nothing here prompts, and a missing required value is an
13
13
  // error naming both the flag and the variable that would supply it.
14
14
 
15
+ import { describeArgument, readEnv, requiredSetting, setting } from '@fabrika/installation-init'
15
16
  import { parsePlatformHosts, parsePlatformScheme } from './placement-options'
16
17
  import { readInstallationSourceDir } from './source-options'
17
18
 
@@ -52,35 +53,6 @@ const FLAGS = [
52
53
  '--source-dir',
53
54
  ] as const
54
55
 
55
- const readFlag = (argv: readonly string[], name: string): string | undefined => {
56
- const prefix = `${name}=`
57
- const matches = argv.filter((arg) => arg.startsWith(prefix))
58
- if (matches.length > 1) {
59
- throw new Error(`${name} was given more than once`)
60
- }
61
- const value = matches[0]?.slice(prefix.length)
62
- return value === undefined || value.trim() === '' ? undefined : value.trim()
63
- }
64
-
65
- const readEnv = (env: Record<string, string | undefined>, name: string): string | undefined => {
66
- const value = env[name]
67
- return value === undefined || value.trim() === '' ? undefined : value.trim()
68
- }
69
-
70
- const setting = (
71
- argv: readonly string[],
72
- env: Record<string, string | undefined>,
73
- flag: string,
74
- variable: string,
75
- ): string | undefined => readFlag(argv, flag) ?? readEnv(env, variable)
76
-
77
- const required = (value: string | undefined, flag: string, variable: string): string => {
78
- if (value === undefined) {
79
- throw new Error(`${flag}=<value> or ${variable} is required`)
80
- }
81
- return value
82
- }
83
-
84
56
  const assertKnownArguments = (argv: readonly string[]): void => {
85
57
  for (const arg of argv) {
86
58
  if (arg === '--dry-run') {
@@ -89,7 +61,7 @@ const assertKnownArguments = (argv: readonly string[]): void => {
89
61
  const name = arg.split('=')[0] ?? arg
90
62
  if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
91
63
  throw new Error(
92
- `unexpected argument \`${arg}\`. Credentials are read from the environment and have no flag; run `
64
+ `unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
93
65
  + '`fabrika platform deploy --provider=zerops --help` for the surface',
94
66
  )
95
67
  }
@@ -123,7 +95,7 @@ const readProject = (argv: readonly string[], env: Record<string, string | undef
123
95
  if (name === undefined) {
124
96
  throw new Error('--project-id=<id> or FABRIKA_ZEROPS_PROJECT_ID is required (or --project-name with --client-id)')
125
97
  }
126
- const clientId = required(setting(argv, env, '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID'), '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID')
98
+ const clientId = requiredSetting(setting(argv, env, '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID'), '--client-id', 'FABRIKA_ZEROPS_CLIENT_ID')
127
99
  return { kind: 'name', clientId, name }
128
100
  }
129
101
 
@@ -140,12 +112,12 @@ export const parsePlatformDeployArgs = (
140
112
  return {
141
113
  project: readProject(argv, env),
142
114
  // A dry run reads, and reading still needs a token; it is the one credential it cannot do without.
143
- accessToken: required(readEnv(env, 'FABRIKA_ZEROPS_ACCESS_TOKEN'), '(no flag)', 'FABRIKA_ZEROPS_ACCESS_TOKEN'),
144
- environment: required(setting(argv, env, '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'), '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'),
115
+ accessToken: requiredSetting(readEnv(env, 'FABRIKA_ZEROPS_ACCESS_TOKEN'), '(no flag)', 'FABRIKA_ZEROPS_ACCESS_TOKEN'),
116
+ environment: requiredSetting(setting(argv, env, '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'), '--env', 'FABRIKA_PLATFORM_ENVIRONMENT'),
145
117
  // Not required for a dry run: it authenticates a write this mode never makes.
146
118
  iamAdminKey: dryRun
147
119
  ? readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY') ?? ''
148
- : required(readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY'), '(no flag)', 'FABRIKA_IAM_PROVISIONING_KEY'),
120
+ : requiredSetting(readEnv(env, 'FABRIKA_IAM_PROVISIONING_KEY'), '(no flag)', 'FABRIKA_IAM_PROVISIONING_KEY'),
149
121
  scheme: readScheme(argv, env),
150
122
  dryRun,
151
123
  ...(hosts === undefined ? {} : { hosts }),
@@ -1,5 +1,12 @@
1
1
  import { reconcileSchema } from '@fabrika/auth'
2
- import { info as consoleInfo, ok as consoleOk, step as consoleStep, warn as consoleWarn } from '@fabrika/installation-init'
2
+ import {
3
+ describeArgument,
4
+ info as consoleInfo,
5
+ ok as consoleOk,
6
+ step as consoleStep,
7
+ UNATTENDED_FLAG,
8
+ warn as consoleWarn,
9
+ } from '@fabrika/installation-init'
3
10
  import type { SchemaReconciler } from '@fabrika/provider-contract'
4
11
  import type { InstallationCli, InstallationCommand } from '@fabrika/provider-contract/installation'
5
12
  import { createZeropsApi, defaultSleep } from '../index'
@@ -7,9 +14,10 @@ import { consoleAdminCollaborators, runPlatformAdmin } from './admin'
7
14
  import { parsePlatformAdminArgs, REISSUE_FLAG } from './admin-options'
8
15
  import { deployPlatform, PLATFORM_CONCURRENT_DEPLOY, PLATFORM_DEPLOY_ORDER, PLATFORM_SEQUENTIAL_DEPLOY } from './deploy'
9
16
  import { parsePlatformDeployArgs } from './deploy-options'
10
- import { consoleInitCollaborators, parseInitArgs, runInit } from './init'
17
+ import { consoleInitCollaborators, runInit } from './init'
18
+ import { parseInitArgs } from './init-options'
11
19
  import { consoleInstallCollaborators, runInstall } from './install'
12
- import { parsePlatformInstallArgs, UNATTENDED_FLAG } from './install-options'
20
+ import { parsePlatformInstallArgs } from './install-options'
13
21
  import { prepareInstallationSource } from './installation-source'
14
22
  import { consoleDeployLog } from './log'
15
23
  import { generatedArtifacts } from './topology/artifacts'
@@ -34,7 +42,7 @@ const USAGE = `Zerops installation
34
42
 
35
43
  Commands:
36
44
  fabrika platform install --provider=zerops [options]
37
- fabrika platform init --provider=zerops <installation> [--repo=<owner>/<name>]
45
+ fabrika platform init --provider=zerops <installation> --tag=<tag> --project-id=<id> [options]
38
46
  fabrika platform plan --provider=zerops
39
47
  fabrika platform deploy --provider=zerops [options]
40
48
  fabrika platform admin --provider=zerops --email=<address> --iam-host=<host> [--scheme=<http|https>] [${REISSUE_FLAG}]
@@ -109,11 +117,12 @@ Creates and maintains the operator's SIDECAR REPOSITORY: the GitHub pipeline tha
109
117
  credentials. ADR-0025 puts that pipeline in a repository the operator owns — fabrika ships the
110
118
  generator, never the pipeline, because one public repository cannot hold every account's credentials.
111
119
 
112
- Interactive and laptop-side. It does the whole job and CONFIRMS before every step that leaves the
113
- operator's disk: reading the project, reading its control and proxy services, creating or pushing the
114
- repository, writing the Environment, and triggering the run.
115
- Declining a write stops there and prints what to run instead; a re-run is safe. Declining the read of
116
- control and proxy only leaves the console and IAM origins out of what init prints.
120
+ Laptop-side and prompt-free: every value comes from a flag or the environment, so a pipeline or an
121
+ agent runs it end to end. It CONFIRMS before every step that leaves the operator's disk: reading the
122
+ project, reading its control and proxy services, creating or pushing the repository, writing the
123
+ Environment, and triggering the run. A stdin that is not a terminal is refused unless \`${UNATTENDED_FLAG}\` is given.
124
+ Declining a write stops there and prints the command that repeats the run; a re-run is safe. Declining
125
+ the read of control and proxy only leaves the console and IAM origins out of what init prints.
117
126
 
118
127
  Init writes nothing to Zerops. It reads control and proxy only to print the console and IAM origins
119
128
  the installation records, and prints none it cannot verify. Public repositories deploy without a
@@ -129,16 +138,30 @@ file afterwards.
129
138
  <installation> names this installation: the GitHub Environment, the default
130
139
  repository \`contember/fabrika-zerops-<installation>\`, and the
131
140
  local checkout directory
132
- --repo=<owner>/<name> a different sidecar repository
141
+
142
+ Options (a flag beats the environment variable beside it):
143
+
144
+ --repo=<owner>/<name> a different sidecar repository
145
+ --checkout=<dir> the sidecar's local checkout, default
146
+ ./fabrika-zerops-<installation>; refused
147
+ inside another git repository
148
+ --tag=<tag> FABRIKA_PLATFORM_TAG required; the published fabrika tag to pin
149
+ --project-id=<id> FABRIKA_ZEROPS_PROJECT_ID required; the project holding the platform
150
+ --env=<name> FABRIKA_PLATFORM_ENVIRONMENT default <installation>
151
+ --iam-host=<host> FABRIKA_PLATFORM_IAM_HOST custom domains: all three hosts or none;
152
+ --console-host=<host> FABRIKA_PLATFORM_CONSOLE_HOST none derives them from the proxy's Zerops
153
+ --operations-host=<host> FABRIKA_PLATFORM_OPERATIONS_HOST subdomains
154
+ ${UNATTENDED_FLAG} answer every confirmation yes —
155
+ required when stdin is not a terminal
133
156
 
134
157
  It writes TWO secrets into that Environment and GENERATES NEITHER — both already belong to the
135
- installation, so a value invented here would not be one it accepts:
158
+ installation, so a value invented here would not be one it accepts. Both are environment only:
136
159
 
137
- FABRIKA_ZEROPS_ACCESS_TOKEN a Zerops INTEGRATION token scoped to this installation's projects
138
- FABRIKA_IAM_PROVISIONING_KEY the px_ admin key this installation's IAM was seeded with
160
+ FABRIKA_ZEROPS_ACCESS_TOKEN required the INTEGRATION token install minted (the \`control\` service's env)
161
+ FABRIKA_IAM_PROVISIONING_KEY required the px_ key install printed
162
+ FABRIKA_ZEROPS_API_URL optional region API base, when not the default
139
163
 
140
- Each is read from a hidden prompt or from the environment variable of the same name, sent to GitHub
141
- over \`gh\` stdin, and never printed and never written to disk.
164
+ Each is sent to GitHub over \`gh\` stdin, and never printed and never written to disk.
142
165
 
143
166
  The generated pipeline pins a published TAG of contember/fabrika-platform and refuses a branch, at
144
167
  init and again in the workflow. It uploads that checkout's exact committed archive to every installation service.
@@ -273,7 +296,7 @@ const neverAborted = (): AbortSignal => new AbortController().signal
273
296
 
274
297
  const runPlan = (argv: readonly string[]): void => {
275
298
  if (argv.length > 0) {
276
- throw new Error(`Unexpected Zerops plan arguments: ${argv.join(' ')}`)
299
+ throw new Error(`Unexpected Zerops plan arguments: ${argv.map(describeArgument).join(' ')}`)
277
300
  }
278
301
  const artifacts = generatedArtifacts()
279
302
  for (const artifact of artifacts) {
@@ -342,7 +365,8 @@ export const installationCli: InstallationCli = {
342
365
  return
343
366
  }
344
367
  if (command === 'init') {
345
- await runInit(parseInitArgs(argv), consoleInitCollaborators())
368
+ const input = parseInitArgs(argv, process.env)
369
+ await runInit(input, consoleInitCollaborators(input))
346
370
  return
347
371
  }
348
372
  if (command === 'plan') {
@@ -0,0 +1,148 @@
1
+ // The CLI surface of `fabrika platform init --provider=zerops`, parsed once.
2
+ //
3
+ // The same two rules `deploy-options.ts` states: every CREDENTIAL comes from the environment and only
4
+ // from the environment, and everything else may come from a flag OR the environment, flag first.
5
+ // Nothing here prompts — a missing required value is an error naming both the flag and the variable —
6
+ // so a pipeline or an agent drives init end to end. Its confirmations stay, answered by `--yes`.
7
+
8
+ import {
9
+ CHECKOUT_FLAG,
10
+ type CommandEnv,
11
+ describeArgument,
12
+ readEnv,
13
+ readFlag,
14
+ requiredSetting,
15
+ setting,
16
+ sidecarCheckoutDir,
17
+ UNATTENDED_FLAG,
18
+ } from '@fabrika/installation-init'
19
+ import { parsePlatformHosts } from './placement-options'
20
+ import { assertPinnedTag, defaultCheckoutName, defaultSidecarRepo } from './sidecar'
21
+
22
+ /** The name every bypass in this platform is gated on, and therefore the one name an installation cannot take. */
23
+ const LOCAL_ENVIRONMENT = 'local'
24
+
25
+ /** Every input `runInit` takes from the operator. */
26
+ export interface InitInput {
27
+ /** Names the GitHub Environment, the default repository and the local checkout directory. */
28
+ readonly installation: string
29
+ readonly repo: string
30
+ /** The sidecar's local checkout: `--checkout`, or `fabrika-zerops-<installation>` in the working directory. */
31
+ readonly checkoutDir: string
32
+ /** The published fabrika-platform tag the sidecar pins in `fabrika.ref`. */
33
+ readonly fabrikaRef: string
34
+ readonly projectId: string
35
+ /** The name this installation calls itself — written to every service as `ENVIRONMENT`. */
36
+ readonly environmentName: string
37
+ /** All three or none: none derives them from the proxy's Zerops subdomains. */
38
+ readonly hosts?: { readonly iam: string; readonly control: string; readonly operations: string }
39
+ /** Region API base, when the installation is not on the default one. */
40
+ readonly apiBaseUrl?: string
41
+ /** The Zerops INTEGRATION token `platform install` minted. Environment only. */
42
+ readonly accessToken: string
43
+ /** The `px_` key this installation's IAM was seeded with. Environment only. */
44
+ readonly provisioningKey: string
45
+ /** `--yes`: answer every confirmation yes. */
46
+ readonly unattended: boolean
47
+ }
48
+
49
+ const FLAGS = ['--repo', CHECKOUT_FLAG, '--tag', '--project-id', '--env', '--iam-host', '--console-host', '--operations-host'] as const
50
+
51
+ const REPO_PATTERN = /^[^/\s]+\/[^/\s]+$/
52
+
53
+ /**
54
+ * The installation's environment name, refused when it claims to be local.
55
+ *
56
+ * The same refusal the services enforce at boot: `local` is the value `localDevLogin`, the ephemeral
57
+ * signing key and the credential-less caller path are all gated on, and a deployed installation is
58
+ * never a loopback address.
59
+ */
60
+ export const checkedEnvironmentName = (declared: string): string => {
61
+ const trimmed = declared.trim()
62
+ if (trimmed === '') {
63
+ throw new Error('an installation environment name is required — it is written to every service as ENVIRONMENT')
64
+ }
65
+ if (trimmed === LOCAL_ENVIRONMENT) {
66
+ throw new Error(
67
+ `\`${LOCAL_ENVIRONMENT}\` is refused: it is the name every development bypass is gated on, and a deployed `
68
+ + 'installation is not a loopback address. Name it after the installation instead',
69
+ )
70
+ }
71
+ return trimmed
72
+ }
73
+
74
+ const assertKnownArguments = (argv: readonly string[]): void => {
75
+ for (const arg of argv) {
76
+ if (arg === UNATTENDED_FLAG || !arg.startsWith('-')) {
77
+ continue
78
+ }
79
+ const name = arg.split('=')[0] ?? arg
80
+ if (!FLAGS.some((flag) => flag === name) || !arg.includes('=')) {
81
+ throw new Error(
82
+ `unexpected argument ${describeArgument(arg)}. Credentials are read from the environment and have no flag; run `
83
+ + '`fabrika platform init --provider=zerops --help` for the surface',
84
+ )
85
+ }
86
+ }
87
+ }
88
+
89
+ const readInstallation = (argv: readonly string[]): string => {
90
+ const positional = argv.filter((arg) => !arg.startsWith('-')).map((arg) => arg.trim())
91
+ if (positional.length > 1) {
92
+ throw new Error('unexpected second positional argument: name ONE installation')
93
+ }
94
+ const installation = positional[0]
95
+ if (installation === undefined || installation === '') {
96
+ throw new Error('Zerops installation init requires an installation name, for example `fabrika platform init --provider=zerops test`')
97
+ }
98
+ return installation
99
+ }
100
+
101
+ const readRepo = (argv: readonly string[], installation: string): string => {
102
+ const repo = readFlag(argv, '--repo')
103
+ if (repo === undefined) {
104
+ return defaultSidecarRepo(installation)
105
+ }
106
+ if (!REPO_PATTERN.test(repo)) {
107
+ throw new Error(`\`${repo}\` is not a <owner>/<name> repository`)
108
+ }
109
+ return repo
110
+ }
111
+
112
+ /** A credential, with the one message that says why there is no flag for it. */
113
+ const credential = (env: CommandEnv, variable: string, what: string): string => {
114
+ const value = readEnv(env, variable)
115
+ if (value === undefined) {
116
+ throw new Error(`${variable} is required: ${what}. It has no flag — a credential on a command line lands in logs and shell history`)
117
+ }
118
+ return value
119
+ }
120
+
121
+ /** Parse the command's arguments and environment into the one input `runInit` takes. */
122
+ export const parseInitArgs = (argv: readonly string[], env: CommandEnv, cwd: string = process.cwd()): InitInput => {
123
+ assertKnownArguments(argv)
124
+ const installation = readInstallation(argv)
125
+ const hosts = parsePlatformHosts({
126
+ iam: setting(argv, env, '--iam-host', 'FABRIKA_PLATFORM_IAM_HOST'),
127
+ control: setting(argv, env, '--console-host', 'FABRIKA_PLATFORM_CONSOLE_HOST'),
128
+ operations: setting(argv, env, '--operations-host', 'FABRIKA_PLATFORM_OPERATIONS_HOST'),
129
+ })
130
+ const apiBaseUrl = readEnv(env, 'FABRIKA_ZEROPS_API_URL')
131
+ return {
132
+ installation,
133
+ repo: readRepo(argv, installation),
134
+ checkoutDir: sidecarCheckoutDir(readFlag(argv, CHECKOUT_FLAG), defaultCheckoutName(installation), cwd),
135
+ fabrikaRef: assertPinnedTag(requiredSetting(setting(argv, env, '--tag', 'FABRIKA_PLATFORM_TAG'), '--tag', 'FABRIKA_PLATFORM_TAG')),
136
+ projectId: requiredSetting(setting(argv, env, '--project-id', 'FABRIKA_ZEROPS_PROJECT_ID'), '--project-id', 'FABRIKA_ZEROPS_PROJECT_ID'),
137
+ environmentName: checkedEnvironmentName(setting(argv, env, '--env', 'FABRIKA_PLATFORM_ENVIRONMENT') ?? installation),
138
+ accessToken: credential(
139
+ env,
140
+ 'FABRIKA_ZEROPS_ACCESS_TOKEN',
141
+ "the Zerops INTEGRATION token `platform install` minted, read from the `control` service's environment",
142
+ ),
143
+ provisioningKey: credential(env, 'FABRIKA_IAM_PROVISIONING_KEY', 'the px_ key `platform install` printed'),
144
+ unattended: argv.includes(UNATTENDED_FLAG),
145
+ ...(hosts === undefined ? {} : { hosts }),
146
+ ...(apiBaseUrl === undefined ? {} : { apiBaseUrl }),
147
+ }
148
+ }