neutrinos-cli 1.0.2 → 2.0.0-beta.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.
Files changed (55) hide show
  1. package/{bin → dist/src/bin}/cli.js +117 -125
  2. package/{cli-auth → dist/src/cli-auth}/auth.js +3 -24
  3. package/{cli-auth → dist/src/cli-auth}/server.js +6 -15
  4. package/{cli-auth → dist/src/cli-auth}/services/auth-utils.js +5 -24
  5. package/{commands → dist/src/commands}/attribute.js +36 -43
  6. package/{commands → dist/src/commands}/build.js +10 -25
  7. package/dist/src/commands/completion.js +298 -0
  8. package/dist/src/commands/deprecate.js +62 -0
  9. package/{commands → dist/src/commands}/dev.js +1 -12
  10. package/dist/src/commands/generate.js +10 -0
  11. package/{commands → dist/src/commands}/new-workspace.js +24 -50
  12. package/{commands → dist/src/commands}/publish.js +53 -200
  13. package/dist/src/commands/select-packages.js +22 -0
  14. package/{commands → dist/src/commands}/serve.js +7 -8
  15. package/dist/src/types/alpha-package.js +1 -0
  16. package/dist/src/types/marketplace.js +1 -0
  17. package/dist/src/types/session.js +1 -0
  18. package/dist/src/types/workspace.js +1 -0
  19. package/{utils → dist/src/utils}/attribute-utils.js +67 -44
  20. package/{utils → dist/src/utils}/check-valid-ws.js +1 -2
  21. package/{utils → dist/src/utils}/copy-utils.js +16 -28
  22. package/{utils → dist/src/utils}/create-client.js +3 -7
  23. package/{utils → dist/src/utils}/file-utils.js +10 -14
  24. package/{utils → dist/src/utils}/generate-component.js +10 -22
  25. package/{utils → dist/src/utils}/generate-module.js +4 -7
  26. package/dist/src/utils/get-package-info.js +60 -0
  27. package/dist/src/utils/get-packages.js +11 -0
  28. package/{utils → dist/src/utils}/inquirer-utils.js +8 -36
  29. package/dist/src/utils/logger.js +11 -0
  30. package/dist/src/utils/marketplace-api-utils.js +16 -0
  31. package/dist/src/utils/path-utils.js +28 -0
  32. package/dist/src/utils/prettify.js +15 -0
  33. package/{utils/user-seesion-utils.js → dist/src/utils/user-session-utils.js} +6 -9
  34. package/package.json +23 -21
  35. package/templates/module/.module.js.hbs +2 -2
  36. package/cli-auth/publish.js +0 -7
  37. package/commands/alpha-publish.js +0 -239
  38. package/commands/deprecate.js +0 -88
  39. package/commands/generate.js +0 -19
  40. package/commands/select-packages.mjs +0 -36
  41. package/templates/project/Dockerfile +0 -15
  42. package/templates/project/helmchart/.helmignore +0 -23
  43. package/templates/project/helmchart/Chart.yaml +0 -24
  44. package/templates/project/helmchart/templates/NOTES.txt +0 -22
  45. package/templates/project/helmchart/templates/_helpers.tpl +0 -62
  46. package/templates/project/helmchart/templates/deployment.yaml +0 -69
  47. package/templates/project/helmchart/templates/ingress.yaml +0 -62
  48. package/templates/project/helmchart/templates/service.yaml +0 -14
  49. package/templates/project/helmchart/values.yaml +0 -74
  50. package/utils/get-package-info.js +0 -53
  51. package/utils/get-packages.js +0 -15
  52. package/utils/logger.js +0 -35
  53. package/utils/marketplace-api-utils.js +0 -34
  54. package/utils/path-utils.js +0 -40
  55. package/utils/prettify.js +0 -36
@@ -1,20 +1,23 @@
1
1
  #! /usr/bin/env node
2
- //@ts-check
3
- const __dirname = fileURLToPath(new URL('.', import.meta.url));
2
+ const [major] = process.versions.node.split('.').map(Number);
3
+ if (major < 22) {
4
+ console.error('neutrinos requires Node.js 22+. Current: ' + process.version);
5
+ process.exit(1);
6
+ }
4
7
  import dotenv from 'dotenv';
