@alwaysai/device-agent 0.1.0 → 0.1.2

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 (100) hide show
  1. package/lib/application-control/config.d.ts +0 -1
  2. package/lib/application-control/config.d.ts.map +1 -1
  3. package/lib/application-control/config.js +15 -29
  4. package/lib/application-control/config.js.map +1 -1
  5. package/lib/application-control/environment-variables.d.ts +7 -3
  6. package/lib/application-control/environment-variables.d.ts.map +1 -1
  7. package/lib/application-control/environment-variables.js +71 -35
  8. package/lib/application-control/environment-variables.js.map +1 -1
  9. package/lib/application-control/environment-variables.test.d.ts +2 -0
  10. package/lib/application-control/environment-variables.test.d.ts.map +1 -0
  11. package/lib/application-control/environment-variables.test.js +163 -0
  12. package/lib/application-control/environment-variables.test.js.map +1 -0
  13. package/lib/application-control/index.d.ts +3 -3
  14. package/lib/application-control/index.d.ts.map +1 -1
  15. package/lib/application-control/index.js +1 -3
  16. package/lib/application-control/index.js.map +1 -1
  17. package/lib/application-control/models.d.ts +0 -1
  18. package/lib/application-control/models.d.ts.map +1 -1
  19. package/lib/application-control/models.js +12 -26
  20. package/lib/application-control/models.js.map +1 -1
  21. package/lib/application-control/status.d.ts +3 -0
  22. package/lib/application-control/status.d.ts.map +1 -1
  23. package/lib/application-control/status.js +19 -1
  24. package/lib/application-control/status.js.map +1 -1
  25. package/lib/application-control/utils.d.ts.map +1 -1
  26. package/lib/application-control/utils.js +2 -2
  27. package/lib/application-control/utils.js.map +1 -1
  28. package/lib/cloud-connection/device-agent-cloud-connection.d.ts +6 -3
  29. package/lib/cloud-connection/device-agent-cloud-connection.d.ts.map +1 -1
  30. package/lib/cloud-connection/device-agent-cloud-connection.js +205 -151
  31. package/lib/cloud-connection/device-agent-cloud-connection.js.map +1 -1
  32. package/lib/cloud-connection/live-updates-handler.d.ts +3 -0
  33. package/lib/cloud-connection/live-updates-handler.d.ts.map +1 -1
  34. package/lib/cloud-connection/live-updates-handler.js +23 -7
  35. package/lib/cloud-connection/live-updates-handler.js.map +1 -1
  36. package/lib/cloud-connection/live-updates-handler.test.d.ts +2 -0
  37. package/lib/cloud-connection/live-updates-handler.test.d.ts.map +1 -0
  38. package/lib/cloud-connection/live-updates-handler.test.js +57 -0
  39. package/lib/cloud-connection/live-updates-handler.test.js.map +1 -0
  40. package/lib/cloud-connection/passthrough-handler.d.ts.map +1 -1
  41. package/lib/cloud-connection/passthrough-handler.js +6 -3
  42. package/lib/cloud-connection/passthrough-handler.js.map +1 -1
  43. package/lib/cloud-connection/shadow-handler.d.ts +11 -3
  44. package/lib/cloud-connection/shadow-handler.d.ts.map +1 -1
  45. package/lib/cloud-connection/shadow-handler.js +22 -7
  46. package/lib/cloud-connection/shadow-handler.js.map +1 -1
  47. package/lib/cloud-connection/shadow-handler.test.js +313 -228
  48. package/lib/cloud-connection/shadow-handler.test.js.map +1 -1
  49. package/lib/cloud-connection/shadow.js +1 -1
  50. package/lib/cloud-connection/shadow.js.map +1 -1
  51. package/lib/environment.d.ts +1 -0
  52. package/lib/environment.d.ts.map +1 -1
  53. package/lib/environment.js +2 -1
  54. package/lib/environment.js.map +1 -1
  55. package/lib/infrastructure/agent-config.d.ts +3 -1
  56. package/lib/infrastructure/agent-config.d.ts.map +1 -1
  57. package/lib/subcommands/app/env-vars.d.ts +1 -1
  58. package/lib/subcommands/app/env-vars.d.ts.map +1 -1
  59. package/lib/subcommands/app/env-vars.js +32 -5
  60. package/lib/subcommands/app/env-vars.js.map +1 -1
  61. package/lib/subcommands/app/index.d.ts.map +1 -1
  62. package/lib/subcommands/app/index.js +4 -1
  63. package/lib/subcommands/app/index.js.map +1 -1
  64. package/lib/subcommands/app/models.d.ts.map +1 -1
  65. package/lib/subcommands/app/models.js +6 -1
  66. package/lib/subcommands/app/models.js.map +1 -1
  67. package/lib/subcommands/app/shadow.d.ts +7 -0
  68. package/lib/subcommands/app/shadow.d.ts.map +1 -0
  69. package/lib/subcommands/app/shadow.js +48 -0
  70. package/lib/subcommands/app/shadow.js.map +1 -0
  71. package/lib/subcommands/app/version.js +2 -2
  72. package/lib/subcommands/app/version.js.map +1 -1
  73. package/lib/util/cloud-mode-ready.d.ts +2 -0
  74. package/lib/util/cloud-mode-ready.d.ts.map +1 -0
  75. package/lib/util/cloud-mode-ready.js +22 -0
  76. package/lib/util/cloud-mode-ready.js.map +1 -0
  77. package/package.json +1 -1
  78. package/readme.md +2 -2
  79. package/src/application-control/config.ts +30 -31
  80. package/src/application-control/environment-variables.test.ts +171 -0
  81. package/src/application-control/environment-variables.ts +102 -43
  82. package/src/application-control/index.ts +3 -9
  83. package/src/application-control/models.ts +14 -29
  84. package/src/application-control/status.ts +20 -0
  85. package/src/application-control/utils.ts +4 -2
  86. package/src/cloud-connection/device-agent-cloud-connection.ts +222 -153
  87. package/src/cloud-connection/live-updates-handler.test.ts +68 -0
  88. package/src/cloud-connection/live-updates-handler.ts +30 -7
  89. package/src/cloud-connection/passthrough-handler.ts +10 -3
  90. package/src/cloud-connection/shadow-handler.test.ts +329 -239
  91. package/src/cloud-connection/shadow-handler.ts +38 -12
  92. package/src/cloud-connection/shadow.ts +1 -1
  93. package/src/environment.ts +2 -0
  94. package/src/infrastructure/agent-config.ts +1 -1
  95. package/src/subcommands/app/env-vars.ts +38 -8
  96. package/src/subcommands/app/index.ts +4 -1
  97. package/src/subcommands/app/models.ts +10 -1
  98. package/src/subcommands/app/shadow.ts +48 -0
  99. package/src/subcommands/app/version.ts +2 -2
  100. package/src/util/cloud-mode-ready.ts +23 -0
