@capsulos/cli 0.0.0-stage → 0.1.0

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/dist/init.js ADDED
@@ -0,0 +1,106 @@
1
+ // `capsulos init`: the one-time setup in an AWS account. Idempotent.
2
+ // - artifact bucket capsulos-artifacts-<account>
3
+ // - build role CapsulosBuildRole (assumed by the image builder)
4
+ // - execution role CapsulosExecutionRole (assumed by every capsule)
5
+ // Network connectors are deliberately not created here: they are an account
6
+ // design decision (which VPC, which security group). Pass one with
7
+ // --egress-connector to record it in capsulos.json.
8
+ import { IAMClient, GetRoleCommand, CreateRoleCommand, PutRolePolicyCommand } from '@aws-sdk/client-iam';
9
+ import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
10
+ import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
11
+ import { credentialsFor, saveConfig } from './config.js';
12
+ const BUILD_ROLE = 'CapsulosBuildRole';
13
+ const EXEC_ROLE = 'CapsulosExecutionRole';
14
+ const TAGS = [
15
+ { Key: 'Project', Value: 'capsulos' },
16
+ { Key: 'ManagedBy', Value: 'capsulos-cli' },
17
+ ];
18
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
19
+ export async function init(opts) {
20
+ const log = opts.log ?? (() => { });
21
+ const credentials = credentialsFor(opts.profile);
22
+ const clientOpts = { region: opts.region, credentials };
23
+ const sts = new STSClient(clientOpts);
24
+ const iam = new IAMClient(clientOpts);
25
+ const s3 = new S3Client(clientOpts);
26
+ const me = await sts.send(new GetCallerIdentityCommand({}));
27
+ const accountId = me.Account;
28
+ if (!accountId)
29
+ throw new Error('could not resolve the AWS account id');
30
+ const bucket = `capsulos-artifacts-${accountId}`;
31
+ try {
32
+ await s3.send(new HeadBucketCommand({ Bucket: bucket }));
33
+ log(`bucket ${bucket} exists`);
34
+ }
35
+ catch {
36
+ await s3.send(new CreateBucketCommand({ Bucket: bucket }));
37
+ log(`bucket ${bucket} created`);
38
+ }
39
+ const trust = JSON.stringify({
40
+ Version: '2012-10-17',
41
+ Statement: [
42
+ {
43
+ Effect: 'Allow',
44
+ Principal: { Service: 'lambda.amazonaws.com' },
45
+ Action: ['sts:AssumeRole', 'sts:TagSession'],
46
+ },
47
+ ],
48
+ });
49
+ let created = false;
50
+ const ensureRole = async (name, description) => {
51
+ try {
52
+ await iam.send(new GetRoleCommand({ RoleName: name }));
53
+ log(`role ${name} exists`);
54
+ }
55
+ catch {
56
+ await iam.send(new CreateRoleCommand({ RoleName: name, AssumeRolePolicyDocument: trust, Description: description, Tags: TAGS }));
57
+ created = true;
58
+ log(`role ${name} created`);
59
+ }
60
+ };
61
+ await ensureRole(BUILD_ROLE, 'capsulos: assumed by the Lambda MicroVMs image builder');
62
+ await iam.send(new PutRolePolicyCommand({
63
+ RoleName: BUILD_ROLE,
64
+ PolicyName: 'capsulos-build',
65
+ PolicyDocument: JSON.stringify({
66
+ Version: '2012-10-17',
67
+ Statement: [
68
+ { Effect: 'Allow', Action: ['s3:GetObject'], Resource: `arn:aws:s3:::${bucket}/*` },
69
+ { Effect: 'Allow', Action: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], Resource: 'arn:aws:logs:*:*:*' },
70
+ ],
71
+ }),
72
+ }));
73
+ await ensureRole(EXEC_ROLE, 'capsulos: assumed by every capsule (S3 mounts)');
74
+ const buckets = opts.s3Buckets ?? [];
75
+ await iam.send(new PutRolePolicyCommand({
76
+ RoleName: EXEC_ROLE,
77
+ PolicyName: 'capsulos-execution',
78
+ PolicyDocument: JSON.stringify({
79
+ Version: '2012-10-17',
80
+ Statement: buckets.length
81
+ ? [
82
+ { Effect: 'Allow', Action: ['s3:ListBucket'], Resource: buckets.map((b) => `arn:aws:s3:::${b}`) },
83
+ { Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'], Resource: buckets.map((b) => `arn:aws:s3:::${b}/*`) },
84
+ ]
85
+ : // IAM rejects an empty statement list; grant nothing explicitly.
86
+ [{ Effect: 'Deny', Action: ['s3:*'], Resource: '*', Sid: 'NoBucketsConfiguredYet' }],
87
+ }),
88
+ }));
89
+ log(buckets.length ? `execution role: S3 access to ${buckets.join(', ')}` : 'execution role: no S3 buckets (pass --s3-bucket)');
90
+ // IAM is eventually consistent: a capsule launched right after a policy
91
+ // change can still be refused for a few seconds.
92
+ log(created ? 'roles just created: waiting 10s for IAM to propagate' : 'policies updated: waiting 10s for IAM to propagate');
93
+ await sleep(10_000);
94
+ const cfg = {
95
+ region: opts.region,
96
+ accountId,
97
+ bucket,
98
+ buildRoleArn: `arn:aws:iam::${accountId}:role/${BUILD_ROLE}`,
99
+ executionRoleArn: `arn:aws:iam::${accountId}:role/${EXEC_ROLE}`,
100
+ ...(opts.egressConnectorArn ? { egressConnectorArn: opts.egressConnectorArn } : {}),
101
+ };
102
+ const path = saveConfig(cfg);
103
+ log(`wrote ${path}`);
104
+ return cfg;
105
+ }
106
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { type Provider } from '@capsulos/sdk';
2
+ export declare function shell(provider: Provider, id: string, cmd?: string): Promise<number>;
package/dist/shell.js ADDED
@@ -0,0 +1,31 @@
1
+ // `capsulos shell <id>`: your terminal, attached to a capsule's pty. Raw mode
2
+ // so that every key (ctrl-c, arrows, tab) goes to the capsule; the session
3
+ // ends when the remote shell exits.
4
+ import { Capsule } from '@capsulos/sdk';
5
+ export async function shell(provider, id, cmd) {
6
+ const capsule = await Capsule.connect(provider, id);
7
+ const { stdin, stdout } = process;
8
+ const size = () => ({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
9
+ const term = await capsule.pty.open({ cmd, ...size(), onData: (chunk) => stdout.write(chunk) });
10
+ const onKey = (data) => term.write(new Uint8Array(data));
11
+ const onResize = () => {
12
+ const s = size();
13
+ term.resize(s.cols, s.rows);
14
+ };
15
+ if (stdin.isTTY)
16
+ stdin.setRawMode(true);
17
+ stdin.resume();
18
+ stdin.on('data', onKey);
19
+ stdout.on('resize', onResize);
20
+ try {
21
+ return await term.wait();
22
+ }
23
+ finally {
24
+ stdin.off('data', onKey);
25
+ stdout.off('resize', onResize);
26
+ if (stdin.isTTY)
27
+ stdin.setRawMode(false);
28
+ stdin.pause();
29
+ }
30
+ }
31
+ //# sourceMappingURL=shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,oCAAoC;AACpC,OAAO,EAAE,OAAO,EAAiB,MAAM,eAAe,CAAC;AAEvD,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,QAAkB,EAAE,EAAU,EAAE,GAAY;IACtE,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAClC,MAAM,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,OAAO,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAEhG,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,GAAG,EAAE;QACpB,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;QACjB,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC,CAAC;IACF,IAAI,KAAK,CAAC,KAAK;QAAE,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACxC,KAAK,CAAC,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxB,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9B,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAC3B,CAAC;YAAS,CAAC;QACT,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,KAAK;YAAE,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACzC,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC;AACH,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { type CapsulosConfig } from './config.js';
2
+ export interface TemplateBuildOptions {
3
+ name: string;
4
+ /** Directory with the Dockerfile (and anything it COPYs). */
5
+ context: string;
6
+ dockerfile?: string;
7
+ /** Prebuilt linux/arm64 capsulod; when omitted, built from --daemon-src with Go. */
8
+ capsulod?: string;
9
+ /** Path to the daemon's Go module (defaults to the monorepo's daemon/). */
10
+ daemonSrc?: string;
11
+ memoryMiB?: number;
12
+ profile?: string;
13
+ description?: string;
14
+ log?: (msg: string) => void;
15
+ }
16
+ export declare function imageArn(cfg: CapsulosConfig, name: string): string;
17
+ /** Resolve or cross-compile capsulod for linux/arm64 (the service runs Graviton). */
18
+ export declare function resolveCapsulod(opts: Pick<TemplateBuildOptions, 'capsulod' | 'daemonSrc' | 'log'>): string;
19
+ /** Build the zip the image builder consumes: context + Dockerfile + capsulod at the root. */
20
+ export declare function assembleArtifact(opts: TemplateBuildOptions, capsulod: string): Buffer;
21
+ export declare function templateBuild(cfg: CapsulosConfig, opts: TemplateBuildOptions): Promise<{
22
+ imageArn: string;
23
+ version: string;
24
+ }>;
@@ -0,0 +1,120 @@
1
+ // `capsulos template build <name>`: a Dockerfile becomes a MicrovmImage.
2
+ // 1. assemble a build context: the user's directory + Dockerfile + capsulod
3
+ // 2. zip it and upload to the artifact bucket
4
+ // 3. CreateMicrovmImage (or UpdateMicrovmImage → new version) with
5
+ // additionalOsCapabilities=ALL (mounts) and the /ready + /run hooks
6
+ // 4. poll until CREATED/UPDATED
7
+ // The Dockerfile must end up running capsulod on :8080 (see templates/base).
8
+ import { execFileSync } from 'node:child_process';
9
+ import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
14
+ import { LambdaMicrovmsClient, CreateMicrovmImageCommand, UpdateMicrovmImageCommand, GetMicrovmImageCommand, } from '@aws-sdk/client-lambda-microvms';
15
+ import { credentialsFor } from './config.js';
16
+ const HOOKS_PORT = 9000;
17
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ export function imageArn(cfg, name) {
19
+ return `arn:aws:lambda:${cfg.region}:${cfg.accountId}:microvm-image:${name}`;
20
+ }
21
+ /** Resolve or cross-compile capsulod for linux/arm64 (the service runs Graviton). */
22
+ export function resolveCapsulod(opts) {
23
+ if (opts.capsulod) {
24
+ if (!existsSync(opts.capsulod))
25
+ throw new Error(`capsulod binary not found: ${opts.capsulod}`);
26
+ return resolve(opts.capsulod);
27
+ }
28
+ const here = dirname(fileURLToPath(import.meta.url));
29
+ const src = opts.daemonSrc ?? resolve(here, '..', '..', '..', 'daemon');
30
+ if (!existsSync(join(src, 'go.mod'))) {
31
+ throw new Error(`no capsulod binary given and no daemon source at ${src}; pass --capsulod <path> (download capsulod-linux-arm64 from https://github.com/tornoai/capsulos/releases) or --daemon-src <dir>`);
32
+ }
33
+ const out = join(mkdtempSync(join(tmpdir(), 'capsulod-')), 'capsulod');
34
+ opts.log?.(`building capsulod for linux/arm64 from ${src}`);
35
+ execFileSync('go', ['build', '-trimpath', '-ldflags', '-s -w', '-o', out, './cmd/capsulod'], {
36
+ cwd: src,
37
+ env: { ...process.env, CGO_ENABLED: '0', GOOS: 'linux', GOARCH: 'arm64' },
38
+ stdio: 'inherit',
39
+ });
40
+ return out;
41
+ }
42
+ /** Build the zip the image builder consumes: context + Dockerfile + capsulod at the root. */
43
+ export function assembleArtifact(opts, capsulod) {
44
+ const context = resolve(opts.context);
45
+ const dockerfile = resolve(opts.dockerfile ?? join(context, 'Dockerfile'));
46
+ if (!existsSync(dockerfile))
47
+ throw new Error(`Dockerfile not found: ${dockerfile}`);
48
+ const stage = mkdtempSync(join(tmpdir(), 'capsulos-template-'));
49
+ try {
50
+ cpSync(context, stage, { recursive: true });
51
+ cpSync(dockerfile, join(stage, 'Dockerfile'));
52
+ cpSync(capsulod, join(stage, 'capsulod'));
53
+ const zipPath = join(stage, '..', `capsulos-${opts.name}.zip`);
54
+ rmSync(zipPath, { force: true });
55
+ execFileSync('zip', ['-q', '-r', '-X', zipPath, '.'], { cwd: stage });
56
+ return readFileSync(zipPath);
57
+ }
58
+ finally {
59
+ rmSync(stage, { recursive: true, force: true });
60
+ }
61
+ }
62
+ export async function templateBuild(cfg, opts) {
63
+ const log = opts.log ?? (() => { });
64
+ const credentials = credentialsFor(opts.profile);
65
+ const s3 = new S3Client({ region: cfg.region, credentials });
66
+ const mvm = new LambdaMicrovmsClient({ region: cfg.region, credentials });
67
+ const capsulod = resolveCapsulod(opts);
68
+ const zip = assembleArtifact(opts, capsulod);
69
+ const key = `templates/${opts.name}.zip`;
70
+ await s3.send(new PutObjectCommand({ Bucket: cfg.bucket, Key: key, Body: zip }));
71
+ log(`uploaded s3://${cfg.bucket}/${key} (${zip.length} bytes)`);
72
+ const arn = imageArn(cfg, opts.name);
73
+ const spec = {
74
+ codeArtifact: { uri: `s3://${cfg.bucket}/${key}` },
75
+ baseImageArn: `arn:aws:lambda:${cfg.region}:aws:microvm-image:al2023-1`,
76
+ buildRoleArn: cfg.buildRoleArn,
77
+ cpuConfigurations: [{ architecture: 'ARM_64' }],
78
+ resources: [{ minimumMemoryInMiB: opts.memoryMiB ?? 512 }],
79
+ // Verified: without this the capsule has Docker's capability set and
80
+ // cannot mount anything (docs/lambda-microvms-findings.md).
81
+ additionalOsCapabilities: ['ALL'],
82
+ hooks: {
83
+ port: HOOKS_PORT,
84
+ microvmHooks: { run: 'ENABLED', runTimeoutInSeconds: 60 },
85
+ microvmImageHooks: { ready: 'ENABLED', readyTimeoutInSeconds: 120 },
86
+ },
87
+ };
88
+ const t0 = Date.now();
89
+ try {
90
+ await mvm.send(new CreateMicrovmImageCommand({
91
+ name: opts.name,
92
+ description: opts.description ?? `capsulos template ${opts.name}`,
93
+ tags: { project: 'capsulos' },
94
+ ...spec,
95
+ }));
96
+ log(`image ${arn} created; building…`);
97
+ }
98
+ catch (err) {
99
+ if (err instanceof Error && err.name === 'ConflictException') {
100
+ await mvm.send(new UpdateMicrovmImageCommand({ imageIdentifier: arn, ...spec }));
101
+ log(`image ${arn} exists → new version; building…`);
102
+ }
103
+ else {
104
+ throw err;
105
+ }
106
+ }
107
+ for (;;) {
108
+ await sleep(10_000);
109
+ const img = await mvm.send(new GetMicrovmImageCommand({ imageIdentifier: arn }));
110
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
111
+ log(`state=${img.state} (t+${elapsed}s)`);
112
+ if (img.state === 'CREATED' || img.state === 'UPDATED') {
113
+ return { imageArn: arn, version: String(img.latestActiveImageVersion ?? '') };
114
+ }
115
+ if (img.state?.endsWith('_FAILED')) {
116
+ throw new Error(`build failed: ${img.state} — see CloudWatch /aws/lambda-microvms/${opts.name}`);
117
+ }
118
+ }
119
+ }
120
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@capsulos/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "capsulos command line: set up your AWS account, build templates, list and kill capsules.",
5
+ "license": "Apache-2.0",
6
+ "keywords": [
7
+ "sandbox",
8
+ "ai-agents",
9
+ "aws",
10
+ "lambda",
11
+ "microvm",
12
+ "firecracker",
13
+ "e2b",
14
+ "daytona"
15
+ ],
16
+ "homepage": "https://github.com/tornoai/capsulos#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/tornoai/capsulos.git",
20
+ "directory": "packages/cli"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/tornoai/capsulos/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "type": "module",
29
+ "bin": {
30
+ "capsulos": "./bin/capsulos.js"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ }
37
+ },
38
+ "main": "./dist/index.js",
39
+ "types": "./dist/index.d.ts",
40
+ "sideEffects": false,
41
+ "files": [
42
+ "bin",
43
+ "dist",
44
+ "src"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "dependencies": {
50
+ "@aws-sdk/client-iam": "^3.1158.0",
51
+ "@aws-sdk/client-lambda-microvms": "^3.1158.0",
52
+ "@aws-sdk/client-s3": "^3.1158.0",
53
+ "@aws-sdk/client-sts": "^3.1158.0",
54
+ "@aws-sdk/credential-providers": "^3.1158.0",
55
+ "@capsulos/provider-docker": "0.1.0",
56
+ "@capsulos/provider-lambda": "0.1.0",
57
+ "@capsulos/sdk": "0.1.0"
58
+ },
59
+ "devDependencies": {
60
+ "@types/node": "^22.20.1",
61
+ "tsx": "^4.23.1",
62
+ "typescript": "^5.9.0",
63
+ "vitest": "^4.1.10"
64
+ },
65
+ "scripts": {
66
+ "typecheck": "tsc --noEmit",
67
+ "build": "tsc -p tsconfig.build.json",
68
+ "test": "vitest run --passWithNoTests",
69
+ "dev": "tsx src/cli.ts"
70
+ }
6
71
  }
package/src/cli.ts ADDED
@@ -0,0 +1,162 @@
1
+ // capsulos <command>
2
+ // init set up the AWS account (bucket + roles) → capsulos.json
3
+ // template build Dockerfile → MicrovmImage
4
+ // run <template> create a capsule, print its id
5
+ // ls <template> capsules of a template
6
+ // shell <id> attach your terminal to a capsule
7
+ // forward <id> <port> http://localhost:<port> → that port inside the capsule
8
+ // kill <id> terminate a capsule
9
+ // `--docker` on run/ls/shell/kill uses the local Docker provider instead of Lambda.
10
+ import { parseArgs } from 'node:util';
11
+ import { DockerProvider } from '@capsulos/provider-docker';
12
+ import { LambdaProvider } from '@capsulos/provider-lambda';
13
+ import { Capsule, type Provider } from '@capsulos/sdk';
14
+ import { credentialsFor, loadConfig } from './config.js';
15
+ import { forward } from './forward.js';
16
+ import { init } from './init.js';
17
+ import { shell } from './shell.js';
18
+ import { templateBuild } from './template.js';
19
+
20
+ const log = (msg: string) => console.error(`[capsulos] ${msg}`);
21
+
22
+ function usage(): never {
23
+ console.error(`usage:
24
+ capsulos init [--region us-east-1] [--profile p] [--s3-bucket b ...] [--egress-connector arn]
25
+ capsulos template build <name> --context <dir> [--dockerfile f] [--capsulod bin | --daemon-src dir] [--memory 512] [--profile p]
26
+ capsulos run <template> [--docker | --profile p]
27
+ capsulos ls <template> [--docker | --profile p]
28
+ capsulos shell <id> [--cmd /bin/bash] [--docker | --profile p]
29
+ capsulos forward <id> <port> [--local-port p] [--docker | --profile p]
30
+ capsulos kill <id> [--docker | --profile p]`);
31
+ process.exit(2);
32
+ }
33
+
34
+ const providerFlags = { profile: { type: 'string' }, docker: { type: 'boolean', default: false } } as const;
35
+
36
+ function provider(opts: { profile?: string; docker?: boolean }): Provider {
37
+ if (opts.docker) return new DockerProvider();
38
+ return lambdaProvider(opts.profile);
39
+ }
40
+
41
+ function lambdaProvider(profile?: string): LambdaProvider {
42
+ const cfg = loadConfig();
43
+ return new LambdaProvider({
44
+ region: cfg.region,
45
+ accountId: cfg.accountId,
46
+ credentials: credentialsFor(profile),
47
+ executionRoleArn: cfg.executionRoleArn,
48
+ ...(cfg.egressConnectorArn ? { egressConnectorArn: cfg.egressConnectorArn } : {}),
49
+ });
50
+ }
51
+
52
+ export async function main(argv = process.argv.slice(2)): Promise<void> {
53
+ const [cmd, ...rest] = argv;
54
+ switch (cmd) {
55
+ case 'init': {
56
+ const { values } = parseArgs({
57
+ args: rest,
58
+ options: {
59
+ region: { type: 'string', default: process.env.AWS_REGION ?? 'us-east-1' },
60
+ profile: { type: 'string' },
61
+ 's3-bucket': { type: 'string', multiple: true },
62
+ 'egress-connector': { type: 'string' },
63
+ },
64
+ });
65
+ const cfg = await init({
66
+ region: values.region!,
67
+ profile: values.profile,
68
+ s3Buckets: values['s3-bucket'],
69
+ egressConnectorArn: values['egress-connector'],
70
+ log,
71
+ });
72
+ console.log(JSON.stringify(cfg, null, 2));
73
+ return;
74
+ }
75
+ case 'template': {
76
+ const [sub, name, ...args] = rest;
77
+ if (sub !== 'build' || !name) usage();
78
+ const { values } = parseArgs({
79
+ args,
80
+ options: {
81
+ context: { type: 'string' },
82
+ dockerfile: { type: 'string' },
83
+ capsulod: { type: 'string' },
84
+ 'daemon-src': { type: 'string' },
85
+ memory: { type: 'string' },
86
+ profile: { type: 'string' },
87
+ description: { type: 'string' },
88
+ },
89
+ });
90
+ if (!values.context) usage();
91
+ const res = await templateBuild(loadConfig(), {
92
+ name,
93
+ context: values.context,
94
+ dockerfile: values.dockerfile,
95
+ capsulod: values.capsulod,
96
+ daemonSrc: values['daemon-src'],
97
+ memoryMiB: values.memory ? Number(values.memory) : undefined,
98
+ profile: values.profile,
99
+ description: values.description,
100
+ log,
101
+ });
102
+ console.log(JSON.stringify(res, null, 2));
103
+ return;
104
+ }
105
+ case 'run': {
106
+ const [template, ...args] = rest;
107
+ if (!template) usage();
108
+ const { values } = parseArgs({ args, options: providerFlags });
109
+ const t0 = Date.now();
110
+ const capsule = await Capsule.create(provider(values), {
111
+ template,
112
+ idle: { suspendAfterSeconds: 600, destroyAfterSeconds: 4 * 3600 },
113
+ });
114
+ log(`capsule ready in ${Date.now() - t0}ms`);
115
+ console.log(capsule.id);
116
+ return;
117
+ }
118
+ case 'ls': {
119
+ const [template, ...args] = rest;
120
+ if (!template) usage();
121
+ const { values } = parseArgs({ args, options: providerFlags });
122
+ const list = await provider(values).list(template);
123
+ for (const c of list) console.log(`${c.id}\t${c.state}\t${c.createdAt?.toISOString() ?? ''}`);
124
+ return;
125
+ }
126
+ case 'shell': {
127
+ const [id, ...args] = rest;
128
+ if (!id) usage();
129
+ const { values } = parseArgs({ args, options: { ...providerFlags, cmd: { type: 'string' } } });
130
+ const code = await shell(provider(values), id, values.cmd);
131
+ process.exit(code);
132
+ }
133
+ case 'forward': {
134
+ const [id, portStr, ...args] = rest;
135
+ const port = Number(portStr);
136
+ if (!id || !Number.isInteger(port) || port < 1) usage();
137
+ const { values } = parseArgs({ args, options: { ...providerFlags, 'local-port': { type: 'string' } } });
138
+ const { url } = await forward(provider(values), id, port, {
139
+ localPort: values['local-port'] ? Number(values['local-port']) : undefined,
140
+ log,
141
+ });
142
+ console.log(url);
143
+ await new Promise<void>((resolve) => process.once('SIGINT', resolve));
144
+ return;
145
+ }
146
+ case 'kill': {
147
+ const [id, ...args] = rest;
148
+ if (!id) usage();
149
+ const { values } = parseArgs({ args, options: providerFlags });
150
+ await provider(values).terminate(id);
151
+ log(`terminated ${id}`);
152
+ return;
153
+ }
154
+ default:
155
+ usage();
156
+ }
157
+ }
158
+
159
+ main().catch((err) => {
160
+ console.error(err instanceof Error ? err.message : err);
161
+ process.exit(1);
162
+ });
package/src/config.ts ADDED
@@ -0,0 +1,47 @@
1
+ // capsulos.json: what `capsulos init` created in the AWS account, read by
2
+ // every other command and by the examples. Lives in the project directory.
3
+ import { readFileSync, writeFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
6
+ import type { LambdaMicrovmsClientConfig } from '@aws-sdk/client-lambda-microvms';
7
+
8
+ export type Credentials = NonNullable<LambdaMicrovmsClientConfig['credentials']>;
9
+
10
+ export interface CapsulosConfig {
11
+ region: string;
12
+ accountId: string;
13
+ /** Artifact bucket for template zips. */
14
+ bucket: string;
15
+ /** Role the image builder assumes (reads the zip, writes build logs). */
16
+ buildRoleArn: string;
17
+ /** Role every capsule assumes (S3 mounts and whatever else you grant). */
18
+ executionRoleArn: string;
19
+ /** Optional egress connector (e.g. a VPC connector with a deny-all security group). */
20
+ egressConnectorArn?: string;
21
+ }
22
+
23
+ export const CONFIG_FILE = 'capsulos.json';
24
+
25
+ export function configPath(cwd = process.cwd()): string {
26
+ return resolve(cwd, CONFIG_FILE);
27
+ }
28
+
29
+ export function loadConfig(cwd = process.cwd()): CapsulosConfig {
30
+ try {
31
+ return JSON.parse(readFileSync(configPath(cwd), 'utf8')) as CapsulosConfig;
32
+ } catch {
33
+ throw new Error(`${CONFIG_FILE} not found in ${cwd}. Run \`capsulos init\` first.`);
34
+ }
35
+ }
36
+
37
+ export function saveConfig(cfg: CapsulosConfig, cwd = process.cwd()): string {
38
+ const p = configPath(cwd);
39
+ writeFileSync(p, `${JSON.stringify(cfg, null, 2)}\n`);
40
+ return p;
41
+ }
42
+
43
+ /** Env vars first (`aws configure export-credentials`), then the profile. */
44
+ export function credentialsFor(profile?: string): Credentials {
45
+ if (process.env.AWS_ACCESS_KEY_ID || !profile) return fromNodeProviderChain();
46
+ return fromNodeProviderChain({ profile });
47
+ }