@playcraft/cli 0.1.4 → 0.1.5

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 { spawn } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { appendFile, lstat, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { appendFile, lstat, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
4
+ import { setTimeout as delay } from 'node:timers/promises';
4
5
  import { join } from 'node:path';
5
6
  import { startLauncherProcess } from '../codex/launcher-client.js';
6
7
  /** Long enough for a cold store on a game template; a stuck registry must not hold the write lease. */
@@ -8,6 +9,7 @@ const INSTALL_TIMEOUT_MS = 10 * 60_000;
8
9
  const LOG_TAIL_LINES = 50;
9
10
  const LOG_TAIL_MAX_CHARS = 8_000;
10
11
  const WRITE_PREFIX = 'dependencies-';
12
+ const SHARE_WAIT_MS = 60_000;
11
13
  /**
12
14
  * Projects are managed with pnpm. It follows pnpm-lock.yaml when the lockfile matches, never
13
15
  * prompts, and writes plain lines to the log. Package scripts run: the workspace is the user's own
@@ -102,7 +104,12 @@ async function installOnce(state, workspaceId, root) {
102
104
  });
103
105
  let outcome;
104
106
  try {
105
- outcome = await runInstall(state, workspaceId, root);
107
+ // Through the launcher the install runs as the development group, which cannot enter the
108
+ // checkout until Runtime has opened it; a local install runs as the owner and need not wait.
109
+ const shared = !launcherSocket(state) || (await waitUntilShared(root));
110
+ outcome = shared
111
+ ? await runInstall(state, workspaceId, root)
112
+ : { code: undefined, timedOut: false, log: '', error: 'workspace_not_shared' };
106
113
  }
107
114
  finally {
108
115
  await state.journal.putManagedWrite({
@@ -119,7 +126,10 @@ async function installOnce(state, workspaceId, root) {
119
126
  status: ok ? 'ready' : 'failed',
120
127
  ...(ok
121
128
  ? { lockHash: plan.lockHash }
122
- : { error: outcome.timedOut ? 'install_timeout' : `install_exit_${outcome.code ?? 'lost'}` }),
129
+ : {
130
+ error: outcome.error ??
131
+ (outcome.timedOut ? 'install_timeout' : `install_exit_${outcome.code ?? 'lost'}`),
132
+ }),
123
133
  startedAt,
124
134
  finishedAt: new Date().toISOString(),
125
135
  logTail: tail(outcome.log),
@@ -144,8 +154,8 @@ async function runInstall(state, workspaceId, root) {
144
154
  log = (log + text).slice(-64 * 1024);
145
155
  pending = pending.then(() => appendFile(logPath, text)).catch(() => undefined);
146
156
  };
147
- const socketPath = process.env.PLAYCRAFT_LAUNCHER_SOCKET;
148
- const launched = socketPath && state.config.workspaceMode === 'multi'
157
+ const socketPath = launcherSocket(state);
158
+ const launched = socketPath
149
159
  ? await startLauncherProcess({
150
160
  socketPath,
151
161
  processId: `${WRITE_PREFIX}${workspaceId}`,
@@ -196,6 +206,29 @@ async function runInstall(state, workspaceId, root) {
196
206
  await pending;
197
207
  return { code, timedOut, log };
198
208
  }
209
+ function launcherSocket(state) {
210
+ const socketPath = process.env.PLAYCRAFT_LAUNCHER_SOCKET;
211
+ return socketPath && state.config.workspaceMode === 'multi' ? socketPath : undefined;
212
+ }
213
+ /**
214
+ * The checkout is cloned private to the control user and opened to the development group by
215
+ * Runtime after registration. Group read, write and search on the root is the sign it is open.
216
+ */
217
+ export async function waitUntilShared(root, timeoutMs = SHARE_WAIT_MS, intervalMs = 500) {
218
+ const deadline = Date.now() + timeoutMs;
219
+ for (;;) {
220
+ try {
221
+ if (((await stat(root)).mode & 0o070) === 0o070)
222
+ return true;
223
+ }
224
+ catch {
225
+ /* not there yet */
226
+ }
227
+ if (Date.now() >= deadline)
228
+ return false;
229
+ await delay(intervalMs);
230
+ }
231
+ }
199
232
  /** A local-only ignore, never committed: the change list must not fill up with installed packages. */
200
233
  async function excludeFromGit(root) {
201
234
  const info = join(root, '.git', 'info');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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.4",
52
- "@playcraft/common": "^0.1.4",
51
+ "@playcraft/build": "^0.1.5",
52
+ "@playcraft/common": "^0.1.5",
53
53
  "better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
54
54
  "chokidar": "^4.0.3",
55
55
  "commander": "^13.1.0",