@playcraft/cli 0.1.1 → 0.1.2
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/workspace-runtime/codex/app-server.js +21 -4
- package/dist/workspace-runtime/codex/model-broker.js +183 -0
- package/dist/workspace-runtime/codex/native-adapter.js +3 -0
- package/dist/workspace-runtime/serve.js +15 -7
- package/dist/workspace-runtime/server/dispatch.js +14 -4
- package/dist/workspace-runtime/server/http.js +2 -2
- package/package.json +3 -3
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
import { writePlaycraftCodexConfig } from './config-toml.js';
|
|
7
7
|
import { createJsonRpcStdioClient } from './jsonrpc-stdio.js';
|
|
8
8
|
import { projectNativeThread } from '@playcraft/common/cloud-workspace/runtime';
|
|
9
|
-
import { collectedEvent } from './native-adapter.js';
|
|
9
|
+
import { collectedEvent, NativePreparationError } from './native-adapter.js';
|
|
10
10
|
function asRecord(value) {
|
|
11
11
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
12
12
|
? value
|
|
@@ -74,8 +74,20 @@ export function createCodexAppServerAdapter(options) {
|
|
|
74
74
|
startPromise ??= start();
|
|
75
75
|
await startPromise;
|
|
76
76
|
}
|
|
77
|
+
async function threadConfig(cwd) {
|
|
78
|
+
try {
|
|
79
|
+
return await options.modelBroker?.threadConfig(cwd) ?? {};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
const reason = error instanceof Error ? error.message : '';
|
|
83
|
+
throw new NativePreparationError(['model_config_required', 'model_callback_insecure',
|
|
84
|
+
'model_credential_scope_mismatch', 'workspace_not_registered'].includes(reason)
|
|
85
|
+
? reason : 'model_credential_unavailable');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
77
88
|
async function start() {
|
|
78
|
-
|
|
89
|
+
await options.modelBroker?.start();
|
|
90
|
+
const token = options.modelBroker ? 'workspace-broker-managed' : await readToken(options);
|
|
79
91
|
const bin = options.command || process.env.PLAYCRAFT_CODEX_BIN || (launcherSocket ? '/usr/local/bin/codex' : 'codex');
|
|
80
92
|
const { command, prefix } = spawnCommand(bin);
|
|
81
93
|
const args = [...prefix, ...(options.args ?? ['app-server'])];
|
|
@@ -98,7 +110,9 @@ export function createCodexAppServerAdapter(options) {
|
|
|
98
110
|
processId: `codex-${randomUUID()}`,
|
|
99
111
|
runtimeEpoch: executionEpoch,
|
|
100
112
|
argv: [command, ...args],
|
|
101
|
-
|
|
113
|
+
// The launcher admits the dev home, not the shared /workspaces root.
|
|
114
|
+
// Each thread supplies its registered workspace separately.
|
|
115
|
+
cwd: options.config.workspaceMode === 'multi' ? '/home/playcraft-dev' : options.config.workspaceRoot,
|
|
102
116
|
env: { CODEX_HOME: homeDir, OPENAI_API_KEY: token },
|
|
103
117
|
});
|
|
104
118
|
}
|
|
@@ -230,7 +244,8 @@ export function createCodexAppServerAdapter(options) {
|
|
|
230
244
|
if (!rpc)
|
|
231
245
|
return false;
|
|
232
246
|
try {
|
|
233
|
-
const result = await rpc.request('thread/resume', { threadId: input.threadId, cwd: input.cwd
|
|
247
|
+
const result = await rpc.request('thread/resume', { threadId: input.threadId, cwd: input.cwd,
|
|
248
|
+
...await threadConfig(input.cwd ?? '') }, 30_000);
|
|
234
249
|
const resumedId = result.thread?.id ?? result.threadId;
|
|
235
250
|
if (resumedId !== input.threadId)
|
|
236
251
|
return false;
|
|
@@ -275,6 +290,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
275
290
|
const result = await rpc.request('thread/start', {
|
|
276
291
|
cwd: input.cwd,
|
|
277
292
|
model,
|
|
293
|
+
...await threadConfig(input.cwd),
|
|
278
294
|
});
|
|
279
295
|
const threadId = result.thread?.id;
|
|
280
296
|
if (!threadId)
|
|
@@ -356,6 +372,7 @@ export function createCodexAppServerAdapter(options) {
|
|
|
356
372
|
await current.stop();
|
|
357
373
|
child = undefined;
|
|
358
374
|
live = undefined;
|
|
375
|
+
await options.modelBroker?.close();
|
|
359
376
|
},
|
|
360
377
|
};
|
|
361
378
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { workspaceRoot } from '../workspaces/registry.js';
|
|
8
|
+
const credentialSchema = z.object({
|
|
9
|
+
token: z.string().min(1), expiresAt: z.string().datetime(),
|
|
10
|
+
configId: z.string().uuid(), model: z.string().min(1).max(128),
|
|
11
|
+
claims: z.object({
|
|
12
|
+
environment: z.string(), userId: z.string(), workspaceId: z.string(), runtimeId: z.string(),
|
|
13
|
+
generation: z.number().int(), runtimeEpoch: z.number().int(),
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
/** Keeps renewable execution credentials in control memory, outside Codex and project files. */
|
|
17
|
+
export class WorkspaceModelBroker {
|
|
18
|
+
state;
|
|
19
|
+
server;
|
|
20
|
+
baseUrl = '';
|
|
21
|
+
entries = new Map();
|
|
22
|
+
pending = new Map();
|
|
23
|
+
requests = new Set();
|
|
24
|
+
constructor(state) {
|
|
25
|
+
this.state = state;
|
|
26
|
+
}
|
|
27
|
+
async start() {
|
|
28
|
+
if (this.server)
|
|
29
|
+
return;
|
|
30
|
+
const server = createServer({ maxHeaderSize: 8192 }, (request, response) => {
|
|
31
|
+
void this.forward(request, response);
|
|
32
|
+
});
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
server.once('error', reject);
|
|
35
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
36
|
+
});
|
|
37
|
+
this.server = server;
|
|
38
|
+
const address = server.address();
|
|
39
|
+
if (!address || typeof address === 'string')
|
|
40
|
+
throw new Error('model_broker_unavailable');
|
|
41
|
+
this.baseUrl = `http://127.0.0.1:${address.port}`;
|
|
42
|
+
}
|
|
43
|
+
async threadConfig(cwd) {
|
|
44
|
+
const workspaceId = basename(cwd);
|
|
45
|
+
if (!this.server || await workspaceRoot(this.state, workspaceId) !== cwd)
|
|
46
|
+
throw new Error('workspace_not_registered');
|
|
47
|
+
let entry = this.entries.get(workspaceId);
|
|
48
|
+
if (!entry || entry.epoch !== this.state.runtimeEpoch) {
|
|
49
|
+
entry = { workspaceId, epoch: this.state.runtimeEpoch, capability: randomBytes(32).toString('base64url') };
|
|
50
|
+
this.entries.set(workspaceId, entry);
|
|
51
|
+
}
|
|
52
|
+
const credential = await this.credential(entry);
|
|
53
|
+
return {
|
|
54
|
+
model: credential.model,
|
|
55
|
+
modelProvider: 'playcraft_workspace',
|
|
56
|
+
config: {
|
|
57
|
+
'model_providers.playcraft_workspace': {
|
|
58
|
+
name: 'PlayCraft Workspace', base_url: `${this.baseUrl}/${workspaceId}`,
|
|
59
|
+
wire_api: 'responses', requires_openai_auth: false,
|
|
60
|
+
http_headers: { Authorization: `Bearer ${entry.capability}` },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
valid(entry) {
|
|
66
|
+
return Boolean(this.server && !this.state.journal.destroyed()
|
|
67
|
+
&& this.state.runtimeEpoch === entry.epoch && this.state.journal.getWorkspace(entry.workspaceId)
|
|
68
|
+
&& this.entries.get(entry.workspaceId) === entry);
|
|
69
|
+
}
|
|
70
|
+
async credential(entry) {
|
|
71
|
+
if (!this.valid(entry))
|
|
72
|
+
throw new Error('model_access_denied');
|
|
73
|
+
if (entry.credential && Date.parse(entry.credential.expiresAt) > Date.now() + 60_000)
|
|
74
|
+
return entry.credential;
|
|
75
|
+
let pending = this.pending.get(entry);
|
|
76
|
+
if (!pending) {
|
|
77
|
+
pending = this.renew(entry).finally(() => this.pending.delete(entry));
|
|
78
|
+
this.pending.set(entry, pending);
|
|
79
|
+
}
|
|
80
|
+
return pending;
|
|
81
|
+
}
|
|
82
|
+
async renew(entry) {
|
|
83
|
+
const config = this.state.config;
|
|
84
|
+
const endpoint = new URL('/runtime/internal/model-credentials', config.callbackUrl);
|
|
85
|
+
if (endpoint.protocol !== 'https:' && !(endpoint.protocol === 'http:'
|
|
86
|
+
&& ['127.0.0.1', 'localhost', '[::1]'].includes(endpoint.hostname)))
|
|
87
|
+
throw new Error('model_callback_insecure');
|
|
88
|
+
const response = await fetch(endpoint, {
|
|
89
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
|
|
90
|
+
headers: { authorization: `Bearer ${config.controlToken}`, 'content-type': 'application/json' },
|
|
91
|
+
body: JSON.stringify({ runtimeId: config.runtimeId, generation: config.generation,
|
|
92
|
+
runtimeEpoch: entry.epoch, workspaceId: entry.workspaceId }),
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const error = await response.json().catch(() => ({}));
|
|
96
|
+
throw new Error(error.reason === 'model_config_required' ? 'model_config_required' : 'model_credential_unavailable');
|
|
97
|
+
}
|
|
98
|
+
const credential = credentialSchema.parse(await response.json());
|
|
99
|
+
const claims = credential.claims;
|
|
100
|
+
if (!this.valid(entry) || claims.workspaceId !== entry.workspaceId || claims.runtimeId !== config.runtimeId
|
|
101
|
+
|| claims.userId !== config.userId || claims.environment !== config.environment
|
|
102
|
+
|| claims.generation !== config.generation || claims.runtimeEpoch !== entry.epoch
|
|
103
|
+
|| Date.parse(credential.expiresAt) <= Date.now() + 1000)
|
|
104
|
+
throw new Error('model_credential_scope_mismatch');
|
|
105
|
+
entry.credential = credential;
|
|
106
|
+
return credential;
|
|
107
|
+
}
|
|
108
|
+
async forward(request, response) {
|
|
109
|
+
const abort = new AbortController();
|
|
110
|
+
this.requests.add(abort);
|
|
111
|
+
const cancel = () => abort.abort();
|
|
112
|
+
response.once('close', cancel);
|
|
113
|
+
try {
|
|
114
|
+
const match = /^\/([0-9a-f-]{36})\/responses$/.exec(request.url ?? '');
|
|
115
|
+
const entry = match && this.entries.get(match[1]);
|
|
116
|
+
const presented = Buffer.from(request.headers.authorization ?? '');
|
|
117
|
+
const expected = Buffer.from(entry ? `Bearer ${entry.capability}` : '');
|
|
118
|
+
if (request.method !== 'POST' || !entry || !expected.length || presented.length !== expected.length
|
|
119
|
+
|| !timingSafeEqual(presented, expected) || !this.valid(entry)) {
|
|
120
|
+
response.writeHead(403);
|
|
121
|
+
response.end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const chunks = [];
|
|
125
|
+
let size = 0;
|
|
126
|
+
for await (const chunk of request) {
|
|
127
|
+
size += chunk.length;
|
|
128
|
+
if (size > 16 * 1024 * 1024) {
|
|
129
|
+
response.writeHead(413);
|
|
130
|
+
response.end();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
chunks.push(Buffer.from(chunk));
|
|
134
|
+
}
|
|
135
|
+
const credential = await this.credential(entry);
|
|
136
|
+
if (!this.valid(entry))
|
|
137
|
+
throw new Error('model_access_denied');
|
|
138
|
+
const expires = setTimeout(cancel, Math.max(0, Date.parse(credential.expiresAt) - Date.now()));
|
|
139
|
+
try {
|
|
140
|
+
const upstream = await fetch(`${this.state.config.modelProxyUrl.replace(/\/+$/, '')}/v1/${credential.configId}/responses`, {
|
|
141
|
+
method: 'POST', redirect: 'error', signal: abort.signal,
|
|
142
|
+
headers: { authorization: `Bearer ${credential.token}`, 'content-type': 'application/json' },
|
|
143
|
+
body: Buffer.concat(chunks),
|
|
144
|
+
});
|
|
145
|
+
if (upstream.status === 401 || upstream.status === 403)
|
|
146
|
+
entry.credential = undefined;
|
|
147
|
+
response.writeHead(upstream.status, {
|
|
148
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json', 'cache-control': 'no-store',
|
|
149
|
+
});
|
|
150
|
+
if (upstream.body)
|
|
151
|
+
await pipeline(Readable.fromWeb(upstream.body), response);
|
|
152
|
+
else
|
|
153
|
+
response.end();
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
clearTimeout(expires);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
if (!response.headersSent) {
|
|
161
|
+
response.writeHead(503, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
162
|
+
response.end(JSON.stringify({ error: { message: 'model_credential_unavailable', type: 'server_error' } }));
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
response.destroy();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
response.off('close', cancel);
|
|
169
|
+
this.requests.delete(abort);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async close() {
|
|
173
|
+
const server = this.server;
|
|
174
|
+
this.server = undefined;
|
|
175
|
+
this.entries.clear();
|
|
176
|
+
for (const request of this.requests)
|
|
177
|
+
request.abort();
|
|
178
|
+
if (server) {
|
|
179
|
+
server.closeAllConnections();
|
|
180
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Failure before a native request was sent; safe to report as failed instead of uncertain. */
|
|
2
|
+
export class NativePreparationError extends Error {
|
|
3
|
+
}
|
|
1
4
|
export function nativeEvent(type, payload) {
|
|
2
5
|
return { type, payload, occurredAt: new Date().toISOString() };
|
|
3
6
|
}
|
|
@@ -13,6 +13,7 @@ import { handleExecutionGroupCrash } from './server/execution-group.js';
|
|
|
13
13
|
import { RuntimeProcessLock } from './server/lock.js';
|
|
14
14
|
import { probeGitBinary } from './workspaces/git.js';
|
|
15
15
|
import { WorkspaceGitCredentialBroker } from './workspaces/git-credential-broker.js';
|
|
16
|
+
import { WorkspaceModelBroker } from './codex/model-broker.js';
|
|
16
17
|
export async function startWorkspaceRuntime(config, options = {}) {
|
|
17
18
|
process.env.PLAYCRAFT_SKIP_UPDATE_CHECK = '1';
|
|
18
19
|
const negotiated = negotiateProtocol(config);
|
|
@@ -31,17 +32,17 @@ export async function startWorkspaceRuntime(config, options = {}) {
|
|
|
31
32
|
journal = new RuntimeJournal(config.stateDir);
|
|
32
33
|
await journal.load();
|
|
33
34
|
await mkdir(config.workspaceRoot, { recursive: true, mode: 0o700 });
|
|
34
|
-
const native = options.nativeAdapter ?? defaultNativeAdapter(config);
|
|
35
35
|
const state = {
|
|
36
36
|
config: { ...config, protocolMinor: negotiated.protocolMinor },
|
|
37
37
|
journal,
|
|
38
38
|
acceptedLeaseFence: journal.acceptedLeaseFence(),
|
|
39
39
|
gitAvailable: await probeGitBinary(),
|
|
40
|
-
native,
|
|
40
|
+
native: options.nativeAdapter,
|
|
41
41
|
runtimeEpoch: Math.max(config.runtimeEpoch, journal.storedRuntimeEpoch() ?? 0),
|
|
42
42
|
executionQueue: Promise.resolve(),
|
|
43
43
|
instanceToken: randomUUID(),
|
|
44
44
|
};
|
|
45
|
+
const native = state.native ??= defaultNativeAdapter(state);
|
|
45
46
|
await journal.bindRuntime({
|
|
46
47
|
runtimeId: config.runtimeId,
|
|
47
48
|
generation: config.generation,
|
|
@@ -61,8 +62,10 @@ export async function startWorkspaceRuntime(config, options = {}) {
|
|
|
61
62
|
if (group)
|
|
62
63
|
await journal.noteExecutionGroup(group);
|
|
63
64
|
}
|
|
64
|
-
catch {
|
|
65
|
-
|
|
65
|
+
catch (error) {
|
|
66
|
+
const code = error instanceof Error ? error.message : '';
|
|
67
|
+
state.nativeError = code === 'model_proxy_token_missing' ? code : 'codex_start_failed';
|
|
68
|
+
console.error(`[workspace-runtime] ${state.nativeError}`);
|
|
66
69
|
}
|
|
67
70
|
const server = createServer((request, response) => {
|
|
68
71
|
void handleRuntimeRequest(state, request, response);
|
|
@@ -135,11 +138,16 @@ function closeServer(server) {
|
|
|
135
138
|
function formatHost(host) {
|
|
136
139
|
return host.includes(':') ? `[${host}]` : host;
|
|
137
140
|
}
|
|
138
|
-
function defaultNativeAdapter(
|
|
141
|
+
function defaultNativeAdapter(state) {
|
|
142
|
+
const config = state.config;
|
|
139
143
|
if (process.env.PLAYCRAFT_NATIVE_ADAPTER === 'loopback')
|
|
140
144
|
return createLoopbackNativeAdapter();
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
const managed = config.workspaceMode === 'multi' && Boolean(process.env.PLAYCRAFT_LAUNCHER_SOCKET);
|
|
146
|
+
if (process.env.PLAYCRAFT_NATIVE_ADAPTER === 'codex'
|
|
147
|
+
|| (!process.env.PLAYCRAFT_NATIVE_ADAPTER && managed)) {
|
|
148
|
+
return createCodexAppServerAdapter({ config,
|
|
149
|
+
...(managed ? { modelBroker: new WorkspaceModelBroker(state) } : {}),
|
|
150
|
+
});
|
|
143
151
|
}
|
|
144
152
|
return undefined;
|
|
145
153
|
}
|
|
@@ -3,6 +3,7 @@ import { revisionScope, workspaceRoot, workspaceSessions } from '../workspaces/r
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { checkpointRecordSchema, fileWritePayloadSchema, gitCommitPayloadSchema, gitPushPayloadSchema, parameterPatchPayloadSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, } from '@playcraft/common/cloud-workspace/runtime';
|
|
6
|
+
import { NativePreparationError } from '../codex/native-adapter.js';
|
|
6
7
|
import { disposeTerminalHost } from './terminal-host.js';
|
|
7
8
|
import { captureWorkspaceTree, materializeSnapshot, publishFileSnapshot, } from '../workspaces/file-snapshot.js';
|
|
8
9
|
import { InvalidWorkspacePathError, resolveWorkspaceFile, restoreWorkspaceFiles, snapshotWorkspaceFiles, writeWorkspaceFile, } from '../workspaces/files.js';
|
|
@@ -117,10 +118,19 @@ async function executeScopedOperation(state, scopeKey, operation, payload) {
|
|
|
117
118
|
}
|
|
118
119
|
async function executeCreate(state, scopeKey, operation) {
|
|
119
120
|
const sessionId = required(operation.sessionId, 'sessionId');
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
let started;
|
|
122
|
+
try {
|
|
123
|
+
started = await native(state).startThread({
|
|
124
|
+
sessionId,
|
|
125
|
+
cwd: await workspaceRoot(state, operation.workspaceId),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof NativePreparationError) {
|
|
130
|
+
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: error.message }));
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
124
134
|
if (!started.threadId) {
|
|
125
135
|
return persist(state, scopeKey, transitionOperation(operation, 'uncertain', { error: 'native_receipt_lost' }));
|
|
126
136
|
}
|
|
@@ -211,7 +211,7 @@ function handshake(state) {
|
|
|
211
211
|
if (item.name === 'conversation') {
|
|
212
212
|
if (conversationReady)
|
|
213
213
|
return capability('conversation', true);
|
|
214
|
-
return
|
|
214
|
+
return capability('conversation', false, destroyed ? 'runtime_destroyed' : state.nativeError ?? 'adapter_not_started');
|
|
215
215
|
}
|
|
216
216
|
if (item.name === 'files') {
|
|
217
217
|
return filesReady
|
|
@@ -543,7 +543,7 @@ function unavailableReason(state, kind) {
|
|
|
543
543
|
return 'runtime_destroyed';
|
|
544
544
|
if (kind === 'preview.publish')
|
|
545
545
|
return 'capability_not_implemented';
|
|
546
|
-
return 'adapter_not_started';
|
|
546
|
+
return state.nativeError ?? 'adapter_not_started';
|
|
547
547
|
}
|
|
548
548
|
function sessionHistoryPath(pathname) {
|
|
549
549
|
const match = pathname.match(/^\/runtime\/workspaces\/([^/]+)\/sessions\/([^/]+)\/history$/);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"@gltf-transform/core": "^4.3.0",
|
|
49
49
|
"@gltf-transform/extensions": "^4.3.0",
|
|
50
50
|
"@gltf-transform/functions": "^4.3.0",
|
|
51
|
-
"@playcraft/build": "^0.1.
|
|
52
|
-
"@playcraft/common": "^0.1.
|
|
51
|
+
"@playcraft/build": "^0.1.2",
|
|
52
|
+
"@playcraft/common": "^0.1.2",
|
|
53
53
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
54
54
|
"chokidar": "^4.0.3",
|
|
55
55
|
"commander": "^13.1.0",
|