Skip to content

Commit 7604bf2

Browse files
committed
feat: Added support for Codify remote files
1 parent 2b5e4fa commit 7604bf2

File tree

11 files changed

+243
-57
lines changed

11 files changed

+243
-57
lines changed

package-lock.json

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"ajv": "^8.12.0",
1818
"ajv-formats": "^3.0.1",
1919
"chalk": "^5.3.0",
20-
"codify-schemas": "^1.0.76",
20+
"codify-schemas": "^1.0.77",
2121
"cors": "^2.8.5",
2222
"debug": "^4.3.4",
2323
"detect-indent": "^7.0.1",

src/api/backend/index.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import * as fsSync from 'node:fs';
2+
import * as fs from 'node:fs/promises';
3+
import path from 'node:path';
4+
import { Readable } from 'node:stream';
5+
import { finished } from 'node:stream/promises';
6+
7+
import { PluginSearchQuery, PluginSearchResult } from './types.js';
8+
9+
const API_BASE_URL = 'https://api.codifycli.com'
10+
11+
export const ApiClient = {
12+
async searchPlugins(query: PluginSearchQuery[]): Promise<PluginSearchResult> {
13+
const body = JSON.stringify({ query });
14+
const res = await fetch(
15+
`${API_BASE_URL}/v1/plugins/versions/search`,
16+
{ method: 'POST', body, headers: { 'Content-Type': 'application/json' } }
17+
);
18+
19+
if (!res.ok) {
20+
const message = await res.text();
21+
throw new Error(message);
22+
}
23+
24+
const json = await res.json();
25+
return json.results as unknown as PluginSearchResult;
26+
},
27+
28+
async downloadPlugin(filePath: string, url: string): Promise<void> {
29+
const { body } = await fetch(url)
30+
31+
const dirname = path.dirname(filePath);
32+
if (!await fs.stat(dirname).then((s) => s.isDirectory()).catch(() => false)) {
33+
await fs.mkdir(dirname, { recursive: true });
34+
}
35+
36+
const ws = fsSync.createWriteStream(filePath)
37+
// Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that
38+
await finished(Readable.fromWeb(body as never).pipe(ws));
39+
},
40+
41+
async getRemoteFileHash(filePath: string, credentials: string): Promise<string> {
42+
const { documentId, fileId } = this.extractCodifyFileInfo(filePath);
43+
44+
const response = await fetch((`https://api.codifycli.com/v1/documents/${documentId}/file/${fileId}/hash`), {
45+
method: 'GET',
46+
headers: {
47+
'Authorization': `Bearer ${credentials}`,
48+
},
49+
});
50+
51+
if (!response.ok) {
52+
throw new Error(`Failed to get remote file hash for ${filePath}`);
53+
}
54+
55+
const data = await response.json();
56+
return data.hash;
57+
},
58+
59+
async updateRemoteFile(filePath: string, content: Blob, credentials: string): Promise<string> {
60+
const { documentId, fileId } = this.extractCodifyFileInfo(filePath);
61+
62+
const formData = new FormData();
63+
formData.append('file', content);
64+
65+
const response = await fetch((`https://api.codifycli.com/v1/documents/${documentId}/file/${fileId}`), {
66+
method: 'PUT',
67+
headers: {
68+
'Authorization': `Bearer ${credentials}`,
69+
},
70+
body: formData,
71+
});
72+
73+
if (!response.ok) {
74+
throw new Error(`Failed to save remote file ${filePath}`);
75+
}
76+
},
77+
78+
extractCodifyFileInfo(url: string) {
79+
const regex = /codify:\/\/(.*):(.*)/
80+
81+
const [, group1, group2] = regex.exec(url) ?? [];
82+
if (!group1 || !group2) {
83+
throw new Error(`Invalid codify url ${url} for file`);
84+
}
85+
86+
return {
87+
documentId: group1,
88+
fileId: group2,
89+
}
90+
},
91+
};

src/api/index.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/common/base-command.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import createDebug from 'debug';
66

