@playcraft/cli 0.1.33 → 0.1.34
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withObservedFile } from '../workspaces/revisions.js';
|
|
2
2
|
import { revisionScope, sessionWorkspaceRoot, workspaceRoot, workspaceSessions } from '../workspaces/registry.js';
|
|
3
3
|
import { abandonTurnChanges, beginTurnChanges, finishTurnChanges } from '../workspaces/turn-changes.js';
|
|
4
|
+
import { shareWorkspaceGit } from '../workspaces/shared-git.js';
|
|
4
5
|
import { join } from 'node:path';
|
|
5
6
|
import { z } from 'zod';
|
|
6
7
|
import { PLATFORM_ROUTES, platformEndpoint, fileWritePayloadSchema, gitCommitPayloadSchema, gitPathsPayloadSchema, gitPushPayloadSchema, gitPullPayloadSchema, gitMergeAbortPayloadSchema, parameterPatchPayloadSchema, productionSessionAttemptId, reasoningEffortSchema, workspacePermissionModeSchema, workspaceComposerSelectionSchema, workspaceReviewPayloadSchema, workspaceCompactPayloadSchema, workspaceSteerPayloadSchema, workspaceApprovalResponseSchema, terminalCreatePayloadSchema, terminalProcessIdentity, transitionOperation, recoverUncertainOperation, workspaceSavePayloadSchema, workspaceSkillsUpdatePayloadSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
@@ -789,7 +790,7 @@ async function executeGitCommit(state, scopeKey, operation, payload) {
|
|
|
789
790
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_git_commit' }));
|
|
790
791
|
}
|
|
791
792
|
try {
|
|
792
|
-
const sha = await commitWorkspaceGit(await
|
|
793
|
+
const sha = await commitWorkspaceGit(await gitWorkspaceRoot(state, operation), parsed.data, boundBranch(state, operation));
|
|
793
794
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
794
795
|
await recordNativeEvents(state, accepted, [
|
|
795
796
|
{
|
|
@@ -826,7 +827,7 @@ async function executeWorkspaceSave(state, scopeKey, operation, payload) {
|
|
|
826
827
|
}
|
|
827
828
|
const binding = operation.workspaceId ? state.journal.getWorkspace(operation.workspaceId) : undefined;
|
|
828
829
|
try {
|
|
829
|
-
const saved = await saveWorkspaceGit(await
|
|
830
|
+
const saved = await saveWorkspaceGit(await gitWorkspaceRoot(state, operation), {
|
|
830
831
|
message: parsed.data.trigger === 'auto' ? 'Autosave workspace' : 'Save workspace',
|
|
831
832
|
push: savePushesBranch(binding?.targetKind, operation.directoryName),
|
|
832
833
|
boundBranch: boundBranch(state, operation),
|
|
@@ -851,7 +852,7 @@ async function executeGitPush(state, scopeKey, operation, payload) {
|
|
|
851
852
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'remote_not_configurable' }));
|
|
852
853
|
}
|
|
853
854
|
try {
|
|
854
|
-
await pushWorkspaceGit(await
|
|
855
|
+
await pushWorkspaceGit(await gitWorkspaceRoot(state, operation), boundBranch(state, operation));
|
|
855
856
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
856
857
|
await recordNativeEvents(state, accepted, [
|
|
857
858
|
{
|
|
@@ -872,7 +873,7 @@ async function executeGitPull(state, scopeKey, operation, payload) {
|
|
|
872
873
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'remote_not_configurable' }));
|
|
873
874
|
}
|
|
874
875
|
try {
|
|
875
|
-
const pulled = await pullWorkspaceGit(await
|
|
876
|
+
const pulled = await pullWorkspaceGit(await gitWorkspaceRoot(state, operation), boundBranch(state, operation));
|
|
876
877
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
877
878
|
await recordNativeEvents(state, accepted, [{
|
|
878
879
|
type: 'git.pulled',
|
|
@@ -890,7 +891,7 @@ async function executeGitMergeAbort(state, scopeKey, operation, payload) {
|
|
|
890
891
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_git_merge_abort' }));
|
|
891
892
|
}
|
|
892
893
|
try {
|
|
893
|
-
await abortWorkspaceMerge(await
|
|
894
|
+
await abortWorkspaceMerge(await gitWorkspaceRoot(state, operation));
|
|
894
895
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
895
896
|
await recordNativeEvents(state, accepted, [{ type: 'git.merge.aborted', payload: {}, occurredAt: new Date().toISOString() }]);
|
|
896
897
|
return persist(state, scopeKey, transitionOperation(accepted, 'completed'));
|
|
@@ -912,7 +913,7 @@ async function executeGitIndex(state, scopeKey, operation, payload) {
|
|
|
912
913
|
return persist(state, scopeKey, transitionOperation(operation, 'failed', { error: 'invalid_git_paths' }));
|
|
913
914
|
}
|
|
914
915
|
try {
|
|
915
|
-
await change.run(await
|
|
916
|
+
await change.run(await gitWorkspaceRoot(state, operation), parsed.data);
|
|
916
917
|
const accepted = await persist(state, scopeKey, transitionOperation(operation, 'accepted'));
|
|
917
918
|
await recordNativeEvents(state, accepted, [
|
|
918
919
|
{ type: change.event, payload: { paths: parsed.data.paths }, occurredAt: new Date().toISOString() },
|
|
@@ -1043,8 +1044,16 @@ function boundBranch(state, operation) {
|
|
|
1043
1044
|
: binding.branchName ?? undefined;
|
|
1044
1045
|
}
|
|
1045
1046
|
function gitErrorCode(error) {
|
|
1047
|
+
// The code is all an operation records; Git's own message stays in the runtime log.
|
|
1048
|
+
console.warn('[workspace-runtime] git operation failed:', error instanceof Error ? error.message : error);
|
|
1046
1049
|
return error instanceof GitWorkspaceError ? error.code : 'git_command_failed';
|
|
1047
1050
|
}
|
|
1051
|
+
/** The workspace checkout, prepared so this process and the development user can both write it. */
|
|
1052
|
+
async function gitWorkspaceRoot(state, operation) {
|
|
1053
|
+
const root = await workspaceRoot(state, operation.workspaceId, operation.directoryName);
|
|
1054
|
+
await shareWorkspaceGit(state, root);
|
|
1055
|
+
return root;
|
|
1056
|
+
}
|
|
1048
1057
|
function parameterPatchError(error) {
|
|
1049
1058
|
if (error instanceof ParameterEditError)
|
|
1050
1059
|
return error.code;
|
|
@@ -515,6 +515,18 @@ export async function ensureWorkspaceGit(root) {
|
|
|
515
515
|
await excludeFromGit(rootReal, '/.tmp/user_uploads/');
|
|
516
516
|
return rootReal;
|
|
517
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* The control user and the development user (terminal, Codex) both write this repository. Git
|
|
520
|
+
* creates directories with the writer's umask, so a directory one user made can refuse the
|
|
521
|
+
* other's objects. With `core.sharedRepository=group` Git makes new directories group-writable for
|
|
522
|
+
* both; the image puts the control user in the development group.
|
|
523
|
+
*/
|
|
524
|
+
export async function shareWorkspaceRepository(root) {
|
|
525
|
+
const rootReal = await existingWorkspaceGit(root);
|
|
526
|
+
const current = await gitAllowFail(rootReal, ['config', '--get', 'core.sharedRepository']);
|
|
527
|
+
if (current.stdout.trim() !== 'group')
|
|
528
|
+
await git(rootReal, ['config', 'core.sharedRepository', 'group']);
|
|
529
|
+
}
|
|
518
530
|
/** Turn snapshots never create a repository; a directory without one simply has no turn changes. */
|
|
519
531
|
async function existingWorkspaceGit(root) {
|
|
520
532
|
const rootReal = await realpath(root);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { launcherSocket, runDeveloperCommand } from './developer-command.js';
|
|
4
|
+
import { shareWorkspaceRepository } from './git.js';
|
|
5
|
+
/**
|
|
6
|
+
* Directories under `.git` this process cannot write: another user created them without group
|
|
7
|
+
* write. Only their owner can change that, so they are reported rather than fixed here.
|
|
8
|
+
*/
|
|
9
|
+
export async function foreignUnwritableGitDirectories(root) {
|
|
10
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : undefined;
|
|
11
|
+
const found = [];
|
|
12
|
+
const visit = async (path) => {
|
|
13
|
+
const stat = await lstat(path).catch(() => undefined);
|
|
14
|
+
if (!stat?.isDirectory())
|
|
15
|
+
return;
|
|
16
|
+
if (stat.uid !== uid && (stat.mode & 0o020) === 0)
|
|
17
|
+
found.push(path);
|
|
18
|
+
for (const entry of await readdir(path).catch(() => []))
|
|
19
|
+
await visit(join(path, entry));
|
|
20
|
+
};
|
|
21
|
+
await visit(join(root, '.git'));
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Prepares a workspace repository for a Git write by this process: new directories become group
|
|
26
|
+
* writable, and directories the development user already made without group write are opened by
|
|
27
|
+
* that user through the launcher. Without a launcher (local runs) both users are the same.
|
|
28
|
+
*/
|
|
29
|
+
export async function shareWorkspaceGit(state, root) {
|
|
30
|
+
await shareWorkspaceRepository(root).catch(() => undefined);
|
|
31
|
+
if (!launcherSocket(state) || !(await foreignUnwritableGitDirectories(root)).length)
|
|
32
|
+
return;
|
|
33
|
+
await runDeveloperCommand(state, {
|
|
34
|
+
processId: `git-share-${Date.now().toString(36)}`,
|
|
35
|
+
cwd: root,
|
|
36
|
+
command: 'find .git -type d -uid "$(id -u)" ! -perm -g+w -exec chmod g+ws {} +',
|
|
37
|
+
timeoutMs: 30_000,
|
|
38
|
+
onOutput: () => undefined,
|
|
39
|
+
}).catch(() => undefined);
|
|
40
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TURN_FILES_CHANGED_EVENT, } from '@playcraft/common/cloud-workspace/runtime';
|
|
2
2
|
import { diffWorkspaceTrees, retainTurnSnapshots, snapshotWorkspaceTree } from './git.js';
|
|
3
|
+
import { shareWorkspaceGit } from './shared-git.js';
|
|
3
4
|
const TURN_ENDS = new Set(['turn/completed', 'turn.completed', 'turn.interrupted', 'turn.failed']);
|
|
4
5
|
/**
|
|
5
6
|
* Snapshots taken when a session's turn starts, until that turn ends. Kept in memory only: a
|
|
@@ -20,6 +21,7 @@ export async function beginTurnChanges(state, sessionId, input) {
|
|
|
20
21
|
turnsOf(state).delete(sessionId);
|
|
21
22
|
if (input.directoryName === 'root')
|
|
22
23
|
return;
|
|
24
|
+
await shareWorkspaceGit(state, input.root).catch(() => undefined);
|
|
23
25
|
const before = await snapshotWorkspaceTree(input.root).catch(() => undefined);
|
|
24
26
|
if (before)
|
|
25
27
|
turnsOf(state).set(sessionId, { ...input, before });
|
|
@@ -42,6 +44,7 @@ export async function finishTurnChanges(state, event) {
|
|
|
42
44
|
return [];
|
|
43
45
|
turnsOf(state).delete(session.sessionId);
|
|
44
46
|
try {
|
|
47
|
+
await shareWorkspaceGit(state, start.root);
|
|
45
48
|
const after = await snapshotWorkspaceTree(start.root);
|
|
46
49
|
const { files, truncated } = await diffWorkspaceTrees(start.root, start.before, after);
|
|
47
50
|
if (!files.length)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
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.34",
|
|
54
|
+
"@playcraft/common": "^0.1.34",
|
|
55
55
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
56
56
|
"chokidar": "^4.0.3",
|
|
57
57
|
"commander": "^13.1.0",
|