@unionstreet/apple-sandboxes 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/LICENSE +201 -0
- package/README.md +113 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.js +73 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +16 -0
- package/dist/paths.js.map +1 -0
- package/dist/runtime.d.ts +61 -0
- package/dist/runtime.js +407 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +13 -0
- package/dist/server.js +105 -0
- package/dist/server.js.map +1 -0
- package/dist/store.d.ts +23 -0
- package/dist/store.js +71 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +15 -0
- package/dist/util.js +63 -0
- package/dist/util.js.map +1 -0
- package/docs/architecture.md +130 -0
- package/package.json +59 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { storePaths } from './paths.js';
|
|
4
|
+
import { ensureDir, exists, readJson, writeJson } from './util.js';
|
|
5
|
+
const prefixes = {
|
|
6
|
+
sandboxes: 'sbx',
|
|
7
|
+
images: 'img',
|
|
8
|
+
snapshots: 'snap',
|
|
9
|
+
volumes: 'vol',
|
|
10
|
+
};
|
|
11
|
+
export class Store {
|
|
12
|
+
paths;
|
|
13
|
+
constructor(root) {
|
|
14
|
+
this.paths = storePaths(root);
|
|
15
|
+
}
|
|
16
|
+
async init() {
|
|
17
|
+
await Promise.all(Object.values(this.paths).map((dir) => ensureDir(dir)));
|
|
18
|
+
}
|
|
19
|
+
dir(kind, id) {
|
|
20
|
+
return path.join(this.paths[kind], id);
|
|
21
|
+
}
|
|
22
|
+
meta(kind, id) {
|
|
23
|
+
return path.join(this.dir(kind, id), 'meta.json');
|
|
24
|
+
}
|
|
25
|
+
async put(kind, value) {
|
|
26
|
+
await writeJson(this.meta(kind, value.id), value);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
async get(kind, idOrName) {
|
|
30
|
+
const direct = this.meta(kind, idOrName);
|
|
31
|
+
if (await exists(direct))
|
|
32
|
+
return readJson(direct);
|
|
33
|
+
for (const item of await this.list(kind)) {
|
|
34
|
+
if (item.name === idOrName)
|
|
35
|
+
return item;
|
|
36
|
+
}
|
|
37
|
+
throw Object.assign(new Error(`${kind.slice(0, -1)} not found`), { status: 404 });
|
|
38
|
+
}
|
|
39
|
+
async list(kind) {
|
|
40
|
+
const dir = this.paths[kind];
|
|
41
|
+
await ensureDir(dir);
|
|
42
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
43
|
+
const metas = entries
|
|
44
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith(`${prefixes[kind]}_`))
|
|
45
|
+
.map((entry) => path.join(dir, entry.name, 'meta.json'));
|
|
46
|
+
const existing = await Promise.all(metas.map(async (file) => ((await exists(file)) ? file : undefined)));
|
|
47
|
+
const values = await Promise.all(existing.filter(Boolean).map((file) => readJson(file)));
|
|
48
|
+
return values.reverse();
|
|
49
|
+
}
|
|
50
|
+
async delete(kind, idOrName) {
|
|
51
|
+
const item = await this.get(kind, idOrName);
|
|
52
|
+
await fs.rm(this.dir(kind, item.id), { recursive: true, force: true });
|
|
53
|
+
return item.id;
|
|
54
|
+
}
|
|
55
|
+
sandboxWorkspace(id) {
|
|
56
|
+
return path.join(this.dir('sandboxes', id), 'workspace');
|
|
57
|
+
}
|
|
58
|
+
sandboxSnapshots(id) {
|
|
59
|
+
return path.join(this.dir('sandboxes', id), 'snapshots');
|
|
60
|
+
}
|
|
61
|
+
imageContext(id) {
|
|
62
|
+
return path.join(this.dir('images', id), 'context');
|
|
63
|
+
}
|
|
64
|
+
snapshotArchive(id) {
|
|
65
|
+
return path.join(this.dir('snapshots', id), 'workspace.tar.gz');
|
|
66
|
+
}
|
|
67
|
+
volumePath(id) {
|
|
68
|
+
return path.join(this.dir('volumes', id), 'data');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export type SandboxState = 'creating' | 'created' | 'running' | 'stopped' | 'deleted' | 'error';
|
|
2
|
+
export type NetworkMode = 'deny' | 'open';
|
|
3
|
+
export interface NetworkPolicy {
|
|
4
|
+
mode?: NetworkMode;
|
|
5
|
+
allowedHosts?: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface VolumeMount {
|
|
8
|
+
volumeId: string;
|
|
9
|
+
mountPath: string;
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateSandboxRequest {
|
|
13
|
+
name?: string;
|
|
14
|
+
image?: string;
|
|
15
|
+
imageId?: string;
|
|
16
|
+
snapshotId?: string;
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
cpus?: number;
|
|
19
|
+
memory?: string;
|
|
20
|
+
network?: boolean | NetworkPolicy;
|
|
21
|
+
autoStart?: boolean;
|
|
22
|
+
idleTimeoutSeconds?: number;
|
|
23
|
+
maxLifetimeSeconds?: number;
|
|
24
|
+
autoDeleteSeconds?: number;
|
|
25
|
+
volumes?: VolumeMount[];
|
|
26
|
+
}
|
|
27
|
+
export interface Sandbox {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
state: SandboxState;
|
|
31
|
+
source: 'inline' | 'image' | 'snapshot';
|
|
32
|
+
image: string;
|
|
33
|
+
imageId?: string;
|
|
34
|
+
snapshotId?: string;
|
|
35
|
+
containerName: string;
|
|
36
|
+
workspace: string;
|
|
37
|
+
cpus: number;
|
|
38
|
+
memory: string;
|
|
39
|
+
network: NetworkPolicy;
|
|
40
|
+
volumes: VolumeMount[];
|
|
41
|
+
createdAt: string;
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
lastActivityAt: string;
|
|
44
|
+
idleTimeoutSeconds?: number;
|
|
45
|
+
maxLifetimeSeconds?: number;
|
|
46
|
+
autoDeleteSeconds?: number;
|
|
47
|
+
ssh?: SshAccess;
|
|
48
|
+
}
|
|
49
|
+
export interface ExecRequest {
|
|
50
|
+
command: string;
|
|
51
|
+
timeoutSeconds?: number;
|
|
52
|
+
env?: Record<string, string>;
|
|
53
|
+
background?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface ExecResult {
|
|
56
|
+
id: string;
|
|
57
|
+
sandboxId: string;
|
|
58
|
+
command: string;
|
|
59
|
+
status: 'queued' | 'running' | 'succeeded' | 'failed' | 'timed_out' | 'error';
|
|
60
|
+
exitCode?: number | null;
|
|
61
|
+
stdout?: string;
|
|
62
|
+
stderr?: string;
|
|
63
|
+
createdAt: string;
|
|
64
|
+
startedAt?: string;
|
|
65
|
+
finishedAt?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface CreateImageRequest {
|
|
68
|
+
name: string;
|
|
69
|
+
image?: string;
|
|
70
|
+
dockerfile?: string;
|
|
71
|
+
build?: boolean;
|
|
72
|
+
cpus?: number;
|
|
73
|
+
memory?: string;
|
|
74
|
+
labels?: Record<string, string>;
|
|
75
|
+
}
|
|
76
|
+
export interface ImageDefinition {
|
|
77
|
+
id: string;
|
|
78
|
+
name: string;
|
|
79
|
+
image: string;
|
|
80
|
+
state: 'created' | 'building' | 'ready' | 'error';
|
|
81
|
+
hasDockerfile: boolean;
|
|
82
|
+
cpus: number;
|
|
83
|
+
memory: string;
|
|
84
|
+
labels: Record<string, string>;
|
|
85
|
+
createdAt: string;
|
|
86
|
+
updatedAt: string;
|
|
87
|
+
error?: string;
|
|
88
|
+
}
|
|
89
|
+
export interface CreateSnapshotRequest {
|
|
90
|
+
name: string;
|
|
91
|
+
sandboxId: string;
|
|
92
|
+
labels?: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
export interface Snapshot {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
state: 'ready' | 'error';
|
|
98
|
+
sourceSandboxId: string;
|
|
99
|
+
image: string;
|
|
100
|
+
archivePath: string;
|
|
101
|
+
archiveBytes: number;
|
|
102
|
+
workspaceBytes: number;
|
|
103
|
+
labels: Record<string, string>;
|
|
104
|
+
createdAt: string;
|
|
105
|
+
updatedAt: string;
|
|
106
|
+
}
|
|
107
|
+
export interface CreateVolumeRequest {
|
|
108
|
+
name: string;
|
|
109
|
+
labels?: Record<string, string>;
|
|
110
|
+
}
|
|
111
|
+
export interface DeleteVolumeOptions {
|
|
112
|
+
force?: boolean;
|
|
113
|
+
}
|
|
114
|
+
export interface Volume {
|
|
115
|
+
id: string;
|
|
116
|
+
name: string;
|
|
117
|
+
path: string;
|
|
118
|
+
labels: Record<string, string>;
|
|
119
|
+
createdAt: string;
|
|
120
|
+
updatedAt: string;
|
|
121
|
+
}
|
|
122
|
+
export interface SshAccess {
|
|
123
|
+
host: string;
|
|
124
|
+
port: number;
|
|
125
|
+
user: string;
|
|
126
|
+
identityFile?: string;
|
|
127
|
+
command: string;
|
|
128
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function id(prefix: string): string;
|
|
2
|
+
export declare function now(): string;
|
|
3
|
+
export declare function ensureDir(dir: string): Promise<void>;
|
|
4
|
+
export declare function readJson<T>(file: string): Promise<T>;
|
|
5
|
+
export declare function writeJson(file: string, value: unknown): Promise<void>;
|
|
6
|
+
export declare function exists(file: string): Promise<boolean>;
|
|
7
|
+
export declare function run(cmd: string, args: string[], opts?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
exitCode: number | null;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function safeJoin(root: string, requested: string): string;
|
package/dist/util.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export function id(prefix) {
|
|
5
|
+
return `${prefix}_${crypto.randomUUID().replaceAll('-', '').slice(0, 16)}`;
|
|
6
|
+
}
|
|
7
|
+
export function now() {
|
|
8
|
+
return new Date().toISOString();
|
|
9
|
+
}
|
|
10
|
+
export async function ensureDir(dir) {
|
|
11
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
export async function readJson(file) {
|
|
14
|
+
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
export async function writeJson(file, value) {
|
|
17
|
+
await ensureDir(path.dirname(file));
|
|
18
|
+
await fs.writeFile(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
19
|
+
}
|
|
20
|
+
export async function exists(file) {
|
|
21
|
+
try {
|
|
22
|
+
await fs.access(file);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function run(cmd, args, opts = {}) {
|
|
30
|
+
const child = spawn(cmd, args, { cwd: opts.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
31
|
+
const stdout = [];
|
|
32
|
+
const stderr = [];
|
|
33
|
+
const timer = opts.timeoutMs
|
|
34
|
+
? setTimeout(() => {
|
|
35
|
+
child.kill('SIGKILL');
|
|
36
|
+
}, opts.timeoutMs)
|
|
37
|
+
: undefined;
|
|
38
|
+
child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk)));
|
|
39
|
+
child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk)));
|
|
40
|
+
const exitCode = await new Promise((resolve) => {
|
|
41
|
+
child.on('error', (error) => {
|
|
42
|
+
stderr.push(Buffer.from(error.message));
|
|
43
|
+
resolve(127);
|
|
44
|
+
});
|
|
45
|
+
child.on('close', resolve);
|
|
46
|
+
});
|
|
47
|
+
if (timer)
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
return {
|
|
50
|
+
exitCode,
|
|
51
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
52
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function safeJoin(root, requested) {
|
|
56
|
+
const target = path.resolve(root, requested.replace(/^\/+/, ''));
|
|
57
|
+
const resolvedRoot = path.resolve(root);
|
|
58
|
+
if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
59
|
+
throw new Error('path escapes root');
|
|
60
|
+
}
|
|
61
|
+
return target;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
`apple-sandboxes` is a local control plane for Apple's native `container` runtime.
|
|
4
|
+
|
|
5
|
+
The shape is inspired by systems like Gondolin and Daytona, but the implementation is intentionally Apple-container-native:
|
|
6
|
+
|
|
7
|
+
- no Docker daemon
|
|
8
|
+
- no Linux host requirement
|
|
9
|
+
- no microVM guest image pipeline
|
|
10
|
+
- no rootful daemon
|
|
11
|
+
|
|
12
|
+
## Primitives
|
|
13
|
+
|
|
14
|
+
### Image
|
|
15
|
+
|
|
16
|
+
An image is a named runtime definition.
|
|
17
|
+
|
|
18
|
+
It can be:
|
|
19
|
+
|
|
20
|
+
- a reference to an existing OCI image, such as `ubuntu:24.04`
|
|
21
|
+
- a Dockerfile/Containerfile stored in the local registry and built with `container build`
|
|
22
|
+
|
|
23
|
+
Images are stored under:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
~/.apple-sandboxes/images
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Sandbox
|
|
30
|
+
|
|
31
|
+
A sandbox is a long-lived Apple container with:
|
|
32
|
+
|
|
33
|
+
- a host-backed `/workspace`
|
|
34
|
+
- optional named volume mounts
|
|
35
|
+
- CPU/memory limits
|
|
36
|
+
- a network policy
|
|
37
|
+
- lifecycle metadata
|
|
38
|
+
|
|
39
|
+
Sandboxes are stored under:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
~/.apple-sandboxes/sandboxes
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Snapshot
|
|
46
|
+
|
|
47
|
+
A snapshot is a reusable `/workspace` archive.
|
|
48
|
+
|
|
49
|
+
Creating a new sandbox from a snapshot restores the archive into a fresh sandbox workspace before the container starts.
|
|
50
|
+
|
|
51
|
+
Snapshots are stored under:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
~/.apple-sandboxes/snapshots
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This is a workspace snapshot, not a VM checkpoint and not a full root filesystem snapshot.
|
|
58
|
+
|
|
59
|
+
### Volume
|
|
60
|
+
|
|
61
|
+
A volume is a named persistent host directory mounted into one or more sandboxes.
|
|
62
|
+
|
|
63
|
+
Volumes are stored under:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
~/.apple-sandboxes/volumes
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Runtime Model
|
|
70
|
+
|
|
71
|
+
Sandbox creation maps to Apple `container create`:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
container create \
|
|
75
|
+
--platform linux/arm64 \
|
|
76
|
+
--cpus N \
|
|
77
|
+
--memory SIZE \
|
|
78
|
+
--mount type=bind,source=WORKSPACE,target=/workspace \
|
|
79
|
+
--workdir /workspace \
|
|
80
|
+
IMAGE \
|
|
81
|
+
sh -lc 'while true; do sleep 3600; done'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Command execution maps to:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
container exec -w /workspace CONTAINER sh -lc COMMAND
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Network Model
|
|
91
|
+
|
|
92
|
+
The first implementation supports:
|
|
93
|
+
|
|
94
|
+
- `deny`: maps to `--network none`
|
|
95
|
+
- `open`: leaves the default Apple container network enabled
|
|
96
|
+
|
|
97
|
+
The intended next step is a host-mediated allowlist model:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"network": {
|
|
102
|
+
"mode": "allowlist",
|
|
103
|
+
"allowedHosts": ["api.github.com"]
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
That is closer to Gondolin's policy direction, but is not implemented yet.
|
|
109
|
+
|
|
110
|
+
## SSH Model
|
|
111
|
+
|
|
112
|
+
SSH is not implemented in this first cut.
|
|
113
|
+
|
|
114
|
+
The intended shape is:
|
|
115
|
+
|
|
116
|
+
- generate ephemeral Ed25519 keys
|
|
117
|
+
- inject the public key into the sandbox
|
|
118
|
+
- run `sshd` inside a prepared sandbox image
|
|
119
|
+
- bind only to a localhost host port
|
|
120
|
+
- return a hardened SSH command with agent/forwarding disabled
|
|
121
|
+
|
|
122
|
+
## API Layers
|
|
123
|
+
|
|
124
|
+
The package exposes:
|
|
125
|
+
|
|
126
|
+
- TypeScript runtime class: `AppleSandboxRuntime`
|
|
127
|
+
- TypeScript HTTP client: `AppleSandboxes`
|
|
128
|
+
- Express API server
|
|
129
|
+
- npm CLI: `apple-sandboxes`
|
|
130
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unionstreet/apple-sandboxes",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Daytona/Gondolin-inspired sandbox API for Apple container on Apple Silicon Macs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"apple-sandboxes": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"docs",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsx src/cli.ts",
|
|
21
|
+
"start": "node dist/cli.js serve",
|
|
22
|
+
"test": "tsx --test test/**/*.test.ts",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepack": "npm run build",
|
|
25
|
+
"prepublishOnly": "npm run typecheck && npm run test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"apple-container",
|
|
29
|
+
"sandbox",
|
|
30
|
+
"agents",
|
|
31
|
+
"local-first",
|
|
32
|
+
"macos"
|
|
33
|
+
],
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"homepage": "https://github.com/UnionStreetAI/apple-sandboxes#readme",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/UnionStreetAI/apple-sandboxes.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/UnionStreetAI/apple-sandboxes/issues"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"commander": "^14.0.2",
|
|
51
|
+
"express": "^5.1.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/express": "^5.0.5",
|
|
55
|
+
"@types/node": "^24.10.1",
|
|
56
|
+
"tsx": "^4.21.0",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
58
|
+
}
|
|
59
|
+
}
|