@playcraft/cli 0.1.28 → 0.1.30
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 +357 -45
- package/dist/workspace-runtime/codex/compact.js +46 -0
- package/dist/workspace-runtime/codex/jsonrpc-stdio.js +26 -4
- package/dist/workspace-runtime/codex/loopback.js +5 -2
- package/dist/workspace-runtime/codex/mcp-config.js +39 -0
- package/dist/workspace-runtime/codex/model-broker.js +8 -3
- package/dist/workspace-runtime/codex/permissions.js +70 -0
- package/dist/workspace-runtime/codex/work-mode.js +15 -0
- package/dist/workspace-runtime/persistence/native-subagents.js +2 -2
- package/dist/workspace-runtime/persistence/sqlite-worker.js +50 -11
- package/dist/workspace-runtime/production/native.js +2 -0
- package/dist/workspace-runtime/production/save-command.js +11 -0
- package/dist/workspace-runtime/production/stages.js +16 -16
- package/dist/workspace-runtime/serve.js +5 -0
- package/dist/workspace-runtime/server/dispatch.js +221 -13
- package/dist/workspace-runtime/server/http.js +54 -4
- package/dist/workspace-runtime/workspaces/composer-context.js +125 -0
- package/dist/workspace-runtime/workspaces/files-search.js +116 -0
- package/package.json +3 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, opendir, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { workspaceFileSearchQuerySchema, workspaceRelativePathSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
5
|
+
const SKIP_DIRECTORIES = new Set(['.git', 'node_modules', '.playcraft', '.next', 'dist', 'build', 'coverage', '.turbo']);
|
|
6
|
+
const SCAN_PAGE_ENTRIES = 1000;
|
|
7
|
+
export class InvalidWorkspaceSearchError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code = 'invalid_workspace_search') {
|
|
10
|
+
super(code);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = 'InvalidWorkspaceSearchError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Enumerates names only. The cursor is scoped to the resolved checkout and query, never to a caller supplied path. */
|
|
16
|
+
export async function searchWorkspaceFiles(root, query, options = {}) {
|
|
17
|
+
const parsed = workspaceFileSearchQuerySchema.safeParse(query);
|
|
18
|
+
if (!parsed.success)
|
|
19
|
+
throw new InvalidWorkspaceSearchError();
|
|
20
|
+
options.signal?.throwIfAborted();
|
|
21
|
+
const rootReal = await realpath(root);
|
|
22
|
+
const needle = parsed.data.query.toLocaleLowerCase();
|
|
23
|
+
const scope = createHash('sha256').update(rootReal).update('\0').update(needle).digest('hex');
|
|
24
|
+
const start = decodeCursor(parsed.data.cursor, scope);
|
|
25
|
+
const files = [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for await (const path of walk(rootReal, rootReal, options.signal)) {
|
|
28
|
+
options.signal?.throwIfAborted();
|
|
29
|
+
offset += 1;
|
|
30
|
+
if (offset <= start)
|
|
31
|
+
continue;
|
|
32
|
+
if (path && path.toLocaleLowerCase().includes(needle)) {
|
|
33
|
+
const target = join(rootReal, ...path.split('/'));
|
|
34
|
+
const info = await lstat(target).catch(ignoreUnavailable);
|
|
35
|
+
const targetReal = info?.isFile() && !info.isSymbolicLink() ? await realpath(target).catch(ignoreUnavailable) : undefined;
|
|
36
|
+
if (targetReal && inside(rootReal, targetReal))
|
|
37
|
+
files.push({ path });
|
|
38
|
+
}
|
|
39
|
+
if (files.length >= parsed.data.limit || offset - start >= SCAN_PAGE_ENTRIES) {
|
|
40
|
+
return { files, truncated: true, nextCursor: encodeCursor(scope, offset) };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { files, truncated: false };
|
|
44
|
+
}
|
|
45
|
+
async function* walk(root, current, signal) {
|
|
46
|
+
signal?.throwIfAborted();
|
|
47
|
+
const currentInfo = await lstat(current).catch(ignoreUnavailable);
|
|
48
|
+
if (!currentInfo?.isDirectory() || currentInfo.isSymbolicLink())
|
|
49
|
+
return;
|
|
50
|
+
const resolved = await realpath(current).catch(ignoreUnavailable);
|
|
51
|
+
if (!resolved || !inside(root, resolved))
|
|
52
|
+
return;
|
|
53
|
+
const directory = await opendir(resolved).catch(ignoreUnavailable);
|
|
54
|
+
if (!directory)
|
|
55
|
+
return;
|
|
56
|
+
// Recheck after opening: a concurrently replaced directory must not reveal another checkout's paths.
|
|
57
|
+
const openedPath = await realpath(current).catch(ignoreUnavailable);
|
|
58
|
+
if (openedPath !== resolved || !inside(root, openedPath)) {
|
|
59
|
+
await directory.close();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for await (const entry of directory) {
|
|
63
|
+
signal?.throwIfAborted();
|
|
64
|
+
if (['.git', 'node_modules', '.playcraft'].includes(entry.name)) {
|
|
65
|
+
yield undefined;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const child = join(resolved, entry.name);
|
|
69
|
+
const path = relative(root, child).split(sep).join('/');
|
|
70
|
+
if (!workspaceRelativePathSchema.safeParse(path).success) {
|
|
71
|
+
yield undefined;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
yield undefined;
|
|
76
|
+
if (!SKIP_DIRECTORIES.has(entry.name))
|
|
77
|
+
yield* walk(root, child, signal);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!entry.isFile()) {
|
|
81
|
+
yield undefined;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
yield path;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function inside(root, path) {
|
|
88
|
+
const value = relative(root, path);
|
|
89
|
+
return value === '' || (value !== '..' && !value.startsWith(`..${sep}`) && !value.startsWith(sep));
|
|
90
|
+
}
|
|
91
|
+
function ignoreUnavailable(error) {
|
|
92
|
+
if (['ENOENT', 'ENOTDIR', 'EACCES'].includes(error.code ?? ''))
|
|
93
|
+
return undefined;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
function encodeCursor(scope, offset) {
|
|
97
|
+
return Buffer.from(JSON.stringify({ v: 1, scope, offset })).toString('base64url');
|
|
98
|
+
}
|
|
99
|
+
function decodeCursor(cursor, scope) {
|
|
100
|
+
if (!cursor)
|
|
101
|
+
return 0;
|
|
102
|
+
try {
|
|
103
|
+
if (!/^[A-Za-z0-9_-]+$/.test(cursor))
|
|
104
|
+
throw new Error();
|
|
105
|
+
const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
106
|
+
if (typeof value !== 'object' || value === null)
|
|
107
|
+
throw new Error();
|
|
108
|
+
const { v, scope: suppliedScope, offset } = value;
|
|
109
|
+
if (v !== 1 || suppliedScope !== scope || typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0)
|
|
110
|
+
throw new Error();
|
|
111
|
+
return offset;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new InvalidWorkspaceSearchError('invalid_workspace_search_cursor');
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@gltf-transform/core": "^4.3.0",
|
|
51
51
|
"@gltf-transform/extensions": "^4.3.0",
|
|
52
52
|
"@gltf-transform/functions": "^4.3.0",
|
|
53
|
-
"@playcraft/build": "^0.1.
|
|
54
|
-
"@playcraft/common": "^0.1.
|
|
53
|
+
"@playcraft/build": "^0.1.30",
|
|
54
|
+
"@playcraft/common": "^0.1.30",
|
|
55
55
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
56
56
|
"chokidar": "^4.0.3",
|
|
57
57
|
"commander": "^13.1.0",
|