@playcraft/cli 0.1.26 → 0.1.27
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.
|
@@ -292,6 +292,42 @@ export async function saveWorkspaceGit(root, input) {
|
|
|
292
292
|
await recordPushedHead(rootReal);
|
|
293
293
|
return { ...(commit ? { commit } : {}), pushed: true, withheld };
|
|
294
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Connects a standalone checkout to the repository it was just given, keeping all its history and
|
|
297
|
+
* uncommitted work. The new repository holds one empty commit on `branch`; it becomes the base of an
|
|
298
|
+
* unborn checkout, or is merged into existing history, so the next push is a plain fast-forward.
|
|
299
|
+
* Credentials come from the workspace's configured helper and never enter the remote URL.
|
|
300
|
+
* Safe to run again after a failure part way.
|
|
301
|
+
*/
|
|
302
|
+
export async function attachWorkspaceRemote(root, repositoryUrl, branch) {
|
|
303
|
+
const rootReal = await ensureWorkspaceGit(root);
|
|
304
|
+
const origin = await gitAllowFail(rootReal, ['remote', 'get-url', 'origin']);
|
|
305
|
+
if (origin.code !== 0)
|
|
306
|
+
await git(rootReal, ['remote', 'add', 'origin', repositoryUrl]);
|
|
307
|
+
else if (origin.stdout.trim() !== repositoryUrl)
|
|
308
|
+
throw new GitWorkspaceError('remote_not_configured', 'origin_mismatch');
|
|
309
|
+
const remoteRef = `refs/remotes/origin/${branch}`;
|
|
310
|
+
const fetched = await gitAllowFail(rootReal, ['fetch', '--no-tags', 'origin', `+refs/heads/${branch}:${remoteRef}`], 90000);
|
|
311
|
+
if (fetched.code !== 0)
|
|
312
|
+
throw new GitWorkspaceError(httpRefusal(fetched.stderr) ?? 'git_command_failed', 'git_fetch_failed');
|
|
313
|
+
const current = (await gitAllowFail(rootReal, ['symbolic-ref', '--quiet', '--short', 'HEAD'])).stdout.trim();
|
|
314
|
+
const born = (await gitAllowFail(rootReal, ['rev-parse', '-q', '--verify', 'HEAD^{commit}'])).code === 0;
|
|
315
|
+
if (current !== branch) {
|
|
316
|
+
await git(rootReal, born ? ['branch', '-M', branch] : ['symbolic-ref', 'HEAD', `refs/heads/${branch}`]);
|
|
317
|
+
}
|
|
318
|
+
if (!born) {
|
|
319
|
+
// Nothing committed yet: start the branch at the remote commit; the files stay as changes.
|
|
320
|
+
await git(rootReal, ['update-ref', `refs/heads/${branch}`, remoteRef]);
|
|
321
|
+
}
|
|
322
|
+
else if ((await gitAllowFail(rootReal, ['merge-base', '--is-ancestor', remoteRef, 'HEAD'])).code !== 0) {
|
|
323
|
+
const merged = await gitAllowFail(rootReal, [...(await commitIdentity(rootReal)), 'merge', '--allow-unrelated-histories', '--no-edit', remoteRef], 60000);
|
|
324
|
+
if (merged.code !== 0) {
|
|
325
|
+
await gitAllowFail(rootReal, ['merge', '--abort']);
|
|
326
|
+
throw new GitWorkspaceError('git_command_failed', merged.stderr.trim() || 'git_merge_failed');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
await git(rootReal, ['branch', `--set-upstream-to=origin/${branch}`, branch]);
|
|
330
|
+
}
|
|
295
331
|
async function unstage(rootReal, paths) {
|
|
296
332
|
if (paths.length === 0)
|
|
297
333
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { lstat, mkdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { lstat, mkdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { isLocalWorkspaceGitAuthMode, sameWorkspaceRegistration, workspaceRegistrationSchema, } from '@playcraft/common/cloud-workspace/runtime';
|
|
@@ -9,6 +9,7 @@ import { scheduleDependencyInstall } from './dependencies.js';
|
|
|
9
9
|
import { ensureWorkspaceSkills, readWorkspaceSkills } from './skills.js';
|
|
10
10
|
import { loadWorkspaceSkillArtifact, workspacePlatformSkillAccess } from './skill-artifact-loader.js';
|
|
11
11
|
import { writeWorkspaceCliContext } from './cli-context.js';
|
|
12
|
+
import { attachWorkspaceRemote } from './git.js';
|
|
12
13
|
export { WorkspaceContextError } from './context-error.js';
|
|
13
14
|
const exec = promisify(execFile);
|
|
14
15
|
export function revisionScope(state, workspaceId, directoryName = 'basic') {
|
|
@@ -106,8 +107,11 @@ export async function registerWorkspace(state, input, leaseFence, preparation, r
|
|
|
106
107
|
const existing = state.journal.getWorkspace(binding.workspaceId);
|
|
107
108
|
const preservedLocalBasic = binding.targetKind !== 'standalone' && binding.preserveLocalBasic === true;
|
|
108
109
|
const localUpgrade = preservedLocalBasic && binding.targetKind === 'main' && existing?.targetKind === 'standalone';
|
|
109
|
-
|
|
110
|
+
// A standalone workspace promoted to a project's main keeps its checkout and gains that remote.
|
|
111
|
+
const remoteUpgrade = !preservedLocalBasic && binding.targetKind === 'main' && existing?.targetKind === 'standalone';
|
|
112
|
+
if (existing && !sameWorkspaceRegistration(existing, binding) && !localUpgrade && !remoteUpgrade)
|
|
110
113
|
throw new WorkspaceContextError('workspace_binding_conflict');
|
|
114
|
+
const attachMarker = join(state.config.stateDir, 'workspace-remote-attach', `${binding.workspaceId}.json`);
|
|
111
115
|
const directories = binding.directories ?? existing?.directories;
|
|
112
116
|
if (directories?.some(directory => directory.directoryName === 'basic' && directory.status === 'reclaimed')) {
|
|
113
117
|
// Registration publishes trusted identity. Only an explicit basic ensure restores the checkout.
|
|
@@ -142,6 +146,28 @@ export async function registerWorkspace(state, input, leaseFence, preparation, r
|
|
|
142
146
|
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
143
147
|
},
|
|
144
148
|
})).stdout.trim();
|
|
149
|
+
// Unfinished if it failed part way: the marker outlives the journal entry written below.
|
|
150
|
+
if (binding.targetKind === 'main' && !preservedLocalBasic && (remoteUpgrade || await lstat(attachMarker).then(() => true, () => false))) {
|
|
151
|
+
if (preparation?.mode !== 'clone' || !state.gitCredentials)
|
|
152
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
153
|
+
await mkdir(join(state.config.stateDir, 'workspace-remote-attach'), { recursive: true, mode: 0o700 });
|
|
154
|
+
await writeFile(attachMarker, JSON.stringify({ repositoryUrl: binding.repositoryUrl, branchName: binding.branchName }), { mode: 0o600 });
|
|
155
|
+
// The credential helper only serves a workspace the journal knows to be bound.
|
|
156
|
+
await state.journal.putWorkspace({ ...binding, directories });
|
|
157
|
+
try {
|
|
158
|
+
await state.gitCredentials.configure(binding, preparation);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new WorkspaceContextError('workspace_git_gateway_unavailable');
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
await attachWorkspaceRemote(root, binding.repositoryUrl, binding.branchName);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new WorkspaceContextError('workspace_remote_attach_failed');
|
|
168
|
+
}
|
|
169
|
+
await rm(attachMarker, { force: true });
|
|
170
|
+
}
|
|
145
171
|
try {
|
|
146
172
|
let checkoutMatches;
|
|
147
173
|
if (binding.targetKind === 'standalone' || preservedLocalBasic) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
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.27",
|
|
54
|
+
"@playcraft/common": "^0.1.27",
|
|
55
55
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
56
56
|
"chokidar": "^4.0.3",
|
|
57
57
|
"commander": "^13.1.0",
|