@@ -0,0 +1,171 @@
1
+ import { AgentConfigFile } from '../infrastructure/agent-config';
2
+ import { readDockerCompose, writeDockerCompose } from './config';
3
+ import { getAllEnvs, setEnv } from './environment-variables';
4
+ import { isAppStarted, restartApp } from './status';
5
+ import { buildApp, requireAppReady } from './utils';
6
+
7
+ jest.mock('./config');
8
+
9
+ jest.mock('./utils');
10
+ jest.mocked(requireAppReady).mockResolvedValue();
11
+ jest.mocked(buildApp).mockResolvedValue();
12
+
13
+ jest.mock('./status');
14
+ jest.mocked(isAppStarted).mockResolvedValue(true);
15
+ jest.mocked(restartApp).mockResolvedValue();
16
+
17
+ jest.mock('../infrastructure/agent-config');
18
+ const mockSetAppInstalling = jest.fn().mockResolvedValue({});
19
+ const mockSetAppInstalled = jest.fn().mockResolvedValue({});
20
+ const mockGetAppVersion = jest.fn().mockResolvedValue('test-version');
21
+ jest.mocked(AgentConfigFile as jest.Mock).mockReturnValue({
22
+ setAppInstalling: mockSetAppInstalling,
23
+ setAppInstalled: mockSetAppInstalled,
24
+ getAppVersion: mockGetAppVersion
25
+ });
26
+ const projectId1 = 'test-project';
27
+
28
+ describe('Test environment variable get and set', () => {
29
+ beforeEach(() => {
30
+ jest.clearAllMocks();
31
+ });
32
+ describe('Test getAllEnvs()', () => {
33
+ test('get all envs with one service and no envs', async () => {
34
+ const compose = {
35
+ services: {
36
+ alwaysai: {}
37
+ }
38
+ };
39
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
40
+
41
+ const envVars = await getAllEnvs({ projectId: projectId1 });
42
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
43
+ projectId: projectId1
44
+ });
45
+ expect(envVars).toEqual({
46
+ alwaysai: {}
47
+ });
48
+ });
49
+ // TODO: Add test with env file
50
+ test('get all envs with one service and two envs', async () => {
51
+ const compose = {
52
+ services: {
53
+ alwaysai: {
54
+ environment: ['TEST=1', 'TEST2=2']
55
+ }
56
+ }
57
+ };
58
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
59
+
60
+ const envVars = await getAllEnvs({ projectId: projectId1 });
61
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
62
+ projectId: projectId1
63
+ });
64
+ expect(envVars).toEqual({
65
+ alwaysai: { TEST: '1', TEST2: '2' }
66
+ });
67
+ });
68
+ test('get all envs with two services and envs', async () => {
69
+ const compose = {
70
+ services: {
71
+ alwaysai: {
72
+ environment: ['TEST=3', 'TEST2=4']
73
+ },
74
+ edgeiq: {
75
+ environment: ['TEST3=5', 'TEST4=6']
76
+ }
77
+ }
78
+ };
79
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
80
+
81
+ const envVars = await getAllEnvs({ projectId: projectId1 });
82
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
83
+ projectId: projectId1
84
+ });
85
+ expect(envVars).toEqual({
86
+ alwaysai: { TEST: '3', TEST2: '4' },
87
+ edgeiq: { TEST3: '5', TEST4: '6' }
88
+ });
89
+ });
90
+ });
91
+ describe('Test setEnv()', () => {
92
+ test('Set one env', async () => {
93
+ const compose = {
94
+ services: {
95
+ alwaysai: {}
96
+ }
97
+ };
98
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
99
+
100
+ const envVars = { alwaysai: { TEST: '1' } };
101
+ await setEnv({ projectId: projectId1, envVars });
102
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
103
+ projectId: projectId1
104
+ });
105
+ expect(jest.mocked(writeDockerCompose)).toBeCalledWith({
106
+ projectId: projectId1,
107
+ dockerCompose: {
108
+ services: {
109
+ alwaysai: {
110
+ environment: ['TEST=1']
111
+ }
112
+ }
113
+ }
114
+ });
115
+ });
116
+ test('Update one env', async () => {
117
+ const environment = ['TEST1=1', 'TEST2=2'];
118
+ const compose = {
119
+ services: {
120
+ alwaysai: { environment }
121
+ }
122
+ };
123
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
124
+
125
+ const envVars = { alwaysai: { TEST1: '2' } };
126
+ await setEnv({ projectId: projectId1, envVars });
127
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
128
+ projectId: projectId1
129
+ });
130
+ expect(jest.mocked(writeDockerCompose)).toBeCalledWith({
131
+ projectId: projectId1,
132
+ dockerCompose: {
133
+ services: {
134
+ alwaysai: {
135
+ environment: ['TEST1=1', 'TEST2=2', 'TEST1=2']
136
+ }
137
+ }
138
+ }
139
+ });
140
+ });
141
+ test('Update one env with two services', async () => {
142
+ const environment = ['TEST1=3', 'TEST2=4'];
143
+ const compose = {
144
+ services: {
145
+ alwaysai: { environment },
146
+ other: { environment: ['OTHER_TEST=1'] }
147
+ }
148
+ };
149
+ jest.mocked(readDockerCompose).mockResolvedValue(compose);
150
+
151
+ const envVars = { alwaysai: { TEST1: '2' } };
152
+ await setEnv({ projectId: projectId1, envVars });
153
+ expect(jest.mocked(readDockerCompose)).toBeCalledWith({
154
+ projectId: projectId1
155
+ });
156
+ expect(jest.mocked(writeDockerCompose)).toBeCalledWith({
157
+ projectId: projectId1,
158
+ dockerCompose: {
159
+ services: {
160
+ alwaysai: {
161
+ environment: ['TEST1=3', 'TEST2=4', 'TEST1=2']
162
+ },
163
+ other: {
164
+ environment: ['OTHER_TEST=1']
165
+ }
166
+ }
167
+ }
168
+ });
169
+ });
170
+ });
171
+ });
@@ -1,72 +1,131 @@
1
1
  import { JsSpawner } from 'alwaysai/lib/util';