77
import { LoginHelper } from '../connect/login-helper.js';
88
import { Event, ctx } from '../events/context.js';
9+
import { LoginOrchestrator } from '../orchestrators/login.js';
910
import { Reporter, ReporterFactory, ReporterType } from '../ui/reporters/reporter.js';
1011
import { SudoUtils } from '../utils/sudo.js';
1112
import { prettyPrintError } from './errors.js';
@@ -65,7 +66,42 @@ export abstract class BaseCommand extends Command {
6566
ctx.pressKeyToContinueCompleted(pluginName)
6667
})
6768

69+
ctx.on(Event.CODIFY_LOGIN_CREDENTIALS_REQUEST, async (pluginName: string) => {
70+
if (pluginName !== 'default') {
71+
throw new Error(`Only the default plugin can request Codify credentials. Instead received ${pluginName}`);
72+
}
73+
74+
if (LoginHelper.get()?.isLoggedIn) {
75+
const credentials = LoginHelper.get()?.credentials?.accessToken;
76+
if (!credentials) {
77+
throw new Error('Unable to retrieve Codify credentials for user...');
78+
}
79+
80+
ctx.codifyLoginCompleted(pluginName, credentials);
81+
} else {
82+
ctx.log('User is not currently logged. Attempt to Login to Codify...');
83+
await LoginOrchestrator.run();
84+
85+
if (LoginHelper.get()?.isLoggedIn) {
86+
const credentials = LoginHelper.get()?.credentials?.accessToken;
87+
if (!credentials) {
88+
throw new Error('Unable to retrieve Codify credentials for user...');
89+
}
90+
91+
ctx.codifyLoginCompleted(pluginName, credentials);
92+
} else {
93+
throw new Error('Unable to login...')
94+
}
95+
}
96+
})
97+
6898
await LoginHelper.load();
99+
100+
// Catch any un-caught exceptions
101+
process.on('uncaughtException', (error) => {
102+
console.log('Caught exception')
103+
this.catch(error);
104+
})
69105
}
70106

