@playcraft/cli 0.1.57-beta → 0.1.58-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.
Files changed (82) hide show
  1. package/dist/commands/workspace.js +43 -0
  2. package/dist/workspace/messages.js +10 -0
  3. package/dist/workspace-runtime/batch/deliver.js +140 -80
  4. package/dist/workspace-runtime/batch/freeze.js +18 -6
  5. package/dist/workspace-runtime/batch/generate.js +68 -64
  6. package/dist/workspace-runtime/batch/prepare.js +108 -93
  7. package/dist/workspace-runtime/batch/records.js +4 -0
  8. package/dist/workspace-runtime/batch/routes.js +10 -2
  9. package/dist/workspace-runtime/batch/source-candidate.js +2 -0
  10. package/dist/workspace-runtime/batch/source-capture.js +1 -0
  11. package/dist/workspace-runtime/batch/source-failure.js +2 -0
  12. package/dist/workspace-runtime/batch/source-publish.js +3 -0
  13. package/dist/workspace-runtime/batch/writer-proof.js +37 -0
  14. package/dist/workspace-runtime/codex/app-server.js +1 -0
  15. package/dist/workspace-runtime/codex/config-toml.js +4 -0
  16. package/dist/workspace-runtime/codex/delivery-tools.js +1 -1
  17. package/dist/workspace-runtime/persistence/journal.js +34 -0
  18. package/dist/workspace-runtime/persistence/sqlite-worker.js +68 -6
  19. package/dist/workspace-runtime/previews/output.js +2 -0
  20. package/dist/workspace-runtime/previews/publish.js +20 -5
  21. package/dist/workspace-runtime/previews/runtime-preview.js +4 -0
  22. package/dist/workspace-runtime/processes/managed-writes.js +47 -5
  23. package/dist/workspace-runtime/serve.js +11 -2
  24. package/dist/workspace-runtime/server/accept-operation.js +22 -2
  25. package/dist/workspace-runtime/server/directory-access.js +414 -0
  26. package/dist/workspace-runtime/server/dispatch.js +61 -23
  27. package/dist/workspace-runtime/server/endpoints.js +1 -0
  28. package/dist/workspace-runtime/server/http.js +52 -10
  29. package/dist/workspace-runtime/server/operation-record.js +2 -0
  30. package/dist/workspace-runtime/server/terminal-host.js +43 -8
  31. package/dist/workspace-runtime/server/workspace-transfer.js +3 -1
  32. package/dist/workspace-runtime/workspaces/append-only-log.js +3 -0
  33. package/dist/workspace-runtime/workspaces/artifact-events.js +5 -2
  34. package/dist/workspace-runtime/workspaces/artifact-files.js +14 -8
  35. package/dist/workspace-runtime/workspaces/artifact-receipts.js +7 -25
  36. package/dist/workspace-runtime/workspaces/artifacts.js +4 -1
  37. package/dist/workspace-runtime/workspaces/asset-library-import.js +9 -4
  38. package/dist/workspace-runtime/workspaces/asset-publication-access.js +7 -9
  39. package/dist/workspace-runtime/workspaces/asset-transfer-publish.js +3 -0
  40. package/dist/workspace-runtime/workspaces/asset-transfer-storage.js +17 -2
  41. package/dist/workspace-runtime/workspaces/attachments.js +9 -0
  42. package/dist/workspace-runtime/workspaces/batch-commit.js +5 -3
  43. package/dist/workspace-runtime/workspaces/binary-write.js +12 -7
  44. package/dist/workspace-runtime/workspaces/cli-context.js +2 -0
  45. package/dist/workspace-runtime/workspaces/cli-credentials.js +1 -0
  46. package/dist/workspace-runtime/workspaces/delivery-build.js +30 -11
  47. package/dist/workspace-runtime/workspaces/delivery-store.js +2 -0
  48. package/dist/workspace-runtime/workspaces/dependencies.js +90 -70
  49. package/dist/workspace-runtime/workspaces/developer-command.js +12 -1
  50. package/dist/workspace-runtime/workspaces/directory-access.js +13 -18
  51. package/dist/workspace-runtime/workspaces/directory-attachments.js +8 -0
  52. package/dist/workspace-runtime/workspaces/directory-create.js +18 -5
  53. package/dist/workspace-runtime/workspaces/directory-maintenance.js +46 -35
  54. package/dist/workspace-runtime/workspaces/directory-mutations.js +8 -3
  55. package/dist/workspace-runtime/workspaces/files.js +4 -0
  56. package/dist/workspace-runtime/workspaces/git-credential-broker.js +3 -0
  57. package/dist/workspace-runtime/workspaces/git-direct-store.js +2 -0
  58. package/dist/workspace-runtime/workspaces/json5-edit.js +36 -0
  59. package/dist/workspace-runtime/workspaces/library-attachments.js +4 -0
  60. package/dist/workspace-runtime/workspaces/parameter-agent-protocol.js +2 -0
  61. package/dist/workspace-runtime/workspaces/parameter-agent-store.js +2 -0
  62. package/dist/workspace-runtime/workspaces/parameter-agent.js +40 -11
  63. package/dist/workspace-runtime/workspaces/parameter-assets.js +1 -0
  64. package/dist/workspace-runtime/workspaces/parameter-initialization-skill-transaction.js +141 -0
  65. package/dist/workspace-runtime/workspaces/parameter-initialization-skill.js +329 -0
  66. package/dist/workspace-runtime/workspaces/parameter-initialization.js +63 -0
  67. package/dist/workspace-runtime/workspaces/parameter-production.js +124 -0
  68. package/dist/workspace-runtime/workspaces/parameters.js +95 -3
  69. package/dist/workspace-runtime/workspaces/platform-export.js +5 -0
  70. package/dist/workspace-runtime/workspaces/prepare.js +4 -0
  71. package/dist/workspace-runtime/workspaces/registry.js +11 -0
  72. package/dist/workspace-runtime/workspaces/remix-references.js +5 -0
  73. package/dist/workspace-runtime/workspaces/shared-write.js +187 -0
  74. package/dist/workspace-runtime/workspaces/skills.js +14 -15
  75. package/dist/workspace-runtime/workspaces/transfer.js +4 -1
  76. package/dist/workspace-runtime/workspaces/workspace-shares.js +235 -0
  77. package/dist/workspace-runtime/workspaces/worktree-attachments.js +2 -0
  78. package/dist/workspace-runtime/workspaces/worktree-checkout.js +11 -6
  79. package/package.json +5 -4
  80. package/project-template/.claude/skills/playcraft-runtime-parameters/SKILL.md +32 -0
  81. package/project-template/.claude/skills/playcraft-runtime-parameters/references/platform-protocol.md +21 -0
  82. package/project-template/.claude/skills/playcraft-runtime-parameters/references/preview-integration.md +57 -0
