@try-works/dsh-model-as-tool 0.1.3 → 0.3.6
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/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +592 -0
- package/client/index.js +1679 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +197 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +369 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +217 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +334 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1351 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +250 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2625 -70
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
- package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +43 -15
- package/remote/spec.mjs +439 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The recipe authoring CLI (RCP-08).
|
|
3
|
+
*
|
|
4
|
+
* Three commands, each a required new tool rather than an existing npm/DSH
|
|
5
|
+
* command: `validate` checks a draft against BOTH authorities (the published
|
|
6
|
+
* `harness-model-recipe.schema.json` shipped beside this module AND the code
|
|
7
|
+
* validator `parseRecipe`), `describe` prints the effective bounds an operator
|
|
8
|
+
* reads before deciding, and `pack` builds a declarative-extension package
|
|
9
|
+
* (manifest + recipes, NO executable code) consumable by the extension install
|
|
10
|
+
* path.
|
|
11
|
+
*
|
|
12
|
+
* Guardrails built in rather than documented:
|
|
13
|
+
*
|
|
14
|
+
* - The schema is read from THIS PACKAGE'S SHIPPED FILE, resolved relative to
|
|
15
|
+
* the module (so a packed install validates against the packed artifact, not
|
|
16
|
+
* some source checkout).
|
|
17
|
+
* - Packing validates the exact production artifact and runs NO lifecycle
|
|
18
|
+
* scripts: the output is written directly (directory + a store-method zip),
|
|
19
|
+
* never through `npm pack`.
|
|
20
|
+
* - Nothing here runs inference, installs or activates anything, or touches
|
|
21
|
+
* the network (RCP-08's creation rules).
|
|
22
|
+
* - Exit codes are a contract: 0 valid, 1 invalid (every refusal named),
|
|
23
|
+
* 2 usage or IO failure.
|
|
24
|
+
*
|
|
25
|
+
* RCP-09 (agent-assisted creation with an authoring grant) is NOT claimed by
|
|
26
|
+
* this module: the CLI is a plain argv/stdio tool, which an agent CAN drive,
|
|
27
|
+
* but the authoring-grant and draft-directory policy belong to the host and are
|
|
28
|
+
* left as the recorded remainder.
|
|
29
|
+
*
|
|
30
|
+
* @module @try-works/tool-mode-recipes/cli
|
|
31
|
+
*/
|
|
32
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
33
|
+
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { dirname, join, parse, resolve, sep } from 'node:path';
|
|
35
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
36
|
+
import { INITIAL_MAX_IMAGE_INPUTS, INITIAL_MAX_OUTPUT_TOKENS, parseRecipe, toCanonicalRecipe, } from "./recipe-format.js";
|
|
37
|
+
import { EDITABLE_FIELDS, policyEditRefusal, templateFiles, validateCreateArguments, } from "./templates.js";
|
|
38
|
+
import { runFixtureSuite, validateFixtureDocument, } from "./fixture-provider.js";
|
|
39
|
+
import { INITIAL_HOST_POLICY } from "./broker.js";
|
|
40
|
+
/** The published schema, shipped beside this module (RCP-02's artifact). */
|
|
41
|
+
const SHIPPED_SCHEMA_URL = new URL('../schema/harness-model-recipe.schema.json', import.meta.url);
|
|
42
|
+
/** Load and compile the shipped schema, once per invocation. */
|
|
43
|
+
function schemaValidator() {
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = readFileSync(fileURLToPath(SHIPPED_SCHEMA_URL), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { validate: () => [], loadError: `the shipped schema could not be read: ${messageOf(error)}` };
|
|
50
|
+
}
|
|
51
|
+
let schema;
|
|
52
|
+
try {
|
|
53
|
+
schema = JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return { validate: () => [], loadError: `the shipped schema is not valid JSON: ${messageOf(error)}` };
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const validate = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
|
|
60
|
+
return {
|
|
61
|
+
validate: value => (validate(value) ? [] : (validate.errors ?? []).map(error => `${error.instancePath === '' ? '(root)' : error.instancePath} ${error.message ?? 'is invalid'}`)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return { validate: () => [], loadError: `the shipped schema does not compile: ${messageOf(error)}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Validate one decoded document against BOTH authorities (RCP-02, RCP-08). */
|
|
69
|
+
export function judgeRecipe(document) {
|
|
70
|
+
const { validate, loadError } = schemaValidator();
|
|
71
|
+
const parsed = parseRecipe(document);
|
|
72
|
+
const schemaViolations = loadError !== undefined ? [loadError] : validate(document);
|
|
73
|
+
return {
|
|
74
|
+
schemaOk: loadError === undefined && schemaViolations.length === 0,
|
|
75
|
+
schemaViolations,
|
|
76
|
+
codeOk: parsed.ok,
|
|
77
|
+
codeViolations: parsed.ok ? [] : parsed.errors,
|
|
78
|
+
...parsed.ok ? { recipe: parsed.recipe } : {},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** Read and decode a recipe file; IO problems are the CALLER's to report as usage failures. */
|
|
82
|
+
function readRecipeFile(path) {
|
|
83
|
+
let text;
|
|
84
|
+
try {
|
|
85
|
+
text = readFileSync(path, 'utf8');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return { ioError: `cannot read ${path}: ${messageOf(error)}` };
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return { document: JSON.parse(text) };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return { ioError: `${path} is not valid JSON: ${messageOf(error)}` };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Print a verdict; returns whether it was fully clean. */
|
|
98
|
+
function printVerdict(io, subject, verdict) {
|
|
99
|
+
io.out(`schema: ${verdict.schemaOk ? 'OK' : `${verdict.schemaViolations.length} violation(s)`}`);
|
|
100
|
+
for (const violation of verdict.schemaViolations)
|
|
101
|
+
io.err(` ${subject}: ${violation}`);
|
|
102
|
+
io.out(`code: ${verdict.codeOk ? 'OK' : `${verdict.codeViolations.length} violation(s)`}`);
|
|
103
|
+
for (const violation of verdict.codeViolations)
|
|
104
|
+
io.err(` ${subject}: ${violation}`);
|
|
105
|
+
return verdict.schemaOk && verdict.codeOk;
|
|
106
|
+
}
|
|
107
|
+
const USAGE = `tool-mode-recipe — author declarative harness-model recipes (RCP-08)
|
|
108
|
+
|
|
109
|
+
validate <recipe.json> validate a draft against the published
|
|
110
|
+
harness-model-recipe.schema.json AND the
|
|
111
|
+
code validator; every refusal is named
|
|
112
|
+
describe <recipe.json> print the effective bounds: tool/contract,
|
|
113
|
+
inputs, output and image ceilings,
|
|
114
|
+
destinations, recording, context
|
|
115
|
+
create --template <image-description|text-title>
|
|
116
|
+
[--name <recipe-id>] [--description <text>]
|
|
117
|
+
[--out <dir>] emit a STATIC draft project (recipe, input/
|
|
118
|
+
output schemas, a starter skill note, a
|
|
119
|
+
starter fixtures file, a README) into a
|
|
120
|
+
NEW directory; nothing is installed,
|
|
121
|
+
activated, downloaded or inferred, and an
|
|
122
|
+
existing target is refused
|
|
123
|
+
edit-draft <recipe.json>
|
|
124
|
+
--set <field> --value <text> a bounded static mutation over the safe
|
|
125
|
+
fields (description, maxOutputTokens,
|
|
126
|
+
imageInputs.max); policy fields are
|
|
127
|
+
refused by name and the result must
|
|
128
|
+
validate before it is written
|
|
129
|
+
test <project-dir>
|
|
130
|
+
--fixtures <fixtures.json> run the recipe(s) through the REAL broker
|
|
131
|
+
against a SCRIPTED fake provider (never a
|
|
132
|
+
real model): binding, budget, failure
|
|
133
|
+
vocabulary and the provider-call ceiling
|
|
134
|
+
pack <recipe.json | project-dir> build a declarative-extension package
|
|
135
|
+
[--name <pkg>] [--version <x.y.z>]
|
|
136
|
+
[--description <text>] [--out <dir>]
|
|
137
|
+
(default --out ./dist) — manifest +
|
|
138
|
+
recipes JSON, NO executable code, no
|
|
139
|
+
lifecycle scripts run
|
|
140
|
+
|
|
141
|
+
--live is NOT provided by this build: a live run requires an explicit deployment
|
|
142
|
+
executor profile with consent and limits (recorded remainder, RCP-08).
|
|
143
|
+
|
|
144
|
+
Exit codes: 0 valid / packed / passed, 1 invalid recipe or fixture failure, 2 usage or IO failure.`;
|
|
145
|
+
/**
|
|
146
|
+
* Run one CLI invocation.
|
|
147
|
+
* @param argv - arguments after the executable name.
|
|
148
|
+
* @param io - the output sinks.
|
|
149
|
+
* @returns a promise of the process exit code.
|
|
150
|
+
*/
|
|
151
|
+
export async function runRecipeCli(argv, io) {
|
|
152
|
+
try {
|
|
153
|
+
return await dispatchRecipeCli(argv, io);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
// The exit-2 contract: an UNEXPECTED failure (IO, filesystem, decode) is a
|
|
157
|
+
// named usage/IO refusal — never a raw stack, never exit 1.
|
|
158
|
+
io.err(`IO failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function dispatchRecipeCli(argv, io) {
|
|
163
|
+
// A NULL-PROTOTYPE flags map: `--__proto__ x` must not touch the object's
|
|
164
|
+
// prototype chain (the name is still refused by the per-command flag set).
|
|
165
|
+
const positional = [];
|
|
166
|
+
const flags = Object.create(null);
|
|
167
|
+
const booleanFlags = new Set(['help', 'live']);
|
|
168
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
169
|
+
const token = argv[index];
|
|
170
|
+
if (!token.startsWith('--')) {
|
|
171
|
+
positional.push(token);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const name = token.slice(2);
|
|
175
|
+
if (name === 'help') {
|
|
176
|
+
io.out(USAGE);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
if (booleanFlags.has(name)) {
|
|
180
|
+
// A boolean flag REFUSES a following value token: `--live <value>` must
|
|
181
|
+
// not silently consume the value and downgrade the live request.
|
|
182
|
+
const next = argv[index + 1];
|
|
183
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
184
|
+
io.err(`the flag --${name} takes no value; ${JSON.stringify(next)} is not a flag`);
|
|
185
|
+
io.err(USAGE);
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
flags[name] = true;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const value = argv[index + 1];
|
|
192
|
+
if (value === undefined || value.startsWith('--')) {
|
|
193
|
+
flags[name] = true;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
flags[name] = value;
|
|
197
|
+
index += 1;
|
|
198
|
+
}
|
|
199
|
+
const [command, ...rest] = positional;
|
|
200
|
+
if (command === undefined) {
|
|
201
|
+
io.err(USAGE);
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
// A mistyped flag must be refused, never silently ignored (the contract the
|
|
205
|
+
// original parser pinned): each command names its accepted flags.
|
|
206
|
+
const acceptedFlags = {
|
|
207
|
+
validate: [], describe: [],
|
|
208
|
+
pack: ['out', 'name', 'version', 'description'],
|
|
209
|
+
create: ['template', 'name', 'description', 'out'],
|
|
210
|
+
'edit-draft': ['set', 'value'],
|
|
211
|
+
test: ['fixtures', 'live'],
|
|
212
|
+
};
|
|
213
|
+
const allowed = acceptedFlags[command];
|
|
214
|
+
if (allowed === undefined) {
|
|
215
|
+
io.err(`unknown command ${JSON.stringify(command)}`);
|
|
216
|
+
io.err(USAGE);
|
|
217
|
+
return 2;
|
|
218
|
+
}
|
|
219
|
+
for (const name of Object.keys(flags)) {
|
|
220
|
+
if (!allowed.includes(name)) {
|
|
221
|
+
io.err(`unknown option --${name} for ${JSON.stringify(command)}`);
|
|
222
|
+
io.err(USAGE);
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Flag-style commands: create / edit-draft / test.
|
|
227
|
+
if (command === 'create') {
|
|
228
|
+
return createCommand(flags, io);
|
|
229
|
+
}
|
|
230
|
+
if (command === 'edit-draft') {
|
|
231
|
+
return editDraftCommand(rest, flags, io);
|
|
232
|
+
}
|
|
233
|
+
if (command === 'test') {
|
|
234
|
+
return await testCommand(rest, flags, io);
|
|
235
|
+
}
|
|
236
|
+
// The original positional commands keep their exact contract.
|
|
237
|
+
if (rest.length !== 1) {
|
|
238
|
+
io.err(USAGE);
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
const subject = rest[0];
|
|
242
|
+
if (command === 'validate' || command === 'describe') {
|
|
243
|
+
const { document, ioError } = readRecipeFile(subject);
|
|
244
|
+
if (ioError !== undefined) {
|
|
245
|
+
io.err(ioError);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
const verdict = judgeRecipe(document);
|
|
249
|
+
const clean = printVerdict(io, subject, verdict);
|
|
250
|
+
if (!clean)
|
|
251
|
+
return 1;
|
|
252
|
+
if (command === 'describe') {
|
|
253
|
+
describeTo(io, verdict.recipe);
|
|
254
|
+
}
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
if (command === 'pack') {
|
|
258
|
+
return packCommand(subject, flags, io);
|
|
259
|
+
}
|
|
260
|
+
io.err(`unknown command ${JSON.stringify(command)}`);
|
|
261
|
+
io.err(USAGE);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Refuse a target whose ancestor chain carries a reparse point (symlink or
|
|
266
|
+
* Windows junction) or a `tool-mode.extension.json` (an installed or packed
|
|
267
|
+
* artifact is immutable). `create`/`edit-draft` never write through either.
|
|
268
|
+
* @param target - the absolute target path (a directory for create, a file for edit-draft).
|
|
269
|
+
* @returns a refusal reason, or undefined when the target is writable-by-construction.
|
|
270
|
+
*/
|
|
271
|
+
function writabilityRefusal(target) {
|
|
272
|
+
const absolute = resolve(target);
|
|
273
|
+
const parsed = parse(absolute);
|
|
274
|
+
// Walk from the FILESYSTEM ROOT, not from cwd: a target on another drive (or
|
|
275
|
+
// outside cwd) must be walked too — the old cwd-relative slice heuristic
|
|
276
|
+
// silently skipped it. Every EXISTING component is lstat'ed (a Windows
|
|
277
|
+
// junction reports as a symlink) and every ancestor is checked for the
|
|
278
|
+
// installed-artifact manifest.
|
|
279
|
+
const components = absolute.slice(parsed.root.length).split(sep).filter(Boolean);
|
|
280
|
+
let cursor = parsed.root;
|
|
281
|
+
// The root itself participates: a drive root that were a reparse point is as
|
|
282
|
+
// much an escape as any deeper component.
|
|
283
|
+
for (const component of components) {
|
|
284
|
+
cursor = join(cursor, component);
|
|
285
|
+
try {
|
|
286
|
+
if (lstatSync(cursor).isSymbolicLink()) {
|
|
287
|
+
return `the target path ${target} passes through a symbolic link or junction at ${cursor}; refused`;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// The component does not exist yet: no deeper component can exist either.
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// The installed-artifact rule applies to EVERY ancestor directory, not only
|
|
296
|
+
// the immediate parent (the reviewer's grandparent-manifest case): the target
|
|
297
|
+
// must not lie inside a directory tree that carries tool-mode.extension.json.
|
|
298
|
+
let ancestor = dirname(absolute);
|
|
299
|
+
for (;;) {
|
|
300
|
+
if (existsSync(join(ancestor, 'tool-mode.extension.json'))) {
|
|
301
|
+
return `the target path ${target} lies inside an installed or packed artifact at ${ancestor}; those are immutable and refused`;
|
|
302
|
+
}
|
|
303
|
+
const parent = dirname(ancestor);
|
|
304
|
+
if (parent === ancestor)
|
|
305
|
+
break;
|
|
306
|
+
ancestor = parent;
|
|
307
|
+
}
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Verify a directory chain is still reparse-free IMMEDIATELY before a write.
|
|
312
|
+
* The TOCTOU residual: a junction flipped between this call and the following
|
|
313
|
+
* open() can still redirect that ONE open; the post-write verification below
|
|
314
|
+
* detects it and no FURTHER file is created (the window is a single sub-
|
|
315
|
+
* millisecond open per file, and the created project is refused and removed).
|
|
316
|
+
*/
|
|
317
|
+
function chainStillClean(dir) {
|
|
318
|
+
return writabilityRefusal(join(dir, '.sentinel'));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Create one file EXCLUSIVELY ('wx') in a chain-verified directory, verifying
|
|
322
|
+
* the chain again after the write. A reparse point observed at any point
|
|
323
|
+
* aborts the operation (the caller refuses the whole command).
|
|
324
|
+
*/
|
|
325
|
+
function safeWriteFile(target, data) {
|
|
326
|
+
const parent = dirname(target);
|
|
327
|
+
const before = chainStillClean(parent);
|
|
328
|
+
if (before !== undefined)
|
|
329
|
+
return before;
|
|
330
|
+
let descriptor;
|
|
331
|
+
try {
|
|
332
|
+
descriptor = openSync(target, 'wx');
|
|
333
|
+
writeFileSync(descriptor, data, 'utf8');
|
|
334
|
+
fsyncSync(descriptor);
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (descriptor !== undefined)
|
|
338
|
+
closeSync(descriptor);
|
|
339
|
+
}
|
|
340
|
+
const after = chainStillClean(parent);
|
|
341
|
+
if (after !== undefined)
|
|
342
|
+
return after;
|
|
343
|
+
if (lstatSync(target).isSymbolicLink()) {
|
|
344
|
+
return `the written file ${target} is a reparse point; refused`;
|
|
345
|
+
}
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
/** `create`: emit the static project files, refusing anything unsafe. */
|
|
349
|
+
function createCommand(flags, io) {
|
|
350
|
+
const template = typeof flags.template === 'string' ? flags.template : undefined;
|
|
351
|
+
const out = resolve(typeof flags.out === 'string' ? flags.out : './draft');
|
|
352
|
+
const name = typeof flags.name === 'string' ? flags.name : basenameOf(out);
|
|
353
|
+
const description = typeof flags.description === 'string' ? flags.description : undefined;
|
|
354
|
+
const validated = validateCreateArguments(template, name, description);
|
|
355
|
+
if (validated.error !== undefined) {
|
|
356
|
+
io.err(validated.error);
|
|
357
|
+
io.err(USAGE);
|
|
358
|
+
return 2;
|
|
359
|
+
}
|
|
360
|
+
const refused = writabilityRefusal(out);
|
|
361
|
+
if (refused !== undefined) {
|
|
362
|
+
io.err(refused);
|
|
363
|
+
return 2;
|
|
364
|
+
}
|
|
365
|
+
if (existsSync(out)) {
|
|
366
|
+
io.err(`create refused: the target ${out} already exists; create never overwrites an existing path`);
|
|
367
|
+
return 2;
|
|
368
|
+
}
|
|
369
|
+
const files = templateFiles(validated.template, name, validated.description);
|
|
370
|
+
// The draft must validate under the SHARED validator BEFORE any file lands.
|
|
371
|
+
const draft = files.find(file => file.path === `recipes/${name}.json`);
|
|
372
|
+
const draftDocument = JSON.parse(draft?.data ?? '{}');
|
|
373
|
+
const verdict = judgeRecipe(draftDocument);
|
|
374
|
+
if (!verdict.schemaOk || !verdict.codeOk) {
|
|
375
|
+
io.err('create refused: the generated draft does not validate — this is a template bug, not your input');
|
|
376
|
+
for (const violation of [...verdict.schemaViolations, ...verdict.codeViolations])
|
|
377
|
+
io.err(` ${violation}`);
|
|
378
|
+
return 1;
|
|
379
|
+
}
|
|
380
|
+
const created = [];
|
|
381
|
+
// Cleanup runs ONLY when the chain re-verifies clean at refusal time:
|
|
382
|
+
// deleting through a now-junctioned path could remove VICTIM files.
|
|
383
|
+
const cleanupOwned = () => {
|
|
384
|
+
try {
|
|
385
|
+
if (chainStillClean(out) !== undefined)
|
|
386
|
+
return;
|
|
387
|
+
for (const path of created.reverse())
|
|
388
|
+
rmSync(path, { recursive: true, force: true });
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Best-effort: the refusal already stands; a stuck file reports honestly
|
|
392
|
+
// on the next run instead of being silently deleted through a link.
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
try {
|
|
396
|
+
mkdirSync(out, { recursive: true });
|
|
397
|
+
created.push(out);
|
|
398
|
+
const postMkdir = chainStillClean(out);
|
|
399
|
+
if (postMkdir !== undefined) {
|
|
400
|
+
cleanupOwned();
|
|
401
|
+
io.err(`create refused after creation: ${postMkdir}`);
|
|
402
|
+
return 2;
|
|
403
|
+
}
|
|
404
|
+
for (const file of files) {
|
|
405
|
+
const target = join(out, file.path);
|
|
406
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
407
|
+
const writeRefusal = safeWriteFile(target, file.data);
|
|
408
|
+
if (writeRefusal !== undefined) {
|
|
409
|
+
cleanupOwned();
|
|
410
|
+
io.err(`create refused mid-write: ${writeRefusal}`);
|
|
411
|
+
return 2;
|
|
412
|
+
}
|
|
413
|
+
created.push(target);
|
|
414
|
+
}
|
|
415
|
+
// Post-write verification: if ANY reparse point appeared anywhere in the
|
|
416
|
+
// chain during the run, the command refuses and removes the project it
|
|
417
|
+
// created — but ONLY along a chain that is still verified clean.
|
|
418
|
+
const finalCheck = writabilityRefusal(out);
|
|
419
|
+
if (finalCheck !== undefined) {
|
|
420
|
+
cleanupOwned();
|
|
421
|
+
io.err(`create refused after verification: ${finalCheck}`);
|
|
422
|
+
return 2;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
cleanupOwned();
|
|
427
|
+
io.err(`create failed: ${messageOf(error)}`);
|
|
428
|
+
return 2;
|
|
429
|
+
}
|
|
430
|
+
io.out(`created draft project ${name} at ${out} (template ${validated.template})`);
|
|
431
|
+
io.out(` files: ${files.map(file => file.path).join(', ')}`);
|
|
432
|
+
io.out(' nothing was installed, activated, downloaded or inferred (RCP-08)');
|
|
433
|
+
io.out(` next: tool-mode-recipe validate recipes/${name}.json, then test ., then pack .`);
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
/** The last path component of an absolute or relative directory path. */
|
|
437
|
+
function basenameOf(path) {
|
|
438
|
+
const parts = path.replace(/[\\/]+$/, '').split(/[\\/]/);
|
|
439
|
+
return parts[parts.length - 1] ?? path;
|
|
440
|
+
}
|
|
441
|
+
/** `edit-draft`: a bounded static mutation that must validate before it is written. */
|
|
442
|
+
function editDraftCommand(positional, flags, io) {
|
|
443
|
+
if (positional.length !== 1) {
|
|
444
|
+
io.err('edit-draft needs exactly one recipe file');
|
|
445
|
+
io.err(USAGE);
|
|
446
|
+
return 2;
|
|
447
|
+
}
|
|
448
|
+
const subject = resolve(positional[0]);
|
|
449
|
+
const field = typeof flags.set === 'string' ? flags.set : undefined;
|
|
450
|
+
const value = flags.value;
|
|
451
|
+
if (field === undefined || typeof value !== 'string') {
|
|
452
|
+
io.err('edit-draft needs --set <field> --value <text>');
|
|
453
|
+
io.err(USAGE);
|
|
454
|
+
return 2;
|
|
455
|
+
}
|
|
456
|
+
if (field.startsWith('policy.')) {
|
|
457
|
+
io.err(policyEditRefusal(field));
|
|
458
|
+
return 2;
|
|
459
|
+
}
|
|
460
|
+
// The whitelist lives on a null-prototype map, so prototype-key lookups
|
|
461
|
+
// ('__proto__', 'constructor', 'toString', …) resolve to undefined and hit
|
|
462
|
+
// this refusal instead of traversing Object.prototype.
|
|
463
|
+
const editable = EDITABLE_FIELDS[field];
|
|
464
|
+
if (editable === undefined) {
|
|
465
|
+
io.err(`editing ${JSON.stringify(field)} is refused: only the safe fields are mutable (`
|
|
466
|
+
+ `${Object.keys(EDITABLE_FIELDS).join(', ')}); rename the draft instead of reshaping its identity`);
|
|
467
|
+
return 2;
|
|
468
|
+
}
|
|
469
|
+
const valueError = editable.validate(value);
|
|
470
|
+
if (valueError !== undefined) {
|
|
471
|
+
io.err(`--set ${field} refused: ${valueError}`);
|
|
472
|
+
return 2;
|
|
473
|
+
}
|
|
474
|
+
const refused = writabilityRefusal(subject);
|
|
475
|
+
if (refused !== undefined) {
|
|
476
|
+
io.err(refused);
|
|
477
|
+
return 2;
|
|
478
|
+
}
|
|
479
|
+
const { document, ioError } = readRecipeFile(subject);
|
|
480
|
+
if (ioError !== undefined) {
|
|
481
|
+
io.err(ioError);
|
|
482
|
+
return 2;
|
|
483
|
+
}
|
|
484
|
+
if (typeof document !== 'object' || document === null || Array.isArray(document)) {
|
|
485
|
+
io.err(`${subject} is not a recipe object`);
|
|
486
|
+
return 2;
|
|
487
|
+
}
|
|
488
|
+
const mutated = { ...document };
|
|
489
|
+
// The DRAFT's intermediate shape is validated BEFORE any mutation: a draft
|
|
490
|
+
// whose imageInputs is a primitive refuses by name instead of crashing.
|
|
491
|
+
const shapeRefusal = editable.precondition(mutated);
|
|
492
|
+
if (shapeRefusal !== undefined) {
|
|
493
|
+
io.err(`--set ${field} refused: ${shapeRefusal}; ${subject} is unchanged`);
|
|
494
|
+
return 2;
|
|
495
|
+
}
|
|
496
|
+
editable.apply(mutated, value);
|
|
497
|
+
const verdict = judgeRecipe(mutated);
|
|
498
|
+
const clean = printVerdict(io, subject, verdict);
|
|
499
|
+
if (!clean) {
|
|
500
|
+
io.err(`edit-draft refused: the edited recipe would not validate; ${subject} is unchanged`);
|
|
501
|
+
return 1;
|
|
502
|
+
}
|
|
503
|
+
// Atomic write: chain-verified exclusive temp + rename, so a crash never
|
|
504
|
+
// leaves a truncated draft and a flipped junction cannot redirect the temp.
|
|
505
|
+
const temporary = `${subject}.tmp-${process.pid}-${Date.now().toString(36)}`;
|
|
506
|
+
const tempRefusal = safeWriteFile(temporary, `${JSON.stringify(mutated, null, 2)}\n`);
|
|
507
|
+
if (tempRefusal !== undefined) {
|
|
508
|
+
rmSync(temporary, { force: true });
|
|
509
|
+
io.err(`cannot write ${subject}: ${tempRefusal}`);
|
|
510
|
+
return 2;
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
renameSync(temporary, subject);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
rmSync(temporary, { force: true });
|
|
517
|
+
io.err(`cannot write ${subject}: ${messageOf(error)}`);
|
|
518
|
+
return 2;
|
|
519
|
+
}
|
|
520
|
+
io.out(`edited ${field} of ${subject}; the result validates`);
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
/** `test`: the REAL broker over the scripted fixture provider. */
|
|
524
|
+
async function testCommand(positional, flags, io) {
|
|
525
|
+
if (flags.live === true) {
|
|
526
|
+
io.err('--live is not provided by this CLI build (RCP-08 remainder): a live run requires an explicit deployment '
|
|
527
|
+
+ 'executor profile with consent and limits. This CLI never invokes a real model; use the fixture suite instead.');
|
|
528
|
+
return 2;
|
|
529
|
+
}
|
|
530
|
+
if (positional.length !== 1) {
|
|
531
|
+
io.err('test needs exactly one project directory');
|
|
532
|
+
io.err(USAGE);
|
|
533
|
+
return 2;
|
|
534
|
+
}
|
|
535
|
+
const project = resolve(positional[0]);
|
|
536
|
+
const fixturesPath = resolve(typeof flags.fixtures === 'string' ? flags.fixtures : join(project, 'tests', 'fixtures.json'));
|
|
537
|
+
const recipesDir = join(project, 'recipes');
|
|
538
|
+
if (!existsSync(recipesDir)) {
|
|
539
|
+
io.err(`no recipes directory at ${recipesDir}; create a project first (tool-mode-recipe create)`);
|
|
540
|
+
return 2;
|
|
541
|
+
}
|
|
542
|
+
const recipePaths = readdirSync(recipesDir).filter(entry => entry.endsWith('.json')).sort()
|
|
543
|
+
.map(entry => join(recipesDir, entry));
|
|
544
|
+
if (recipePaths.length === 0) {
|
|
545
|
+
io.err(`no recipe JSON files in ${recipesDir}`);
|
|
546
|
+
return 2;
|
|
547
|
+
}
|
|
548
|
+
if (!existsSync(fixturesPath)) {
|
|
549
|
+
io.err(`no fixtures file at ${fixturesPath}; pass --fixtures or create a project first`);
|
|
550
|
+
return 2;
|
|
551
|
+
}
|
|
552
|
+
let fixture;
|
|
553
|
+
try {
|
|
554
|
+
const parsed = JSON.parse(readFileSync(fixturesPath, 'utf8'));
|
|
555
|
+
const validated = validateFixtureDocument(parsed);
|
|
556
|
+
if (validated.error !== undefined || validated.document === undefined) {
|
|
557
|
+
io.err(`the fixtures file is not usable: ${validated.error ?? 'unknown shape'}`);
|
|
558
|
+
return 2;
|
|
559
|
+
}
|
|
560
|
+
fixture = validated.document;
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
io.err(`the fixtures file is not valid JSON: ${messageOf(error)}`);
|
|
564
|
+
return 2;
|
|
565
|
+
}
|
|
566
|
+
let allPassed = true;
|
|
567
|
+
const failedCases = [];
|
|
568
|
+
for (const path of recipePaths) {
|
|
569
|
+
const { document, ioError } = readRecipeFile(path);
|
|
570
|
+
if (ioError !== undefined) {
|
|
571
|
+
io.err(ioError);
|
|
572
|
+
return 2;
|
|
573
|
+
}
|
|
574
|
+
const verdict = judgeRecipe(document);
|
|
575
|
+
if (!verdict.schemaOk || !verdict.codeOk) {
|
|
576
|
+
io.err(`${path} does not validate; the fixture suite runs only validated recipes`);
|
|
577
|
+
allPassed = false;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
io.out(`testing ${path} against ${fixturesPath}`);
|
|
581
|
+
const verdicts = await runFixtureSuite(verdict.recipe, fixture, io, INITIAL_HOST_POLICY);
|
|
582
|
+
for (const entry of verdicts) {
|
|
583
|
+
if (!entry.pass) {
|
|
584
|
+
allPassed = false;
|
|
585
|
+
failedCases.push(entry.caseId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!allPassed) {
|
|
590
|
+
io.err(`test failed: fixture case(s) ${failedCases.join(', ')} did not match their expected outcomes`);
|
|
591
|
+
return 1;
|
|
592
|
+
}
|
|
593
|
+
io.out('test passed: every fixture case matched its expected outcome against the scripted provider (not a real model)');
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Print the effective bounds for a recipe (RCP-08's describe).
|
|
598
|
+
*
|
|
599
|
+
* The recipe's own ceilings are RES-10's initial maxima by construction (the
|
|
600
|
+
* validator refuses anything above them); the host may narrow further at
|
|
601
|
+
* runtime, which the output says rather than hides.
|
|
602
|
+
*/
|
|
603
|
+
function describeTo(io, recipe) {
|
|
604
|
+
const names = Object.keys(recipe.inputs);
|
|
605
|
+
const required = names.filter(name => recipe.inputs[name]?.required === true);
|
|
606
|
+
const images = names.filter(name => recipe.inputs[name]?.type === 'image');
|
|
607
|
+
io.out(`${recipe.tool.name} (contract ${recipe.tool.contractId}) — recipe ${recipe.id}@${recipe.version}`);
|
|
608
|
+
io.out(` description: ${recipe.description}`);
|
|
609
|
+
io.out(` inputs: ${names.length} (${required.length} required: ${required.join(', ') || '(none)'}`
|
|
610
|
+
+ `${images.length > 0 ? `; ${images.length} image: ${images.join(', ')}` : ''})`);
|
|
611
|
+
io.out(` output ceiling: at most ${recipe.maxOutputTokens} tokens `
|
|
612
|
+
+ `(RES-10 initial maximum ${INITIAL_MAX_OUTPUT_TOKENS}; the host policy may narrow this)`);
|
|
613
|
+
io.out(` image ceiling: at most ${recipe.imageInputs.max} image input(s) `
|
|
614
|
+
+ `(RES-10 initial maximum ${INITIAL_MAX_IMAGE_INPUTS})`);
|
|
615
|
+
io.out(` recording: ${recipe.policy.record ? 'requested' : 'not requested'} — a host policy that `
|
|
616
|
+
+ `requires recording will refuse a ${recipe.policy.record ? 'recorded' : 'record: false'} recipe rather than silently drop it`);
|
|
617
|
+
io.out(` destinations: ${recipe.policy.allowRemote ? 'local or remote, by the host policy' : 'local only, by the recipe'}`);
|
|
618
|
+
io.out(' context: none supplied — a recipe is data; no conversation, tools, attachments or memory are inherited (HLM-05)');
|
|
619
|
+
if (recipe.modelSelection !== undefined) {
|
|
620
|
+
io.out(` executor: approved-profile "${recipe.modelSelection.profile}" (RCP-05) — the operator binds the profile; `
|
|
621
|
+
+ 'an unresolved binding keeps the tool unavailable with EXECUTOR_NOT_CONFIGURED');
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
io.out(' executor: caller-model (RCP-05) — the invoking agent\'s authorized harness model, resolved once per invocation');
|
|
625
|
+
}
|
|
626
|
+
if (recipe.response !== undefined) {
|
|
627
|
+
io.out(` response: ${recipe.response.mode} into payload field "${recipe.response.field}" (RCP-04)`);
|
|
628
|
+
if (recipe.response.mode === 'json') {
|
|
629
|
+
io.out(' disclosure: json mode is prompt-enforced and locally validated in this build — the provider\'s '
|
|
630
|
+
+ 'verified structured-output feature is NOT negotiated (RCP-04\'s disclosure requirement)');
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
io.out(' response: plain text payload (RCP-04)');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/** The declarative-manifest template, mirroring the shipped declarative package. */
|
|
638
|
+
function manifestFor(name, version, description, recipes) {
|
|
639
|
+
return {
|
|
640
|
+
protocolVersion: '1.1.0',
|
|
641
|
+
id: name,
|
|
642
|
+
version,
|
|
643
|
+
displayName: name,
|
|
644
|
+
description,
|
|
645
|
+
license: 'MIT',
|
|
646
|
+
activation: { policy: 'operator', deactivatable: true },
|
|
647
|
+
runtimeApi: 1,
|
|
648
|
+
compatibility: {
|
|
649
|
+
hostApi: '^1.1.0',
|
|
650
|
+
requiredFeatures: ['declarative-recipes-v1'],
|
|
651
|
+
},
|
|
652
|
+
execution: { kind: 'declarative' },
|
|
653
|
+
permissions: {
|
|
654
|
+
networkOrigins: [],
|
|
655
|
+
inputFiles: false,
|
|
656
|
+
storage: 'none',
|
|
657
|
+
secrets: [],
|
|
658
|
+
nativeCode: false,
|
|
659
|
+
},
|
|
660
|
+
models: [],
|
|
661
|
+
tools: [],
|
|
662
|
+
implementations: recipes.map(recipe => ({
|
|
663
|
+
id: recipe.id,
|
|
664
|
+
contractId: recipe.tool.contractId,
|
|
665
|
+
contractVersion: '1.0.0',
|
|
666
|
+
recipeVersion: recipe.version,
|
|
667
|
+
})),
|
|
668
|
+
skills: [],
|
|
669
|
+
licenses: [
|
|
670
|
+
{
|
|
671
|
+
layer: 'code',
|
|
672
|
+
name: 'MIT',
|
|
673
|
+
url: 'https://opensource.org/license/mit',
|
|
674
|
+
openSource: true,
|
|
675
|
+
redistribution: 'permitted',
|
|
676
|
+
attributionRequired: false,
|
|
677
|
+
obligations: [],
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
layer: 'weights',
|
|
681
|
+
name: 'none: the package carries no weights',
|
|
682
|
+
url: 'https://example.invalid/none',
|
|
683
|
+
openSource: false,
|
|
684
|
+
redistribution: 'not-permitted',
|
|
685
|
+
attributionRequired: false,
|
|
686
|
+
obligations: ['the recipes reference a harness model supplied by the deployment, so no weights are distributed here'],
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
layer: 'engine',
|
|
690
|
+
name: 'none: the host broker is the engine',
|
|
691
|
+
url: 'https://example.invalid/none',
|
|
692
|
+
openSource: false,
|
|
693
|
+
redistribution: 'not-permitted',
|
|
694
|
+
attributionRequired: false,
|
|
695
|
+
obligations: ['execution happens in the host broker, which is the point of a declarative package'],
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
layer: 'sdk',
|
|
699
|
+
name: 'none: a declarative package links no SDK at runtime',
|
|
700
|
+
url: 'https://example.invalid/none',
|
|
701
|
+
openSource: false,
|
|
702
|
+
redistribution: 'not-permitted',
|
|
703
|
+
attributionRequired: false,
|
|
704
|
+
obligations: ['the host needs no runtime link for a package that ships no module'],
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
layer: 'conversion-artifacts',
|
|
708
|
+
name: 'none: no conversion is performed',
|
|
709
|
+
url: 'https://example.invalid/none',
|
|
710
|
+
openSource: false,
|
|
711
|
+
redistribution: 'permitted',
|
|
712
|
+
attributionRequired: false,
|
|
713
|
+
obligations: [],
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
network: { requiredTelemetry: false, otherNetworkUse: false },
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/** Run `pack`, emitting a package directory and a store-method zip. */
|
|
720
|
+
function packCommand(subject, options, io) {
|
|
721
|
+
const out = resolve(options.out ?? './dist');
|
|
722
|
+
let name = options.name;
|
|
723
|
+
let version = options.version;
|
|
724
|
+
const recipePaths = [];
|
|
725
|
+
const isFile = existsSync(subject) && statSync(subject).isFile();
|
|
726
|
+
if (isFile) {
|
|
727
|
+
recipePaths.push(resolve(subject));
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
const dir = resolve(subject);
|
|
731
|
+
if (!existsSync(dir)) {
|
|
732
|
+
io.err(`no recipe or project directory at ${subject}`);
|
|
733
|
+
return 2;
|
|
734
|
+
}
|
|
735
|
+
const packagePath = join(dir, 'package.json');
|
|
736
|
+
if (existsSync(packagePath)) {
|
|
737
|
+
let manifest = {};
|
|
738
|
+
try {
|
|
739
|
+
manifest = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
io.err(`the project package.json is not valid JSON: ${messageOf(error)}`);
|
|
743
|
+
return 2;
|
|
744
|
+
}
|
|
745
|
+
if (name === undefined && typeof manifest.name === 'string')
|
|
746
|
+
name = manifest.name;
|
|
747
|
+
if (version === undefined && typeof manifest.version === 'string')
|
|
748
|
+
version = manifest.version;
|
|
749
|
+
}
|
|
750
|
+
const recipesDir = join(dir, 'recipes');
|
|
751
|
+
const source = existsSync(recipesDir) ? recipesDir : dir;
|
|
752
|
+
for (const entry of readdirSync(source).sort()) {
|
|
753
|
+
if (!entry.endsWith('.json'))
|
|
754
|
+
continue;
|
|
755
|
+
if (source === dir && (entry === 'package.json' || entry === 'tool-mode.extension.json'))
|
|
756
|
+
continue;
|
|
757
|
+
recipePaths.push(join(source, entry));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (name === undefined || !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(name)) {
|
|
761
|
+
io.err('pack needs a package name: pass --name <pkg> or pack a project directory with a package.json');
|
|
762
|
+
return 2;
|
|
763
|
+
}
|
|
764
|
+
if (recipePaths.length === 0) {
|
|
765
|
+
io.err('no recipe JSON files found to pack');
|
|
766
|
+
return 2;
|
|
767
|
+
}
|
|
768
|
+
const recipes = [];
|
|
769
|
+
let clean = true;
|
|
770
|
+
for (const path of recipePaths) {
|
|
771
|
+
const { document, ioError } = readRecipeFile(path);
|
|
772
|
+
if (ioError !== undefined) {
|
|
773
|
+
io.err(ioError);
|
|
774
|
+
clean = false;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const verdict = judgeRecipe(document);
|
|
778
|
+
const fileClean = printVerdict(io, path, verdict);
|
|
779
|
+
if (!fileClean) {
|
|
780
|
+
clean = false;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
// Two recipes claiming one tool name would publish two implementations of
|
|
784
|
+
// one contract under one manifest; the packer refuses rather than guessing.
|
|
785
|
+
const recipe = verdict.recipe;
|
|
786
|
+
const clash = recipes.find(existing => existing.tool.name === recipe.tool.name);
|
|
787
|
+
if (clash !== undefined) {
|
|
788
|
+
io.err(`${path} claims the tool "${recipe.tool.name}" already claimed by recipe ${clash.id}; `
|
|
789
|
+
+ 'one package serves one implementation per tool');
|
|
790
|
+
clean = false;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
recipes.push(recipe);
|
|
794
|
+
}
|
|
795
|
+
if (!clean) {
|
|
796
|
+
io.err('pack refused: the production artifact is only packed from recipes that validate');
|
|
797
|
+
return 1;
|
|
798
|
+
}
|
|
799
|
+
const packageVersion = version ?? (recipes.length === 1 ? recipes[0]?.version : undefined) ?? '0.1.0';
|
|
800
|
+
const description = options.description
|
|
801
|
+
?? `Declarative recipe package ${name}: ${recipes.map(recipe => recipe.tool.name).join(', ')}.`;
|
|
802
|
+
const files = [];
|
|
803
|
+
files.push({
|
|
804
|
+
path: 'package.json',
|
|
805
|
+
data: `${JSON.stringify({
|
|
806
|
+
name,
|
|
807
|
+
version: packageVersion,
|
|
808
|
+
type: 'module',
|
|
809
|
+
license: 'MIT',
|
|
810
|
+
description,
|
|
811
|
+
files: ['tool-mode.extension.json', 'recipes'],
|
|
812
|
+
toolMode: { manifest: './tool-mode.extension.json' },
|
|
813
|
+
}, null, 2)}\n`,
|
|
814
|
+
});
|
|
815
|
+
files.push({
|
|
816
|
+
path: 'tool-mode.extension.json',
|
|
817
|
+
data: `${JSON.stringify(manifestFor(name, packageVersion, description, recipes), null, 2)}\n`,
|
|
818
|
+
});
|
|
819
|
+
for (const recipe of recipes) {
|
|
820
|
+
// Pack emits the CANONICAL RCP-02 shape: the packaged artifact is the
|
|
821
|
+
// vocabulary the spec publishes, and the lossless binding constraints keep
|
|
822
|
+
// the canonical round trip identical to the frozen recipe.
|
|
823
|
+
files.push({ path: `recipes/${recipe.id}.json`, data: `${JSON.stringify(toCanonicalRecipe(recipe), null, 2)}\n` });
|
|
824
|
+
}
|
|
825
|
+
const packageDir = join(out, `${name.replaceAll('/', '__')}-${packageVersion}`);
|
|
826
|
+
mkdirSync(packageDir, { recursive: true });
|
|
827
|
+
for (const file of files) {
|
|
828
|
+
const target = join(packageDir, file.path);
|
|
829
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
830
|
+
writeFileSync(target, file.data, 'utf8');
|
|
831
|
+
}
|
|
832
|
+
const zipPath = `${packageDir}.zip`;
|
|
833
|
+
writeFileSync(zipPath, writeZip(files.map(file => ({
|
|
834
|
+
path: file.path,
|
|
835
|
+
data: Buffer.from(file.data, 'utf8'),
|
|
836
|
+
}))));
|
|
837
|
+
io.out(`packed ${name}@${packageVersion}`);
|
|
838
|
+
io.out(` package: ${packageDir}`);
|
|
839
|
+
io.out(` archive: ${zipPath} (store-method zip, consumable by the extension install path)`);
|
|
840
|
+
io.out(` contents: ${files.map(file => file.path).join(', ')}`);
|
|
841
|
+
io.out(' NO executable code ships, and no lifecycle scripts were run (RCP-08)');
|
|
842
|
+
return 0;
|
|
843
|
+
}
|
|
844
|
+
/** Best-effort message from an arbitrary thrown value. */
|
|
845
|
+
function messageOf(error) {
|
|
846
|
+
try {
|
|
847
|
+
return error instanceof Error ? error.message : String(error);
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return '<unrenderable>';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// ---------------------------------------------------------------------------
|
|
854
|
+
// A minimal, dependency-free ZIP writer (STORE method, deterministic order).
|
|
855
|
+
// ---------------------------------------------------------------------------
|
|
856
|
+
/** The CRC-32 table and update, for the store-method entries. */
|
|
857
|
+
const CRC_TABLE = (() => {
|
|
858
|
+
const table = new Uint32Array(256);
|
|
859
|
+
for (let n = 0; n < 256; n += 1) {
|
|
860
|
+
let c = n;
|
|
861
|
+
for (let k = 0; k < 8; k += 1)
|
|
862
|
+
c = (c & 1) !== 0 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
863
|
+
table[n] = c >>> 0;
|
|
864
|
+
}
|
|
865
|
+
return table;
|
|
866
|
+
})();
|
|
867
|
+
function crc32(bytes) {
|
|
868
|
+
let crc = 0xffffffff;
|
|
869
|
+
for (const byte of bytes)
|
|
870
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
871
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
872
|
+
}
|
|
873
|
+
/** Write a STORE-method zip over the given files, in the given order. */
|
|
874
|
+
function writeZip(files) {
|
|
875
|
+
const chunks = [];
|
|
876
|
+
const central = [];
|
|
877
|
+
let offset = 0;
|
|
878
|
+
const put = (buffer) => {
|
|
879
|
+
chunks.push(buffer);
|
|
880
|
+
offset += buffer.byteLength;
|
|
881
|
+
};
|
|
882
|
+
for (const file of files) {
|
|
883
|
+
const name = Buffer.from(file.path, 'utf8');
|
|
884
|
+
const crc = crc32(new Uint8Array(file.data));
|
|
885
|
+
const header = Buffer.alloc(30);
|
|
886
|
+
header.writeUInt32LE(0x04034b50, 0);
|
|
887
|
+
header.writeUInt16LE(20, 4); // version needed
|
|
888
|
+
header.writeUInt16LE(0, 6); // flags
|
|
889
|
+
header.writeUInt16LE(0, 8); // STORE
|
|
890
|
+
header.writeUInt16LE(0, 10); // time
|
|
891
|
+
header.writeUInt16LE(0x21, 12); // date (1980-01-01, deterministic)
|
|
892
|
+
header.writeUInt32LE(crc, 14);
|
|
893
|
+
header.writeUInt32LE(file.data.byteLength, 18);
|
|
894
|
+
header.writeUInt32LE(file.data.byteLength, 22);
|
|
895
|
+
header.writeUInt16LE(name.byteLength, 26);
|
|
896
|
+
header.writeUInt16LE(0, 28); // extra length
|
|
897
|
+
put(header);
|
|
898
|
+
put(name);
|
|
899
|
+
put(file.data);
|
|
900
|
+
const entry = Buffer.alloc(46);
|
|
901
|
+
entry.writeUInt32LE(0x02014b50, 0);
|
|
902
|
+
entry.writeUInt16LE(20, 4); // version made by
|
|
903
|
+
entry.writeUInt16LE(20, 6); // version needed
|
|
904
|
+
entry.writeUInt16LE(0, 8);
|
|
905
|
+
entry.writeUInt16LE(0, 10);
|
|
906
|
+
entry.writeUInt16LE(0, 12);
|
|
907
|
+
entry.writeUInt16LE(0x21, 14);
|
|
908
|
+
entry.writeUInt32LE(crc, 16);
|
|
909
|
+
entry.writeUInt32LE(file.data.byteLength, 20);
|
|
910
|
+
entry.writeUInt32LE(file.data.byteLength, 24);
|
|
911
|
+
entry.writeUInt16LE(name.byteLength, 28);
|
|
912
|
+
entry.writeUInt16LE(0, 30);
|
|
913
|
+
entry.writeUInt16LE(0, 32);
|
|
914
|
+
entry.writeUInt16LE(0, 34);
|
|
915
|
+
entry.writeUInt16LE(0, 36); // external attrs
|
|
916
|
+
entry.writeUInt32LE(offset - header.byteLength - name.byteLength - file.data.byteLength, 42); // local header offset
|
|
917
|
+
central.push(entry);
|
|
918
|
+
central.push(name);
|
|
919
|
+
}
|
|
920
|
+
const centralSize = central.reduce((sum, buffer) => sum + buffer.byteLength, 0);
|
|
921
|
+
const centralStart = offset;
|
|
922
|
+
for (const buffer of central)
|
|
923
|
+
put(buffer);
|
|
924
|
+
const end = Buffer.alloc(22);
|
|
925
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
926
|
+
end.writeUInt16LE(0, 4);
|
|
927
|
+
end.writeUInt16LE(0, 6);
|
|
928
|
+
end.writeUInt16LE(files.length, 8);
|
|
929
|
+
end.writeUInt16LE(files.length, 10);
|
|
930
|
+
end.writeUInt32LE(centralSize, 12);
|
|
931
|
+
end.writeUInt32LE(centralStart, 16);
|
|
932
|
+
end.writeUInt16LE(0, 20);
|
|
933
|
+
put(end);
|
|
934
|
+
return Buffer.concat(chunks);
|
|
935
|
+
}
|
|
936
|
+
//# sourceMappingURL=cli.js.map
|