71107
protected async catch(err: Error): Promise<void> {

src/events/context.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export enum Event {
1717
SUDO_REQUEST_GRANTED = 'sudo_request_granted',
1818
PRESS_KEY_TO_CONTINUE_REQUEST = 'press_key_to_continue_request',
1919
PRESS_KEY_TO_CONTINUE_COMPLETED = 'press_key_to_continue_completed',
20+
CODIFY_LOGIN_CREDENTIALS_REQUEST = 'codify_login_credentials_request',
21+
CODIFY_LOGIN_CREDENTIALS_COMPLETED = 'codify_login_credentials_completed',
2022
}
2123

2224
export enum ProcessName {
@@ -116,6 +118,14 @@ export const ctx = new class {
116118
this.emitter.emit(Event.PRESS_KEY_TO_CONTINUE_COMPLETED, pluginName);
117119
}
118120

121+
codifyLoginRequested(pluginName: string) {
122+
this.emitter.emit(Event.CODIFY_LOGIN_CREDENTIALS_REQUEST, pluginName);
123+
}
124+
125+
codifyLoginCompleted(pluginName: string, credentials: string) {
126+
this.emitter.emit(Event.CODIFY_LOGIN_CREDENTIALS_COMPLETED, pluginName, credentials);
127+
}
128+
119129
async subprocess<T>(name: string, run: () => Promise<T>): Promise<T> {
120130
this.emitter.emit(Event.SUB_PROCESS_START, name);
121131
const result = await run();

src/orchestrators/import.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import chalk from 'chalk';
2+
import fs from 'node:fs/promises';
13
import path from 'node:path';
24

5+
import { ApiClient } from '../api/backend/index.js';
36
import { InitializationResult, PluginInitOrchestrator } from '../common/initialize-plugins.js';
7+
import { LoginHelper } from '../connect/login-helper.js';
48
import { Project } from '../entities/project.js';
59
import { ResourceConfig } from '../entities/resource-config.js';
610
import { ResourceInfo } from '../entities/resource-info.js';
@@ -15,6 +19,7 @@ import { PromptType, Reporter } from '../ui/reporters/reporter.js';
1519
import { FileUtils } from '../utils/file.js';
1620
import { groupBy, sleep } from '../utils/index.js';
1721
import { wildCardMatch } from '../utils/wild-card-match.js';
22+
import { LoginOrchestrator } from './login.js';
1823

1924
export type ImportResult = { result: ResourceConfig[], errors: string[] }
2025

@@ -161,8 +166,10 @@ export class ImportOrchestrator {
161166
pluginManager: PluginManager,
162167
args: ImportArgs,
163168
): Promise<void> {
164-
const multipleCodifyFiles = project.codifyFiles.length > 1;
169+
// Special handling for remote-file resources. Offer to save them remotely if any changes are detected on import.
170+
await ImportOrchestrator.handleCodifyRemoteFiles(reporter, importResult);
165171

172+
const multipleCodifyFiles = project.codifyFiles.length > 1;
166173
const saveType = await ImportOrchestrator.getSaveType(reporter, project, args);
167174

168175
// Update an existing file
@@ -259,6 +266,61 @@ export class ImportOrchestrator {
259266
await sleep(100);
260267
}
261268

269+
// Special handling for codify cloud files. Import and refresh can automatically save file updates.
270+
static async handleCodifyRemoteFiles(reporter: Reporter, importResult: ImportResult) {
271+
try {
272+
if (!importResult.result.some((r) => r.type === 'remote-file')) {
273+
return;
274+
}
275+
276+
if (!LoginHelper.get()?.isLoggedIn) {
277+
await LoginOrchestrator.run();
278+
}
279+
280+
const credentials = LoginHelper.get()!.credentials!.accessToken;
281+
282+
const filesToUpdate = [];
283+
const remoteFiles = importResult.result.filter((r) => r.type === 'remote-file');
284+
for (const file of remoteFiles) {
285+
if (!file.parameters.remote || !file.parameters.hash) {
286+
continue;
287+
}
288+
289+
const hash = await ApiClient.getRemoteFileHash(file.parameters.remote as string, credentials);
290+
if (hash !== file.parameters.hash) {
291+
filesToUpdate.push(file);
292+
}
293+
}
294+
295+
if (filesToUpdate.length === 0) {
296+
return;
297+
}
298+
299+
const fileNames = filesToUpdate.map((f) => `'${f.parameters.path}'`).join(', ')
300+
const shouldUpdate = await reporter.promptConfirmation(
301+
`The following files have been updated: [${fileNames}].\nDo you want to upload the changes to Codify cloud? ${chalk.bold('(Warning this will override any existing data!)')}`,
302+
);
303+
304+
if (!shouldUpdate) {
305+
return;
306+
}
307+
308+
for (const file of filesToUpdate) {
309+
if (!file.parameters.path) {
310+
console.warn(`Unable to find file path for file ${file.parameters.remote}`)
311+
continue;
312+
}
313+
314+
const content = await fs.readFile(file.parameters.path as string);
315+
await ApiClient.updateRemoteFile(file.parameters.remote as string, new Blob([content]), credentials);
316+
}
317+
318+
ctx.log('Successfully uploaded changes to Codify cloud');
319+
} catch {
320+
console.warn('Unable to process remote-files');
321+
}
322+
}
323+
262324
private static matchTypeIds(typeIds: string[], validTypeIds: string[]): string[] {
263325
const result: string[] = [];
264326
const unsupportedTypeIds: string[] = [];

src/orchestrators/refresh.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export class RefreshOrchestrator {
3737

3838
reporter.displayImportResult(importResult, false);
3939

40+
41+
// Special handling for remote-file resources. Offer to save them remotely if any changes are detected on import.
42+
await ImportOrchestrator.handleCodifyRemoteFiles(reporter, importResult);
43+
4044
const resourceInfoList = await pluginManager.getMultipleResourceInfo(
4145
project.resourceConfigs.map((r) => r.type),
4246
);

0 commit comments

Comments
 (0)