@sublang/playbook 6.0.0 → 7.0.0
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/README.md +14 -10
- package/docs/cli.md +102 -57
- package/docs/configuration.md +57 -16
- package/package.json +3 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1171 -983
- package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +70 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +864 -68
- package/reference/sdlc/code.playbook/playbook-captain.ts +1296 -64
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +4 -14
- package/reference/sdlc/decide.playbook/decide.playbook.js +77 -13
- package/reference/sdlc/decide.playbook/decide.playbook.ts +93 -14
- package/slc/link.md +35 -12
- package/src/runtime.d.ts +14 -2
- package/src/runtime.ts +26 -6
- package/src/xstate-playbook-runtime.js +64 -14
- package/src/xstate-playbook-runtime.ts +79 -13
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +359 -57
- package/src/xstate-runtime.ts +491 -71
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// `@sublang/playbook/xstate-runtime`, which Node resolves by walking up
|
|
7
7
|
// from the artifact's own directory; a globally installed host therefore
|
|
8
8
|
// fails at artifact load in a bare directory. Before importing such a
|
|
9
|
-
// module, `playbook
|
|
10
|
-
// resolution parent and, only when a probe fails,
|
|
9
|
+
// module, both `playbook` front ends probe both specifiers with the module's
|
|
10
|
+
// path as resolution parent and, only when a probe fails, symlink the running
|
|
11
11
|
// host's own installed package roots beside the module. It never shells
|
|
12
12
|
// out to `npm link` and never installs from the registry.
|
|
13
13
|
|
|
@@ -133,18 +133,24 @@ function linkState(linkPath) {
|
|
|
133
133
|
return existsSync(linkPath) ? 'live' : 'dangling';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
export class EngineProvisioningError extends Error {
|
|
137
|
+
constructor(code, message) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.name = 'EngineProvisioningError';
|
|
140
|
+
this.code = code;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// PBCLI-36/37: host-neutral probe and provisioning core. It returns
|
|
145
|
+
// structured link notices and throws a coded error; CLI hosts own prefixes
|
|
146
|
+
// and streams, so the same preparation has no baked-in presentation.
|
|
140
147
|
export async function provisionEngine({
|
|
141
148
|
modulePath,
|
|
142
|
-
stderr,
|
|
143
149
|
enabled = true,
|
|
144
150
|
hostRoots,
|
|
145
151
|
}) {
|
|
146
152
|
const missing = missingEngineLinks(modulePath);
|
|
147
|
-
if (missing.length === 0) return {};
|
|
153
|
+
if (missing.length === 0) return { createdLinks: [] };
|
|
148
154
|
|
|
149
155
|
const moduleDir = dirname(modulePath);
|
|
150
156
|
if (!enabled) {
|
|
@@ -154,35 +160,27 @@ export async function provisionEngine({
|
|
|
154
160
|
for (const name of missing) {
|
|
155
161
|
const linkPath = join(moduleDir, 'node_modules', name);
|
|
156
162
|
if (linkState(linkPath) === 'dangling') {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
throw new EngineProvisioningError(
|
|
164
|
+
'stale-link',
|
|
165
|
+
`${linkPath} is a stale engine link to missing ` +
|
|
166
|
+
`${readlinkSync(linkPath)}; rerun without --no-provision to relink`,
|
|
160
167
|
);
|
|
161
|
-
return { code: 1 };
|
|
162
168
|
}
|
|
163
169
|
}
|
|
164
|
-
return {};
|
|
170
|
+
return { createdLinks: [] };
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
const manifestPath = declaringManifest(moduleDir);
|
|
168
174
|
if (manifestPath !== undefined) {
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
throw new EngineProvisioningError(
|
|
176
|
+
'declared-install-missing',
|
|
177
|
+
`${manifestPath} declares @sublang/playbook; ` +
|
|
171
178
|
'provisioning would shadow the project install — run the ' +
|
|
172
|
-
"project's dependency install (e.g. npm install) instead
|
|
179
|
+
"project's dependency install (e.g. npm install) instead",
|
|
173
180
|
);
|
|
174
|
-
return { code: 1 };
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
roots = hostRoots ?? defaultHostRoots();
|
|
180
|
-
} catch (error) {
|
|
181
|
-
stderr.write(
|
|
182
|
-
`playbook run: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
183
|
-
);
|
|
184
|
-
return { code: 1 };
|
|
185
|
-
}
|
|
183
|
+
const roots = hostRoots ?? defaultHostRoots();
|
|
186
184
|
|
|
187
185
|
// PBCLI-37: validate every destination before mutating any, so an
|
|
188
186
|
// occupied-path refusal leaves the module directory unchanged rather
|
|
@@ -194,11 +192,11 @@ export async function provisionEngine({
|
|
|
194
192
|
if (state === 'occupied' || state === 'live') {
|
|
195
193
|
// A live-but-unresolvable link is as foreign as a real directory:
|
|
196
194
|
// neither is a link this host may replace.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
throw new EngineProvisioningError(
|
|
196
|
+
'occupied-link',
|
|
197
|
+
`cannot provision ${linkPath}: the path is already ` +
|
|
198
|
+
`occupied${state === 'live' ? ' by a foreign symbolic link' : ''}`,
|
|
200
199
|
);
|
|
201
|
-
return { code: 1 };
|
|
202
200
|
}
|
|
203
201
|
plans.push({
|
|
204
202
|
linkPath,
|
|
@@ -207,22 +205,70 @@ export async function provisionEngine({
|
|
|
207
205
|
});
|
|
208
206
|
}
|
|
209
207
|
|
|
210
|
-
const created = [];
|
|
211
208
|
try {
|
|
212
209
|
for (const { linkPath, target, dangling } of plans) {
|
|
213
210
|
if (dangling) await unlink(linkPath);
|
|
214
211
|
await mkdir(dirname(linkPath), { recursive: true });
|
|
215
212
|
await symlink(target, linkPath, 'dir');
|
|
216
|
-
created.push(`${linkPath} -> ${target}`);
|
|
217
213
|
}
|
|
218
214
|
} catch (error) {
|
|
219
215
|
// PBCLI-37: a filesystem failure is a load fault, not a raw crash.
|
|
220
|
-
|
|
221
|
-
'
|
|
222
|
-
|
|
216
|
+
throw new EngineProvisioningError(
|
|
217
|
+
'filesystem',
|
|
218
|
+
'cannot provision engine links: ' +
|
|
219
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
223
220
|
);
|
|
224
|
-
return { code: 1 };
|
|
225
221
|
}
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
return {
|
|
223
|
+
createdLinks: plans.map(({ linkPath, target }) => ({
|
|
224
|
+
path: linkPath,
|
|
225
|
+
target,
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// PBCLI-36/46 (DR-024 as amended by DR-031): adapt the low-level
|
|
231
|
+
// probe/symlink operation to launch-config's prepare-or-throw contract. Both
|
|
232
|
+
// front ends install this hook, filesystem URLs are prepared before the
|
|
233
|
+
// catalog import transaction, and bare/custom specifiers stay untouched.
|
|
234
|
+
export function prepareConfiguredRegistries({
|
|
235
|
+
enabled = true,
|
|
236
|
+
stderr,
|
|
237
|
+
hostRoots,
|
|
238
|
+
commandName = 'playbook',
|
|
239
|
+
}) {
|
|
240
|
+
return async ({ from }) => {
|
|
241
|
+
if (!from.startsWith('file:')) return from;
|
|
242
|
+
const result = await provisionEngine({
|
|
243
|
+
modulePath: fileURLToPath(from),
|
|
244
|
+
enabled,
|
|
245
|
+
hostRoots,
|
|
246
|
+
});
|
|
247
|
+
if (result.createdLinks.length > 0) {
|
|
248
|
+
await writeStream(
|
|
249
|
+
stderr,
|
|
250
|
+
`${commandName}: provisioned ${result.createdLinks
|
|
251
|
+
.map(({ path, target }) => `${path} -> ${target}`)
|
|
252
|
+
.join(', ')}\n`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return from;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function writeStream(stream, text) {
|
|
260
|
+
const ready = stream.write(text);
|
|
261
|
+
if (ready !== false || typeof stream.once !== 'function') return;
|
|
262
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
263
|
+
const onDrain = () => {
|
|
264
|
+
stream.off?.('error', onError);
|
|
265
|
+
resolvePromise();
|
|
266
|
+
};
|
|
267
|
+
const onError = (error) => {
|
|
268
|
+
stream.off?.('drain', onDrain);
|
|
269
|
+
rejectPromise(error);
|
|
270
|
+
};
|
|
271
|
+
stream.once('drain', onDrain);
|
|
272
|
+
stream.once('error', onError);
|
|
273
|
+
});
|
|
228
274
|
}
|