netlify-cli 16.9.2 → 17.0.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/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
3
  "description": "Netlify command line tool",
4
- "version": "16.9.2",
4
+ "version": "17.0.0",
5
5
  "author": "Netlify Inc.",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=16.16.0"
8
+ "node": ">=18.18.2"
9
9
  },
10
10
  "files": [
11
11
  "/bin",
@@ -44,13 +44,12 @@
44
44
  "dependencies": {
45
45
  "@bugsnag/js": "7.20.2",
46
46
  "@fastify/static": "6.10.2",
47
+ "@netlify/blobs": "^4.0.0",
47
48
  "@netlify/build": "29.23.4",
48
49
  "@netlify/build-info": "7.10.1",
49
50
  "@netlify/config": "20.9.0",
50
51
  "@netlify/edge-bundler": "9.4.1",
51
52
  "@netlify/local-functions-proxy": "1.1.1",
52
- "@netlify/sdk": "^1.1.5",
53
- "@netlify/serverless-functions-api": "1.10.0",
54
53
  "@netlify/zip-it-and-ship-it": "9.25.4",
55
54
  "@octokit/rest": "19.0.13",
56
55
  "ansi-escapes": "6.2.0",
@@ -149,7 +148,7 @@
149
148
  "update-notifier": "6.0.2",
150
149
  "uuid": "9.0.0",
151
150
  "wait-port": "1.0.4",
152
- "winston": "3.8.2",
153
- "write-file-atomic": "5.0.1"
151
+ "write-file-atomic": "5.0.1",
152
+ "zod": "^3.22.4"
154
153
  }
155
154
  }
