@eighty4/c2 0.0.1 → 0.0.2-1
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.
- package/README.md +0 -2
- package/lib/attachments.spec.ts +53 -0
- package/lib/build.spec.ts +93 -0
- package/lib/c2.api.ts +1 -0
- package/lib/cli.spec.ts +70 -0
- package/lib/expression.spec.ts +74 -0
- package/lib/fs.testing.ts +21 -0
- package/lib_js/attachments.js +31 -0
- package/lib_js/build.js +42 -0
- package/lib_js/c2.api.js +1 -0
- package/lib_js/c2.bin.js +35 -0
- package/lib_js/cli.js +44 -0
- package/lib_js/expression.js +47 -0
- package/lib_js/fs.js +15 -0
- package/lib_js/fs.testing.js +14 -0
- package/lib_types/attachments.d.ts +10 -0
- package/lib_types/attachments.d.ts.map +1 -0
- package/lib_types/build.d.ts +5 -0
- package/lib_types/build.d.ts.map +1 -0
- package/lib_types/c2.api.d.ts +2 -0
- package/lib_types/c2.api.d.ts.map +1 -0
- package/lib_types/c2.bin.d.ts +3 -0
- package/lib_types/c2.bin.d.ts.map +1 -0
- package/lib_types/cli.d.ts +10 -0
- package/lib_types/cli.d.ts.map +1 -0
- package/lib_types/expression.d.ts +2 -0
- package/lib_types/expression.d.ts.map +1 -0
- package/lib_types/fs.d.ts +4 -0
- package/lib_types/fs.d.ts.map +1 -0
- package/lib_types/fs.testing.d.ts +4 -0
- package/lib_types/fs.testing.d.ts.map +1 -0
- package/package.json +18 -18
- package/lib/fs.bun.ts +0 -5
- /package/{c2.ts → lib/c2.bin.ts} +0 -0
- /package/lib/{fs.node.ts → fs.ts} +0 -0
package/README.md
CHANGED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { collectAttachments } from '#c2/attachments.ts'
|
|
4
|
+
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing.ts'
|
|
5
|
+
|
|
6
|
+
let tmpDir: string
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => (tmpDir = await makeTempDir()))
|
|
9
|
+
|
|
10
|
+
afterEach(async () => await removeDir(tmpDir))
|
|
11
|
+
|
|
12
|
+
test('collect cloud config yml', async () => {
|
|
13
|
+
await makeFile('init-cloud.yml', 'whoopie', tmpDir)
|
|
14
|
+
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
15
|
+
{
|
|
16
|
+
path: join(tmpDir, 'init-cloud.yml'),
|
|
17
|
+
content: 'whoopie',
|
|
18
|
+
filename: 'init-cloud.yml',
|
|
19
|
+
type: 'cloud-config',
|
|
20
|
+
},
|
|
21
|
+
])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('collect shell script', async () => {
|
|
25
|
+
await makeFile('init-cloud.sh', 'whoopie', tmpDir)
|
|
26
|
+
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
27
|
+
{
|
|
28
|
+
path: join(tmpDir, 'init-cloud.sh'),
|
|
29
|
+
content: 'whoopie',
|
|
30
|
+
filename: 'init-cloud.sh',
|
|
31
|
+
type: 'x-shellscript',
|
|
32
|
+
},
|
|
33
|
+
])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('sorts attachments by filename', async () => {
|
|
37
|
+
await makeFile('01-init-cloud.sh', 'whoopie', tmpDir)
|
|
38
|
+
await makeFile('02-init-cloud.sh', 'whoopie', tmpDir)
|
|
39
|
+
expect(await collectAttachments(tmpDir)).toStrictEqual([
|
|
40
|
+
{
|
|
41
|
+
path: join(tmpDir, '01-init-cloud.sh'),
|
|
42
|
+
content: 'whoopie',
|
|
43
|
+
filename: '01-init-cloud.sh',
|
|
44
|
+
type: 'x-shellscript',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
path: join(tmpDir, '02-init-cloud.sh'),
|
|
48
|
+
content: 'whoopie',
|
|
49
|
+
filename: '02-init-cloud.sh',
|
|
50
|
+
type: 'x-shellscript',
|
|
51
|
+
},
|
|
52
|
+
])
|
|
53
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { buildUserData } from '#c2/build.ts'
|
|
3
|
+
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing.ts'
|
|
4
|
+
|
|
5
|
+
let tmpDir: string
|
|
6
|
+
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
tmpDir = await makeTempDir()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
afterEach(async () => removeDir(tmpDir))
|
|
12
|
+
|
|
13
|
+
test('build user data of single file', async () => {
|
|
14
|
+
const initCloudYml = 'whoopie'
|
|
15
|
+
await makeFile('init-cloud.yml', initCloudYml, tmpDir)
|
|
16
|
+
expect(await buildUserData(tmpDir)).toStrictEqual('whoopie')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('build user data of single file with template expression', async () => {
|
|
20
|
+
const whoopie = await makeTempDir()
|
|
21
|
+
await makeFile('whoopie', 'whoopie', whoopie)
|
|
22
|
+
await makeFile(
|
|
23
|
+
'init-cloud.yml',
|
|
24
|
+
`\${{ file('${whoopie}/whoopie')}}`,
|
|
25
|
+
tmpDir,
|
|
26
|
+
)
|
|
27
|
+
expect(await buildUserData(tmpDir)).toStrictEqual('whoopie')
|
|
28
|
+
await removeDir(whoopie)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('build user data multipart message', async () => {
|
|
32
|
+
await makeFile('1-init-cloud.yml', 'whoopie', tmpDir)
|
|
33
|
+
await makeFile('2-init-cloud.sh', 'cushion', tmpDir)
|
|
34
|
+
const boundary = Bun.randomUUIDv7()
|
|
35
|
+
expect(
|
|
36
|
+
await buildUserData(tmpDir, { attachmentBoundary: boundary }),
|
|
37
|
+
).toStrictEqual(
|
|
38
|
+
`Content-Type: multipart/mixed; boundary=${boundary}
|
|
39
|
+
MIME-Version: 1.0
|
|
40
|
+
Number-Attachments: 2
|
|
41
|
+
--${boundary}
|
|
42
|
+
Content-Type: text/cloud-config; charset="us-ascii"
|
|
43
|
+
MIME-Version: 1.0
|
|
44
|
+
Content-Transfer-Encoding: 7bit
|
|
45
|
+
Content-Disposition: attachment; filename="1-init-cloud.yml"
|
|
46
|
+
|
|
47
|
+
whoopie
|
|
48
|
+
--${boundary}
|
|
49
|
+
Content-Type: text/x-shellscript; charset="us-ascii"
|
|
50
|
+
MIME-Version: 1.0
|
|
51
|
+
Content-Transfer-Encoding: 7bit
|
|
52
|
+
Content-Disposition: attachment; filename="2-init-cloud.sh"
|
|
53
|
+
|
|
54
|
+
cushion
|
|
55
|
+
--${boundary}
|
|
56
|
+
`,
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('build user data multipart with template expression', async () => {
|
|
61
|
+
const whoopie = await makeTempDir()
|
|
62
|
+
await makeFile('whoopie', 'whoopie', whoopie)
|
|
63
|
+
await makeFile(
|
|
64
|
+
'1-init-cloud.yml',
|
|
65
|
+
`\${{ file('${whoopie}/whoopie')}}`,
|
|
66
|
+
tmpDir,
|
|
67
|
+
)
|
|
68
|
+
await makeFile('2-init-cloud.sh', 'cushion', tmpDir)
|
|
69
|
+
const boundary = Bun.randomUUIDv7()
|
|
70
|
+
expect(
|
|
71
|
+
await buildUserData(tmpDir, { attachmentBoundary: boundary }),
|
|
72
|
+
).toStrictEqual(
|
|
73
|
+
`Content-Type: multipart/mixed; boundary=${boundary}
|
|
74
|
+
MIME-Version: 1.0
|
|
75
|
+
Number-Attachments: 2
|
|
76
|
+
--${boundary}
|
|
77
|
+
Content-Type: text/cloud-config; charset="us-ascii"
|
|
78
|
+
MIME-Version: 1.0
|
|
79
|
+
Content-Transfer-Encoding: 7bit
|
|
80
|
+
Content-Disposition: attachment; filename="1-init-cloud.yml"
|
|
81
|
+
|
|
82
|
+
whoopie
|
|
83
|
+
--${boundary}
|
|
84
|
+
Content-Type: text/x-shellscript; charset="us-ascii"
|
|
85
|
+
MIME-Version: 1.0
|
|
86
|
+
Content-Transfer-Encoding: 7bit
|
|
87
|
+
Content-Disposition: attachment; filename="2-init-cloud.sh"
|
|
88
|
+
|
|
89
|
+
cushion
|
|
90
|
+
--${boundary}
|
|
91
|
+
`,
|
|
92
|
+
)
|
|
93
|
+
})
|
package/lib/c2.api.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type BuildUserDataOpts, buildUserData } from '#c2/build.ts'
|
package/lib/cli.spec.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { parseArgs } from '#c2/cli.ts'
|
|
3
|
+
|
|
4
|
+
test('parseArgs', () => {
|
|
5
|
+
expect(
|
|
6
|
+
parseArgs([
|
|
7
|
+
'/Users/who/.bun/bin/bun',
|
|
8
|
+
'/Users/who/user-data/c2.ts',
|
|
9
|
+
'user_data_dir',
|
|
10
|
+
]),
|
|
11
|
+
).toStrictEqual({
|
|
12
|
+
base64: false,
|
|
13
|
+
userDataDir: 'user_data_dir',
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('parseArgs errors without USER_DATA_DIR', () => {
|
|
18
|
+
expect(() =>
|
|
19
|
+
parseArgs(['/Users/who/.bun/bin/bun', '/Users/who/user-data/c2.ts']),
|
|
20
|
+
).toThrow()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('parseArgs errors with extra USER_DATA_DIR', () => {
|
|
24
|
+
expect(() =>
|
|
25
|
+
parseArgs([
|
|
26
|
+
'/Users/who/.bun/bin/bun',
|
|
27
|
+
'/Users/who/user-data/c2.ts',
|
|
28
|
+
'user_data_dir',
|
|
29
|
+
'some_other_arg',
|
|
30
|
+
]),
|
|
31
|
+
).toThrow('')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('parseArgs with --base64', () => {
|
|
35
|
+
expect(
|
|
36
|
+
parseArgs([
|
|
37
|
+
'/Users/who/.bun/bin/bun',
|
|
38
|
+
'/Users/who/user-data/c2.ts',
|
|
39
|
+
'--base64',
|
|
40
|
+
'user_data_dir',
|
|
41
|
+
]),
|
|
42
|
+
).toStrictEqual({
|
|
43
|
+
base64: true,
|
|
44
|
+
userDataDir: 'user_data_dir',
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('parseArgs with --http PORT', () => {
|
|
49
|
+
expect(
|
|
50
|
+
parseArgs([
|
|
51
|
+
'/Users/who/.bun/bin/bun',
|
|
52
|
+
'/Users/who/user-data/c2.ts',
|
|
53
|
+
'--http',
|
|
54
|
+
'6666',
|
|
55
|
+
'user_data_dir',
|
|
56
|
+
]),
|
|
57
|
+
).toStrictEqual({ httpPort: 6666, userDataDir: 'user_data_dir' })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('parseArgs with --http bunk', () => {
|
|
61
|
+
expect(() =>
|
|
62
|
+
parseArgs([
|
|
63
|
+
'/Users/who/.bun/bin/bun',
|
|
64
|
+
'/Users/who/user-data/c2.ts',
|
|
65
|
+
'--http',
|
|
66
|
+
'bunk',
|
|
67
|
+
'user_data_dir',
|
|
68
|
+
]),
|
|
69
|
+
).toThrow('--http bunk is not a valid http port')
|
|
70
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { evalTemplateExpressions } from '#c2/expression.ts'
|
|
4
|
+
import { makeFile, makeTempDir, removeDir } from '#c2/fs.testing.ts'
|
|
5
|
+
|
|
6
|
+
let files: Array<string> = []
|
|
7
|
+
let tmpDir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => (tmpDir = await makeTempDir()))
|
|
10
|
+
|
|
11
|
+
afterEach(async () => await removeDir(tmpDir))
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
for (const p of files) {
|
|
15
|
+
await Bun.file(p).delete()
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('env() found', async () => {
|
|
20
|
+
const envVar = 'C2_TEST_' + Bun.randomUUIDv7().substring(0, 8).toUpperCase()
|
|
21
|
+
Bun.env[envVar] = Bun.randomUUIDv7()
|
|
22
|
+
expect(await evalTemplateExpressions(`\${{ env('${envVar}') }}`)).toBe(
|
|
23
|
+
Bun.env[envVar],
|
|
24
|
+
)
|
|
25
|
+
delete Bun.env[envVar]
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('env() not found', async () => {
|
|
29
|
+
const envVar = 'C2_TEST_' + Bun.randomUUIDv7().substring(0, 8).toUpperCase()
|
|
30
|
+
expect(() => evalTemplateExpressions(` \${{ env('${envVar}') }}`)).toThrow(
|
|
31
|
+
`env var \`${envVar}\` does not exist`,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('file() not found', async () => {
|
|
36
|
+
const path = join(await makeTempDir(), 'whoopie')
|
|
37
|
+
expect(() => evalTemplateExpressions(`\${{ file('${path}') }}`)).toThrow(
|
|
38
|
+
`ENOENT: no such file or directory, open '${path}'`,
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('file() absolute', async () => {
|
|
43
|
+
const content = Bun.randomUUIDv7()
|
|
44
|
+
const path = await makeFile('.user-data', content, tmpDir)
|
|
45
|
+
expect(await evalTemplateExpressions(`\${{ file('${path}') }}`)).toBe(
|
|
46
|
+
content,
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('file() home', async () => {
|
|
51
|
+
const content = Bun.randomUUIDv7()
|
|
52
|
+
const filename = `.user-data.${Bun.randomUUIDv7()}`
|
|
53
|
+
const path = join(Bun.env.HOME!, filename)
|
|
54
|
+
await makeFile(path, content)
|
|
55
|
+
files.push(path)
|
|
56
|
+
expect(await evalTemplateExpressions(`\${{ file('~/${filename}') }}`)).toBe(
|
|
57
|
+
content,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('file() relative', async () => {
|
|
62
|
+
const content = Bun.randomUUIDv7()
|
|
63
|
+
const filename = `.user-data.${Bun.randomUUIDv7()}`
|
|
64
|
+
await makeFile(filename, content, tmpDir)
|
|
65
|
+
const prevPwd = process.cwd()
|
|
66
|
+
process.chdir(tmpDir)
|
|
67
|
+
try {
|
|
68
|
+
expect(
|
|
69
|
+
await evalTemplateExpressions(`\${{ file('${filename}') }}`),
|
|
70
|
+
).toBe(content)
|
|
71
|
+
} finally {
|
|
72
|
+
process.chdir(prevPwd)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export async function makeFile(
|
|
6
|
+
path: string,
|
|
7
|
+
content: string,
|
|
8
|
+
pathPrefix?: string,
|
|
9
|
+
): Promise<string> {
|
|
10
|
+
const p = !!pathPrefix ? join(pathPrefix, path) : path
|
|
11
|
+
await Bun.file(p).write(content)
|
|
12
|
+
return p
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function makeTempDir(): Promise<string> {
|
|
16
|
+
return await mkdtemp(join(tmpdir(), 'c2-test-'))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function removeDir(p: string): Promise<void> {
|
|
20
|
+
await rm(p, { force: true, recursive: true })
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readDirListing, readToString } from '#c2/fs.ts';
|
|
2
|
+
export async function collectAttachments(dir) {
|
|
3
|
+
const result = [];
|
|
4
|
+
for (const filename of await readDirListing(dir)) {
|
|
5
|
+
const path = `${dir}/${filename}`;
|
|
6
|
+
const content = await readToString(path);
|
|
7
|
+
const type = resolveAttachmentType(filename, content);
|
|
8
|
+
result.push({ content, filename, path, type });
|
|
9
|
+
}
|
|
10
|
+
return result.sort(compareAttachmentFilenames);
|
|
11
|
+
}
|
|
12
|
+
function compareAttachmentFilenames(a1, a2) {
|
|
13
|
+
if (a1.filename === a2.filename)
|
|
14
|
+
return 0;
|
|
15
|
+
if (a1.filename > a2.filename)
|
|
16
|
+
return 1;
|
|
17
|
+
return -1;
|
|
18
|
+
}
|
|
19
|
+
export function resolveAttachmentType(filename, content) {
|
|
20
|
+
if (filename.endsWith('.yml') ||
|
|
21
|
+
(filename.endsWith('.yaml') &&
|
|
22
|
+
content.trim().startsWith('#cloud-config'))) {
|
|
23
|
+
return 'cloud-config';
|
|
24
|
+
}
|
|
25
|
+
else if (filename.endsWith('.sh')) {
|
|
26
|
+
return 'x-shellscript';
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw new Error(`unsupported file type ${filename}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
package/lib_js/build.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { collectAttachments } from '#c2/attachments.ts';
|
|
2
|
+
import { evalTemplateExpressions } from '#c2/expression.ts';
|
|
3
|
+
import { readToString } from '#c2/fs.ts';
|
|
4
|
+
export async function buildUserData(userDataDir, opts) {
|
|
5
|
+
const attachments = await collectAttachments(userDataDir);
|
|
6
|
+
switch (attachments.length) {
|
|
7
|
+
case 0:
|
|
8
|
+
throw new Error(`nothing found in dir ${userDataDir}`);
|
|
9
|
+
case 1:
|
|
10
|
+
return evalTemplateExpressions(await readToString(attachments[0].path));
|
|
11
|
+
default:
|
|
12
|
+
return buildMultipartUserData(attachments, opts?.attachmentBoundary);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function createBoundary() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
async function buildMultipartUserData(attachments, boundary = createBoundary()) {
|
|
19
|
+
let result = `Content-Type: multipart/mixed; boundary=${boundary}
|
|
20
|
+
MIME-Version: 1.0
|
|
21
|
+
Number-Attachments: ${attachments.length}
|
|
22
|
+
--${boundary}
|
|
23
|
+
`;
|
|
24
|
+
for (const attachment of attachments) {
|
|
25
|
+
let content;
|
|
26
|
+
try {
|
|
27
|
+
content = await evalTemplateExpressions(attachment.content);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw new Error(`error templating ${attachment.filename}: ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
result += `Content-Type: text/${attachment.type}; charset="us-ascii"
|
|
33
|
+
MIME-Version: 1.0
|
|
34
|
+
Content-Transfer-Encoding: 7bit
|
|
35
|
+
Content-Disposition: attachment; filename="${attachment.filename}"
|
|
36
|
+
|
|
37
|
+
${content}
|
|
38
|
+
--${boundary}
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
package/lib_js/c2.api.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildUserData } from '#c2/build.ts';
|
package/lib_js/c2.bin.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildUserData } from '#c2/build.ts';
|
|
3
|
+
import { parseArgs } from '#c2/cli.ts';
|
|
4
|
+
import { doesDirExist } from '#c2/fs.ts';
|
|
5
|
+
let args;
|
|
6
|
+
try {
|
|
7
|
+
args = parseArgs();
|
|
8
|
+
}
|
|
9
|
+
catch (e) {
|
|
10
|
+
if (e.message) {
|
|
11
|
+
console.error(e.message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (!args || args.help) {
|
|
15
|
+
const optional = (s) => `\u001b[90m${s}\u001b[0m`;
|
|
16
|
+
const required = (s) => `\u001b[1m${s}\u001b[0m`;
|
|
17
|
+
errorExit(`c2 ${optional('[[--base64] | [--http PORT]]')} ${required('USER_DATA_DIR')}`);
|
|
18
|
+
}
|
|
19
|
+
if (args.httpPort) {
|
|
20
|
+
errorExit('--http PORT is not yet implemented');
|
|
21
|
+
}
|
|
22
|
+
if (!(await doesDirExist(args.userDataDir))) {
|
|
23
|
+
errorExit(`${args.userDataDir} directory does not exist`);
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const userData = await buildUserData(args.userDataDir);
|
|
27
|
+
console.log(args.base64 ? btoa(userData) : userData);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
errorExit(e.message);
|
|
31
|
+
}
|
|
32
|
+
function errorExit(msg) {
|
|
33
|
+
console.error(msg);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
package/lib_js/cli.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function parseArgs(args) {
|
|
2
|
+
if (!args) {
|
|
3
|
+
args = process.argv;
|
|
4
|
+
}
|
|
5
|
+
args = [...args];
|
|
6
|
+
while (!args.shift().endsWith('/c2.ts')) { }
|
|
7
|
+
let base64 = false;
|
|
8
|
+
let httpPort;
|
|
9
|
+
let userData = [];
|
|
10
|
+
let expectHttpPort = false;
|
|
11
|
+
for (const arg of args) {
|
|
12
|
+
if (expectHttpPort) {
|
|
13
|
+
expectHttpPort = false;
|
|
14
|
+
httpPort = parseInt(arg, 10);
|
|
15
|
+
if (isNaN(httpPort)) {
|
|
16
|
+
throw new Error(`--http ${arg} is not a valid http port`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else if (arg === '-h' || arg === '--help') {
|
|
20
|
+
return { help: true };
|
|
21
|
+
}
|
|
22
|
+
else if (arg === '--base64') {
|
|
23
|
+
base64 = true;
|
|
24
|
+
}
|
|
25
|
+
else if (arg === '--http') {
|
|
26
|
+
expectHttpPort = true;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
userData.push(arg);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
switch (userData.length) {
|
|
33
|
+
case 1:
|
|
34
|
+
const userDataDir = userData[0];
|
|
35
|
+
if (typeof httpPort !== 'undefined') {
|
|
36
|
+
return { httpPort, userDataDir };
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return { base64, userDataDir };
|
|
40
|
+
}
|
|
41
|
+
default:
|
|
42
|
+
throw new Error();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import MagicString from 'magic-string';
|
|
2
|
+
import { readToString } from '#c2/fs.ts';
|
|
3
|
+
export async function evalTemplateExpressions(content) {
|
|
4
|
+
const regex = new RegExp(/\${{\s*(.*)\s*}}/g);
|
|
5
|
+
let match;
|
|
6
|
+
const expressions = [];
|
|
7
|
+
while ((match = regex.exec(content)) != null) {
|
|
8
|
+
expressions.push({
|
|
9
|
+
index: match.index,
|
|
10
|
+
innie: match[1],
|
|
11
|
+
outie: match[0],
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (!expressions.length) {
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
const ms = new MagicString(content);
|
|
18
|
+
for (const expression of expressions) {
|
|
19
|
+
ms.update(expression.index, expression.index + expression.outie.length, await evaluate(expression.innie));
|
|
20
|
+
}
|
|
21
|
+
return ms.toString();
|
|
22
|
+
}
|
|
23
|
+
async function evaluate(expression) {
|
|
24
|
+
let match;
|
|
25
|
+
if ((match = expression.match(/env\(\s*'(.*)'\s*\)/)) != null) {
|
|
26
|
+
const envVarKey = match[1];
|
|
27
|
+
if (!/[A-Z_]+/.test(envVarKey)) {
|
|
28
|
+
throw new Error(`env var expression \`${envVarKey}\` is not valid syntax`);
|
|
29
|
+
}
|
|
30
|
+
const envVarValue = process.env[envVarKey];
|
|
31
|
+
if (!envVarValue) {
|
|
32
|
+
throw new Error(`env var \`${envVarKey}\` does not exist`);
|
|
33
|
+
}
|
|
34
|
+
return envVarValue;
|
|
35
|
+
}
|
|
36
|
+
else if ((match = expression.match(/file\(\s*'(.*)'\s*\)/)) != null) {
|
|
37
|
+
let path = match[1];
|
|
38
|
+
if (path.startsWith('~/')) {
|
|
39
|
+
if (!process.env.HOME) {
|
|
40
|
+
throw new Error(`file \`${path}\` cannot be resolved without env var HOME`);
|
|
41
|
+
}
|
|
42
|
+
path = `${process.env.HOME}${path.substring(1)}`;
|
|
43
|
+
}
|
|
44
|
+
return readToString(path);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`unsupported expression: ${expression}`);
|
|
47
|
+
}
|
package/lib_js/fs.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
export async function doesDirExist(p) {
|
|
3
|
+
try {
|
|
4
|
+
return (await stat(p)).isDirectory();
|
|
5
|
+
}
|
|
6
|
+
catch (ignore) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export async function readDirListing(p) {
|
|
11
|
+
return readdir(p);
|
|
12
|
+
}
|
|
13
|
+
export async function readToString(p) {
|
|
14
|
+
return readFile(p, 'utf8');
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export async function makeFile(path, content, pathPrefix) {
|
|
5
|
+
const p = !!pathPrefix ? join(pathPrefix, path) : path;
|
|
6
|
+
await Bun.file(p).write(content);
|
|
7
|
+
return p;
|
|
8
|
+
}
|
|
9
|
+
export async function makeTempDir() {
|
|
10
|
+
return await mkdtemp(join(tmpdir(), 'c2-test-'));
|
|
11
|
+
}
|
|
12
|
+
export async function removeDir(p) {
|
|
13
|
+
await rm(p, { force: true, recursive: true });
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AttachmentType = 'cloud-config' | 'x-shellscript';
|
|
2
|
+
export interface Attachment {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
filename: string;
|
|
6
|
+
type: AttachmentType;
|
|
7
|
+
}
|
|
8
|
+
export declare function collectAttachments(dir: string): Promise<Array<Attachment>>;
|
|
9
|
+
export declare function resolveAttachmentType(filename: string, content: string): AttachmentType;
|
|
10
|
+
//# sourceMappingURL=attachments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachments.d.ts","sourceRoot":"","sources":["../lib/attachments.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,eAAe,CAAA;AAE7D,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,cAAc,CAAA;CACvB;AAED,wBAAsB,kBAAkB,CACpC,GAAG,EAAE,MAAM,GACZ,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAS5B;AAWD,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAChB,cAAc,CAYhB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../lib/build.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GAAG;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,wBAAsB,aAAa,CAC/B,WAAW,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE,iBAAiB,GACzB,OAAO,CAAC,MAAM,CAAC,CAYjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c2.api.d.ts","sourceRoot":"","sources":["../lib/c2.api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"c2.bin.d.ts","sourceRoot":"","sources":["../lib/c2.bin.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../lib/cli.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAChB;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACd;IACI,IAAI,CAAC,EAAE,KAAK,CAAA;IACZ,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACtB,CAAA;AAEP,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,UAAU,CAsC1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expression.d.ts","sourceRoot":"","sources":["../lib/expression.ts"],"names":[],"mappings":"AASA,wBAAsB,uBAAuB,CACzC,OAAO,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAuBjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../lib/fs.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM9D;AAED,wBAAsB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAEtE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.testing.d.ts","sourceRoot":"","sources":["../lib/fs.testing.ts"],"names":[],"mappings":"AAIA,wBAAsB,QAAQ,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,CAIjB;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAEnD;AAED,wBAAsB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eighty4/c2",
|
|
3
|
-
"version": "0.0.1",
|
|
3
|
+
"version": "0.0.2-1",
|
|
4
4
|
"author": "Adam McKee <adam.be.g84d@gmail.com>",
|
|
5
5
|
"repository": "https://github.com/eighty4/c2",
|
|
6
6
|
"homepage": "https://github.com/eighty4/c2",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"devops",
|
|
9
|
-
"cloud
|
|
9
|
+
"cloud-init",
|
|
10
10
|
"cloud config",
|
|
11
|
-
"user data"
|
|
11
|
+
"user data",
|
|
12
|
+
"linux",
|
|
13
|
+
"cloud"
|
|
12
14
|
],
|
|
13
15
|
"description": "Cross platform cloud config tooling for cloud-init",
|
|
14
16
|
"license": "BSD-2-Clause",
|
|
@@ -17,20 +19,19 @@
|
|
|
17
19
|
"bun": ">=1.2",
|
|
18
20
|
"node": ">=23"
|
|
19
21
|
},
|
|
20
|
-
"main": "lib/build.ts",
|
|
21
22
|
"bin": {
|
|
22
|
-
"c2": "./c2.
|
|
23
|
+
"c2": "./lib_js/c2.bin.js"
|
|
23
24
|
},
|
|
25
|
+
"main": "./lib_js/c2.api.js",
|
|
26
|
+
"imports": {
|
|
27
|
+
"#c2/*.js": "./lib_js/*.js",
|
|
28
|
+
"#c2/*.ts": "./lib/*.ts"
|
|
29
|
+
},
|
|
30
|
+
"types": "./lib_types",
|
|
24
31
|
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
25
33
|
"fmtcheck": "prettier --check .",
|
|
26
|
-
"typecheck": "tsc"
|
|
27
|
-
},
|
|
28
|
-
"imports": {
|
|
29
|
-
"#c2/fs.ts": {
|
|
30
|
-
"bun": "./lib/fs.bun.ts",
|
|
31
|
-
"node": "./lib/fs.node.ts"
|
|
32
|
-
},
|
|
33
|
-
"#c2/*": "./lib/*"
|
|
34
|
+
"typecheck": "tsc --noEmit"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
36
37
|
"magic-string": "0.30.17"
|
|
@@ -42,10 +43,9 @@
|
|
|
42
43
|
"typescript": "5.8.2"
|
|
43
44
|
},
|
|
44
45
|
"files": [
|
|
45
|
-
"lib
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"CHANGELOG.md"
|
|
49
|
-
"c2.ts"
|
|
46
|
+
"lib/*",
|
|
47
|
+
"lib_js/*",
|
|
48
|
+
"lib_types/*",
|
|
49
|
+
"CHANGELOG.md"
|
|
50
50
|
]
|
|
51
51
|
}
|
package/lib/fs.bun.ts
DELETED
/package/{c2.ts → lib/c2.bin.ts}
RENAMED
|
File without changes
|
|
File without changes
|