Skip to content

Commit 7da35eb

Browse files
committed
Added remote files + small fixes
1 parent 75aa290 commit 7da35eb

File tree

9 files changed

+245
-38
lines changed

9 files changed

+245
-38
lines changed

package.json

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "0.15.1",
3+
"version": "0.16.1",
44
"description": "",
55
"main": "dist/index.js",
66
"scripts": {
@@ -22,51 +22,52 @@
2222
"dependencies": {
2323
"ajv": "^8.12.0",
2424
"ajv-formats": "^2.1.1",
25-
"semver": "^7.6.0",
26-
"codify-plugin-lib": "1.0.176",
27-
"codify-schemas": "1.0.63",
2825
"chalk": "^5.3.0",
26+
"codify-plugin-lib": "../codify-plugin-lib",
27+
"codify-schemas": "1.0.63",
2928
"debug": "^4.3.4",
30-
"plist": "^3.1.0",
3129
"lodash.isequal": "^4.5.0",
30+
"nanoid": "^5.0.9",
31+
"plist": "^3.1.0",
32+
"semver": "^7.6.0",
3233
"strip-ansi": "^7.1.0",
33-
"nanoid": "^5.0.9"
34+
"trash": "^10.0.0"
3435
},
3536
"devDependencies": {
36-
"rollup": "^4.12.0",
37-
"@rollup/plugin-json": "^6.1.0",
38-
"@rollup/plugin-typescript": "^11.1.6",
37+
"@apidevtools/json-schema-ref-parser": "^11.7.2",
38+
"@fastify/merge-json-schemas": "^0.2.0",
39+
"@oclif/prettier-config": "^0.2.1",
40+
"@oclif/test": "^3",
3941
"@rollup/plugin-commonjs": "^25.0.7",
42+
"@rollup/plugin-json": "^6.1.0",
4043
"@rollup/plugin-node-resolve": "^15.2.3",
4144
"@rollup/plugin-replace": "^6.0.2",
4245
"@rollup/plugin-terser": "^0.4.4",
43-
"@oclif/prettier-config": "^0.2.1",
44-
"@oclif/test": "^3",
45-
"@types/node": "^18",
46-
"@types/semver": "^7.5.4",
47-
"@types/mock-fs": "^4.13.4",
46+
"@rollup/plugin-typescript": "^11.1.6",
4847
"@types/chalk": "^2.2.0",
4948
"@types/commander": "^2.12.2",
5049
"@types/debug": "4.1.12",
51-
"@types/plist": "^3.0.5",
5250
"@types/lodash.isequal": "^4.5.8",
53-
"codify-plugin-test": "0.0.51",
51+
"@types/mock-fs": "^4.13.4",
52+
"@types/node": "^18",
53+
"@types/plist": "^3.0.5",
54+
"@types/semver": "^7.5.4",
55+
"codify-plugin-test": "0.0.52",
5456
"commander": "^12.1.0",
5557
"eslint": "^8.51.0",
5658
"eslint-config-oclif": "^5",
5759
"eslint-config-oclif-typescript": "^3",
60+
"eslint-config-prettier": "^9.0.0",
5861
"glob": "^11.0.0",
59-
"vitest": "^1.4.0",
62+
"merge-json-schemas": "^1.0.0",
6063
"mock-fs": "^5.2.0",
64+
"rollup": "^4.12.0",
6165
"shx": "^0.3.3",
6266
"ts-node": "^10.9.1",
6367
"tslib": "^2.6.2",
64-
"typescript": "^5",
65-
"eslint-config-prettier": "^9.0.0",
6668
"tsx": "^4.7.2",
67-
"@fastify/merge-json-schemas": "^0.2.0",
68-
"@apidevtools/json-schema-ref-parser": "^11.7.2",
69-
"merge-json-schemas": "^1.0.0"
69+
"typescript": "^5",
70+
"vitest": "^1.4.0"
7071
},
7172
"engines": {
7273
"node": ">=18.0.0"

src/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import { AsdfLocalResource } from './resources/asdf/asdf-local.js';
88
import { AsdfPluginResource } from './resources/asdf/asdf-plugin.js';
99
import { AwsCliResource } from './resources/aws-cli/cli/aws-cli.js';
1010
import { AwsProfileResource } from './resources/aws-cli/profile/aws-profile.js';
11+
import { DockerResource } from './resources/docker/docker.js';
12+
import { RemoteFileResource } from './resources/file/remote-file.js';
1113
import { GitCloneResource } from './resources/git/clone/git-repository.js';
1214
import { GitResource } from './resources/git/git/git-resource.js';
1315
import { GitLfsResource } from './resources/git/lfs/git-lfs.js';
1416
import { WaitGithubSshKey } from './resources/git/wait-github-ssh-key/wait-github-ssh-key.js';
1517
import { HomebrewResource } from './resources/homebrew/homebrew.js';
1618
import { JenvResource } from './resources/java/jenv/jenv.js';
19+
import { MacportsResource } from './resources/macports/macports.js';
20+
import { Npm } from './resources/node/npm/npm.js';
1721
import { NvmResource } from './resources/node/nvm/nvm.js';
1822
import { Pnpm } from './resources/node/pnpm/pnpm.js';
1923
import { PgcliResource } from './resources/pgcli/pgcli.js';
@@ -24,7 +28,6 @@ import { VenvProject } from './resources/python/venv/venv-project.js';
2428
import { Virtualenv } from './resources/python/virtualenv/virtualenv.js';
2529
import { VirtualenvProject } from './resources/python/virtualenv/virtualenv-project.js';
2630
import { ActionResource } from './resources/scripting/action.js';
27-
import { FileResource } from './resources/scripting/file.js';
2831
import { AliasResource } from './resources/shell/alias/alias-resource.js';
2932
import { PathResource } from './resources/shell/path/path-resource.js';
3033
import { SshAddResource } from './resources/ssh/ssh-add.js';
@@ -33,9 +36,7 @@ import { SshKeyResource } from './resources/ssh/ssh-key.js';
3336
import { TerraformResource } from './resources/terraform/terraform.js';
3437
import { VscodeResource } from './resources/vscode/vscode.js';
3538
import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js';
36-
import { MacportsResource } from './resources/macports/macports.js';
37-
import { Npm } from './resources/node/npm/npm.js';
38-
import { DockerResource } from './resources/docker/docker.js';
39+
import { FileResource } from './resources/file/file.js';
3940

4041
runPlugin(Plugin.create(
4142
'default',
@@ -66,6 +67,7 @@ runPlugin(Plugin.create(
6667
new SshAddResource(),
6768
new ActionResource(),
6869
new FileResource(),
70+
new RemoteFileResource(),
6971
new Virtualenv(),
7072
new VirtualenvProject(),
7173
new Pnpm(),
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema",
3+
"$id": "https://www.codifycli.com/file.json",
4+
"title": "File resource",
5+
"description": "Manages a file.",
6+
"type": "object",
7+
"properties": {
8+
"path": {
9+
"type": "string",
10+
"description": "The location of the file."
11+
},
12+
"remote": {
13+
"type": "string",
14+
"description": "The contents of the file."
15+
},
16+
"onlyCreate": {
17+
"type": "boolean",
18+
"description": "Forces the resource to only create the file if it doesn't exist but don't detect any content changes."
19+
}
20+
},
21+
"required": ["path", "remote"],
22+
"additionalProperties": false
23+
}

src/resources/file/remote-file.ts

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import {
2+
CodifyCliSender,
3+
CreatePlan,
4+
DestroyPlan,
5+
ModifyPlan,
6+
ParameterChange,
7+
RefreshContext,
8+
Resource,
9+
ResourceSettings
10+
} from 'codify-plugin-lib';
11+
import { ResourceConfig } from 'codify-schemas';
12+
import { createHash } from 'node:crypto';
13+
import * as fsSync from 'node:fs';
14+
import fs from 'node:fs/promises';
15+
import path from 'node:path';
16+
import { Readable } from 'node:stream';
17+
import { finished } from 'node:stream/promises';
18+
import trash from 'trash';
19+
20+
import { FileUtils } from '../../utils/file-utils.js';
21+
import schema from './remote-file-schema.json'
22+
23+
export interface FileConfig extends ResourceConfig{
24+
path: string;
25+
remote?: string;
26+
hash?: string;
27+
onlyCreate: boolean;
28+
}
29+
30+
export class RemoteFileResource extends Resource<FileConfig> {
31+
getSettings(): ResourceSettings<FileConfig> {
32+
return {
33+
id: 'remote-file',
34+
allowMultiple: true,
35+
schema,
36+
parameterSettings: {
37+
path: { type: 'directory' },
38+
remote: { type: 'string', canModify: true },
39+
onlyCreate: { type: 'boolean', setting: true, default: false },
40+
hash: { type: 'string', canModify: true },
41+
},
42+
transformation: {
43+
to: async (input: Partial<FileConfig>) => {
44+
if (this.isRemoteCodifyFile(input.remote!)) {
45+
return this.getRemoteCodifyFileConfig(input)
46+
}
47+
48+
return input;
49+
},
50+
from: async (input: Partial<FileConfig>) => input,
51+
}
52+
}
53+
}
54+
55+
async refresh(parameters: Partial<FileConfig>, context: RefreshContext<FileConfig>): Promise<Partial<FileConfig> | null> {
56+
if (!parameters.path) {
57+
throw new Error('Path must be specified');
58+
}
59+
60+
if (!(await FileUtils.exists(parameters.path))) {
61+
return null;
62+
}
63+
64+
const current = await fs.readFile(parameters.path, 'utf8');
65+
const currentHash = createHash('md5').update(current).digest('hex');
66+
67+
return {
68+
...parameters,
69+
hash: parameters.onlyCreate ? parameters.hash : currentHash,
70+
}
71+
}
72+
73+
async create(plan: CreatePlan<FileConfig>): Promise<void> {
74+
return this.updateCodifyFile(plan.desiredConfig);
75+
}
76+
77+
async modify(pc: ParameterChange<FileConfig>, plan: ModifyPlan<FileConfig>): Promise<void> {
78+
return this.updateCodifyFile(plan.desiredConfig);
79+
}
80+
81+
destroy(plan: DestroyPlan<FileConfig>): Promise<void> {
82+
return trash(plan.currentConfig.path);
83+
}
84+
85+
private async updateCodifyFile(config: FileConfig) {
86+
const { path: filePath, remote } = config;
87+
const resolvedPath = path.resolve(filePath);
88+
89+
if (!(await FileUtils.dirExists(path.dirname(resolvedPath)))) {
90+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
91+
}
92+
93+
if (this.isRemoteCodifyFile(remote!)) {
94+
const { documentId, fileId } = this.extractCodifyFileInfo(remote!);
95+
const fileStream = fsSync.createWriteStream(filePath, { flags: 'wx' });
96+
97+
const credentials = await CodifyCliSender.getCodifyCliCredentials();
98+
const response = await fetch(`https://api.codifycli.com/v1/documents/${documentId}/file/${fileId}`, {
99+
method: 'GET',
100+
headers: {
101+
'Authorization': `Bearer ${credentials}`,
102+
},
103+
});
104+
105+
if (!response.ok) {
106+
throw new Error(`Unable to fetch file ${remote}, ${await response.text()}`);
107+
}
108+
109+
console.log(`Updating file ${filePath} with contents from ${remote}`);
110+
await finished(Readable.fromWeb(response.body as any).pipe(fileStream));
111+
} else {
112+
console.log(`Updating file ${filePath} with contents`);
113+
await fs.writeFile(filePath, remote ?? '');
114+
}
115+
116+
console.log(`Finished updating file ${filePath}`);
117+
}
118+
119+
private isRemoteCodifyFile(contents: string) {
120+
return contents?.startsWith('codify://');
121+
}
122+
123+
private async getRemoteCodifyFileConfig(parameters: Partial<FileConfig>): Promise<Partial<FileConfig>> {
124+
const { documentId, fileId } = this.extractCodifyFileInfo(parameters.remote!);
125+
126+
const credentials = await CodifyCliSender.getCodifyCliCredentials();
127+
const response = await fetch((`https://api.codifycli.com/v1/documents/${documentId}/file/${fileId}/hash`), {
128+
method: 'GET',
129+
headers: {
130+
'Authorization': `Bearer ${credentials}`,
131+
},
132+
});
133+
134+
if (!response.ok) {
135+
return {
136+
...parameters,
137+
hash: undefined,
138+
}
139+
}
140+
141+
const data = await response.json();
142+
143+
return {
144+
...parameters,
145+
hash: data.hash,
146+
};
147+
}
148+
149+
private extractCodifyFileInfo(url: string) {
150+
const regex = /codify:\/\/(.*):(.*)/
151+
152+
const [, group1, group2] = regex.exec(url) ?? [];
153+
if (!group1 || !group2) {
154+
throw new Error(`Invalid codify url ${url} for file`);
155+
}
156+
157+
return {
158+
documentId: group1,
159+
fileId: group2,
160+
}
161+
}
162+
}

src/resources/scripting/action.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CreatePlan, DestroyPlan, getPty, Resource, ResourceSettings } from 'codify-plugin-lib';
2+
import { RefreshContext } from 'codify-plugin-lib/src/resource/resource.js';
23
import { StringIndexedObject } from 'codify-schemas';
34

45
import { SpawnStatus, codifySpawn } from '../../utils/codify-spawn.js';
@@ -26,13 +27,12 @@ export class ActionResource extends Resource<ActionConfig> {
2627
}
2728
}
2829

29-
async refresh(parameters: Partial<ActionConfig>): Promise<Partial<ActionConfig> | Partial<ActionConfig>[] | null> {
30+
async refresh(parameters: Partial<ActionConfig>, context: RefreshContext<ActionConfig>): Promise<Partial<ActionConfig> | Partial<ActionConfig>[] | null> {
3031
const $ = getPty();
3132

3233
// Always run if condition doesn't exist
33-
// TODO: Remove hack. Right now we're returning null to simulate CREATE and a value for NO-OP
3434
if (!parameters.condition) {
35-
return null;
35+
return context.commandType === 'validationPlan' ? parameters : null;
3636
}
3737

3838
const { condition, action, cwd } = parameters;

src/resources/ssh/ssh-config-schema.json

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,7 @@
6060
"type": "boolean",
6161
"description": "Specifies whether to use password authentication."
6262
}
63-
},
64-
"oneOf": [
65-
{
66-
"required": ["Host"]
67-
},
68-
{
69-
"required": ["Match"]
70-
}
71-
]
63+
}
7264
}
7365
}
7466
},

test/file/remote-file.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { PluginTester } from 'codify-plugin-test';
3+
import * as path from 'node:path';
4+
import { execSync } from 'node:child_process';
5+
import fs from 'node:fs';
6+
import { ResourceOperation } from 'codify-schemas';
7+
import os from 'node:os';
8+
9+
describe('File integration tests', async () => {
10+
const pluginPath = path.resolve('./src/index.ts');
11+
12+
it('Can download a file from codify cloud', { timeout: 300000 }, async () => {
13+
await PluginTester.fullTest(pluginPath, [
14+
{
15+
type: 'remote-file',
16+
path: './my_test_file',
17+
remote: 'codify://29198d08-08fb-44aa-a014-c45690aa822b:favicon.svg',
18+
}
19+
], {
20+
validateApply: async () => {
21+
expect(fs.existsSync('./my_test_file')).to.be.true;
22+
23+
console.log(fs.readFileSync('./my_test_file', 'utf8'));
24+
}
25+
});
26+
})
27+
})

0 commit comments

Comments
 (0)