@@ -74,7 +74,7 @@ const build = async (options, command) => {
74
74
  export const createBuildCommand = (program) =>
75
75
  program
76
76
  .command('build')
77
- .description('(Beta) Build on your local machine')
77
+ .description('Build on your local machine')
78
78
  .option(
79
79
  '--context <context>',
80
80
  'Specify a build context or branch (contexts: "production", "deploy-preview", "branch-deploy", "dev")',
@@ -50,7 +50,7 @@ export const createCompletionCommand = (program) => {
50
50
 
51
51
  return program
52
52
  .command('completion')
53
- .description('(Beta) Generate shell completion script\nRun this command to see instructions for your shell.')
53
+ .description('Generate shell completion script\nRun this command to see instructions for your shell.')
54
54
  .addExamples(['netlify completion:install'])
55
55
  .action((options, command) => {
56
56
  command.help()
@@ -3,6 +3,7 @@ import process from 'process'
3
3
 
4
4
  import { Option } from 'commander'
5
5
 
6
+ import { getBlobsContext } from '../../lib/blobs/blobs.mjs'
6
7
  import { promptEditorHelper } from '../../lib/edge-functions/editor-helper.mjs'
7
8
  import { startFunctionsServer } from '../../lib/functions/server.mjs'
8
9
  import { printBanner } from '../../utils/banner.mjs'
@@ -161,8 +162,15 @@ const dev = async (options, command) => {
161
162
  },
162
163
  })
163
164
 
165
+ const blobsContext = await getBlobsContext({
166
+ debug: options.debug,
167
+ projectRoot: command.workingDir,
168
+ siteID: site.id ?? 'unknown-site-id',
169
+ })
170
+
164
171
  const functionsRegistry = await startFunctionsServer({
165
172
  api,
173
+ blobsContext,
166
174
  command,
167
175
  config,
168
176
  debug: options.debug,
@@ -202,6 +210,7 @@ const dev = async (options, command) => {
202
210
 
203
211
  await startProxyServer({
204
212
  addonsUrls,
213
+ blobsContext,
205
214
  config,
206
215
  configPath: configPathOverride,
207
216
  debug: options.debug,
@@ -1,14 +1,11 @@
1
- /* eslint-disable import/extensions */
1
+ import fs from 'fs'
2
2
  import { resolve } from 'path'
3
3
  import { exit, env } from 'process'
4
4
 
5
- // eslint-disable-next-line n/no-missing-import
6
- import { getConfiguration } from '@netlify/sdk/cli-utils'
7
- // eslint-disable-next-line n/no-unpublished-import
8
- import fs from 'fs-extra'
9
5
  import inquirer from 'inquirer'
10
6
  import yaml from 'js-yaml'
11
7
  import fetch from 'node-fetch'
8
+ import { z } from 'zod'
12
9
 
13
10
  import { getBuildOptions } from '../../lib/build.mjs'
14
11
  import { getToken, chalk, log } from '../../utils/command-helpers.mjs'
@@ -176,7 +173,7 @@ export async function registerIntegration(workingDir, siteId, accountId, localIn
176
173
  })
177
174
 
178
175
  const filePath = resolve(workingDir, 'integration.yaml')
179
- await fs.writeFile(filePath, updatedIntegrationConfig)
176
+ await fs.promises.writeFile(filePath, updatedIntegrationConfig)
180
177
 
181
178
  log(chalk.yellow('Your integration.yaml file has been updated. Please commit and push these changes.'))
182
179
  }
@@ -304,12 +301,66 @@ export async function updateIntegration(
304
301
  })
305
302
 
306
303
  const filePath = resolve(workingDir, 'integration.yaml')
307
- await fs.writeFile(filePath, updatedIntegrationConfig)
304
+ await fs.promises.writeFile(filePath, updatedIntegrationConfig)
308
305
 
309
306
  log(chalk.yellow('Changes to the integration.yaml file are complete. Please commit and push these changes.'))
310
307
  }
311
308
  }
312
309
 
310
+ const possibleFiles = ['integration.yaml', 'integration.yml', 'integration.netlify.yaml', 'integration.netlify.yml']
311
+ const IntegrationConfigurationSchema = z.object({
312
+ name: z.string().optional(),
313
+ description: z.string().optional(),
314
+ slug: z.string().regex(/^[a-z\d-]+$/, 'slug must be lowercase with dashes'),
315
+ scopes: z
316
+ .object({
317
+ all: z.boolean().optional(),
318
+ site: z.array(z.enum(['read', 'write'])).optional(),
319
+ env: z.array(z.enum(['read', 'write', 'delete'])).optional(),
320
+ user: z.array(z.enum(['read', 'write'])).optional(),
321
+ })
322
+ .optional(),
323
+ integrationLevel: z.enum(['site', 'team', 'team-and-site']).optional(),
324
+ })
325
+
326
+ const getConfigurationFile = (workingDir) => {
327
+ const pwd = workingDir
328
+
329
+ const fileName = possibleFiles.find((configFileName) => fs.existsSync(resolve(pwd, configFileName)))
330
+
331
+ return fileName
332
+ }
333
+
334
+ export const getConfiguration = (workingDir) => {
335
+ const pwd = workingDir
336
+
337
+ const fileName = getConfigurationFile(workingDir)
338
+
339
+ if (!fileName) {
340
+ throw new Error('No configuration file found')
341
+ }
342
+
343
+ try {
344
+ const { config } = yaml.load(fs.readFileSync(resolve(pwd, fileName), 'utf-8'))
345
+
346
+ if (!config) {
347
+ throw new Error('No configuration found')
348
+ }
349
+
350
+ const parseResult = IntegrationConfigurationSchema.safeParse(config)
351
+
352
+ if (!parseResult.success) {
353
+ console.error(parseResult.error.message)
354
+ throw new Error('Invalid Configuration')
355
+ }
356
+
357
+ return config
358
+ } catch (error) {
359
+ console.error(error)
360
+ console.error(`No configuration found in ${fileName} in ${pwd}`)
361
+ exit(1)
362
+ }
363
+ }
313
364
  /**
314
365
  * The deploy command for Netlify Integrations
315
366
  * @param {import('commander').OptionValues} options
@@ -330,7 +381,7 @@ const deploy = async (options, command) => {
330
381
  // Confirm that a site is linked and that the user is logged in
331
382
  checkOptions(buildOptions)
332
383
 
333
- const { description, integrationLevel, name, scopes, slug } = await getConfiguration()
384
+ const { description, integrationLevel, name, scopes, slug } = await getConfiguration(command.workingDir)
334
385
  const localIntegrationConfig = { name, description, scopes, slug, integrationLevel }
335
386
 
336
387
  const { accountId } = await getSiteInformation({
@@ -394,4 +445,3 @@ export const createDeployCommand = (program) =>
394
445
  .option('-a, --auth <token>', 'Netlify auth token to deploy with', env.NETLIFY_AUTH_TOKEN)
395
446
  .option('-s, --site <name-or-id>', 'A site name or ID to deploy to', env.NETLIFY_SITE_ID)
396
447
  .action(deploy)
397
- /* eslint-enable import/extensions */
@@ -27,6 +27,6 @@ const recipesListCommand = async () => {
27
27
  export const createRecipesListCommand = (program) =>
28
28
  program
29
29
  .command('recipes:list')
30
- .description(`(Beta) List the recipes available to create and modify files in a project`)
30
+ .description(`List the recipes available to create and modify files in a project`)
31
31
  .addExamples(['netlify recipes:list'])
32
32
  .action(recipesListCommand)
@@ -81,7 +81,7 @@ export const createRecipesCommand = (program) => {
81
81
  program
82
82
  .command('recipes')
83
83
  .argument('[name]', 'name of the recipe')
84
- .description(`(Beta) Create and modify files in a project using pre-defined recipes`)
84
+ .description(`Create and modify files in a project using pre-defined recipes`)
85
85
  .option('-n, --name <name>', 'recipe name to use')
86
86
  .addExamples(['netlify recipes my-recipe', 'netlify recipes --name my-recipe'])
87
87
  .action(recipesCommand)
@@ -167,7 +167,7 @@ export const createServeCommand = (program) =>
167
167
  program
168
168
  .command('serve')
169
169
  .description(
170
- '(Beta) Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.',
170
+ 'Build the site for production and serve locally. This does not watch the code for changes, so if you need to rebuild your site then you must exit and run `serve` again.',
171
171
  )
172
172
  .option(
173
173
  '--context <context>',
@@ -0,0 +1,50 @@
1
+ import path from 'path'
2
+
3
+ import { BlobsServer } from '@netlify/blobs'
4
+ import { v4 as uuidv4 } from 'uuid'
5
+
6
+ import { getPathInProject } from '../settings.mjs'
7
+
8
+ /**
9
+ * @typedef BlobsContext
10
+ * @type {object}
11
+ * @property {string} edgeURL
12
+ * @property {string} deployID
13
+ * @property {string} siteID
14
+ * @property {string} token
15
+ */
16
+
17
+ /**
18
+ * Starts a local Blobs server and returns a context object that lets functions
19
+ * connect to it.
20
+ *
21
+ * @param {object} options
22
+ * @param {boolean} options.debug
23
+ * @param {string} options.projectRoot
24
+ * @param {string} options.siteID
25
+ * @returns {Promise<BlobsContext>}
26
+ */
27
+ export const getBlobsContext = async ({ debug, projectRoot, siteID }) => {
28
+ const token = uuidv4()
29
+ const { port } = await startBlobsServer({ debug, projectRoot, token })
30
+ const context = {
31
+ deployID: '0',
32
+ edgeURL: `http://localhost:${port}`,
33
+ siteID,
34
+ token,
35
+ }
36
+
37
+ return context
38
+ }
39
+
40
+ const startBlobsServer = async ({ debug, projectRoot, token }) => {
41
+ const directory = path.resolve(projectRoot, getPathInProject(['blobs']))
42
+ const server = new BlobsServer({
43
+ debug,
44
+ directory,
45
+ token,
46
+ })
47
+ const { port } = await server.start()
48
+
49
+ return { port }
50
+ }
@@ -1,5 +1,5 @@
1
1
  import { env } from 'process'
2
2
 
3
- const latestBootstrapURL = 'https://650bfd807b21ed000893e25c--edge.netlify.com/bootstrap/index-combined.ts'
3
+ const latestBootstrapURL = 'https://6539213a19a93a000876a033--edge.netlify.com/bootstrap/index-combined.ts'
4
4
 
5
5
  export const getBootstrapURL = () => env.NETLIFY_EDGE_BOOTSTRAP || latestBootstrapURL
@@ -2,6 +2,7 @@
2
2
  import { Buffer } from 'buffer'
3
3
 
4
4
  export const headers = {
5
+ BlobsInfo: 'x-nf-blobs-info',
5
6
  DeployID: 'x-nf-deploy-id',
6
7
  FeatureFlags: 'x-nf-feature-flags',
7
8
  ForwardedHost: 'x-forwarded-host',
@@ -66,6 +66,7 @@ export const createAccountInfoHeader = (accountInfo = {}) => {
66
66
  *
67
67
  * @param {object} config
68
68
  * @param {*} config.accountId
69
+ * @param {import("../blobs/blobs.mjs").BlobsContext} config.blobsContext
69
70
  * @param {*} config.config
70
71
  * @param {*} config.configPath
71
72
  * @param {*} config.debug
@@ -85,6 +86,7 @@ export const createAccountInfoHeader = (accountInfo = {}) => {
85
86
  */
86
87
  export const initializeProxy = async ({
87
88
  accountId,
89
+ blobsContext,
88
90
  config,
89
91
  configPath,
90
92
  debug,
@@ -151,6 +153,12 @@ export const initializeProxy = async ({
151
153
  req.headers[headers.Site] = createSiteInfoHeader(siteInfo)
152
154
  req.headers[headers.Account] = createAccountInfoHeader({ id: accountId })
153
155
 
156
+ if (blobsContext?.edgeURL && blobsContext?.token) {
157
+ req.headers[headers.BlobsInfo] = Buffer.from(
158
+ JSON.stringify({ url: blobsContext.edgeURL, token: blobsContext.token }),
159
+ ).toString('base64')
160
+ }
161
+
154
162
  await registry.initialize()
155
163
 
156
164
  const url = new URL(req.url, `http://${LOCAL_HOST}:${mainPort}`)
@@ -1,4 +1,5 @@
1
1
  // @ts-check
2
+ import { Buffer } from 'buffer'
2
3
  import { basename, extname } from 'path'
3
4
  import { version as nodeVersion } from 'process'
4
5
 
@@ -23,6 +24,7 @@ const getNextRun = function (schedule) {
23
24
 
24
25
  export default class NetlifyFunction {
25
26
  constructor({
27
+ blobsContext,
26
28
  config,
27
29
  directory,
28
30
  displayName,
@@ -34,6 +36,7 @@ export default class NetlifyFunction {
34
36
  timeoutBackground,
35
37
  timeoutSynchronous,
36
38
  }) {
39
+ this.blobsContext = blobsContext
37
40
  this.buildError = null
38
41
  this.config = config
39
42
  this.directory = directory
@@ -181,7 +184,7 @@ export default class NetlifyFunction {
181
184
  }
182
185
 
183
186
  // Invokes the function and returns its response object.
184
- async invoke(event, context) {
187
+ async invoke(event, context = {}) {
185
188
  await this.buildQueue
186
189
 
187
190
  if (this.buildError) {
@@ -189,10 +192,24 @@ export default class NetlifyFunction {
189
192
  }
190
193
 
191
194
  const timeout = this.isBackground ? this.timeoutBackground : this.timeoutSynchronous
195
+ const environment = {}
196
+
197
+ if (this.blobsContext) {
198
+ const payload = JSON.stringify({
199
+ url: this.blobsContext.edgeURL,
200
+ token: this.blobsContext.token,
201
+ })
202
+
203
+ context.custom = {
204
+ ...context?.custom,
205
+ blobs: Buffer.from(payload).toString('base64'),
206
+ }
207
+ }
192
208
 
193
209
  try {
194
210
  const result = await this.runtime.invokeFunction({
195
211
  context,
212
+ environment,
196
213
  event,
197
214
  func: this,
198
215
  timeout,
@@ -34,6 +34,7 @@ const ZIP_EXTENSION = '.zip'
34
34
 
35
35
  export class FunctionsRegistry {
36
36
  constructor({
37
+ blobsContext,
37
38
  capabilities,
38
39
  config,
39
40
  debug = false,
@@ -52,6 +53,13 @@ export class FunctionsRegistry {
52
53
  this.timeouts = timeouts
53
54
  this.settings = settings
54
55
 
56
+ /**
57
+ * Context object for Netlify Blobs
58
+ *
59
+ * @type {import("../blobs/blobs.mjs").BlobsContext}
60
+ */
61
+ this.blobsContext = blobsContext
62
+
55
63
  /**
56
64
  * An object to be shared among all functions in the registry. It can be
57
65
  * used to cache the results of the build function — e.g. it's used in
@@ -493,6 +501,7 @@ export class FunctionsRegistry {
493
501
  }
494
502
 
495
503
  const func = new NetlifyFunction({
504
+ blobsContext: this.blobsContext,
496
505
  config: this.config,
497
506
  directory: directories.find((directory) => mainFile.startsWith(directory)),
498
507
  mainFile,
@@ -51,13 +51,14 @@ export const getBuildFunction = async ({ config, directory, errorExit, func, pro
51
51
 
52
52
  const workerURL = new URL('worker.mjs', import.meta.url)
53
53
 
54
- export const invokeFunction = async ({ context, event, func, timeout }) => {
54
+ export const invokeFunction = async ({ context, environment, event, func, timeout }) => {
55
55
  if (func.buildData.runtimeAPIVersion !== 2) {
56
56
  return await invokeFunctionDirectly({ context, event, func, timeout })
57
57
  }
58
58
 
59
59
  const workerData = {
60
60
  clientContext: JSON.stringify(context),
61
+ environment,
61
62
  event,
62
63
  // If a function builder has defined a `buildPath` property, we use it.
63
64
  // Otherwise, we'll invoke the function's main file.
@@ -1,4 +1,5 @@
1
1
  import { createServer } from 'net'
2
+ import process from 'process'
2
3
  import { isMainThread, workerData, parentPort } from 'worker_threads'
3
4
 
4
5
  import { isStream } from 'is-stream'
@@ -13,7 +14,12 @@ sourceMapSupport.install()
13
14
 
14
15
  lambdaLocal.getLogger().level = 'alert'
15
16
 
16
- const { clientContext, entryFilePath, event, timeoutMs } = workerData
17
+ const { clientContext, entryFilePath, environment = {}, event, timeoutMs } = workerData
18
+
19
+ // Injecting into the environment any properties passed in by the parent.
20
+ for (const key in environment) {
21
+ process.env[key] = environment[key]
22
+ }
17
23
 
18
24
  const lambdaFunc = await import(entryFilePath)
19
25
 
@@ -137,6 +137,7 @@ export const createHandler = function (options) {
137
137
  'client-ip': [remoteAddress],
138
138
  'x-nf-client-connection-ip': [remoteAddress],
139
139
  'x-nf-account-id': [options.accountId],
140
+ 'x-nf-site-id': [options?.siteInfo?.id],
140
141
  [efHeaders.Geo]: Buffer.from(JSON.stringify(geoLocation)).toString('base64'),
141
142
  }).reduce((prev, [key, value]) => ({ ...prev, [key]: Array.isArray(value) ? value : [value] }), {})
142
143
  const rawQuery = new URLSearchParams(requestQuery).toString()
@@ -245,6 +246,7 @@ const getFunctionsServer = (options) => {
245
246
  /**
246
247
  *
247
248
  * @param {object} options
249
+ * @param {import("../blobs/blobs.mjs").BlobsContext} options.blobsContext
248
250
  * @param {import('../../commands/base-command.mjs').default} options.command
249
251
  * @param {*} options.capabilities
250
252
  * @param {*} options.config
@@ -258,8 +260,19 @@ const getFunctionsServer = (options) => {
258
260
  * @returns {Promise<import('./registry.mjs').FunctionsRegistry | undefined>}
259
261
  */
260
262
  export const startFunctionsServer = async (options) => {
261
- const { capabilities, command, config, debug, loadDistFunctions, settings, site, siteInfo, siteUrl, timeouts } =
262
- options
263
+ const {
264
+ blobsContext,
265
+ capabilities,
266
+ command,
267
+ config,
268
+ debug,
269
+ loadDistFunctions,
270
+ settings,
271
+ site,
272
+ siteInfo,
273
+ siteUrl,
274
+ timeouts,
275
+ } = options
263
276
  const internalFunctionsDir = await getInternalFunctionsDir({ base: site.root })
264
277
  const functionsDirectories = []
265
278
  let manifest
@@ -306,6 +319,7 @@ export const startFunctionsServer = async (options) => {
306
319
  }
307
320
 
308
321
  const functionsRegistry = new FunctionsRegistry({
322
+ blobsContext,
309
323
  capabilities,
310
324
  config,
311
325
  debug,
@@ -28,7 +28,7 @@ export const deploySite = async (
28
28
  concurrentHash = DEFAULT_CONCURRENT_HASH,
29
29
  concurrentUpload = DEFAULT_CONCURRENT_UPLOAD,
30
30
  configPath = null,
31
- deployId: deployIdOpt = null,
31
+ deployId,
32
32
  deployTimeout = DEFAULT_DEPLOY_TIMEOUT,
33
33
  draft = false,
34
34
  filter,
@@ -37,8 +37,6 @@ export const deploySite = async (
37
37
  hashAlgorithm,
38
38
  manifestPath,
39
39
  maxRetry = DEFAULT_MAX_RETRY,
40
- // API calls this the 'title'
41
- message: title,
42
40
  siteEnv,
43
41
  siteRoot,
44
42
  skipFunctionsCache,
@@ -120,9 +118,9 @@ For more information, visit https://ntl.fyi/cli-native-modules.`)
120
118
  phase: 'start',
121
119
  })
122
120
 
123
- let deploy
124
- let deployParams = cleanDeep({
121
+ const deployParams = cleanDeep({
125
122
  siteId,
123
+ deploy_id: deployId,
126
124
  body: {
127
125
  files,
128
126
  functions,
@@ -133,19 +131,11 @@ For more information, visit https://ntl.fyi/cli-native-modules.`)
133
131
  draft,
134
132
  },
135
133
  })
136
- if (deployIdOpt === null) {
137
- if (title) {
138
- deployParams = { ...deployParams, title }
139
- }
140
- deploy = await api.createSiteDeploy(deployParams)
141
- } else {
142
- deployParams = { ...deployParams, deploy_id: deployIdOpt }
143
- deploy = await api.updateSiteDeploy(deployParams)
144
- }
134
+ let deploy = await api.updateSiteDeploy(deployParams)
145
135
 
146
136
  if (deployParams.body.async) deploy = await waitForDiff(api, deploy.id, siteId, deployTimeout)
147
137
 
148
- const { id: deployId, required: requiredFiles, required_functions: requiredFns } = deploy
138
+ const { required: requiredFiles, required_functions: requiredFns } = deploy
149
139
 
150
140
  statusCb({
151
141
  type: 'create-deploy',
@@ -38,6 +38,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
38
38
  * @param {object} params
39
39
  * @param {string=} params.accountId
40
40
  * @param {*} params.addonsUrls
41
+ * @param {import("../lib/blobs/blobs.mjs").BlobsContext} blobsContext
41
42
  * @param {import('../commands/types.js').NetlifyOptions["config"]} params.config
42
43
  * @param {string} [params.configPath] An override for the Netlify config path
43
44
  * @param {boolean} params.debug
@@ -58,6 +59,7 @@ export const generateInspectSettings = (edgeInspect, edgeInspectBrk) => {
58
59
  export const startProxyServer = async ({
59
60
  accountId,
60
61
  addonsUrls,
62
+ blobsContext,
61
63
  config,
62
64
  configPath,
63
65
  debug,
@@ -76,6 +78,7 @@ export const startProxyServer = async ({
76
78
  }) => {
77
79
  const url = await startProxy({
78
80
  addonsUrls,
81
+ blobsContext,
79
82
  config,
80
83
  configPath: configPath || site.configPath,
81
84
  debug,
@@ -675,6 +675,7 @@ export const getProxyUrl = function (settings) {
675
675
  export const startProxy = async function ({
676
676
  accountId,
677
677
  addonsUrls,
678
+ blobsContext,
678
679
  config,
679
680
  configPath,
680
681
  debug,
@@ -693,6 +694,7 @@ export const startProxy = async function ({
693
694
  const secondaryServerPort = settings.https ? await getAvailablePort() : null
694
695
  const functionsServer = settings.functionsPort ? `http://127.0.0.1:${settings.functionsPort}` : null
695
696
  const edgeFunctionsProxy = await initializeEdgeFunctionsProxy({
697
+ blobsContext,
696
698
  config,
697
699
  configPath,
698
700
  debug,