2
- import { AgentConfigFile } from '../infrastructure/agent-config';
3
2
  import { readDockerCompose, writeDockerCompose } from './config';
4
- import { getAppDir } from './utils';
3
+ import { buildApp, getAppDir, requireAppReady } from './utils';
4
+ import { logger } from '../util/logger';
5
+ import { AgentConfigFile } from '../infrastructure/agent-config';
6
+ import { isAppStarted, restartApp } from './status';
7
+
8
+ export interface EnvVars {
9
+ [service: string]: {
10
+ [name: string]: string;
11
+ };
12
+ }
13
+
14
+ export async function setEnv(props: { projectId: string; envVars: EnvVars }) {
15
+ const { projectId, envVars } = props;
16
+ await requireAppReady({ projectId });
17
+ const appReleaseHash = await AgentConfigFile().getAppVersion({
18
+ projectId
19
+ });
20
+ await AgentConfigFile().setAppInstalling({
21
+ projectId,
22
+ version: appReleaseHash
23
+ });
5
24
 
6
- export async function setEnv(props: {
7
- projectId: string;
8
- vars: string[];
9
- service?: string;
10
- }) {
11
- const { projectId, vars } = props;
12
- if (!(await AgentConfigFile().isAppReady({ projectId }))) {
13
- throw new Error(`App ${projectId} is not ready!`);
14
- }
15
25
  const composeParsed = await readDockerCompose({ projectId });
16
- if ('services' in composeParsed) {
17
- let services: string[] = Object.keys(composeParsed['services']);
18
- if (props.service) {
19
- if (services.includes(props.service)) {
20
- services = [props.service];
21
- } else {
22
- throw new Error(`Service ${props.service} not found in ${services}`);
23
- }
26
+ if (!('services' in composeParsed)) {
27
+ throw new Error(`Docker compose file for ${projectId} has no services!`);
28
+ }
29
+ const services: string[] = Object.keys(envVars);
30
+ for (const s of services) {
31
+ if (!Object.keys(composeParsed['services']).includes(s)) {
32
+ throw new Error(
33
+ `Service ${s} not found in ${JSON.stringify(
34
+ composeParsed['services'],
35
+ null,
36
+ 2
37
+ )}`
38
+ );
24
39
  }
25
- for (const s of services) {
26
- const service = composeParsed['services'][s];
27
- // The environment field overrides the env files, so by appending to
28
- // the environment list we can assure the value will be used
29
- if ('environment' in service) {
30
- const environment: string[] = service['environment'];
31
- composeParsed['services'][s]['environment'] = environment.concat(vars);
32
- } else {
33
- composeParsed['services'][s]['environment'] = vars;
34
- }
40
+ const envVarList: string[] = [];
41
+ for (const envVar of Object.keys(envVars[s])) {
42
+ envVarList.push(`${envVar}=${envVars[s][envVar]}`);
43
+ }
44
+ const service = composeParsed['services'][s];
45
+ // The environment field overrides the env files, so by appending to
46
+ // the environment list we can assure the value will be used over
47
+ // the env_file.
48
+
49
+ // NOTE: We are only appending the new values to the end of the
50
+ // environment variable list, which will override but not replace
51
+ // previous instances of the same environment variable.
52
+ if ('environment' in service) {
53
+ const environment: string[] = service['environment'];
54
+ composeParsed['services'][s]['environment'] =
55
+ environment.concat(envVarList);
56
+ } else {
57
+ composeParsed['services'][s]['environment'] = envVarList;
35
58
  }
36
59
  }
37
60
  await writeDockerCompose({ projectId, dockerCompose: composeParsed });
61
+
62
+ const appDir = getAppDir(projectId);
63
+ await buildApp({ appDir });
64
+
65
+ await AgentConfigFile().setAppInstalled({
66
+ projectId,
67
+ version: appReleaseHash
68
+ });
69
+
70
+ if (await isAppStarted({ projectId })) {
71
+ await restartApp({ projectId });
72
+ }
73
+ logger.info(
74
+ `Updated environment variables for ${projectId}: ${JSON.stringify(
75
+ envVars,
76
+ null,
77
+ 2
78
+ )}`
79
+ );
38
80
  }
39
81
 
40
- export async function getAllEnvs(props: { projectId: string }) {
82
+ async function convertStringEnvsToKeyVal(stringEnvs: string[]) {
83
+ const envVars = {};
84
+ stringEnvs.forEach((env: string) => {
85
+ const keyVal = env.split('=');
86
+ envVars[keyVal[0]] = keyVal[1];
87
+ });
88
+ return envVars;
89
+ }
90
+
91
+ export async function getAllEnvs(props: {
92
+ projectId: string;
93
+ }): Promise<EnvVars> {
41
94
  const { projectId } = props;
42
- if (!(await AgentConfigFile().isAppReady({ projectId }))) {
43
- throw new Error(`App ${projectId} is not ready!`);
44
- }
95
+ await requireAppReady({ projectId });
45
96
  const appDir = getAppDir(projectId);
46
97
  const spawner = JsSpawner({ path: appDir });
47
- const envVars = {};
98
+ const envVars: EnvVars = {};
48
99
  const composeParsed = await readDockerCompose({ projectId });
49
100
  if ('services' in composeParsed) {
50
101
  const services = Object.keys(composeParsed['services']);
51
102
  for (const s of services) {
52
- envVars[s] = [];
103
+ envVars[s] = {};
53
104
  const service = composeParsed['services'][s];
105
+ // Read env_file first, since it is lowest on the hierarchy
54
106
  if ('env_file' in service) {
55
107
  const envFiles: string[] = service['env_file'];
56
108
  for (const ef of envFiles) {
57
- envVars[s] = envVars[s].concat(
58
- (await spawner.readFile(ef)).split('\n')
59
- );
109
+ if (!(await spawner.exists(ef))) {
110
+ logger.error(
111
+ `Skipping env file ${ef} for service ${s}: not found!`
112
+ );
113
+ continue;
114
+ }
115
+ let envFileLines = (await spawner.readFile(ef)).split('\n');
116
+ // Filter out empty lines and comment lines
117
+ envFileLines = envFileLines.filter((v) => {
118
+ return v !== '' && !v.includes('#');
119
+ });
120
+ const newEnvVars = await convertStringEnvsToKeyVal(envFileLines);
121
+ envVars[s] = { ...envVars[s], ...newEnvVars };
60
122
  }
61
123
  }
62
124
  if ('environment' in service) {
63
125
  const environment: string[] = service['environment'];
64
- envVars[s] = envVars[s].concat(environment);
126
+ const newEnvVars = await convertStringEnvsToKeyVal(environment);
127
+ envVars[s] = { ...envVars[s], ...newEnvVars };
65
128
  }
66
- // Filter out empty lines and comment lines
67
- envVars[s] = envVars[s].filter((v) => {
68
- return v !== '' && !v.includes('#');
69
- });
70
129
  }
71
130
  }
72
131
 
@@ -1,9 +1,4 @@
1
- import {
2
- readAppCfgFile,
3
- updateAppCfg,
4
- readDockerCompose,
5
- writeDockerCompose
6
- } from './config';
1
+ import { readAppCfgFile, updateAppCfg } from './config';
7
2
  import { installApp, uninstallApp } from './install';
8
3
  import { rollbackApp } from './backup';
9
4
  import {
@@ -14,13 +9,11 @@ import {
14
9
  restartApp
15
10
  } from './status';
16
11
  import { ModelDetails } from './types';
17
- import { getAllEnvs, setEnv } from './environment-variables';
12
+ import { EnvVars, getAllEnvs, setEnv } from './environment-variables';
18
13
 
19
14
  export {
20
15
  readAppCfgFile,
21
16
  updateAppCfg,
22
- readDockerCompose,
23
- writeDockerCompose,
24
17
  installApp,
25
18
  uninstallApp,
26
19
  rollbackApp,
@@ -30,6 +23,7 @@ export {
30
23
  stopApp,
31
24
  restartApp,
32
25
  ModelDetails,
26
+ EnvVars,
33
27
  getAllEnvs,
34
28
  setEnv
35
29
  };
@@ -13,8 +13,7 @@ import { join, dirname } from 'path';
13
13
  import { AgentConfigFile } from '../infrastructure/agent-config';
14
14
  import { copyDir } from '../util/copy-dir';
15
15
  import { runInDir } from '../util/run-in-dir';
16
- import { restartApp } from './status';
17
-
16
+ import { isAppStarted, restartApp } from './status';
18
17
  import { ModelDetails } from './types';
19
18
  import {
20
19
  buildApp,
@@ -24,7 +23,7 @@ import {
24
23
  } from './utils';
25
24
  import { MODEL_JSON_FILE_NAME } from 'alwaysai/lib/core/model';
26
25
  import { APP_MODELS_DIRECTORY_NAME } from 'alwaysai/lib/constants';
27
- import { readAppCfgFile, writeAppCfgFile } from './config';
26
+ import { writeAppCfgFile } from './config';
28
27
  import { AppConfig } from '@alwaysai/app-configuration-schemas';
29
28
 
30
29
  export async function getAppModels(props: { projectId: string }) {
@@ -158,27 +157,21 @@ export async function installModelsWithPresignedURLs(
158
157
  export async function updateModelsWithPresignedUrls(props: {
159
158
  projectId: string;
160
159
  modelInstallPayloads: ModelInstallPayload[];
161
- appReleaseHash: string;
162
160
  newAppCfg: AppConfig;
163
161
  }) {
164
- const { projectId, modelInstallPayloads, appReleaseHash, newAppCfg } = props;
162
+ const { projectId, modelInstallPayloads, newAppCfg } = props;
165
163
  logger.info(`Installing models for ${projectId}`);
166
164
  const spawner = JsSpawner();
167
165
  const appDir = getAppDir(projectId);
168
166
 
169
- if (await AgentConfigFile().isAppPresent({ projectId })) {
170
- if (!(await AgentConfigFile().isAppReady({ projectId }))) {
171
- throw new Error('Application already has installation in progress!');
172
- }
173
- logger.info('Updating installed application');
174
- await AgentConfigFile().setAppInstalling({
175
- projectId,
176
- version: appReleaseHash
177
- });
178
- } else {
179
- throw new Error('Application is not installed!');
180
- }
181
- const ogAppCfg = await readAppCfgFile({ projectId });
167
+ await requireAppReady({ projectId });
168
+ const appReleaseHash = await AgentConfigFile().getAppVersion({
169
+ projectId
170
+ });
171
+ await AgentConfigFile().setAppInstalling({
172
+ projectId,
173
+ version: appReleaseHash
174
+ });
182
175
 
183
176
  const ogDir = path.join(appDir, APP_MODELS_DIRECTORY_NAME);
184
177
  // Copy all current models to restore dir in case of failure
@@ -206,19 +199,11 @@ export async function updateModelsWithPresignedUrls(props: {
206
199
  version: appReleaseHash
207
200
  });
208
201
 
209
- await restartApp({ projectId });
202
+ if (await isAppStarted({ projectId })) {
203
+ await restartApp({ projectId });
204
+ }
210
205
 
211
206
  logger.info(`Models installed for project ${projectId}`);
212
- /* Leave error handling to higher level so errors are sent to cloud
213
- } catch (e) {
214
- logger.error(
215
- 'Error updating app models from presigned URL, restoring models.',
216
- e.message
217
- );
218
- await spawner.rimraf(ogDir);
219
- await copyDir({ srcPath: restoreDir, destPath: ogDir });
220
- await writeAppCfgFile({ projectId, appCfg: ogAppCfg });
221
- */
222
207
  } finally {
223
208
  await spawner.rimraf(tmpDir);
224
209
  await spawner.rimraf(restoreDir);
@@ -26,6 +26,8 @@ export async function getAppStatus(props: {
26
26
  };
27
27
  if (!(await AgentConfigFile().isAppReady({ projectId }))) {
28
28
  // App is being installed or updated
29
+ // FIXME: We may be able to get the service status even
30
+ // though an update is in progress
29
31
  return { appDetails, services: [] };
30
32
  }
31
33
 
@@ -95,6 +97,24 @@ export async function getAppStatus(props: {
95
97
  return { appDetails, services };
96
98
  }
97
99
 
100
+ export async function isAppStarted(props: {
101
+ projectId: string;
102
+ }): Promise<boolean> {
103
+ const { projectId } = props;
104
+ const appStatus = await getAppStatus({ projectId });
105
+ if (appStatus.services.length === 0) {
106
+ // Services list will be empty if app has not yet been started.
107
+ // NOTE: This depends on internal handling in getAppStatus()
108
+ return false;
109
+ }
110
+ for (const service of appStatus.services) {
111
+ if (service.state === keyMirrors.appState.stopped) {
112
+ return false;
113
+ }
114
+ }
115
+ return true;
116
+ }
117
+
98
118
  export async function getAppLogs(props: {
99
119
  projectId: string;
100
120
  services?: string[];
@@ -28,7 +28,7 @@ export async function requireAppInstalled(props: { projectId: string }) {
28
28
  const { projectId } = props;
29
29
  // Ensure an app that is being installed or updated is not modified
30
30
  if (!(await AgentConfigFile().isAppPresent({ projectId }))) {
31
- throw new Error('Application is not installed');
31
+ throw new Error(`Application ${projectId} is not installed`);
32
32
  }
33
33
  }
34
34
 
@@ -36,7 +36,9 @@ export async function requireAppReady(props: { projectId: string }) {
36
36
  const { projectId } = props;
37
37
  await requireAppInstalled({ projectId });
38
38
  if (!(await AgentConfigFile().isAppReady({ projectId }))) {
39
- throw new Error('Application is not done installing or updating');
39
+ throw new Error(
40
+ `Application ${projectId} is not done installing or updating`
41
+ );
40
42
  }
41
43
  }
42
44