8
+ import { PACKAGE_ROOT } from '../utils/path-utils.js';
5
9
  dotenv.config({
6
- path: join(__dirname, '../.env'),
10
+ path: join(PACKAGE_ROOT, '.env'),
7
11
  });
8
- ////////////////////////////////////
9
12
  import { greenBright } from 'colorette';
10
- import { Command, InvalidArgumentError } from 'commander';
13
+ import { Argument, Command, Option } from 'commander';
11
14
  import EventEmitter from 'node:events';
12
15
  import { cwd, env, exit } from 'node:process';
16
+ import { readFileSync, realpathSync } from 'node:fs';
13
17
  import { fileURLToPath } from 'node:url';
14
18
  import open from 'open';
15
- import { join } from 'path';
19
+ import { join, resolve } from 'node:path';
16
20
  import { init } from '../cli-auth/server.js';
17
- import { publish as alphaPublish } from '../commands/alpha-publish.js';
18
21
  import { addAttribute } from '../commands/attribute.js';
19
22
  import { build } from '../commands/build.js';
20
23
  import { deprecate } from '../commands/deprecate.js';
@@ -23,25 +26,25 @@ import { generate } from '../commands/generate.js';
23
26
  import { createWorkspace } from '../commands/new-workspace.js';
24
27
  import { publish } from '../commands/publish.js';
25
28
  import { startPluginsServer } from '../commands/serve.js';
29
+ import { completion } from '../commands/completion.js';
30
+ import { getPackages } from '../utils/get-packages.js';
26
31
  import { validateWorkspace } from '../utils/check-valid-ws.js';
27
32
  import { done, failed, inprogress, log } from '../utils/logger.js';
28
33
  import { authConfigJson } from '../utils/path-utils.js';
34
+ const { version: CLI_VERSION } = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
29
35
  export const createProgram = () => {
30
36
  const program = new Command();
31
-
32
- program.version('1.0.0', '-v, --version', 'Output the version number');
33
-
37
+ program.version(CLI_VERSION, '-v, --version', 'Output the version number');
34
38
  program
35
39
  .command('new <name>')
36
40
  .description('Create a new project')
37
41
  .action((name) => {
38
- createWorkspace(join(cwd(), name), name);
39
- });
42
+ createWorkspace(join(cwd(), name), name);
43
+ });
40
44
  const generateCmd = program
41
45
  .command('generate')
42
46
  .alias('g')
43
47
  .description('Generate a new plugin. Run "help generate" for more information.');
44
-
45
48
  generateCmd
46
49
  .command('component')
47
50
  .alias('c')