@@ -0,0 +1,141 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ const ownerSchema = z.object({ kind: z.literal('playcraft.parameter-skill.v1'), pid: z.number().int().positive(), token: z.string().uuid() }).strict();
6
+ export const PARAMETER_SKILL_TRANSACTION = 'parameter-initialization-skill.transaction.json';
7
+ const transactionSchema = z.object({ version: z.literal(1), owner: ownerSchema,
8
+ templateHash: z.string().regex(/^[a-f0-9]{64}$/), artifactHash: z.string().regex(/^[a-f0-9]{64}$/), sourceAdded: z.boolean(),
9
+ originalDeclaration: z.string().nullable(), originalLock: z.string().nullable(),
10
+ nextDeclaration: z.string(), nextLock: z.string(),
11
+ }).strict();
12
+ export async function readParameterSkillTransactionFile(path, maximum = 8 * 1024 * 1024) {
13
+ try {
14
+ const stat = await lstat(path);
15
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > maximum)
16
+ throw new Error('workspace_skills_parameter_recovery_conflict');
17
+ return await readFile(path);
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return undefined;
22
+ throw error;
23
+ }
24
+ }
25
+ function parseOwner(bytes) {
26
+ try {
27
+ return ownerSchema.parse(JSON.parse(bytes.toString('utf8')));
28
+ }
29
+ catch {
30
+ throw Object.assign(new Error('workspace_skills_parameter_recovery_conflict'), { code: 'EEXIST' });
31
+ }
32
+ }
33
+ function alive(pid) {
34
+ try {
35
+ process.kill(pid, 0);
36
+ return true;
37
+ }
38
+ catch (error) {
39
+ return error.code !== 'ESRCH';
40
+ }
41
+ }
42
+ async function removeExact(path, expected) {
43
+ const current = await readParameterSkillTransactionFile(path);
44
+ if (current === undefined || !current.equals(expected))
45
+ throw new Error('workspace_skills_parameter_recovery_conflict');
46
+ await rm(path);
47
+ }
48
+ /** Each dead recovery owner has its own exclusive successor, avoiding an unchecked lease takeover. */
49
+ async function recoveryClaim(management, previous, claims, depth = 0) {
50
+ if (depth > 32)
51
+ throw new Error('workspace_skills_parameter_recovery_conflict');
52
+ const path = join(management, `parameter-initialization-${previous.token}.recovery.lock`);
53
+ const owner = { kind: 'playcraft.parameter-skill.v1', pid: process.pid, token: randomUUID() };
54
+ const bytes = Buffer.from(JSON.stringify(owner));
55
+ // private: lease identities and crash recovery records belong to the controller.
56
+ try {
57
+ await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
58
+ claims.push({ path, bytes });
59
+ }
60
+ catch (error) {
61
+ if (error.code !== 'EEXIST')
62
+ throw error;
63
+ const existing = await readParameterSkillTransactionFile(path, 4096);
64
+ if (!existing)
65
+ throw error;
66
+ const held = parseOwner(existing);
67
+ if (alive(held.pid))
68
+ throw error;
69
+ claims.push({ path, bytes: existing });
70
+ await recoveryClaim(management, held, claims, depth + 1);
71
+ }
72
+ }
73
+ export async function acquireParameterSkillLease(management, recover) {
74
+ const path = join(management, 'skills-write.lock');
75
+ const owner = { kind: 'playcraft.parameter-skill.v1', pid: process.pid, token: randomUUID() };
76
+ const bytes = Buffer.from(JSON.stringify(owner));
77
+ for (let attempt = 0; attempt < 2; attempt++) {
78
+ try {
79
+ // private
80
+ await writeFile(path, bytes, { flag: 'wx', mode: 0o600 });
81
+ return { owner, release: () => removeExact(path, bytes) };
82
+ }
83
+ catch (error) {
84
+ if (error.code !== 'EEXIST')
85
+ throw error;
86
+ const previousBytes = await readParameterSkillTransactionFile(path, 4096);
87
+ if (!previousBytes)
88
+ throw error;
89
+ const previous = parseOwner(previousBytes);
90
+ if (alive(previous.pid))
91
+ throw error;
92
+ const claims = [];
93
+ let claimed = false;
94
+ try {
95
+ await recoveryClaim(management, previous, claims);
96
+ claimed = true;
97
+ const current = await readParameterSkillTransactionFile(path, 4096);
98
+ if (!current?.equals(previousBytes) || alive(previous.pid))
99
+ throw new Error('workspace_skills_parameter_recovery_conflict');
100
+ const marker = join(management, PARAMETER_SKILL_TRANSACTION);
101
+ const transactionBytes = await readParameterSkillTransactionFile(marker);
102
+ if (transactionBytes) {
103
+ let transaction;
104
+ try {
105
+ transaction = transactionSchema.parse(JSON.parse(transactionBytes.toString('utf8')));
106
+ }
107
+ catch {
108
+ throw new Error('workspace_skills_parameter_recovery_conflict');
109
+ }
110
+ if (transaction.owner.pid !== previous.pid || transaction.owner.token !== previous.token)
111
+ throw new Error('workspace_skills_parameter_recovery_conflict');
112
+ await recover(transaction);
113
+ await removeExact(marker, transactionBytes);
114
+ }
115
+ await removeExact(path, previousBytes);
116
+ }
117
+ finally {
118
+ if (claimed)
119
+ for (const claim of claims.reverse())
120
+ await removeExact(claim.path, claim.bytes);
121
+ }
122
+ }
123
+ }
124
+ throw new Error('workspace_skills_parameter_recovery_conflict');
125
+ }
126
+ export async function writeParameterSkillTransaction(management, transaction) {
127
+ const path = join(management, PARAMETER_SKILL_TRANSACTION);
128
+ const bytes = Buffer.from(JSON.stringify(transactionSchema.parse(transaction)));
129
+ if (await readParameterSkillTransactionFile(path))
130
+ throw new Error('workspace_skills_parameter_recovery_conflict');
131
+ const temporary = `${path}.${transaction.owner.token}.tmp`;
132
+ try {
133
+ // private
134
+ await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
135
+ await rename(temporary, path);
136
+ }
137
+ finally {
138
+ await rm(temporary, { force: true });
139
+ }
140
+ return () => removeExact(path, bytes);
141
+ }
@@ -0,0 +1,329 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, readFile, readdir, rename, rm } from 'node:fs/promises';
4
+ import { dirname, join } from 'node:path';
5
+ import { canonicalProjectSkillsJson, createCanonicalBundleArchive, declarationDigest, dependencyDigest, PROJECT_SKILL_SOURCE_ROOTS, readProjectSkillSource, readProjectSkillsLockFile, validateProjectSkillsSnapshot, } from '@playcraft/common/project-skills';
6
+ import { resolveCliPackageRoot } from '../../scaffold/compose.js';
7
+ import { excludeFromGit } from './dependencies.js';
8
+ import { ensureSharedDirectory, openSharedFile, shareIfNeeded } from './shared-write.js';
9
+ import { acquireParameterSkillLease, readParameterSkillTransactionFile, writeParameterSkillTransaction } from './parameter-initialization-skill-transaction.js';
10
+ export const PARAMETER_INITIALIZATION_SKILL = 'playcraft-runtime-parameters';
11
+ const sourceRoot = `.agents/skills/${PARAMETER_INITIALIZATION_SKILL}`;
12
+ const skillRef = `builtin/${PARAMETER_INITIALIZATION_SKILL}`;
13
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
14
+ async function safeDirectory(root, path) {
15
+ let current = root;
16
+ for (const segment of path.split('/')) {
17
+ current = join(current, segment);
18
+ const stat = await lstat(current).catch((error) => {
19
+ if (error.code === 'ENOENT')
20
+ return undefined;
21
+ throw error;
22
+ });
23
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
24
+ throw new Error('workspace_skills_invalid');
25
+ }
26
+ await ensureSharedDirectory(root, current);
27
+ return current;
28
+ }
29
+ async function writeExclusiveShared(root, path, data) {
30
+ const handle = await openSharedFile(root, path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW);
31
+ try {
32
+ await handle.writeFile(data);
33
+ }
34
+ finally {
35
+ await handle.close();
36
+ }
37
+ }
38
+ async function optionalFile(path) {
39
+ try {
40
+ const stat = await lstat(path);
41
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 1024 * 1024)
42
+ throw new Error('workspace_skills_invalid');
43
+ return await readFile(path);
44
+ }
45
+ catch (error) {
46
+ if (error.code === 'ENOENT')
47
+ return undefined;
48
+ throw error;
49
+ }
50
+ }
51
+ async function installSource(root, files, token) {
52
+ const directory = join(root, sourceRoot);
53
+ const staging = await safeDirectory(root, `.playcraft/parameter-initialization-${token}`);
54
+ const temporary = join(staging, 'source');
55
+ await ensureSharedDirectory(root, temporary);
56
+ await safeDirectory(root, '.agents/skills');
57
+ try {
58
+ for (const file of files) {
59
+ if (dirname(file.path) !== '.')
60
+ await safeDirectory(temporary, dirname(file.path));
61
+ await writeExclusiveShared(root, join(temporary, file.path), file.content);
62
+ }
63
+ if (await lstat(directory).then(() => true, error => { if (error.code === 'ENOENT')
64
+ return false; throw error; }))
65
+ throw new Error('workspace_skills_parameter_conflict');
66
+ await rename(temporary, directory);
67
+ }
68
+ catch (error) {
69
+ await rm(staging, { recursive: true, force: true });
70
+ throw error;
71
+ }
72
+ await rm(staging, { recursive: true, force: true }).catch(() => undefined);
73
+ }
74
+ async function preserveArtifact(root, artifactHash, archive, token) {
75
+ const directory = await safeDirectory(root, '.playcraft/skill-artifacts');
76
+ const path = join(directory, `${artifactHash}.tar.gz`);
77
+ const existing = await readParameterSkillTransactionFile(path);
78
+ if (existing) {
79
+ if (!existing.equals(archive))
80
+ throw new Error('workspace_skills_digest_mismatch');
81
+ await shareIfNeeded(root, path);
82
+ return;
83
+ }
84
+ const temporary = `${path}.${token}.tmp`;
85
+ try {
86
+ await writeExclusiveShared(root, temporary, archive);
87
+ await rename(temporary, path);
88
+ }
89
+ finally {
90
+ await rm(temporary, { force: true });
91
+ }
92
+ }
93
+ function snapshot(originalDeclaration, originalLock) {
94
+ if ((originalDeclaration === undefined) !== (originalLock === null))
95
+ throw new Error('workspace_skills_invalid');
96
+ if (originalDeclaration !== undefined)
97
+ return validateProjectSkillsSnapshot(originalDeclaration.toString('utf8'), originalLock);
98
+ const declaration = { schemaVersion: 1, entries: [] };
99
+ return { declaration, lock: { lockVersion: 3, declarationHash: declarationDigest(declaration), entries: [] } };
100
+ }
101
+ function selectedSnapshot(previous, templateHash, artifactHash) {
102
+ const next = structuredClone(previous);
103
+ const matches = next.declaration.entries.filter(entry => entry.entryKey === PARAMETER_INITIALIZATION_SKILL
104
+ || entry.source.skillRef === skillRef || entry.source.kind === 'repository' && entry.source.root === sourceRoot);
105
+ if (matches.length > 1)
106
+ throw new Error('workspace_skills_parameter_conflict');
107
+ const existing = matches[0];
108
+ let selected;
109
+ if (existing) {
110
+ const locked = next.lock.entries.find(entry => entry.entryKey === existing.entryKey);
111
+ if (existing.source.kind !== 'repository' || existing.source.root !== sourceRoot
112
+ || existing.source.skillRef !== skillRef || existing.surface !== 'sandbox-agent' || existing.projectFiles?.length
113
+ || locked.dependencies.length || locked.revision.kind !== 'source' || locked.sourceTreeHash !== templateHash
114
+ || locked.revision.artifactHash !== artifactHash)
115
+ throw new Error('workspace_skills_parameter_conflict');
116
+ selected = existing;
117
+ selected.enabled = true;
118
+ }
119
+ else {
120
+ selected = { entryKey: PARAMETER_INITIALIZATION_SKILL, source: { kind: 'repository', root: sourceRoot, skillRef },
121
+ enabled: true, surface: 'sandbox-agent' };
122
+ const locked = { entryKey: selected.entryKey, sourceTreeHash: templateHash,
123
+ revision: { kind: 'source', skillRef, sourceRevision: `src-v1-sha256:${templateHash}`, artifactHash },
124
+ dependencies: [], dependencyLockHash: dependencyDigest([], []) };
125
+ next.declaration.entries.push(selected);
126
+ next.lock.entries.push(locked);
127
+ }
128
+ next.lock.declarationHash = declarationDigest(next.declaration);
129
+ validateProjectSkillsSnapshot(next.declaration, next.lock);
130
+ return { next, selected, sourceAdded: !existing };
131
+ }
132
+ async function cleanStaging(root, transaction, files, archive) {
133
+ const directory = join(root, '.playcraft', `parameter-initialization-${transaction.owner.token}`);
134
+ const expected = new Map(files.map(file => [`source/${file.path}`, Buffer.from(file.content)]));
135
+ const visit = async (path, relative = '') => {
136
+ const stat = await lstat(path);
137
+ if (stat.isSymbolicLink())
138
+ throw new Error('workspace_skills_parameter_recovery_conflict');
139
+ if (stat.isDirectory()) {
140
+ if (relative && ![...expected.keys()].some(file => file.startsWith(`${relative}/`)))
141
+ throw new Error('workspace_skills_parameter_recovery_conflict');
142
+ for (const name of await readdir(path))
143
+ await visit(join(path, name), relative ? `${relative}/${name}` : name);
144
+ }
145
+ else {
146
+ const bytes = await readParameterSkillTransactionFile(path);
147
+ const wanted = expected.get(relative);
148
+ if (!bytes || !wanted || !wanted.subarray(0, bytes.length).equals(bytes))
149
+ throw new Error('workspace_skills_parameter_recovery_conflict');
150
+ }
151
+ };
152
+ if (await lstat(directory).then(() => true, error => { if (error.code === 'ENOENT')
153
+ return false; throw error; })) {
154
+ await visit(directory);
155
+ await rm(directory, { recursive: true });
156
+ }
157
+ const temporaryFiles = [
158
+ ['skills.json', Buffer.from(transaction.nextDeclaration)], ['skills.lock.json', Buffer.from(transaction.nextLock)],
159
+ [`skill-artifacts/${transaction.artifactHash}.tar.gz`, archive],
160
+ ];
161
+ for (const [name, expectedBytes] of temporaryFiles) {
162
+ const path = join(root, '.playcraft', `${name}.${transaction.owner.token}.tmp`);
163
+ const bytes = await readParameterSkillTransactionFile(path);
164
+ if (bytes) {
165
+ if (!expectedBytes.subarray(0, bytes.length).equals(bytes))
166
+ throw new Error('workspace_skills_parameter_recovery_conflict');
167
+ await rm(path);
168
+ }
169
+ }
170
+ }
171
+ async function recoverTransaction(root, transaction, template, artifactHash, archive) {
172
+ if (transaction.templateHash !== template.sourceTreeHash || transaction.artifactHash !== artifactHash)
173
+ throw new Error('workspace_skills_parameter_recovery_conflict');
174
+ const originalDeclaration = transaction.originalDeclaration === null ? undefined : Buffer.from(transaction.originalDeclaration, 'base64');
175
+ if (originalDeclaration && originalDeclaration.toString('base64') !== transaction.originalDeclaration)
176
+ throw new Error('workspace_skills_parameter_recovery_conflict');
177
+ const previous = snapshot(originalDeclaration, transaction.originalLock);
178
+ const { next, selected, sourceAdded } = selectedSnapshot(previous, template.sourceTreeHash, artifactHash);
179
+ if (sourceAdded !== transaction.sourceAdded || `${canonicalProjectSkillsJson(next.declaration)}\n` !== transaction.nextDeclaration
180
+ || `${canonicalProjectSkillsJson(next.lock)}\n` !== transaction.nextLock)
181
+ throw new Error('workspace_skills_parameter_recovery_conflict');
182
+ const declarationPath = join(root, '.playcraft/skills.json'), lockPath = join(root, '.playcraft/skills.lock.json');
183
+ const declaration = await optionalFile(declarationPath), lock = await readProjectSkillsLockFile(root);
184
+ const oldDeclaration = declaration === undefined ? originalDeclaration === undefined : !!originalDeclaration?.equals(declaration);
185
+ const newDeclaration = declaration?.equals(Buffer.from(transaction.nextDeclaration));
186
+ const oldLock = lock === transaction.originalLock, newLock = lock === transaction.nextLock;
187
+ if (!(oldDeclaration && oldLock || newDeclaration && (oldLock || newLock)))
188
+ throw new Error('workspace_skills_parameter_recovery_conflict');
189
+ const sourceExists = await lstat(join(root, sourceRoot)).then(() => true, error => { if (error.code === 'ENOENT')
190
+ return false; throw error; });
191
+ if (!sourceExists) {
192
+ if (!sourceAdded || !oldDeclaration || !oldLock)
193
+ throw new Error('workspace_skills_parameter_recovery_conflict');
194
+ }
195
+ else {
196
+ if ((await readProjectSkillSource(root, selected)).sourceTreeHash !== template.sourceTreeHash)
197
+ throw new Error('workspace_skills_source_changed');
198
+ // The exact source and pair identify the interrupted import; finish only its recorded transition.
199
+ for (const [path, content] of [[declarationPath, transaction.nextDeclaration], [lockPath, transaction.nextLock]]) {
200
+ if ((await optionalFile(path))?.equals(Buffer.from(content)))
201
+ continue;
202
+ const temporary = `${path}.${randomUUID()}.tmp`;
203
+ try {
204
+ await writeExclusiveShared(root, temporary, content);
205
+ await rename(temporary, path);
206
+ }
207
+ finally {
208
+ await rm(temporary, { force: true });
209
+ }
210
+ }
211
+ }
212
+ await cleanStaging(root, transaction, template.files, archive);
213
+ }
214
+ /** A deliberate initialization selects one versioned project skill without synchronizing other sources. */
215
+ export async function ensureParameterInitializationSkill(root) {
216
+ const rootStat = await lstat(root);
217
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
218
+ throw new Error('workspace_skills_invalid');
219
+ const templateEntry = { entryKey: PARAMETER_INITIALIZATION_SKILL,
220
+ source: { kind: 'repository', root: `project-template/.claude/skills/${PARAMETER_INITIALIZATION_SKILL}`, skillRef },
221
+ enabled: true, surface: 'sandbox-agent' };
222
+ const template = await readProjectSkillSource(resolveCliPackageRoot(), templateEntry);
223
+ const archive = createCanonicalBundleArchive(template.files.map(file => ({ path: file.path, data: Buffer.from(file.content) })));
224
+ const artifactHash = digest(archive);
225
+ await excludeFromGit(root, '/.playcraft/skills/');
226
+ await excludeFromGit(root, '/.playcraft/skill-artifacts/');
227
+ await excludeFromGit(root, '/.playcraft/skills-write.lock');
228
+ await excludeFromGit(root, '/.playcraft/parameter-initialization-*');
229
+ const management = await safeDirectory(root, '.playcraft');
230
+ const lease = await acquireParameterSkillLease(management, transaction => recoverTransaction(root, transaction, template, artifactHash, archive));
231
+ const staged = [];
232
+ let ownedSource = false, committed = false, cleanupAllowed = true;
233
+ let removeTransaction;
234
+ try {
235
+ const declarationPath = join(management, 'skills.json'), lockPath = join(management, 'skills.lock.json');
236
+ const originalDeclaration = await optionalFile(declarationPath);
237
+ const originalLock = await readProjectSkillsLockFile(root);
238
+ const previous = snapshot(originalDeclaration, originalLock);
239
+ const { next, selected, sourceAdded } = selectedSnapshot(previous, template.sourceTreeHash, artifactHash);
240
+ if (!sourceAdded) {
241
+ if ((await readProjectSkillSource(root, selected)).sourceTreeHash !== template.sourceTreeHash)
242
+ throw new Error('workspace_skills_source_changed');
243
+ await safeDirectory(root, sourceRoot);
244
+ for (const file of template.files) {
245
+ if (dirname(file.path) !== '.')
246
+ await safeDirectory(root, `${sourceRoot}/${dirname(file.path)}`);
247
+ await shareIfNeeded(root, join(root, sourceRoot, file.path));
248
+ }
249
+ }
250
+ else {
251
+ for (const source of PROJECT_SKILL_SOURCE_ROOTS) {
252
+ const path = join(root, source, PARAMETER_INITIALIZATION_SKILL);
253
+ const exists = await lstat(path).then(() => true, error => {
254
+ if (error.code === 'ENOENT')
255
+ return false;
256
+ throw error;
257
+ });
258
+ if (exists)
259
+ throw new Error('workspace_skills_parameter_conflict');
260
+ }
261
+ }
262
+ if (canonicalProjectSkillsJson(previous) !== canonicalProjectSkillsJson(next)) {
263
+ removeTransaction = await writeParameterSkillTransaction(management, { version: 1, owner: lease.owner,
264
+ templateHash: template.sourceTreeHash, artifactHash, sourceAdded,
265
+ originalDeclaration: originalDeclaration?.toString('base64') ?? null, originalLock,
266
+ nextDeclaration: `${canonicalProjectSkillsJson(next.declaration)}\n`, nextLock: `${canonicalProjectSkillsJson(next.lock)}\n` });
267
+ if (sourceAdded) {
268
+ await installSource(root, template.files, lease.owner.token);
269
+ ownedSource = true;
270
+ }
271
+ await preserveArtifact(root, artifactHash, archive, lease.owner.token);
272
+ const pending = [];
273
+ for (const [path, value] of [[declarationPath, next.declaration], [lockPath, next.lock]]) {
274
+ const temporary = `${path}.${lease.owner.token}.tmp`;
275
+ staged.push(temporary);
276
+ await writeExclusiveShared(root, temporary, `${canonicalProjectSkillsJson(value)}\n`);
277
+ pending.push(temporary);
278
+ }
279
+ const currentDeclaration = await optionalFile(declarationPath);
280
+ if (currentDeclaration === undefined ? originalDeclaration !== undefined
281
+ : originalDeclaration === undefined || !currentDeclaration.equals(originalDeclaration))
282
+ throw new Error('skills_revision_conflict');
283
+ if (await readProjectSkillsLockFile(root) !== originalLock)
284
+ throw new Error('skills_revision_conflict');
285
+ await rename(pending[0], declarationPath);
286
+ try {
287
+ await rename(pending[1], lockPath);
288
+ }
289
+ catch (error) {
290
+ try {
291
+ if (originalDeclaration === undefined)
292
+ await rm(declarationPath, { force: true });
293
+ else {
294
+ const restore = `${declarationPath}.${randomUUID()}.tmp`;
295
+ staged.push(restore);
296
+ await writeExclusiveShared(root, restore, originalDeclaration);
297
+ await rename(restore, declarationPath);
298
+ }
299
+ }
300
+ catch (restoreError) {
301
+ cleanupAllowed = false;
302
+ throw restoreError;
303
+ }
304
+ throw error;
305
+ }
306
+ }
307
+ else
308
+ await preserveArtifact(root, artifactHash, archive, lease.owner.token);
309
+ committed = true;
310
+ await shareIfNeeded(root, declarationPath);
311
+ await shareIfNeeded(root, lockPath);
312
+ return { entryKey: selected.entryKey, lockHash: digest(canonicalProjectSkillsJson(next.lock)) };
313
+ }
314
+ finally {
315
+ if (cleanupAllowed && ownedSource && !committed) {
316
+ const entry = { entryKey: PARAMETER_INITIALIZATION_SKILL,
317
+ source: { kind: 'repository', root: sourceRoot }, enabled: true, surface: 'sandbox-agent' };
318
+ if ((await readProjectSkillSource(root, entry)).sourceTreeHash !== template.sourceTreeHash)
319
+ throw new Error('workspace_skills_source_changed');
320
+ await rm(join(root, sourceRoot), { recursive: true });
321
+ }
322
+ if (cleanupAllowed) {
323
+ for (const path of staged)
324
+ await rm(path, { force: true });
325
+ await removeTransaction?.();
326
+ await lease.release();
327
+ }
328
+ }
329
+ }
@@ -0,0 +1,63 @@
1
+ import JSON5 from 'json5';
2
+ import { workspaceRelativePathSchema } from '@playcraft/common/cloud-workspace/runtime';
3
+ import { PREVIEW_RUNTIME_DIRECTORY, readPreviewRuntimeDistribution } from '../../preview-runtime/distribution.js';
4
+ import { ParameterAgentError, parameterDigest, readParameterAgentFile } from './parameter-agent-inputs.js';
5
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
6
+ const path = (value) => workspaceRelativePathSchema.safeParse(value).success;
7
+ async function input(root, file) {
8
+ try {
9
+ return await readParameterAgentFile(root, file);
10
+ }
11
+ catch (error) {
12
+ if (error.code === 'ENOENT')
13
+ throw new ParameterAgentError('parameter_agent_preview_inputs_not_ready');
14
+ throw error;
15
+ }
16
+ }
17
+ /** Check admission inputs without building a preview, deleting outputs or declaring fields live. */
18
+ export async function inspectParameterInitialization(root, document, report) {
19
+ const manifestBytes = await input(root, '.playcraft/workspace.json');
20
+ let manifest;
21
+ try {
22
+ manifest = record(JSON5.parse(manifestBytes.toString('utf8')));
23
+ }
24
+ catch {
25
+ throw new ParameterAgentError('parameter_agent_preview_manifest_not_ready');
26
+ }
27
+ const parameters = record(manifest?.parameters);
28
+ const preview = record(manifest?.previewRuntime);
29
+ const allowed = new Set(['version', 'buildScript', 'outputPath', 'reportPath', 'configContractVersion', 'entryPath', 'viteConfigPath', 'mountId', 'gameFilePaths']);
30
+ if (!parameters || parameters.schemaPath !== document.schemaPath || parameters.dataPath !== document.dataPath ||
31
+ !document.platformI18nPath || !preview || Object.keys(preview).some(key => !allowed.has(key)) || preview.version !== 1 ||
32
+ typeof preview.buildScript !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:_-]{0,63}$/.test(preview.buildScript) ||
33
+ !path(preview.outputPath) || !preview.outputPath.startsWith('.playcraft-editor-preview/') || !preview.outputPath.endsWith('.html') ||
34
+ !path(preview.reportPath) || !preview.reportPath.startsWith('.playcraft-editor-preview/') || !preview.reportPath.endsWith('.json') ||
35
+ typeof preview.configContractVersion !== 'string' || !preview.configContractVersion.length || preview.configContractVersion.length > 128 ||
36
+ !path(preview.entryPath) || (preview.viteConfigPath !== undefined && !path(preview.viteConfigPath)) ||
37
+ (preview.mountId !== undefined && (typeof preview.mountId !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(preview.mountId))) ||
38
+ !Array.isArray(preview.gameFilePaths) || preview.gameFilePaths.length > 256 || !preview.gameFilePaths.every(path))
39
+ throw new ParameterAgentError('parameter_agent_preview_manifest_not_ready');
40
+ const files = new Set(preview.gameFilePaths);
41
+ if (!files.has(preview.entryPath) || report.candidates.some(candidate => candidate.status === 'implemented' && !candidate.consumers.some(consumer => files.has(consumer.path))))
42
+ throw new ParameterAgentError('parameter_agent_preview_inputs_not_ready');
43
+ const packageData = JSON.parse((await input(root, 'package.json')).toString('utf8'));
44
+ if (typeof packageData.scripts?.[preview.buildScript] !== 'string' || !packageData.scripts[preview.buildScript].trim())
45
+ throw new ParameterAgentError('parameter_agent_preview_script_not_ready');
46
+ const hashes = [['.playcraft/workspace.json', parameterDigest(manifestBytes)]];
47
+ for (const file of [...files, preview.entryPath, ...(preview.viteConfigPath ? [preview.viteConfigPath] : [])])
48
+ hashes.push([file, parameterDigest(await input(root, file))]);
49
+ const trusted = await readPreviewRuntimeDistribution();
50
+ const copiedManifest = await input(root, `${PREVIEW_RUNTIME_DIRECTORY}/manifest.json`);
51
+ if (!copiedManifest.equals(trusted.manifestBytes))
52
+ throw new ParameterAgentError('parameter_agent_preview_sdk_not_ready');
53
+ hashes.push([`${PREVIEW_RUNTIME_DIRECTORY}/manifest.json`, parameterDigest(copiedManifest)]);
54
+ for (const [name, expected] of Object.entries(trusted.manifest.files)) {
55
+ const file = `${PREVIEW_RUNTIME_DIRECTORY}/${name}`;
56
+ const bytes = await input(root, file);
57
+ const actual = parameterDigest(bytes);
58
+ if (actual !== expected)
59
+ throw new ParameterAgentError('parameter_agent_preview_sdk_not_ready');
60
+ hashes.push([file, actual]);
61
+ }
62
+ return parameterDigest(JSON.stringify(hashes));
63
+ }
@@ -0,0 +1,124 @@
1
+ import { lstat, readdir, realpath, unlink } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import JSON5 from 'json5';
4
+ import { z } from 'zod';
5
+ import { workspaceRelativePathSchema } from '@playcraft/common/cloud-workspace/runtime';
6
+ import { ParameterAgentError, parameterDigest, readParameterAgentFile } from './parameter-agent-inputs.js';
7
+ const reportPath = '.playcraft-editor-preview/release-report.json';
8
+ const marker = /runtime\.parameters\.|playcraft-preview-storage:|__playcraft_preview_context__/i;
9
+ const slash = (value) => value.replaceAll('\\', '/').replaceAll('\0', '').split('?')[0];
10
+ const relativePath = z.string().refine(value => workspaceRelativePathSchema.safeParse(value).success);
11
+ const reportSchema = z.object({
12
+ version: z.literal(1), target: z.literal('playable-release'), previewRuntimeExcluded: z.literal(true),
13
+ moduleIds: z.array(z.string().min(1).max(4096)).min(1).max(10000),
14
+ artifacts: z.array(z.object({ path: relativePath, sha256: z.string().regex(/^[a-f0-9]{64}$/) }).strict()).min(1).max(4096),
15
+ }).strict();
16
+ async function safePath(root, path, missing = false) {
17
+ let current = root;
18
+ const segments = path.split('/');
19
+ for (let index = 0; index < segments.length; index++) {
20
+ current = resolve(current, segments[index]);
21
+ const stat = await lstat(current).catch(error => {
22
+ if (missing && error.code === 'ENOENT')
23
+ return undefined;
24
+ throw error;
25
+ });
26
+ if (!stat)
27
+ continue;
28
+ if (stat.isSymbolicLink() || (index < segments.length - 1 && !stat.isDirectory()))
29
+ throw new ParameterAgentError('parameter_agent_production_path_not_ready');
30
+ }
31
+ return current;
32
+ }
33
+ /** Remove only the old fixed guard report; never follow its parents into source or delivery files. */
34
+ export async function prepareParameterProductionCheck(root) {
35
+ try {
36
+ const base = await realpath(root);
37
+ const manifest = JSON5.parse((await readParameterAgentFile(base, '.playcraft/workspace.json')).toString('utf8'));
38
+ const entry = manifest.previewRuntime?.entryPath;
39
+ if (!workspaceRelativePathSchema.safeParse(entry).success)
40
+ throw new ParameterAgentError('parameter_agent_production_entry_not_ready');
41
+ const file = await safePath(base, reportPath, true);
42
+ const stat = await lstat(file).catch(error => {
43
+ if (error.code === 'ENOENT')
44
+ return undefined;
45
+ throw error;
46
+ });
47
+ if (stat && (!stat.isFile() || stat.nlink !== 1))
48
+ throw new ParameterAgentError('parameter_agent_production_path_not_ready');
49
+ if (stat)
50
+ await unlink(file);
51
+ return { root: base, startedAt: Date.now(), previewEntry: slash(resolve(base, entry)), requestedPreviewEntry: slash(resolve(root, entry)) };
52
+ }
53
+ catch (error) {
54
+ if (error instanceof ParameterAgentError)
55
+ throw error;
56
+ throw new ParameterAgentError('parameter_agent_production_report_not_ready');
57
+ }
58
+ }
59
+ /** Inspect the reported module boundary and every actual delivery file, not a claimed success flag. */
60
+ export async function inspectParameterProductionCheck(plan, assertActive = () => { }) {
61
+ try {
62
+ assertActive();
63
+ const file = await safePath(plan.root, reportPath);
64
+ const stat = await lstat(file);
65
+ if (!stat.isFile() || stat.nlink !== 1 || stat.mtimeMs + 1 < plan.startedAt)
66
+ throw new ParameterAgentError('parameter_agent_production_report_not_ready');
67
+ const bytes = await readParameterAgentFile(plan.root, reportPath);
68
+ const parsed = reportSchema.safeParse(JSON.parse(bytes.toString('utf8')));
69
+ if (!parsed.success)
70
+ throw new ParameterAgentError('parameter_agent_production_report_not_ready');
71
+ const report = parsed.data;
72
+ const sdkRoot = slash(resolve(plan.root, '.playcraft/preview-runtime')) + '/';
73
+ for (const moduleId of report.moduleIds) {
74
+ const id = slash(moduleId);
75
+ if (id === plan.previewEntry || id === plan.requestedPreviewEntry || id.startsWith(sdkRoot) ||
76
+ /(?:^|\/)(?:@playcraft\/)?preview-runtime(?:\/|@|$)/.test(id) || /@playcraft\+preview-runtime@/.test(id))
77
+ throw new ParameterAgentError('parameter_agent_production_modules_not_ready');
78
+ }
79
+ const declared = new Map(report.artifacts.map(artifact => [artifact.path, artifact.sha256]));
80
+ if (declared.size !== report.artifacts.length || new Set(report.moduleIds).size !== report.moduleIds.length)
81
+ throw new ParameterAgentError('parameter_agent_production_report_not_ready');
82
+ const output = await safePath(plan.root, 'dist');
83
+ if (!(await lstat(output)).isDirectory())
84
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
85
+ let count = 0;
86
+ let total = 0;
87
+ let directories = 0;
88
+ const walk = async (directory, prefix = '', depth = 0) => {
89
+ assertActive();
90
+ if (depth > 24 || ++directories > 4096)
91
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
92
+ const entries = await readdir(directory, { withFileTypes: true });
93
+ if (entries.length > 4096)
94
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
95
+ for (const entry of entries) {
96
+ assertActive();
97
+ const relative = prefix + entry.name;
98
+ if (!workspaceRelativePathSchema.safeParse(relative).success || entry.isSymbolicLink())
99
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
100
+ if (entry.isDirectory())
101
+ await walk(resolve(directory, entry.name), relative + '/', depth + 1);
102
+ else if (entry.isFile()) {
103
+ if (++count > 4096)
104
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
105
+ const content = await readParameterAgentFile(plan.root, `dist/${relative}`, 50 * 1024 * 1024);
106
+ total += content.byteLength;
107
+ if (total > 128 * 1024 * 1024 || declared.get(relative) !== parameterDigest(content) || marker.test(content.toString()))
108
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
109
+ }
110
+ else
111
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
112
+ }
113
+ };
114
+ await walk(output);
115
+ if (count !== declared.size || parameterDigest(await readParameterAgentFile(plan.root, reportPath)) !== parameterDigest(bytes))
116
+ throw new ParameterAgentError('parameter_agent_production_outputs_not_ready');
117
+ return { reportHash: parameterDigest(bytes), artifactCount: count, moduleCount: report.moduleIds.length };
118
+ }
119
+ catch (error) {
120
+ if (error instanceof ParameterAgentError || (error instanceof Error && error.name === 'AbortError'))
121
+ throw error;
122
+ throw new ParameterAgentError('parameter_agent_production_report_not_ready');
123
+ }
124
+ }