@stacksjs/ts-cloud 0.7.31 → 0.7.32

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.
@@ -78,6 +78,17 @@ function buildPantryServiceScript(services) {
78
78
  }
79
79
 
80
80
  // src/drivers/shared/db-provision.ts
81
+ function isLocalDatabase(database) {
82
+ return !database?.host || database.host === "127.0.0.1" || database.host === "localhost";
83
+ }
84
+ function pgAdminCommand(database, tool = "psql") {
85
+ const port = database?.port ?? 5432;
86
+ if (isLocalDatabase(database))
87
+ return `${tool} -p ${port} -U postgres`;
88
+ const user = database?.username || "postgres";
89
+ const env = database?.password ? `PGPASSWORD='${database.password.replace(/'/g, `'\\''`)}' ` : "";
90
+ return `${env}${tool} -h ${database.host} -p ${port} -U ${user} -w`;
91
+ }
81
92
  function enabled(spec) {
82
93
  return spec === true || typeof spec === "object" && spec != null;
83
94
  }
@@ -121,7 +132,7 @@ function buildServicesProvisionScript(services = {}, _options = {}) {
121
132
  function buildDatabaseSetupScript(database, services = {}) {
122
133
  if (!database?.name)
123
134
  return [];
124
- if (database.host && database.host !== "127.0.0.1" && database.host !== "localhost")
135
+ if (!isLocalDatabase(database))
125
136
  return [];
126
137
  const name = database.name;
127
138
  const user = database.username || name;
@@ -154,10 +165,11 @@ function buildDatabaseSetupScript(database, services = {}) {
154
165
  return lines;
155
166
  };
156
167
  const extraUsers2 = database.users || [];
168
+ const pgPort = database.port ?? 5432;
157
169
  return [
158
170
  pantryEnvActivation(),
159
- "for i in $(seq 1 30); do pg_isready -h 127.0.0.1 -p 5432 -q && break; sleep 2; done",
160
- "psql -h 127.0.0.1 -p 5432 -U postgres <<'TS_CLOUD_PG_EOF'",
171
+ `for i in $(seq 1 30); do pg_isready -p ${pgPort} -q && break; sleep 2; done`,
172
+ `${pgAdminCommand(database)} <<'TS_CLOUD_PG_EOF'`,
161
173
  ...pgEnsureRole(user, pass),
162
174
  `SELECT 'CREATE DATABASE ${pgIdent(name)} OWNER ${pgIdent(user)}' ` + `WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${pgLit(name)})\\gexec`,
163
175
  ...extraUsers2.flatMap((u) => [...pgEnsureRole(u.username, u.password), ...pgGrant(u)]),
@@ -1388,7 +1400,7 @@ function buildBackupRestoreScript(database, options = {}) {
1388
1400
  const name = database.name;
1389
1401
  const isPg = database.engine === "postgres";
1390
1402
  const locate = options.from ? `TS_CLOUD_DUMP="${options.from}"` : `TS_CLOUD_DUMP="$(find ${BACKUP_OUTPUT_DIR} -type f -name '*${name}*.sql' -o -type f -name '*${name}*.sql.gz' 2>/dev/null | xargs -r ls -1t 2>/dev/null | head -1)"`;
1391
- const client = isPg ? `psql -h 127.0.0.1 -p ${database.port ?? 5432} -U postgres -d "${name}"` : `mysql --socket=${engineSocket(database.engine)} -u root "${name}"`;
1403
+ const client = isPg ? `${pgAdminCommand(database)} -d "${name}"` : `mysql --socket=${engineSocket(database.engine)} -u root "${name}"`;
1392
1404
  return [
1393
1405
  "set -uo pipefail",
1394
1406
  'eval "$(cd /opt/pantry && pantry env 2>/dev/null)" || true',
@@ -5098,4 +5110,4 @@ function buildCloudFrontOriginConfig(options) {
5098
5110
  CustomErrorResponses: { Quantity: 0 }
5099
5111
  };
5100
5112
  }
5101
- export { siteInstallBase, isPhpSite, resolveSiteDeployTarget, resolveSiteKind, validateDeploymentConfig, PANTRY_PROJECT_DIR, DEFAULT_RPX_CERTS_DIR, normalizeRoutePath, deriveRouteId, buildRpxConfig, buildRpxLbConfig, renderRpxLauncher, RPX_DIR, RPX_LAUNCHER_PATH, RPX_SERVICE_NAME, buildRpxProvisionScript, buildRpxFragmentRefreshScript, BACKUP_RUNNER_PATH, buildBackupRestoreScript, buildUbuntuBootstrapScript, AwsDriver, resolveHetznerLocation, HetznerClient, resolveHetznerApiToken2 as resolveHetznerApiToken, normalizeSshPublicKey, wrapCloudInitUserData, HetznerDriver, LocalBoxDriver, isBoxMode, createCloudDriver, CloudDriverFactory, cloudDrivers, ensureSshKey, ensureFirewall, ensureServer, serverPublicIpv4, buildSshArgs, sshExec, sshExecOrThrow, scpUpload, waitForSsh, waitForCloudInit, UBUNTU_2404_AMI_PARAM, buildBoxUserData, HetznerBoxProvisioner, AwsBoxProvisioner, createBoxProvisioner, releasePaths, buildRollbackScript, resolveExecStart, buildSiteDeployScript, buildStaticSiteDeployScript, releaseTarballTmpPath, buildAwsArtifactFetch, buildLocalArtifactFetch, MANAGEMENT_DASHBOARD_SITE, DASHBOARD_CREDENTIALS_FILE, resolveDashboardAuth, resolveUiSource, ensureManagementDashboard, buildManagementDashboardArtifact, resolveSiteFramework, deploySiteRelease, deployAllComputeSites, reloadRpxGateway, MANAGED_CACHE_POLICY_OPTIMIZED, MANAGED_CACHE_POLICY_DISABLED, MANAGED_ORIGIN_REQUEST_POLICY_ALL_VIEWER, buildCloudFrontOriginConfig };
5113
+ export { siteInstallBase, isPhpSite, resolveSiteDeployTarget, resolveSiteKind, validateDeploymentConfig, PANTRY_PROJECT_DIR, pgAdminCommand, DEFAULT_RPX_CERTS_DIR, normalizeRoutePath, deriveRouteId, buildRpxConfig, buildRpxLbConfig, renderRpxLauncher, RPX_DIR, RPX_LAUNCHER_PATH, RPX_SERVICE_NAME, buildRpxProvisionScript, buildRpxFragmentRefreshScript, BACKUP_RUNNER_PATH, buildBackupRestoreScript, buildUbuntuBootstrapScript, AwsDriver, resolveHetznerLocation, HetznerClient, resolveHetznerApiToken2 as resolveHetznerApiToken, normalizeSshPublicKey, wrapCloudInitUserData, HetznerDriver, LocalBoxDriver, isBoxMode, createCloudDriver, CloudDriverFactory, cloudDrivers, ensureSshKey, ensureFirewall, ensureServer, serverPublicIpv4, buildSshArgs, sshExec, sshExecOrThrow, scpUpload, waitForSsh, waitForCloudInit, UBUNTU_2404_AMI_PARAM, buildBoxUserData, HetznerBoxProvisioner, AwsBoxProvisioner, createBoxProvisioner, releasePaths, buildRollbackScript, resolveExecStart, buildSiteDeployScript, buildStaticSiteDeployScript, releaseTarballTmpPath, buildAwsArtifactFetch, buildLocalArtifactFetch, MANAGEMENT_DASHBOARD_SITE, DASHBOARD_CREDENTIALS_FILE, resolveDashboardAuth, resolveUiSource, ensureManagementDashboard, buildManagementDashboardArtifact, resolveSiteFramework, deploySiteRelease, deployAllComputeSites, reloadRpxGateway, MANAGED_CACHE_POLICY_OPTIMIZED, MANAGED_CACHE_POLICY_DISABLED, MANAGED_ORIGIN_REQUEST_POLICY_ALL_VIEWER, buildCloudFrontOriginConfig };
@@ -22,13 +22,14 @@ import {
22
22
  buildBackupRestoreScript,
23
23
  buildRollbackScript,
24
24
  createCloudDriver,
25
+ pgAdminCommand,
25
26
  releasePaths,
26
27
  resolveHetznerLocation,
27
28
  resolveSiteFramework,
28
29
  resolveSiteKind,
29
30
  resolveUiSource,
30
31
  siteInstallBase
31
- } from "./chunk-0c69nbaa.js";
32
+ } from "./chunk-dah449r1.js";
32
33
  import {
33
34
  artifactKey,
34
35
  composeServerlessAppTemplate,
@@ -10913,33 +10914,33 @@ function mysqlExec(engine, sql) {
10913
10914
  const sock = engine === "mariadb" ? SOCKETS.mariadb : SOCKETS.mysql;
10914
10915
  return [`mysql --socket=${sock} -u root <<'TS_CLOUD_SQL_EOF'`, ...sql, "TS_CLOUD_SQL_EOF"];
10915
10916
  }
10916
- function pgExec(sql) {
10917
- return ["psql -h 127.0.0.1 -p 5432 -U postgres -tA <<'TS_CLOUD_PG_EOF'", ...sql, "TS_CLOUD_PG_EOF"];
10917
+ function pgExec(sql, database) {
10918
+ return [`${pgAdminCommand(database)} -tA <<'TS_CLOUD_PG_EOF'`, ...sql, "TS_CLOUD_PG_EOF"];
10918
10919
  }
10919
- function buildListScript(engine) {
10920
+ function buildListScript(engine, database) {
10920
10921
  if (engine === "postgres") {
10921
10922
  return pgExec([
10922
10923
  "SELECT 'DB=' || datname FROM pg_database WHERE datistemplate = false;",
10923
10924
  "SELECT 'USER=' || usename FROM pg_user;"
10924
- ]);
10925
+ ], database);
10925
10926
  }
10926
10927
  return mysqlExec(engine, [
10927
10928
  "SELECT CONCAT('DB=', schema_name) FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys');",
10928
10929
  "SELECT DISTINCT CONCAT('USER=', User) FROM mysql.user WHERE User NOT IN ('root', 'mysql.sys', 'mysql.session', 'mysql.infoschema', 'debian-sys-maint');"
10929
10930
  ]);
10930
10931
  }
10931
- function buildCreateDatabaseScript(engine, name) {
10932
+ function buildCreateDatabaseScript(engine, name, database) {
10932
10933
  if (engine === "postgres") {
10933
10934
  return pgExec([
10934
10935
  `SELECT 'CREATE DATABASE ${pgIdent(name)}' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = ${pgLit(name)})\\gexec`
10935
- ]);
10936
+ ], database);
10936
10937
  }
10937
10938
  return mysqlExec(engine, [
10938
10939
  `CREATE DATABASE IF NOT EXISTS \`${mysqlIdent(name)}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`