@@ -49,12 +52,12 @@ export const createProgram = () => {
49
52
  .option('-d,--description <description>', 'Description of the package')
50
53
  .description('Generate a new component plugin')
51
54
  .action(async (name, options) => {
52
- await generate.component({
53
- name,
54
- dir: cwd(),
55
- description: options.description,
56
- });
55
+ await generate.component({
56
+ name,
57
+ dir: cwd(),
58
+ description: options.description,
57
59
  });
60
+ });
58
61
  generateCmd
59
62
  .command('module')
60
63
  .alias('m')
@@ -62,21 +65,18 @@ export const createProgram = () => {
62
65
  .option('-d,--description <description>', 'Description of the package')
63
66
  .description('Generate a new module plugin')
64
67
  .action(async (name, options) => {
65
- await generate.module({
66
- name,
67
- dir: cwd(),
68
- description: options.description,
69
- });
68
+ await generate.module({
69
+ name,
70
+ dir: cwd(),
71
+ description: options.description,
70
72
  });
73
+ });
71
74
  generateCmd
72
75
  .command('attribute')
73
76
  .alias('a')
74
77
  .argument('[componentName]', 'Component name')
75
78
  .option('-t , --type <attributeType>', 'Type of attribute can be property, event, validation')
76
- .option(
77
- '-u , --ui-type <uiType>',
78
- 'Ui-type of attribute can be ===> input, toggle, dropdown, multi-select, typed-input, range, color-picker, data-source, data-set, table-actions, data-mapping',
79
- )
79
+ .option('-u , --ui-type <uiType>', 'Ui-type of attribute can be ===> input, toggle, dropdown, multi-select, typed-input, range, color-picker, data-source, data-set, table-actions, data-mapping')
80
80
  .option('-l , --label <label>', 'Label for an attribute')
81
81
  .option('-e , --event <event>', 'Event name for an attribute if -t/--type is event')
82
82
  .option('-c , --category <category>', 'Category for an attribute')
@@ -87,86 +87,63 @@ export const createProgram = () => {
87
87
  .option('-n , --validation <validation>', 'Validation for an attribute')
88
88
  .description('Generate an Attribute for component')
89
89
  .action(async (componentName, options) => {
90
- return await addAttribute(componentName, cwd(), options);
91
- });
92
-
90
+ return await addAttribute(componentName, cwd(), options);
91
+ });
93
92
  const authCommand = program.command('auth').description('perform: login, logout, or status');
94
93
  authCommand
95
94
  .command('login')
96
95
  .description('authenticate with the server')
97
- .action((action) => {
98
- return new Promise(async (resolve, reject) => {
99
- inprogress(`logging in...`);
100
- const cliServer = await init({
101
- tokenSetPath: authConfigJson(),
102
- });
103
- if (!cliServer) {
104
- failed('Issuer not found');
105
- return;
106
- }
107
- const eventEmitter = new EventEmitter();
108
- eventEmitter.on('cli-auth:login', () => {
109
- done('login successful');
110
- resolve();
111
- process.exit(0);
112
- });
113
- eventEmitter.on('cli-auth:login-failed', () => {
114
- failed('login failed');
115
- reject();
116
- process.exit(1);
117
- });
118
- process.on('SIGINT', () => {
119
- failed('exitting unexpectedly');
120
- cliServer.stop();
121
- exit(0);
122
- });
123
- process.on('SIGTERM', () => {
124
- failed('exitting unexpectedly');
125
- cliServer.stop();
126
- exit(0);
127
- });
128
- await cliServer.start(eventEmitter);
129
- const url = `http://localhost:${env.PORT}/login`;
130
- inprogress(`open to login ${greenBright(url)}`);
131
- await open(url);
96
+ .action(() => {
97
+ return new Promise(async (resolve, reject) => {
98
+ inprogress('logging in...');
99
+ const cliServer = await init({
100
+ tokenSetPath: authConfigJson(),
101
+ });
102
+ if (!cliServer) {
103
+ failed('Issuer not found');
104
+ return;
105
+ }
106
+ const eventEmitter = new EventEmitter();
107
+ eventEmitter.on('cli-auth:login', () => {
108
+ done('login successful');
109
+ resolve();
110
+ process.exit(0);
111
+ });
112
+ eventEmitter.on('cli-auth:login-failed', () => {
113
+ failed('login failed');
114
+ reject();
115
+ process.exit(1);
116
+ });
117
+ process.on('SIGINT', () => {
118
+ failed('exiting unexpectedly');
119
+ void cliServer.stop();
120
+ exit(0);
121
+ });
122
+ process.on('SIGTERM', () => {
123
+ failed('exiting unexpectedly');
124
+ void cliServer.stop();
125
+ exit(0);
132
126
  });
127
+ await cliServer.start(eventEmitter);
128
+ const url = `http://localhost:${env.PORT}/login`;
129
+ inprogress(`open to login ${greenBright(url)}`);
130
+ await open(url);
133
131
  });
132
+ });
134
133
  authCommand
135
134
  .command('state')
136
135
  .description('get the current state of auth')
137
136
  .action(() => {
138
- console.log('state');
139
- });
140
-
137
+ console.log('state');
138
+ });
141
139
  program
142
140
  .command('publish')
143
141
  .argument('[packageName]', 'Component name')
144
- .option(
145
- '-t, --version-type <versionType>',
146
- 'Version type for verion up can be one of "patch", "minor", "major"',
147
- )
148
- .option('-d, --display-name <displayName>', 'Display Name for the package')
149
- .option('-i, --icon <iconPath>', 'Icon path')
150
- .option('-m, --images <imagePaths...>', 'Image paths')
151
- .description('Bundle and publish the plugin')
152
- .action(async (packageName, options) => {
153
- if (
154
- options.versionType &&
155
- !['patch', 'minor', 'major'].includes((options.versionType || '').toLowerCase())
156
- ) {
157
- failed('Invalid version type should be one of "patch", "minor", "major"');
158
- exit(0);
159
- }
160
- return await publish(packageName, cwd(), options);
161
- });
162
-
163
- program
164
- .command('alpha:publish')
165
- .argument('[packageName]', 'Component name')
166
- .option(
167
- '-t, --version-type <versionType>',
168
- 'Version type for version up can be one of "patch", "minor", "major"',
169
- )
142
+ .addOption(new Option('-t, --version-type <versionType>', 'Version type for version up').choices([
143
+ 'patch',
144
+ 'minor',
145
+ 'major',
146
+ ]))
170
147
  .option('-d, --display-name <displayName>', 'Display Name for the package')
171
148
  .option('-i, --icon <iconPath>', 'Icon path')
172
149
  .option('-m, --images <imagePaths...>', 'Image paths')
@@ -174,56 +151,65 @@ export const createProgram = () => {
174
151
  .option('--all', 'Select all the packages')
175
152
  .description('Bundle and publish the plugin')
176
153
  .action(async (packageName, options) => {
177
- if (
178
- options.versionType &&
179
- !['patch', 'minor', 'major'].includes((options.versionType || '').toLowerCase())
180
- ) {
181
- failed('Invalid version type should be one of "patch", "minor", "major"');
182
- exit(0);
183
- }
184
- return await alphaPublish(packageName, cwd(), options);
185
- });
186
-
154
+ return await publish(packageName, cwd(), options);
155
+ });
187
156
  program
188
157
  .command('deprecate')
189
158
  .option('-y, --yes', 'Confirmation to deprecate a package')
190
159
  .option('--all', 'Select all the packages')
191
160
  .argument('[packageName]', 'Package name')
192
161
  .action(async (packageName, options) => {
193
- return await deprecate(packageName, cwd(), options);
194
- });
195
-
162
+ return await deprecate(packageName, cwd(), options);
163
+ });
196
164
  program
