@playcraft/cli 0.1.58-beta → 0.1.60-beta
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/cli-root-help.js +1 -0
- package/dist/commands/references.js +55 -0
- package/dist/index.js +21 -3
- package/dist/references/client.js +69 -0
- package/dist/references/messages.js +57 -0
- package/dist/references/protocol.js +32 -0
- package/dist/utils/agent-generation-provenance.js +1 -1
- package/dist/workspace-runtime/batch/control.js +3 -3
- package/dist/workspace-runtime/batch/deliver.js +4 -3
- package/dist/workspace-runtime/batch/generate.js +4 -3
- package/dist/workspace-runtime/batch/install-log.js +49 -0
- package/dist/workspace-runtime/batch/policy.js +71 -22
- package/dist/workspace-runtime/batch/prepare.js +29 -13
- package/dist/workspace-runtime/batch/processes.js +86 -14
- package/dist/workspace-runtime/batch/writer-proof.js +2 -2
- package/dist/workspace-runtime/codex/app-server.js +305 -21
- package/dist/workspace-runtime/codex/generation-provenance-broker.js +17 -8
- package/dist/workspace-runtime/codex/launcher-client.js +34 -24
- package/dist/workspace-runtime/codex/loopback.js +2 -0
- package/dist/workspace-runtime/codex/model-broker.js +48 -8
- package/dist/workspace-runtime/codex/remix-references-broker.js +184 -0
- package/dist/workspace-runtime/codex/scheduled-task-tools.js +80 -0
- package/dist/workspace-runtime/serve.js +4 -1
- package/dist/workspace-runtime/server/directory-access.js +1 -1
- package/dist/workspace-runtime/server/dispatch.js +14 -6
- package/dist/workspace-runtime/server/http.js +43 -2
- package/dist/workspace-runtime/server/terminal-host.js +82 -1
- package/dist/workspace-runtime/server/workspace-storage.js +193 -0
- package/dist/workspace-runtime/workspaces/empty-template.js +2 -0
- package/dist/workspace-runtime/workspaces/registry.js +44 -3
- package/dist/workspace-runtime/workspaces/remix-reference-skill.js +126 -0
- package/dist/workspace-runtime/workspaces/scheduled-task-proposals.js +27 -0
- package/dist/workspace-runtime/workspaces/skills.js +3 -1
- package/dist/workspace-runtime/workspaces/storage-snapshot.js +235 -0
- package/package.json +3 -3
- package/project-template/.claude/skills/playcraft-remix-references/SKILL.md +45 -0
- package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +0 -2
- package/project-template-v2/.claude/skills/playcraft-remix-references/SKILL.md +45 -0
- package/project-template-v2/.playcraft/skills.lock.json +7 -0
- package/project-templates/empty/AGENTS.md +9 -0
- package/project-templates/phaser/.claude/skills/playcraft-remix-references/SKILL.md +45 -0
- package/project-templates/threejs/.claude/skills/playcraft-remix-references/SKILL.md +45 -0
|
@@ -2,6 +2,8 @@ import { execFile, spawn } from 'node:child_process';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { startLauncherProcess, stopLauncherProcess } from '../codex/launcher-client.js';
|
|
6
|
+
import { launcherSocket } from '../workspaces/developer-command.js';
|
|
5
7
|
export async function processBirth(pid) {
|
|
6
8
|
if (process.platform !== 'linux') {
|
|
7
9
|
return promisify(execFile)('ps', ['-p', String(pid), '-o', 'lstart='], { timeout: 2000 }).then(value => value.stdout.trim() || undefined, () => undefined);
|
|
@@ -23,12 +25,27 @@ export function groupExited(pid) {
|
|
|
23
25
|
return error.code === 'ESRCH';
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
/** A missing local process cannot replace the launcher's proof for a development-user group. */
|
|
29
|
+
export function processHasExited(record) {
|
|
30
|
+
return record.stopped || (!record.launcher && groupExited(record.pid));
|
|
31
|
+
}
|
|
26
32
|
/**
|
|
27
33
|
* Stops one run's process group. A mismatched birth means the pid was reused, which is not proof
|
|
28
34
|
* that the original writers exited, so the caller must not report writersStopped.
|
|
29
35
|
*/
|
|
30
36
|
export async function stopProcessGroup(record) {
|
|
31
|
-
if (record.stopped
|
|
37
|
+
if (record.stopped)
|
|
38
|
+
return true;
|
|
39
|
+
if (record.launcher) {
|
|
40
|
+
try {
|
|
41
|
+
await stopLauncherProcess(record.launcher);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (groupExited(record.pid))
|
|
32
49
|
return true;
|
|
33
50
|
if (record.birth && await processBirth(record.pid) !== record.birth)
|
|
34
51
|
return false;
|
|
@@ -46,27 +63,82 @@ export async function stopProcessGroup(record) {
|
|
|
46
63
|
}
|
|
47
64
|
return groupExited(record.pid);
|
|
48
65
|
}
|
|
49
|
-
export async function runProcessGroup(cwd, command, role, onStart) {
|
|
50
|
-
const
|
|
66
|
+
export async function runProcessGroup(cwd, command, role, onStart, options) {
|
|
67
|
+
const socketPath = options && launcherSocket(options.state);
|
|
68
|
+
if (options && options.state.config.workspaceMode === 'multi' &&
|
|
69
|
+
options.state.config.environment !== 'local' && !socketPath)
|
|
70
|
+
throw new Error('development_launcher_unavailable');
|
|
71
|
+
let launched;
|
|
72
|
+
let tracked;
|
|
73
|
+
if (socketPath) {
|
|
74
|
+
// Persist uncertainty before sending start: a lost acknowledgement is not a rejected launch.
|
|
75
|
+
tracked = { pid: 0, role, stopped: false, launcher: {
|
|
76
|
+
socketPath, processId: options.processId, runtimeEpoch: options.state.runtimeEpoch, instanceId: '',
|
|
77
|
+
} };
|
|
78
|
+
await onStart(tracked);
|
|
79
|
+
try {
|
|
80
|
+
launched = await startLauncherProcess({
|
|
81
|
+
socketPath, processId: options.processId, runtimeEpoch: options.state.runtimeEpoch,
|
|
82
|
+
argv: ['/usr/bin/bash', '-lc', `{ ${command}; } </dev/null 2>&1`], cwd, env: {},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.launchRejected) {
|
|
87
|
+
tracked.stopped = true;
|
|
88
|
+
await onStart(tracked);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
launched?.stdin?.end();
|
|
94
|
+
const child = launched ? undefined : spawn('bash', ['-lc', command], {
|
|
51
95
|
cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
96
|
env: { PATH: process.env.PATH, HOME: cwd, GIT_TERMINAL_PROMPT: '0' },
|
|
53
97
|
});
|
|
54
|
-
const pid = child
|
|
98
|
+
const pid = launched?.pid ?? child?.pid;
|
|
55
99
|
if (!pid)
|
|
56
100
|
throw new Error('batch_process_not_started');
|
|
57
101
|
let output = '';
|
|
58
|
-
const collect = (chunk) => {
|
|
59
|
-
|
|
60
|
-
|
|
102
|
+
const collect = (chunk) => {
|
|
103
|
+
output = (output + String(chunk)).slice(-64 * 1024);
|
|
104
|
+
options?.onOutput?.(chunk);
|
|
105
|
+
};
|
|
106
|
+
(launched?.stdout ?? child?.stdout)?.on('data', collect);
|
|
107
|
+
(launched?.stderr ?? child?.stderr)?.on('data', collect);
|
|
61
108
|
// Subscribe before any await. A short command can exit during processBirth or onStart,
|
|
62
109
|
// and a late listener never sees that close event.
|
|
63
110
|
const finished = new Promise((resolve) => {
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
if (launched)
|
|
112
|
+
launched.onExit(code => resolve(code ?? null));
|
|
113
|
+
else {
|
|
114
|
+
child.once('error', () => resolve(null));
|
|
115
|
+
child.once('close', resolve);
|
|
116
|
+
}
|
|
66
117
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
118
|
+
tracked = { pid, birth: await processBirth(pid), stopped: false, role,
|
|
119
|
+
...(launched ? { launcher: launched.identity } : {}) };
|
|
120
|
+
try {
|
|
121
|
+
await onStart(tracked);
|
|
122
|
+
const code = await finished;
|
|
123
|
+
if (launched) {
|
|
124
|
+
try {
|
|
125
|
+
await launched.stop();
|
|
126
|
+
tracked.stopped = true;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
collect(`\n[install] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
130
|
+
// Keep the directory occupied until the launcher confirms stop.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else
|
|
134
|
+
tracked.stopped = groupExited(pid);
|
|
135
|
+
return { code, output, process: tracked };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (launched)
|
|
139
|
+
await launched.stop().catch(() => undefined);
|
|
140
|
+
else
|
|
141
|
+
await stopProcessGroup(tracked);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
72
144
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { processHasExited, stopProcessGroup } from './processes.js';
|
|
3
3
|
/** An interrupt acknowledgement is not proof that the native turn and its writers have stopped. */
|
|
4
4
|
export async function stopMemberWriters(state, record) {
|
|
5
5
|
if (!record)
|
|
6
6
|
return true;
|
|
7
7
|
let stopped = true;
|
|
8
8
|
for (const process of record.processes) {
|
|
9
|
-
if (
|
|
9
|
+
if (processHasExited(process))
|
|
10
10
|
process.stopped = true;
|
|
11
11
|
else
|
|
12
12
|
process.stopped = await stopProcessGroup(process);
|