@north-light/crouter 0.3.216 → 0.3.218
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/api/client.d.ts +5 -2
- package/dist/api/client.js +7 -1
- package/dist/api/dto/canvas.d.ts +9 -0
- package/dist/api/dto/nodes.d.ts +27 -1
- package/dist/builtin-memory/04-base-worker.md +7 -1
- package/dist/builtin-memory/internal/plugins.md +67 -0
- package/dist/builtin-memory/memory-read-orientation.md +13 -0
- package/dist/clients/attach/chrome/bash-jobs.js +1 -1
- package/dist/clients/attach/photon_rs_bg.wasm +0 -0
- package/dist/clients/attach/viewer.js +516 -516
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
- package/dist/clients/inbox/tui/input.js +12 -8
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +9 -3
- package/dist/commands/human/shared.js +1 -1
- package/dist/commands/memory/edit.js +6 -1
- package/dist/commands/memory/lint.d.ts +1 -6
- package/dist/commands/memory/lint.js +18 -126
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +17 -2
- package/dist/commands/memory/read.js +15 -6
- package/dist/commands/memory/shared.d.ts +33 -3
- package/dist/commands/memory/shared.js +180 -1
- package/dist/commands/memory/write.js +6 -1
- package/dist/commands/pkg/browse/catalog.js +2 -0
- package/dist/commands/pkg/browse/model.d.ts +4 -1
- package/dist/commands/pkg/plugin-manage.js +209 -148
- package/dist/core/__tests__/human-cancel-guard.test.js +30 -12
- package/dist/core/bash-jobs.d.ts +2 -5
- package/dist/core/bash-jobs.js +4 -8
- package/dist/core/command-plugins/bundle.d.ts +4 -1
- package/dist/core/command-plugins/bundle.js +16 -2
- package/dist/core/human/__tests__/page.test.js +12 -0
- package/dist/core/human/component-docs.js +6 -1
- package/dist/core/human/page-schema.d.ts +14 -0
- package/dist/core/human/page-schema.js +24 -7
- package/dist/core/human/page.d.ts +2 -0
- package/dist/core/human/page.js +17 -6
- package/dist/core/human/scan.d.ts +5 -4
- package/dist/core/human/scan.js +7 -4
- package/dist/core/human/tickets.js +9 -2
- package/dist/core/io.js +3 -2
- package/dist/core/manifest.d.ts +2 -0
- package/dist/core/manifest.js +5 -0
- package/dist/core/memory/extensions.d.ts +30 -0
- package/dist/core/memory/extensions.js +219 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +25 -0
- package/dist/core/substrate/frontmatter-validation.d.ts +13 -0
- package/dist/core/substrate/frontmatter-validation.js +101 -0
- package/dist/core/substrate/index.d.ts +1 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.d.ts +1 -0
- package/dist/daemon/api/__tests__/nodes-activity-query.test.js +101 -0
- package/dist/daemon/api/handlers/canvas.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +4 -3
- package/dist/daemon/api/handlers/nodes.d.ts +1 -0
- package/dist/daemon/api/handlers/nodes.js +101 -19
- package/dist/daemon/api/handlers/reports.d.ts +4 -0
- package/dist/daemon/api/handlers/reports.js +14 -8
- package/dist/daemon/crtrd.js +7 -5
- package/dist/daemon/manage.d.ts +3 -0
- package/dist/daemon/manage.js +14 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +4 -3
- package/dist/pi-extensions/canvas-bash-valve.js +19 -6
- package/dist/pi-extensions/canvas-preview-result.d.ts +0 -8
- package/dist/pi-extensions/canvas-preview-result.js +9 -23
- package/dist/types.d.ts +31 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
1
|
+
import { dirname, join, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
2
2
|
import { renameSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { defineLeaf } from '../../core/command.js';
|
|
@@ -7,9 +7,12 @@ import { findMarketplaceByName, findPluginByName, listAllPlugins } from '../../c
|
|
|
7
7
|
import { pluginsDir, ensureProjectScopeRoot, userScopeRoot, resolveScopeArg, projectScopeRoot, requireScopeRoot, } from '../../core/scope.js';
|
|
8
8
|
import { updateConfig, updateState, ensureScopeInitialized, invalidPluginKindsReasons, invalidPluginBinReasons, invalidPluginRequiresReasons, normalizeRequires } from '../../core/config.js';
|
|
9
9
|
import { invalidPageComponentsReasons } from '../../core/human/page-catalog.js';
|
|
10
|
-
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink, atomicWriteJson } from '../../core/fs-utils.js';
|
|
11
|
-
import { clone,
|
|
12
|
-
import { readMarketplaceManifest, readPluginManifest } from '../../core/manifest.js';
|
|
10
|
+
import { pathExists, ensureDir, removePath, nowIso, linkOrCopy, isSymlink, readSymlinkTarget, atomicWriteJson, readText, walkFiles } from '../../core/fs-utils.js';
|
|
11
|
+
import { clone, deriveNameFromUrl, currentSha, gitSync, isGitRepo } from '../../core/git.js';
|
|
12
|
+
import { readMarketplaceManifest, readPluginManifest, validatePluginManifestMemoryExtensions } from '../../core/manifest.js';
|
|
13
|
+
import { memoryExtensionValidationCatalog, validateMemoryExtensionValues } from '../../core/memory/extensions.js';
|
|
14
|
+
import { lintSubstrateFrontmatter } from '../../core/substrate/frontmatter-validation.js';
|
|
15
|
+
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
13
16
|
import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../core/command-plugins/discovery.js';
|
|
14
17
|
import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
|
|
15
18
|
import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
|
|
@@ -118,6 +121,39 @@ function assertManifestPageComponentsValid(manifest, where) {
|
|
|
118
121
|
throw general(`plugin manifest at ${where} declares an invalid page_components block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
|
|
119
122
|
}
|
|
120
123
|
}
|
|
124
|
+
/** Validate a package candidate before it gains an active plugin path. The
|
|
125
|
+
* candidate namespace shadows any installed declaration of the same name. */
|
|
126
|
+
function assertPluginCandidateValid(manifest, root, scope, scopeRootPath, where) {
|
|
127
|
+
try {
|
|
128
|
+
validatePluginManifestMemoryExtensions(manifest);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw general(`plugin manifest at ${where} declares invalid memory_extensions: ${detail}`);
|
|
133
|
+
}
|
|
134
|
+
assertManifestKindsValid(manifest, where);
|
|
135
|
+
assertManifestPageComponentsValid(manifest, where);
|
|
136
|
+
assertManifestBinValid(manifest, where);
|
|
137
|
+
assertManifestRequiresValid(manifest, where);
|
|
138
|
+
const memoryDir = join(root, 'memory');
|
|
139
|
+
const catalogDocPath = join(scopeRootPath, 'memory', '__plugin_candidate__.md');
|
|
140
|
+
for (const file of walkFiles(memoryDir, (name) => name.endsWith('.md'), (dir) => dir.startsWith('.'))) {
|
|
141
|
+
let frontmatter;
|
|
142
|
+
try {
|
|
143
|
+
frontmatter = parseFrontmatterGeneric(readText(file)).data;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw general(`plugin memory document ${relative(root, file)} has invalid YAML frontmatter: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`);
|
|
147
|
+
}
|
|
148
|
+
const substrateError = lintSubstrateFrontmatter(frontmatter);
|
|
149
|
+
if (substrateError !== null) {
|
|
150
|
+
throw general(`plugin memory document ${relative(root, file)} is invalid: ${substrateError}`);
|
|
151
|
+
}
|
|
152
|
+
for (const issue of validateMemoryExtensionValues(frontmatter?.['extensions'], memoryExtensionValidationCatalog({ scope, path: catalogDocPath }, { candidate: manifest }))) {
|
|
153
|
+
throw general(`plugin memory document ${relative(root, file)} is invalid: ${issue.path}: ${issue.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
121
157
|
function invocationTransport(bundle) {
|
|
122
158
|
return { kind: 'http', endpoint: new URL(bundle.endpoint).origin, ...(bundle.authEnv !== undefined ? { authEnv: bundle.authEnv } : {}) };
|
|
123
159
|
}
|
|
@@ -139,7 +175,8 @@ function writeStagedBundle(stagingRoot, manifest, bundle) {
|
|
|
139
175
|
writeFileSync(target, document.bytes);
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
|
-
async function validateStagedBundle(name, scope, root, manifest) {
|
|
178
|
+
async function validateStagedBundle(name, scope, root, scopeRootPath, manifest) {
|
|
179
|
+
assertPluginCandidateValid(manifest, root, scope, scopeRootPath, `${root}/.crouter-plugin/plugin.json`);
|
|
143
180
|
const staged = { name, scope, root, manifest, enabled: true };
|
|
144
181
|
const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES), await coreCommandPaths());
|
|
145
182
|
if (validation.issues.length > 0) {
|
|
@@ -198,6 +235,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
198
235
|
// copy is what makes the components authorable — and dropping it on a
|
|
199
236
|
// later archive that no longer declares them is what retires them.
|
|
200
237
|
...(validated.bundle.page_components !== undefined ? { page_components: validated.bundle.page_components } : {}),
|
|
238
|
+
...(validated.bundle.memory_extensions !== undefined ? { memory_extensions: validated.bundle.memory_extensions } : {}),
|
|
201
239
|
};
|
|
202
240
|
const tmpRoot = join(scopeRootPath, 'tmp');
|
|
203
241
|
const staging = join(tmpRoot, `${name}.${process.pid}`);
|
|
@@ -210,7 +248,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
210
248
|
try {
|
|
211
249
|
ensureDir(staging);
|
|
212
250
|
writeStagedBundle(staging, manifest, validated.bundle);
|
|
213
|
-
await validateStagedBundle(name, scope, staging, manifest);
|
|
251
|
+
await validateStagedBundle(name, scope, staging, scopeRootPath, manifest);
|
|
214
252
|
let movedPrevious = false;
|
|
215
253
|
if (pathExists(root)) {
|
|
216
254
|
renameSync(root, previous);
|
|
@@ -265,21 +303,6 @@ function classifyInstallRef(raw) {
|
|
|
265
303
|
function isMarketplaceRelativeSource(source) {
|
|
266
304
|
return source.startsWith('./') || source.startsWith('../') || (!isAbsolute(source) && !isGitUrl(source));
|
|
267
305
|
}
|
|
268
|
-
function pullGitCheckout(root, label, opts) {
|
|
269
|
-
const shaBefore = currentSha(root);
|
|
270
|
-
if (!isGitRepo(root)) {
|
|
271
|
-
return { updated: false, sha: shaBefore ?? '' };
|
|
272
|
-
}
|
|
273
|
-
const res = pull(root);
|
|
274
|
-
if (res.status !== 0) {
|
|
275
|
-
if (opts.strict) {
|
|
276
|
-
throw general(`git pull failed for ${label}: ${res.stderr.trim()}`);
|
|
277
|
-
}
|
|
278
|
-
return { updated: false, sha: shaBefore ?? '' };
|
|
279
|
-
}
|
|
280
|
-
const shaAfter = currentSha(root);
|
|
281
|
-
return { updated: shaBefore !== shaAfter, sha: shaAfter ?? '' };
|
|
282
|
-
}
|
|
283
306
|
function refreshMarketplace(name, cache, opts) {
|
|
284
307
|
const cached = cache.get(name);
|
|
285
308
|
if (cached !== undefined)
|
|
@@ -290,28 +313,70 @@ function refreshMarketplace(name, cache, opts) {
|
|
|
290
313
|
throw notFound(`marketplace not found: ${name}`);
|
|
291
314
|
return null;
|
|
292
315
|
}
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
316
|
+
const activeManifest = readMarketplaceManifest(marketplace.root);
|
|
317
|
+
if (activeManifest === null)
|
|
318
|
+
throw notFound(`marketplace manifest not found: ${marketplace.root}/.crouter-marketplace/marketplace.json`);
|
|
319
|
+
if (!isGitRepo(marketplace.root)) {
|
|
320
|
+
const unchanged = { marketplace, manifest: activeManifest, pull: { updated: false, sha: currentSha(marketplace.root) ?? '' } };
|
|
321
|
+
cache.set(name, unchanged);
|
|
322
|
+
return unchanged;
|
|
323
|
+
}
|
|
324
|
+
const activeSha = currentSha(marketplace.root);
|
|
325
|
+
const staging = join(dirname(marketplace.root), `.${name}.${process.pid}.${Date.now()}.candidate`);
|
|
326
|
+
const previous = `${staging}.previous`;
|
|
327
|
+
try {
|
|
328
|
+
clone(marketplace.url, staging, { ref: marketplace.ref, depth: 1 });
|
|
329
|
+
const manifest = readMarketplaceManifest(staging);
|
|
330
|
+
if (manifest === null)
|
|
331
|
+
throw notFound(`marketplace manifest not found in candidate checkout: ${staging}/.crouter-marketplace/marketplace.json`);
|
|
332
|
+
const observers = listAllPlugins().filter((plugin) => plugin.sourceMarketplace === name && isSymlink(plugin.root));
|
|
333
|
+
for (const plugin of observers) {
|
|
334
|
+
const entry = manifest.plugins.find((candidate) => candidate.name === plugin.name);
|
|
335
|
+
if (entry === undefined || !isMarketplaceRelativeSource(entry.source)) {
|
|
336
|
+
throw general(`marketplace candidate removes relative-source plugin "${plugin.name}" while it is installed`);
|
|
337
|
+
}
|
|
338
|
+
const symlinkTarget = readSymlinkTarget(plugin.root);
|
|
339
|
+
if (symlinkTarget === null)
|
|
340
|
+
throw general(`installed marketplace plugin "${plugin.name}" has no readable symlink target`);
|
|
341
|
+
const observedRelativeSource = relative(marketplace.root, resolve(dirname(plugin.root), symlinkTarget));
|
|
342
|
+
const candidateRoot = resolve(staging, entry.source);
|
|
343
|
+
if (candidateRoot !== resolve(staging, observedRelativeSource)) {
|
|
344
|
+
throw general(`marketplace candidate moves installed relative-source plugin "${plugin.name}" from ${observedRelativeSource} to ${entry.source}`);
|
|
345
|
+
}
|
|
346
|
+
if (!pathExists(candidateRoot))
|
|
347
|
+
throw notFound(`marketplace candidate source path does not exist for plugin "${plugin.name}": ${candidateRoot}`);
|
|
348
|
+
const candidateManifest = readPluginManifest(candidateRoot);
|
|
349
|
+
if (candidateManifest === null)
|
|
350
|
+
throw notFound(`plugin manifest not found in marketplace candidate: ${candidateRoot}/.crouter-plugin/plugin.json`);
|
|
351
|
+
const pluginScopeRoot = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
|
|
352
|
+
assertPluginCandidateValid(candidateManifest, candidateRoot, plugin.scope, pluginScopeRoot, `${candidateRoot}/.crouter-plugin/plugin.json`);
|
|
301
353
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
354
|
+
renameSync(marketplace.root, previous);
|
|
355
|
+
try {
|
|
356
|
+
renameSync(staging, marketplace.root);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
renameSync(previous, marketplace.root);
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
removePath(previous);
|
|
363
|
+
for (const plugin of observers) {
|
|
364
|
+
const entry = manifest.plugins.find((candidate) => candidate.name === plugin.name);
|
|
365
|
+
updatePluginVersion(plugin.scope, plugin.name, entry.version ?? plugin.manifest.version);
|
|
366
|
+
}
|
|
367
|
+
updateState(marketplace.scope, (s) => {
|
|
368
|
+
if (s.marketplaces[name] === undefined)
|
|
369
|
+
s.marketplaces[name] = {};
|
|
370
|
+
s.marketplaces[name].last_updated = nowIso();
|
|
371
|
+
});
|
|
372
|
+
const refreshed = { marketplace, manifest, pull: { updated: activeSha !== currentSha(marketplace.root), sha: currentSha(marketplace.root) ?? '' } };
|
|
373
|
+
cache.set(name, refreshed);
|
|
374
|
+
return refreshed;
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
removePath(staging);
|
|
378
|
+
if (pathExists(previous) && !pathExists(marketplace.root))
|
|
379
|
+
renameSync(previous, marketplace.root);
|
|
315
380
|
}
|
|
316
381
|
}
|
|
317
382
|
function updatePluginVersion(scope, name, version) {
|
|
@@ -336,46 +401,28 @@ function installFromGit(source, ref, scope) {
|
|
|
336
401
|
const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
|
|
337
402
|
ensureScopeInitialized(scope, scopeRootPath);
|
|
338
403
|
const pDir = join(scopeRootPath, 'plugins');
|
|
339
|
-
ensureDir(pDir);
|
|
340
404
|
const tempName = deriveNameFromUrl(source);
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
throw general(`plugin directory already exists: ${tempDir}\nRemove the existing plugin first with \`pkg plugin remove ${tempName}\`.`);
|
|
344
|
-
}
|
|
345
|
-
clone(source, tempDir, { ref: ref !== undefined ? ref : undefined, depth: 1 });
|
|
346
|
-
const manifest = readPluginManifest(tempDir);
|
|
347
|
-
if (manifest === null) {
|
|
348
|
-
removePath(tempDir);
|
|
349
|
-
throw general(`cloned repo does not contain a valid .crouter-plugin/plugin.json: ${source}`);
|
|
350
|
-
}
|
|
405
|
+
const staging = join(scopeRootPath, 'tmp', `${tempName}.${process.pid}.${Date.now()}`);
|
|
406
|
+
ensureDir(join(scopeRootPath, 'tmp'));
|
|
351
407
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
408
|
+
clone(source, staging, { ref, depth: 1 });
|
|
409
|
+
const manifest = readPluginManifest(staging);
|
|
410
|
+
if (manifest === null)
|
|
411
|
+
throw general(`cloned repo does not contain a valid .crouter-plugin/plugin.json: ${source}`);
|
|
412
|
+
assertPluginCandidateValid(manifest, staging, scope, scopeRootPath, `${source}/.crouter-plugin/plugin.json`);
|
|
413
|
+
const finalDir = join(pDir, manifest.name);
|
|
414
|
+
if (pathExists(finalDir))
|
|
415
|
+
throw general(`plugin "${manifest.name}" is already installed at ${finalDir}`);
|
|
416
|
+
ensureDir(pDir);
|
|
417
|
+
renameSync(staging, finalDir);
|
|
418
|
+
updateConfig(scope, (cfg) => {
|
|
419
|
+
cfg.plugins[manifest.name] = { enabled: true, version: manifest.version };
|
|
420
|
+
});
|
|
421
|
+
return { name: manifest.name, scope, path: finalDir };
|
|
356
422
|
}
|
|
357
|
-
|
|
358
|
-
removePath(
|
|
359
|
-
throw error;
|
|
360
|
-
}
|
|
361
|
-
const finalName = manifest.name;
|
|
362
|
-
let finalDir = tempDir;
|
|
363
|
-
if (finalName !== tempName) {
|
|
364
|
-
const candidateDir = join(pDir, finalName);
|
|
365
|
-
if (pathExists(candidateDir)) {
|
|
366
|
-
removePath(tempDir);
|
|
367
|
-
throw general(`plugin "${finalName}" is already installed at ${candidateDir}`);
|
|
368
|
-
}
|
|
369
|
-
renameSync(tempDir, candidateDir);
|
|
370
|
-
finalDir = candidateDir;
|
|
423
|
+
finally {
|
|
424
|
+
removePath(staging);
|
|
371
425
|
}
|
|
372
|
-
updateConfig(scope, (cfg) => {
|
|
373
|
-
cfg.plugins[finalName] = {
|
|
374
|
-
enabled: true,
|
|
375
|
-
version: manifest.version,
|
|
376
|
-
};
|
|
377
|
-
});
|
|
378
|
-
return { name: finalName, scope, path: finalDir };
|
|
379
426
|
}
|
|
380
427
|
function installFromLocal(source, scope) {
|
|
381
428
|
const sourcePath = resolve(source);
|
|
@@ -383,11 +430,8 @@ function installFromLocal(source, scope) {
|
|
|
383
430
|
if (manifest === null) {
|
|
384
431
|
throw notFound(`plugin manifest not found at ${sourcePath}/.crouter-plugin/plugin.json`);
|
|
385
432
|
}
|
|
386
|
-
assertManifestKindsValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
387
|
-
assertManifestPageComponentsValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
388
|
-
assertManifestBinValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
389
|
-
assertManifestRequiresValid(manifest, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
390
433
|
const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
|
|
434
|
+
assertPluginCandidateValid(manifest, sourcePath, scope, scopeRootPath, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
391
435
|
ensureScopeInitialized(scope, scopeRootPath);
|
|
392
436
|
const destDir = join(scopeRootPath, 'plugins', manifest.name);
|
|
393
437
|
if (pathExists(destDir)) {
|
|
@@ -416,33 +460,36 @@ export function installFromMarketplace(ref, scope) {
|
|
|
416
460
|
const destRoot = requireScopeRoot(scope);
|
|
417
461
|
ensureScopeInitialized(scope, destRoot);
|
|
418
462
|
const destPluginDir = join(destRoot, 'plugins', ref.plugin);
|
|
463
|
+
if (pathExists(destPluginDir))
|
|
464
|
+
throw general(`plugin "${ref.plugin}" is already installed at ${destPluginDir}`);
|
|
465
|
+
let pluginManifest;
|
|
419
466
|
if (isMarketplaceRelativeSource(entry.source)) {
|
|
420
467
|
const sourcePath = join(refreshed.marketplace.root, entry.source);
|
|
421
|
-
if (!pathExists(sourcePath))
|
|
468
|
+
if (!pathExists(sourcePath))
|
|
422
469
|
throw notFound(`plugin source path does not exist: ${sourcePath}`);
|
|
423
|
-
|
|
470
|
+
const candidateManifest = readPluginManifest(sourcePath);
|
|
471
|
+
if (candidateManifest === null)
|
|
472
|
+
throw notFound(`plugin manifest not found at ${sourcePath}/.crouter-plugin/plugin.json`);
|
|
473
|
+
assertPluginCandidateValid(candidateManifest, sourcePath, scope, destRoot, `${sourcePath}/.crouter-plugin/plugin.json`);
|
|
424
474
|
linkOrCopy(sourcePath, destPluginDir);
|
|
475
|
+
pluginManifest = candidateManifest;
|
|
425
476
|
}
|
|
426
477
|
else {
|
|
427
|
-
|
|
428
|
-
|
|
478
|
+
const staging = join(destRoot, 'tmp', `${ref.plugin}.${process.pid}.${Date.now()}`);
|
|
479
|
+
ensureDir(join(destRoot, 'tmp'));
|
|
480
|
+
try {
|
|
481
|
+
clone(entry.source, staging, { depth: 1 });
|
|
482
|
+
const candidateManifest = readPluginManifest(staging);
|
|
483
|
+
if (candidateManifest === null)
|
|
484
|
+
throw notFound(`plugin manifest not found at ${staging}/.crouter-plugin/plugin.json`);
|
|
485
|
+
assertPluginCandidateValid(candidateManifest, staging, scope, destRoot, `${staging}/.crouter-plugin/plugin.json`);
|
|
486
|
+
ensureDir(join(destRoot, 'plugins'));
|
|
487
|
+
renameSync(staging, destPluginDir);
|
|
488
|
+
pluginManifest = candidateManifest;
|
|
489
|
+
}
|
|
490
|
+
finally {
|
|
491
|
+
removePath(staging);
|
|
429
492
|
}
|
|
430
|
-
clone(entry.source, destPluginDir, { depth: 1 });
|
|
431
|
-
}
|
|
432
|
-
const pluginManifest = readPluginManifest(destPluginDir);
|
|
433
|
-
if (pluginManifest === null) {
|
|
434
|
-
removePath(destPluginDir);
|
|
435
|
-
throw notFound(`plugin manifest not found at ${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
436
|
-
}
|
|
437
|
-
try {
|
|
438
|
-
assertManifestKindsValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
439
|
-
assertManifestPageComponentsValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
440
|
-
assertManifestBinValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
441
|
-
assertManifestRequiresValid(pluginManifest, `${destPluginDir}/.crouter-plugin/plugin.json`);
|
|
442
|
-
}
|
|
443
|
-
catch (error) {
|
|
444
|
-
removePath(destPluginDir);
|
|
445
|
-
throw error;
|
|
446
493
|
}
|
|
447
494
|
const version = entry.version ?? pluginManifest.version;
|
|
448
495
|
updateConfig(scope, (cfg) => {
|
|
@@ -456,6 +503,58 @@ export function installFromMarketplace(ref, scope) {
|
|
|
456
503
|
});
|
|
457
504
|
return { name: ref.plugin, scope, path: destPluginDir };
|
|
458
505
|
}
|
|
506
|
+
function updateGitPluginCandidate(plugin) {
|
|
507
|
+
if (isSymlink(plugin.root)) {
|
|
508
|
+
const manifest = readPluginManifest(plugin.root);
|
|
509
|
+
if (manifest === null)
|
|
510
|
+
throw notFound(`plugin manifest not found at ${plugin.root}/.crouter-plugin/plugin.json`);
|
|
511
|
+
const scopeRootPath = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
|
|
512
|
+
assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
513
|
+
return { updated: false, sha: currentSha(plugin.root) ?? '', manifest };
|
|
514
|
+
}
|
|
515
|
+
if (!isGitRepo(plugin.root)) {
|
|
516
|
+
const manifest = readPluginManifest(plugin.root);
|
|
517
|
+
if (manifest === null)
|
|
518
|
+
throw notFound(`plugin manifest not found at ${plugin.root}/.crouter-plugin/plugin.json`);
|
|
519
|
+
const scopeRootPath = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
|
|
520
|
+
assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
521
|
+
return { updated: false, sha: '', manifest };
|
|
522
|
+
}
|
|
523
|
+
const remote = gitSync(['config', '--get', 'remote.origin.url'], plugin.root).stdout.trim();
|
|
524
|
+
if (remote === '')
|
|
525
|
+
throw general(`plugin "${plugin.name}" has no origin remote to stage an update from`);
|
|
526
|
+
const branch = gitSync(['symbolic-ref', '--short', 'HEAD'], plugin.root).stdout.trim();
|
|
527
|
+
if (branch === '')
|
|
528
|
+
throw general(`plugin "${plugin.name}" is detached at its installed revision and has no tracked branch to update`);
|
|
529
|
+
const scopeRootPath = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
|
|
530
|
+
const staging = join(scopeRootPath, 'tmp', `${plugin.name}.${process.pid}.${Date.now()}.candidate`);
|
|
531
|
+
const previous = `${staging}.previous`;
|
|
532
|
+
const before = currentSha(plugin.root);
|
|
533
|
+
ensureDir(join(scopeRootPath, 'tmp'));
|
|
534
|
+
try {
|
|
535
|
+
clone(remote, staging, { ref: branch, depth: 1 });
|
|
536
|
+
const manifest = readPluginManifest(staging);
|
|
537
|
+
if (manifest === null)
|
|
538
|
+
throw notFound(`plugin manifest not found in update candidate: ${staging}/.crouter-plugin/plugin.json`);
|
|
539
|
+
assertPluginCandidateValid(manifest, staging, plugin.scope, scopeRootPath, `${staging}/.crouter-plugin/plugin.json`);
|
|
540
|
+
renameSync(plugin.root, previous);
|
|
541
|
+
try {
|
|
542
|
+
renameSync(staging, plugin.root);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
renameSync(previous, plugin.root);
|
|
546
|
+
throw error;
|
|
547
|
+
}
|
|
548
|
+
removePath(previous);
|
|
549
|
+
const sha = currentSha(plugin.root) ?? '';
|
|
550
|
+
return { updated: before !== sha, sha, manifest };
|
|
551
|
+
}
|
|
552
|
+
finally {
|
|
553
|
+
removePath(staging);
|
|
554
|
+
if (pathExists(previous) && !pathExists(plugin.root))
|
|
555
|
+
renameSync(previous, plugin.root);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
459
558
|
async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
460
559
|
const sourceInstalled = isSourceInstalled(plugin);
|
|
461
560
|
if (!sourceInstalled && plugin.manifest.bundle !== undefined) {
|
|
@@ -464,63 +563,25 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
464
563
|
}
|
|
465
564
|
if (plugin.manifest.transport?.kind === 'http' && !sourceInstalled)
|
|
466
565
|
rejectLegacyHttpPlugin(plugin.name);
|
|
467
|
-
const previousSha = currentSha(plugin.root);
|
|
468
|
-
let contentUpdate = { updated: false, sha: currentSha(plugin.root) ?? '' };
|
|
469
566
|
let marketplaceVersion;
|
|
567
|
+
let marketplaceUpdated = false;
|
|
470
568
|
if (plugin.sourceMarketplace !== undefined) {
|
|
471
569
|
const refreshed = refreshMarketplace(plugin.sourceMarketplace, marketplaceCache, opts);
|
|
472
570
|
if (refreshed !== null) {
|
|
473
|
-
const entry = refreshed.manifest.plugins.find((
|
|
571
|
+
const entry = refreshed.manifest.plugins.find((candidate) => candidate.name === plugin.name);
|
|
474
572
|
marketplaceVersion = entry?.version;
|
|
475
|
-
|
|
476
|
-
const shaAfter = currentSha(plugin.root);
|
|
477
|
-
contentUpdate = { updated: refreshed.pull.updated, sha: shaAfter ?? refreshed.pull.sha };
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
480
|
-
contentUpdate = pullGitCheckout(plugin.root, `plugin "${plugin.name}"`, opts);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
else {
|
|
485
|
-
contentUpdate = pullGitCheckout(plugin.root, `plugin "${plugin.name}"`, opts);
|
|
486
|
-
}
|
|
487
|
-
const manifest = readPluginManifest(plugin.root);
|
|
488
|
-
try {
|
|
489
|
-
if (manifest === null) {
|
|
490
|
-
throw general(`plugin manifest not found after updating ${plugin.root}: ${plugin.root}/.crouter-plugin/plugin.json`);
|
|
491
|
-
}
|
|
492
|
-
assertManifestKindsValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
493
|
-
assertManifestPageComponentsValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
494
|
-
assertManifestBinValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
495
|
-
assertManifestRequiresValid(manifest, `${plugin.root}/.crouter-plugin/plugin.json`);
|
|
496
|
-
}
|
|
497
|
-
catch (error) {
|
|
498
|
-
if (contentUpdate.updated && previousSha !== null && isGitRepo(plugin.root)) {
|
|
499
|
-
restoreSourceCheckout(plugin.root, previousSha);
|
|
500
|
-
// Marketplace plugins are symlinks into the shared marketplace checkout.
|
|
501
|
-
// Its cached manifest and pull state must move back with that checkout so
|
|
502
|
-
// later entries in this bulk update neither see the rejected revision nor
|
|
503
|
-
// record its versions as accepted.
|
|
504
|
-
if (plugin.sourceMarketplace !== undefined) {
|
|
505
|
-
const cached = marketplaceCache.get(plugin.sourceMarketplace);
|
|
506
|
-
if (cached !== undefined) {
|
|
507
|
-
marketplaceCache.set(plugin.sourceMarketplace, {
|
|
508
|
-
...cached,
|
|
509
|
-
pull: { updated: false, sha: currentSha(cached.marketplace.root) ?? '' },
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
}
|
|
573
|
+
marketplaceUpdated = refreshed.pull.updated;
|
|
513
574
|
}
|
|
514
|
-
throw error;
|
|
515
575
|
}
|
|
516
|
-
|
|
576
|
+
const candidate = updateGitPluginCandidate(plugin);
|
|
577
|
+
updatePluginVersion(plugin.scope, plugin.name, marketplaceVersion ?? candidate.manifest.version);
|
|
517
578
|
const commands = await commandReport(plugin.name, plugin.scope);
|
|
518
|
-
const warnings = missingRequirementWarnings(manifest);
|
|
579
|
+
const warnings = missingRequirementWarnings(candidate.manifest);
|
|
519
580
|
return {
|
|
520
581
|
name: plugin.name,
|
|
521
582
|
...(plugin.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
|
|
522
|
-
updated:
|
|
523
|
-
sha:
|
|
583
|
+
updated: marketplaceUpdated || candidate.updated,
|
|
584
|
+
sha: candidate.sha,
|
|
524
585
|
...(commands !== undefined ? { commands } : {}),
|
|
525
586
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
526
587
|
};
|
|
@@ -13,7 +13,7 @@ import { createNode } from '../canvas/canvas.js';
|
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
14
|
import { nodeDir } from '../canvas/paths.js';
|
|
15
15
|
import { atomicWriteJson, bindTicketReplyRoute, pageManifestPath } from '../human/convention.js';
|
|
16
|
-
import { cancelTicket } from '../human/tickets.js';
|
|
16
|
+
import { cancelTicket, readTicketResult } from '../human/tickets.js';
|
|
17
17
|
import { humanCancel } from '../../commands/human/queue.js';
|
|
18
18
|
import { createApiServer } from '../../daemon/api/server.js';
|
|
19
19
|
import { apiSocketPath } from '../canvas/paths.js';
|
|
@@ -46,7 +46,7 @@ function node(id) {
|
|
|
46
46
|
}
|
|
47
47
|
/** Publish a reply-bearing page ticket in the bridge node's directory, exactly as
|
|
48
48
|
* `human send` does: reply route first, then the page manifest. */
|
|
49
|
-
function publishTicket(id) {
|
|
49
|
+
function publishTicket(id, slots = []) {
|
|
50
50
|
const dir = nodeDir(id);
|
|
51
51
|
mkdirSync(dir, { recursive: true });
|
|
52
52
|
bindTicketReplyRoute(dir, id);
|
|
@@ -58,7 +58,7 @@ function publishTicket(id) {
|
|
|
58
58
|
delivery: { placement: 'inline', inbox: true, reply: true },
|
|
59
59
|
document: 'page.tsx',
|
|
60
60
|
steps: 1,
|
|
61
|
-
slots
|
|
61
|
+
slots,
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
before(() => {
|
|
@@ -80,13 +80,31 @@ after(() => {
|
|
|
80
80
|
rmSync(home, { recursive: true, force: true });
|
|
81
81
|
delete process.env['CRTR_HOME'];
|
|
82
82
|
});
|
|
83
|
-
test('cancel
|
|
84
|
-
const
|
|
85
|
-
createNode(node(
|
|
86
|
-
publishTicket(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
test('cancel accepts newer page config and treats an already-canceled ticket as a no-op', async () => {
|
|
84
|
+
const newerId = 'newerPage';
|
|
85
|
+
createNode(node(newerId));
|
|
86
|
+
publishTicket(newerId, [{
|
|
87
|
+
id: 'decision',
|
|
88
|
+
kind: 'options',
|
|
89
|
+
step: 0,
|
|
90
|
+
config: {
|
|
91
|
+
label: 'Decision',
|
|
92
|
+
body: 'Choose one.',
|
|
93
|
+
options: [{ id: 'yes', label: 'Yes' }],
|
|
94
|
+
mode: 'single',
|
|
95
|
+
futureField: true,
|
|
96
|
+
},
|
|
97
|
+
}]);
|
|
98
|
+
const canceled = (await humanCancel.run({ ticket_id: newerId }));
|
|
99
|
+
assert.equal(canceled['canceled'], true);
|
|
100
|
+
assert.equal(canceled['ticket_id'], newerId);
|
|
101
|
+
assert.equal(readTicketResult(nodeDir(newerId))?.kind, 'canceled');
|
|
102
|
+
const settledId = 'canceledJob';
|
|
103
|
+
createNode(node(settledId));
|
|
104
|
+
publishTicket(settledId);
|
|
105
|
+
cancelTicket(nodeDir(settledId), { actor: 'human' });
|
|
106
|
+
const settled = (await humanCancel.run({ ticket_id: settledId }));
|
|
107
|
+
assert.equal(settled['canceled'], false);
|
|
108
|
+
assert.equal(settled['reason'], 'already_resolved');
|
|
109
|
+
assert.equal(settled['ticket_id'], settledId);
|
|
92
110
|
});
|
package/dist/core/bash-jobs.d.ts
CHANGED
|
@@ -77,11 +77,8 @@ export declare function jobStartedAtMs(jobId: string): number | undefined;
|
|
|
77
77
|
* notice, the `node bash background` result/toast, and the attach cockpit)
|
|
78
78
|
* shows it: `<60s -> "12s"`; `60s-59m -> "3m 40s"`; `>=60m -> "1h 04m"`. */
|
|
79
79
|
export declare function formatBashElapsed(ms: number): string;
|
|
80
|
-
/** The command line worth SHOWING for a job
|
|
81
|
-
*
|
|
82
|
-
* literal first line names no command at all: skip leading assignment lines and
|
|
83
|
-
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
84
|
-
* command is nothing but assignments falls back to its first non-empty line. */
|
|
80
|
+
/** The command line worth SHOWING for a job: the first non-empty line of
|
|
81
|
+
* `cmd.sh`, whitespace-collapsed to one line. */
|
|
85
82
|
export declare function bashJobCommandLine(command: string): string;
|
|
86
83
|
export interface BashJobStatus {
|
|
87
84
|
jobId: string;
|
package/dist/core/bash-jobs.js
CHANGED
|
@@ -189,15 +189,11 @@ export function formatBashElapsed(ms) {
|
|
|
189
189
|
const minutes = totalMinutes % 60;
|
|
190
190
|
return `${hours}h ${String(minutes).padStart(2, '0')}m`;
|
|
191
191
|
}
|
|
192
|
-
/** The command line worth SHOWING for a job
|
|
193
|
-
*
|
|
194
|
-
* literal first line names no command at all: skip leading assignment lines and
|
|
195
|
-
* return the first real one, whitespace-collapsed to a single line. A job whose
|
|
196
|
-
* command is nothing but assignments falls back to its first non-empty line. */
|
|
192
|
+
/** The command line worth SHOWING for a job: the first non-empty line of
|
|
193
|
+
* `cmd.sh`, whitespace-collapsed to one line. */
|
|
197
194
|
export function bashJobCommandLine(command) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
return (real ?? lines[0] ?? '').replace(/\s+/g, ' ');
|
|
195
|
+
const first = command.split('\n').map((line) => line.trim()).find((line) => line !== '');
|
|
196
|
+
return (first ?? '').replace(/\s+/g, ' ');
|
|
201
197
|
}
|
|
202
198
|
/** Every live backgrounded bash job for this node — job.bg present, job.exit
|
|
203
199
|
* absent — whether handed off by the menu, the valve deadline, or broker
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ValidatedCommandManifest } from '../command-manifests/manifest.js';
|
|
2
|
-
import type { KindConfig, PageComponentRegistration } from '../../types.js';
|
|
2
|
+
import type { KindConfig, MemoryExtensionDeclarations, PageComponentRegistration } from '../../types.js';
|
|
3
3
|
export interface BundleIssue {
|
|
4
4
|
code: string;
|
|
5
5
|
path?: string;
|
|
@@ -20,6 +20,9 @@ export interface ValidatedBundle {
|
|
|
20
20
|
* manifest, where `resolvePageComponents` reads it — see
|
|
21
21
|
* `PluginManifest.page_components`. */
|
|
22
22
|
page_components?: PageComponentRegistration[];
|
|
23
|
+
/** Plugin-owned memory metadata declarations copied into the synthesized
|
|
24
|
+
* installed manifest alongside the archive's memory documents. */
|
|
25
|
+
memory_extensions?: MemoryExtensionDeclarations;
|
|
23
26
|
commands: ValidatedCommandManifest;
|
|
24
27
|
commandsBytes: Uint8Array;
|
|
25
28
|
directories: ReadonlyArray<{
|
|
@@ -2,6 +2,7 @@ import { Parser } from 'tar';
|
|
|
2
2
|
import { validateCommandManifest } from '../command-manifests/manifest.js';
|
|
3
3
|
import { invalidPluginKindsReasons } from '../config.js';
|
|
4
4
|
import { invalidPageComponentsReasons, normalizePageComponents } from '../human/page-catalog.js';
|
|
5
|
+
import { validateMemoryExtensionDeclarations } from '../memory/extensions.js';
|
|
5
6
|
function issue(code, message, received, expected, next, path) {
|
|
6
7
|
return { code, message, received, expected, next, ...(path !== undefined ? { path } : {}) };
|
|
7
8
|
}
|
|
@@ -144,10 +145,10 @@ function parseBundleMetadata(bytes) {
|
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
const record = raw;
|
|
147
|
-
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds' && key !== 'page_components');
|
|
148
|
+
const unknownKeys = Object.keys(record).filter((key) => key !== 'bundleVersion' && key !== 'kinds' && key !== 'page_components' && key !== 'memory_extensions');
|
|
148
149
|
if (record['bundleVersion'] !== 1 || unknownKeys.length > 0) {
|
|
149
150
|
return {
|
|
150
|
-
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most kinds and
|
|
151
|
+
issue: bundleInvalid('bundle.json must contain bundleVersion 1 and at most kinds, page_components, and memory_extensions blocks', JSON.stringify(raw), 'an object with bundleVersion set to 1 and optional kinds, page_components, and memory_extensions', 'Set bundle.json to {"bundleVersion":1} plus optional contribution blocks.', 'bundle.json'),
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
// Install-time strictness: a bad block fails the install loudly here, because
|
|
@@ -169,9 +170,21 @@ function parseBundleMetadata(bytes) {
|
|
|
169
170
|
};
|
|
170
171
|
}
|
|
171
172
|
}
|
|
173
|
+
let memoryExtensions;
|
|
174
|
+
if (record['memory_extensions'] !== undefined) {
|
|
175
|
+
try {
|
|
176
|
+
memoryExtensions = validateMemoryExtensionDeclarations(record['memory_extensions']);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return {
|
|
180
|
+
issue: bundleInvalid('bundle.json memory_extensions block is invalid', error instanceof Error ? error.message : String(error), 'a valid memory_extensions declaration object', 'Fix the memory_extensions declarations in bundle.json.', 'bundle.json'),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
172
184
|
return {
|
|
173
185
|
...(record['kinds'] !== undefined ? { kinds: record['kinds'] } : {}),
|
|
174
186
|
...(record['page_components'] !== undefined ? { page_components: normalizePageComponents(record['page_components']) } : {}),
|
|
187
|
+
...(memoryExtensions !== undefined ? { memory_extensions: memoryExtensions } : {}),
|
|
175
188
|
};
|
|
176
189
|
}
|
|
177
190
|
function parseCommands(bytes, reservedCoreNames, coreCommandPaths) {
|
|
@@ -219,6 +232,7 @@ export async function validatePluginBundle(archive, options) {
|
|
|
219
232
|
bundleVersion: 1,
|
|
220
233
|
...(metadata.kinds !== undefined ? { kinds: metadata.kinds } : {}),
|
|
221
234
|
...(metadata.page_components !== undefined ? { page_components: metadata.page_components } : {}),
|
|
235
|
+
...(metadata.memory_extensions !== undefined ? { memory_extensions: metadata.memory_extensions } : {}),
|
|
222
236
|
commands: commandValidation.commands,
|
|
223
237
|
commandsBytes: commands.bytes,
|
|
224
238
|
directories: parsed.members
|