197
165
  .command('build')
198
- .argument('<type>', 'Type of build: "plugins" or "docker"', (value, prevValue) => {
199
- if (value !== 'plugins' && value !== 'docker') {
200
- throw new InvalidArgumentError('"build" can be "plugins" or "docker"');
201
- }
202
- return value;
203
- })
166
+ .addArgument(new Argument('<type>', 'Type of build').choices(['plugins', 'docker']))
204
167
  .argument('[pluginName]', 'Name of the plugin')
205
168
  .option('--module', 'Build the plugin as a module')
206
169
  .option('--all', 'Build all the plugin of the workspace')
207
170
  .description('Build the plugin')
208
171
  .action((type, pluginName, options) => {
209
- build[type](cwd(), pluginName, options || {});
210
- });
172
+ build[type](cwd(), pluginName ?? '', options ?? {});
173
+ });
211
174
  program
212
175
  .command('start')
213
176
  .option('-p, --port <port>', 'Port number for serving the plugin')
214
177
  .description('start the live server for the plugin')
215
178
  .action((options) => {
216
- servePlugin(cwd(), options.port || 6969);
217
- });
179
+ servePlugin(cwd(), Number(options.port) || 6969);
180
+ });
218
181
  program
219
182
  .command('serve')
183
+ .option('-p, --port <port>', 'Port number for serving the plugins', '3000')
220
184
  .description('Start the express server that serves the plugins')