10939
10940
  ]);
10940
10941
  }
10941
- function buildCreateUserScript(engine, input) {
10942
- const { username, password, database, access: access4 } = input;
10942
+ function buildCreateUserScript(engine, input, database) {
10943
+ const { username, password, database: grantDb, access: access4 } = input;
10943
10944
  if (engine === "postgres") {
10944
10945
  const lines2 = [
10945
10946
  "DO $$ BEGIN",
@@ -10947,10 +10948,10 @@ function buildCreateUserScript(engine, input) {
10947
10948
  ` ELSE ALTER ROLE ${pgIdent(username)} LOGIN PASSWORD ${pgLit(password)}; END IF;`,
10948
10949
  "END $$;"
10949
10950
  ];
10950
- if (database) {
10951
- lines2.push(access4 === "readonly" ? `GRANT CONNECT ON DATABASE ${pgIdent(database)} TO ${pgIdent(username)};` : `GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(database)} TO ${pgIdent(username)};`);
10951
+ if (grantDb) {
10952
+ lines2.push(access4 === "readonly" ? `GRANT CONNECT ON DATABASE ${pgIdent(grantDb)} TO ${pgIdent(username)};` : `GRANT ALL PRIVILEGES ON DATABASE ${pgIdent(grantDb)} TO ${pgIdent(username)};`);
10952
10953
  }
10953
- return pgExec(lines2);
10954
+ return pgExec(lines2, database);
10954
10955
  }
10955
10956
  const priv = access4 === "readonly" ? "SELECT" : "ALL PRIVILEGES";
10956
10957
  const lines = [
@@ -10959,8 +10960,8 @@ function buildCreateUserScript(engine, input) {
10959
10960
  `ALTER USER '${mysqlLit(username)}'@'%' IDENTIFIED BY '${mysqlLit(password)}';`,
10960
10961
  `ALTER USER '${mysqlLit(username)}'@'localhost' IDENTIFIED BY '${mysqlLit(password)}';`
10961
10962
  ];
10962
- if (database) {
10963
- lines.push(`GRANT ${priv} ON \`${mysqlIdent(database)}\`.* TO '${mysqlLit(username)}'@'%';`, `GRANT ${priv} ON \`${mysqlIdent(database)}\`.* TO '${mysqlLit(username)}'@'localhost';`);
10963
+ if (grantDb) {
10964
+ lines.push(`GRANT ${priv} ON \`${mysqlIdent(grantDb)}\`.* TO '${mysqlLit(username)}'@'%';`, `GRANT ${priv} ON \`${mysqlIdent(grantDb)}\`.* TO '${mysqlLit(username)}'@'localhost';`);
10964
10965
  }
10965
10966
  lines.push("FLUSH PRIVILEGES;");
10966
10967
  return mysqlExec(engine, lines);
@@ -11002,24 +11003,24 @@ async function runDb(config6, environment, commands, comment) {
11002
11003
  }
11003
11004
  async function listDatabases(config6, environment) {
11004
11005
  const engine = resolveDbEngine(config6);
11005
- const r = await runDb(config6, environment, buildListScript(engine), "ts-cloud db:list");
11006
+ const r = await runDb(config6, environment, buildListScript(engine, resolveAppDatabase(config6)), "ts-cloud db:list");
11006
11007
  const parsed = r.ok && r.stdout ? parseDbList(r.stdout) : { databases: [], users: [] };
11007
11008
  return { ...r, engine, ...parsed };
11008
11009
  }
11009
11010
  async function createDatabase(config6, environment, name) {
11010
11011
  const engine = resolveDbEngine(config6);
11011
- return runDb(config6, environment, buildCreateDatabaseScript(engine, name), `ts-cloud db:create ${name}`);
11012
+ return runDb(config6, environment, buildCreateDatabaseScript(engine, name, resolveAppDatabase(config6)), `ts-cloud db:create ${name}`);
11012
11013
  }
11013
11014
  async function createDatabaseUser(config6, environment, input) {
11014
11015
  const engine = resolveDbEngine(config6);
11015
- return runDb(config6, environment, buildCreateUserScript(engine, input), `ts-cloud db:user ${input.username}`);
11016
+ return runDb(config6, environment, buildCreateUserScript(engine, input, resolveAppDatabase(config6)), `ts-cloud db:user ${input.username}`);
11016
11017
  }
11017
11018
  var DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
11018
- function buildBackupScript(engine, name, destDir = DB_BACKUP_DIR) {
11019
+ function buildBackupScript(engine, name, destDir = DB_BACKUP_DIR, database) {
11019
11020
  const file = `${destDir}/${name}-$(date +%Y%m%d-%H%M%S).sql.gz`;
11020
11021
  const mkdir4 = `mkdir -p ${destDir}`;
11021
11022
  if (engine === "postgres")
11022
- return [mkdir4, `pg_dump -h 127.0.0.1 -p 5432 -U postgres ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
11023
+ return [mkdir4, `${pgAdminCommand(database, "pg_dump")} ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
11023
11024
  const sock = engine === "mariadb" ? SOCKETS.mariadb : SOCKETS.mysql;
11024
11025
  return [mkdir4, `mysqldump --socket=${sock} -u root ${name} | gzip > "${file}"`, `echo "BACKUP=${file}"`, `ls -l "${file}"`];
11025
11026
  }
@@ -11045,7 +11046,7 @@ async function backupDatabase(config6, environment, name) {
11045
11046
  if (!isValidDbIdentifier(name))
11046
11047
  return { ok: false, error: "Database name must be a valid identifier.", database: name };
11047
11048
  const engine = resolveDbEngine(config6);
11048
- const r = await runDb(config6, environment, buildBackupScript(engine, name), `ts-cloud db:backup ${name}`);
11049
+ const r = await runDb(config6, environment, buildBackupScript(engine, name, DB_BACKUP_DIR, resolveAppDatabase(config6)), `ts-cloud db:backup ${name}`);
11049
11050
  return { ...r, database: name };
11050
11051
  }
11051
11052
  async function listDatabaseBackups(config6, environment) {
@@ -4,20 +4,20 @@
4
4
  * the pantry UNIX socket; Postgres over local TCP — mirroring the provisioning
5
5
  * path in {@link import('../drivers/shared/db-provision')}.
6
6
  */
7
- import type { CloudConfig, EnvironmentType } from '@ts-cloud/core';
7
+ import type { CloudConfig, DatabaseConfig, EnvironmentType } from '@ts-cloud/core';
8
8
  export type DbEngine = 'mysql' | 'mariadb' | 'postgres';
9
9
  /** Valid SQL identifier for a database/user name (kept strict for safety). */
10
10
  export declare function isValidDbIdentifier(value: string): boolean;
11
11
  export declare function resolveDbEngine(config: CloudConfig): DbEngine;
12
- export declare function buildListScript(engine: DbEngine): string[];
13
- export declare function buildCreateDatabaseScript(engine: DbEngine, name: string): string[];
12
+ export declare function buildListScript(engine: DbEngine, database?: DatabaseConfig): string[];
13
+ export declare function buildCreateDatabaseScript(engine: DbEngine, name: string, database?: DatabaseConfig): string[];
14
14
  export interface CreateUserInput {
15
15
  username: string;
16
16
  password: string;
17
17
  database?: string;
18
18
  access?: 'all' | 'readonly';
19
19
  }
20
- export declare function buildCreateUserScript(engine: DbEngine, input: CreateUserInput): string[];
20
+ export declare function buildCreateUserScript(engine: DbEngine, input: CreateUserInput, database?: DatabaseConfig): string[];
21
21
  export declare function parseDbList(output: string): {
22
22
  databases: string[];
23
23
  users: string[];
@@ -40,9 +40,11 @@ export declare const DB_BACKUP_DIR = "/var/backups/ts-cloud/databases";
40
40
  /**
41
41
  * Script that dumps a single database to a timestamped, gzipped file. The name
42
42
  * is a validated SQL identifier (no shell metacharacters), so it is safe to
43
- * embed directly. The timestamp is computed on the box.
43
+ * embed directly. The timestamp is computed on the box. Postgres connects over
44
+ * the local unix socket for a co-located engine, or TCP with credentials for
45
+ * an external host (see {@link pgAdminCommand}).
44
46
  */
45
- export declare function buildBackupScript(engine: DbEngine, name: string, destDir?: string): string[];
47
+ export declare function buildBackupScript(engine: DbEngine, name: string, destDir?: string, database?: DatabaseConfig): string[];
46
48
  /** Script that lists the most recent dumps (newest first). */
47
49
  export declare function buildListBackupsScript(destDir?: string): string[];
48
50
  export declare function parseBackups(output: string): Array<{
@@ -12,7 +12,7 @@ import {
12
12
  sanitizeCloudConfig,
13
13
  setMaintenance,
14
14
  startLocalDashboardServer
15
- } from "../chunk-crcjdm09.js";
15
+ } from "../chunk-jgenfdz6.js";
16
16
  import {
17
17
  buildAndPushServerlessImage
18
18
  } from "../chunk-tskj9fay.js";
@@ -30,7 +30,7 @@ import {
30
30
  resolveUiSource,
31
31
  siteInstallBase,
32
32
  validateDeploymentConfig
33
- } from "../chunk-0c69nbaa.js";
33
+ } from "../chunk-dah449r1.js";
34
34
  import"../chunk-stt1z5cx.js";
35
35
  import"../chunk-93hjhs78.js";
36
36
  import {
@@ -50,7 +50,7 @@ import {
50
50
  waitForCloudInit,
51
51
  waitForSsh,
52
52
  wrapCloudInitUserData
53
- } from "../chunk-0c69nbaa.js";
53
+ } from "../chunk-dah449r1.js";
54
54
  import"../chunk-stt1z5cx.js";
55
55
  import"../chunk-93hjhs78.js";
56
56
  import"../chunk-qpj3edwz.js";
@@ -34,7 +34,9 @@ export interface BackupRestoreOptions {
34
34
  * Build the commands that restore a database from a ts-backups dump on the box.
35
35
  * With no `from`, the newest dump under {@link BACKUP_OUTPUT_DIR} matching the
36
36
  * database name is used. Handles plain `.sql` and gzipped `.sql.gz`. MySQL/
37
- * MariaDB restore over the root unix socket; Postgres via `psql -U postgres`.
37
+ * MariaDB restore over the root unix socket; Postgres over the local unix
38
+ * socket for a co-located engine, or TCP with credentials for an external
39
+ * host (see {@link pgAdminCommand}).
38
40
  * Returns `[]` when the database has no name.
39
41
  */
40
42
  export declare function buildBackupRestoreScript(database: DatabaseConfig | undefined, options?: BackupRestoreOptions): string[];
@@ -6,10 +6,32 @@
6
6
  * install nothing and just wire `.env` — see {@link buildManagedDbEnv}.
7
7
  *
8
8
  * pantry services listen on TCP localhost ports (mysql/mariadb 3306, postgres
9
- * 5432, redis 6379, memcached 11211, meilisearch 7700); DB setup connects over
10
- * TCP, and the engine clients are on PATH via `pantry env`.
9
+ * 5432, redis 6379, memcached 11211, meilisearch 7700) and the engine clients
10
+ * are on PATH via `pantry env`. Admin commands (db setup, dumps, restores)
11
+ * connect over the engine's local unix socket — see {@link pgAdminCommand}.
11
12
  */
12
13
  import type { ComputeServicesConfig, DatabaseConfig } from '@ts-cloud/core';
14
+ /**
15
+ * True when the database is co-located with the box (the managed-services
16
+ * engine): no host configured, or an explicit loopback host. Anything else is
17
+ * an external/managed database reached over TCP.
18
+ */
19
+ export declare function isLocalDatabase(database: DatabaseConfig | undefined): boolean;
20
+ /**
21
+ * Build the connection prefix for a postgres admin command (`psql`/`pg_dump`)
22
+ * run on the box.
23
+ *
24
+ * The pantry postgres pg_hba grants `trust` on the local unix socket but
25
+ * requires md5 password auth over TCP loopback — and the `postgres` superuser
26
+ * has no password — so against the co-located engine admin clients MUST use
27
+ * the socket: omit `-h` entirely and the pantry client uses its compiled-in
28
+ * default socket dir, which matches the server's by construction (verified
29
+ * on-box: `psql -U postgres` from root connects passwordless). Against an
30
+ * external/managed host, use TCP with the configured credentials
31
+ * (`PGPASSWORD` inline + `-w`, so a missing password fails fast instead of
32
+ * prompting forever).
33
+ */
34
+ export declare function pgAdminCommand(database: DatabaseConfig | undefined, tool?: 'psql' | 'pg_dump'): string;
13
35
  /**
14
36
  * Build pantry install + enable/start commands for each requested on-box
15
37
  * service. Idempotent (pantry install/enable/start are no-ops when satisfied).
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ import {
55
55
  sanitizeCloudConfig,
56
56
  setMaintenance,
57
57
  startLocalDashboardServer
58
- } from "./chunk-crcjdm09.js";
58
+ } from "./chunk-jgenfdz6.js";
59
59
  import {
60
60
  buildAndPushServerlessImage
61
61
  } from "./chunk-tskj9fay.js";
@@ -105,7 +105,7 @@ import {
105
105
  waitForCloudInit,
106
106
  waitForSsh,
107
107
  wrapCloudInitUserData
108
- } from "./chunk-0c69nbaa.js";
108
+ } from "./chunk-dah449r1.js";
109
109
  import {
110
110
  ABTestManager,
111
111
  AI,