185
+ .action((options) => {
186
+ startPluginsServer(cwd(), Number(options.port));
187
+ });
188
+ program
189
+ .command('completion')
190
+ .description('Output shell completion script')
191
+ .addArgument(new Argument('<shell>', 'Shell type').choices(['bash', 'zsh']))
192
+ .action((shell) => {
193
+ completion(program, shell);
194
+ });
195
+ program
196
+ .command('__list-packages', { hidden: true })
197
+ .description('List workspace package names (used by shell completion)')
221
198
  .action(() => {
222
- startPluginsServer(cwd());
223
- });
224
- program.hook('preAction', async (thisCmd, actionCmd) => {
199
+ try {
200
+ const packages = getPackages(cwd());
201
+ for (const p of packages) {
202
+ const pkgJson = JSON.parse(readFileSync(join(p, 'package.json'), 'utf-8'));
203
+ process.stdout.write(pkgJson.name + '\n');
204
+ }
205
+ }
206
+ catch {
207
+ // Silently fail — completion should never error
208
+ }
209
+ });
210
+ program.hook('preAction', async (_thisCmd, actionCmd) => {
225
211
  const cmd = actionCmd.name();
226
- if (cmd === 'new' || cmd === 'login') {
212
+ if (cmd === 'new' || cmd === 'login' || cmd === 'completion' || cmd === '__list-packages') {
227
213
  return;
228
214
  }
229
215
  if (!validateWorkspace(cwd())) {
@@ -231,9 +217,15 @@ export const createProgram = () => {
231
217
  exit(1);
232
218
  }
233
219
  });
234
-
235
220
  return program;
236
221
  };
237
-
238
- const program = createProgram();
239
- program.parse(process.argv);
222
+ // Only parse when run directly (not when imported for testing/introspection)
223
+ const self = realpathSync(fileURLToPath(import.meta.url));
224
+ const entry = process.argv[1] ? realpathSync(resolve(process.argv[1])) : '';
225
+ if (self === entry) {
226
+ const program = createProgram();
227
+ program.parseAsync(process.argv).catch((err) => {
228
+ failed(err.message || 'An unexpected error occurred');
229
+ exit(1);
230
+ });
231
+ }
@@ -1,46 +1,25 @@
1
- import EventEmitter from 'node:events';
2
1
  import { existsSync } from 'node:fs';
3
2
  import { mkdir, writeFile } from 'node:fs/promises';
4
3
  import { sep } from 'node:path';
5
4
  import { authUtils } from './services/auth-utils.js';
6
-
7
- /**
8
- * @param {import('openid-client').Client} client
9
- * @param {string} tokenSetPath
10
- */
11
5
  export const auth = (client, tokenSetPath) => {
12
6
  const authStateNonce = {
13
7
  state: '',
14
8
  nonce: '',
15
9
  };
16
- /**
17
- *
18
- * @param {import('fastify').FastifyRequest} req
19
- * @param {import('fastify').FastifyReply} reply
20
- */
21
10
  return {
22
- /**
23
- * @param {import('fastify').FastifyInstance} server
24
- * @param {EventEmitter} eventEmitter
25
- */
26
11
  mount: (server, eventEmitter) => {
27
- server.get('/login', async (request, reply) => {
12
+ server.get('/login', async (_request, reply) => {
28
13
  const redirectUrl = await authUtils.login(client, authStateNonce);
29
14
  reply.redirect(redirectUrl);
30
15
  });
31
16
  server.get('/login-callback', async (request, reply) => {
32
- const { tokenSet } = await authUtils.loginCallback(
33
- client.callbackParams(request),
34
- client,
35
- authStateNonce,
36
- );
17
+ const { tokenSet } = await authUtils.loginCallback(client.callbackParams(request.raw), client, authStateNonce);
37
18
  const dirPath = tokenSetPath.substring(0, tokenSetPath.lastIndexOf(sep));
38
19
  if (!existsSync(dirPath)) {
39
20
  await mkdir(dirPath, { recursive: true });
40
21
  }
41
- await writeFile(tokenSetPath, JSON.stringify(tokenSet, null, 2), {
42
- encoding: 'utf-8',
43
- });
22
+ await writeFile(tokenSetPath, JSON.stringify(tokenSet, null, 2), { encoding: 'utf-8' });
44
23
  if (tokenSet) {
45
24
  reply.send('Login successful');
46
25
  eventEmitter.emit('cli-auth:login');
@@ -1,25 +1,20 @@
1
- import fastify from 'fastify';
2
1
  import { env } from 'node:process';
2
+ import fastify from 'fastify';
3
3
  import { Issuer } from 'openid-client';
4
4
  import { auth } from './auth.js';
5
-
6
- /**
7
- * @param {{tokenSetPath: string}} config
8
- * @returns
9
- */
10
5
  export const init = async (config) => {
11
6
  if (!env.ISSUER_URL) {
12
7
  console.error('Issuer URL not found');
13
- return;
8
+ return null;
14
9
  }
15
10
  const issuer = await Issuer.discover(env.ISSUER_URL);
16
11
  if (!issuer) {
17
12
  console.error('Issuer not found');
18
- return;
13
+ return null;
19
14
  }
20
15
  if (!env.IDS_CLIENT_ID || !env.IDS_CLIENT_SECRET) {
21
16
  console.error('Client ID or Client Secret not found');
22
- return;
17
+ return null;
23
18
  }
24
19
  const client = new issuer.Client({
25
20
  client_id: env.IDS_CLIENT_ID,
@@ -29,16 +24,12 @@ export const init = async (config) => {
29
24
  const auth_ = auth(client, config.tokenSetPath);
30
25
  return {
31
26
  start: async (eventEmitter) => {
32
- const port = parseInt(env.PORT || '32231');
27
+ const port = parseInt(env.PORT ?? '32231', 10);
33
28
  auth_.mount(server, eventEmitter);
34
- await server.listen({
35
- port,
36
- });
37
- return server;
29
+ await server.listen({ port });
38
30
  },
39
31
  stop: async () => {
40
32
  await server.close();
41
- return server;
42
33
  },
43
34
  };
44
35
  };
@@ -1,19 +1,15 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { env } from 'node:process';
4
-
5
4
  export const authUtils = {
6
- /**
7
- * @param {import('openid-client').Client} client
8
- * @param {{state: string, nonce: string}} authStateNonce
9
- */
10
5
  login: async (client, authStateNonce) => {
6
+ const port = env.PORT ?? '32231';
11
7
  const authorizationRequest = {
12
- redirect_uri: `http://localhost:${env.PORT}/login-callback`,
8
+ redirect_uri: `http://localhost:${port}/login-callback`,
13
9
  scope: 'openid profile email address phone offline_access user',
14
10
  state: randomBytes(16).toString('hex'),
15
11
  nonce: randomBytes(16).toString('hex'),
16
- response_type: client.response_types[0],
12
+ response_type: client.metadata.response_types?.[0] ?? 'code',
17
13
  prompt: 'consent',
18
14
  };
19
15
  authStateNonce.state = authorizationRequest.state;
@@ -21,14 +17,7 @@ export const authUtils = {
21
17
  const redirect = client.authorizationUrl(authorizationRequest);
22
18
  return redirect;
23
19
  },
24
- /**
25
- * @param {*} callbackParams
26
- * @param {import('openid-client').Client} client
27
- * @param {{state: string, nonce: string}} authStateNonce
28
- * @returns {Promise<{redirect: string, tokenSet: null | any}>}
29
- */
30
20
  loginCallback: async (callbackParams, client, authStateNonce) => {
31
- /** @type {{redirect: string, tokenSet: null | any}} */
32
21
  const result = {
33
22
  redirect: '',
34
23
  tokenSet: null,
@@ -37,7 +26,8 @@ export const authUtils = {
37
26
  result.redirect = '/login';
38
27
  return result;
39
28
  }
40
- const tokenset = await client.callback(`http://localhost:${env.PORT}/login-callback`, callbackParams, {
29
+ const port = env.PORT ?? '32231';
30
+ const tokenset = await client.callback(`http://localhost:${port}/login-callback`, callbackParams, {
41
31
  nonce: authStateNonce.nonce,
42
32
  state: authStateNonce.state,
43
33
  });
@@ -47,19 +37,10 @@ export const authUtils = {
47
37
  result.redirect = '/login-success';
48
38
  return result;
49
39
  },
50
- /**
51
- * @param {import('openid-client').Client} client
52
- * @param {string} tokenSetPath
53
- * @returns {Promise<import('openid-client').IntrospectionResponse>}
54
- */
55
40
  verifyToken: async (client, tokenSetPath) => {
56
41
  const tokenSet = JSON.parse(await readFile(tokenSetPath, 'utf-8'));
57
42
  return await client.introspect(tokenSet.access_token);
58
43
  },
59
- /**
60
- * @param {import('openid-client').Client} client
61
- * @param {string} tokenSetPath
62
- */
63
44
  refreshToken: async (client, tokenSetPath) => {
64
45
  const tokenSet = JSON.parse(await readFile(tokenSetPath, 'utf-8'));
65
46
  const updatedTokenSet = await client.refresh(tokenSet.refresh_token);