@field/cascade 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/cli/{chunk-OM6BEME6.js → chunk-2L34ERS2.js} +3 -3
- package/dist/cli/{chunk-3RXL5GHB.js → chunk-JEW5MPGI.js} +4 -4
- package/dist/cli/{chunk-ST6LROUG.js → chunk-OBZAP3GG.js} +4 -4
- package/dist/cli/{chunk-73S4JZXK.js → chunk-QAGATJGE.js} +5 -5
- package/dist/cli/{chunk-MP653A5O.js → chunk-QKL7CCRS.js} +84 -10
- package/dist/cli/{chunk-2JEU3MGB.js → chunk-W5PJ5RMP.js} +155 -83
- package/dist/cli/{chunk-EU4ZFZ7Y.js → chunk-W7OUAT7J.js} +2 -2
- package/dist/cli/{cliCommands-JJN5Z3ZC.js → cliCommands-XVFRD7CF.js} +3 -3
- package/dist/cli/{commands-PHYFYA3F.js → commands-M2XLTPTZ.js} +7 -7
- package/dist/cli/{doctorCommand-ORWQRNYP.js → doctorCommand-RAEI7GVY.js} +2 -2
- package/dist/cli/{editorCommands-NFR32JOY.js → editorCommands-5BBG4S6V.js} +6 -6
- package/dist/cli/{graphCommands-EOUOBOIJ.js → graphCommands-MULQ7VD2.js} +2 -2
- package/dist/cli/{graphFile-YTGJB5F5.js → graphFile-CITF36ZM.js} +3 -3
- package/dist/cli/{helpCommand-PULH6ZYB.js → helpCommand-X2QDANM3.js} +2 -2
- package/dist/cli/index.js +13 -13
- package/dist/cli/{mcp-JVZFQOEX.js → mcp-YRIGRLYQ.js} +3 -3
- package/dist/cli/{parity-X6DEH2DV.js → parity-3N3HTKWX.js} +4 -4
- package/dist/cli/{project-VYGIA7JX.js → project-SJF5ZF3F.js} +2 -2
- package/dist/cli/{projectRuntime-2Q4JKEH3.js → projectRuntime-NY4XMDFH.js} +3 -3
- package/dist/cli/{render-PT4MWI6U.js → render-Y23W6MY6.js} +10 -5
- package/dist/cli/{runCommand-HNPQVZMS.js → runCommand-XOG7AVC5.js} +3 -3
- package/dist/cli/{runner-5XPXVW7J.js → runner-DNJO5WO2.js} +5 -5
- package/dist/cli/{share-MHF6TNRK.js → share-B7HTCOJX.js} +5 -5
- package/dist/cli/{webBuild-WPPMGGW7.js → webBuild-2K4COCGY.js} +4 -4
- package/dist/contracts/capabilities.d.ts +21 -0
- package/dist/player/app.js +8 -5
- package/dist/player/embed.js +2 -0
- package/dist/player/types.d.ts +6 -0
- package/dist/runtime/definition/architecture.js +89 -0
- package/doc/PROJECT_AUTHORING.md +60 -1
- package/doc/WEB_PLAYER.md +9 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -138,7 +138,7 @@ Definition rules:
|
|
|
138
138
|
- Vector props support `min`, `max`, and `step`, shared across components. Props may declare a default `expression`; an explicitly saved value overrides that default, including when it equals the numeric default.
|
|
139
139
|
- WebGPU nodes declare `gpu` and use its shared device and per-node `cache`. Portable GPU nodes can run through optional Dawn in the CLI; browser-only declarations remain refused. Import constants and explicit `readTexture` (RGBA8 only) from `cascade/gpu`. Graph texture exchange remains future work. See `doc/HEADLESS_GPU.md` for bounded agent renders, diagnostics and hardware tests.
|
|
140
140
|
- Treat graph wires as the primary data boundary. A node may observe only its connected inputs, its props, and declared host capabilities; it must not inspect sibling nodes or the graph to discover data.
|
|
141
|
-
- Deterministic node modules may not retain module-level mutable values, access ambient state such as `globalThis`, browser storage, or `process`, run top-level effects, or import sibling node implementations. `cascade check` reports these as `architecture/*` diagnostics.
|
|
141
|
+
- Deterministic node modules may not retain module-level mutable values, access ambient state such as `globalThis`, browser storage, or `process`, run top-level effects, or import sibling node implementations. `cascade check` reports these as `architecture/*` diagnostics. Pure module-level helpers are allowed: function declarations, and `const` values that cannot change (literals and arithmetic, `Math.PI`, arrow functions, arrays and objects under `as const` or `Object.freeze`). `let`, `new Map()`, a bare `[]`/`{}`, a regex or any other call is module state.
|
|
142
142
|
- A cache may memoize computation from wired inputs, but it must never supply unwired data. Make cache use an explicitly labelled prop and keep cache keys derived from the complete wired input and relevant props.
|
|
143
143
|
- Keep model/provider integrations in the project. Cascade core has no Google,
|
|
144
144
|
Anthropic, OpenAI, or generic `ai` capability. Use normal typed outputs such
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,56 @@ Notable changes to Cascade. Newest first.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.13.0 — 2026-11-09
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Browser player without chrome.** `mount(…, { chrome: false })`, or
|
|
12
|
+
`player.html?chrome=0` in a plain iframe, shows the output alone on a
|
|
13
|
+
transparent page: no dark frame, Play button or frame field. See
|
|
14
|
+
`doc/WEB_PLAYER.md`.
|
|
15
|
+
- **Read-only roots outside the project.** `"reads": ["../other/data"]` in
|
|
16
|
+
`cascade.json` lets file reads (`files`, `cascade/io`, Studio media) resolve
|
|
17
|
+
into another folder by `..` or a symlink; writes there are still refused,
|
|
18
|
+
and any other path out of the project still is. See "Reading another
|
|
19
|
+
project's data" in `doc/PROJECT_AUTHORING.md`.
|
|
20
|
+
- **Named exports and a keyed cache for nodes.** `"writes": ["renders"]` in
|
|
21
|
+
`cascade.json` lets the `files` capability write stably named files into a
|
|
22
|
+
declared project folder beside `.cascade-cache/`. `files.cache.get(key)` /
|
|
23
|
+
`files.cache.put(key, data)` (contract `KeyedCache`) keep entries under
|
|
24
|
+
`.cascade-cache/keyed/<key>`, found again by key on the next run, where
|
|
25
|
+
`cascade/io` names carry a content hash. `files.write` is now atomic, and
|
|
26
|
+
normalises before its folder test, so `.cascade-cache/../x` is refused.
|
|
27
|
+
- **Node files may hold immutable module-level constants.** Helper functions
|
|
28
|
+
were already allowed; `architecture/module-state` now also passes `const`s
|
|
29
|
+
that cannot change (`const TAU = Math.PI * 2`, `[...] as const`,
|
|
30
|
+
`Object.freeze({...})`, arrow-function helpers). `let`, `new Map()`, a bare
|
|
31
|
+
array or object, a regex and other calls are still refused.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **The browser player re-cooks only what changed.** `setProp`, `setInput`
|
|
36
|
+
and each frame now cook the changed node and what lies downstream of it;
|
|
37
|
+
an unchanged upstream node keeps its outputs (`reuse: "strict"`), so a
|
|
38
|
+
paint prop no longer re-runs a slow data node above it.
|
|
39
|
+
- **`cascade render --workers` accepts a piece animated on twos.** Two frames
|
|
40
|
+
whose node saved the same drawing land on one content-named cache file
|
|
41
|
+
(`.cascade-cache/<node>.<hash>.png`); that is the same bytes, not a
|
|
42
|
+
clobber, so the fixed-path probe no longer refuses it.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **`FileCapability` has a required `cache: KeyedCache` member.** A custom
|
|
47
|
+
host that implements the `files` capability itself must now provide
|
|
48
|
+
`cache.get(key)` and `cache.put(key, data)`; it no longer type-checks
|
|
49
|
+
without them. Studio and the headless host already do.
|
|
50
|
+
|
|
51
|
+
### Known gaps
|
|
52
|
+
|
|
53
|
+
- **Ramp's stop editor and Image's file picker are not in Studio yet.** The
|
|
54
|
+
definition-v1 port of the image nodes left them out. Graphs that already
|
|
55
|
+
set them render unchanged.
|
|
56
|
+
|
|
7
57
|
## 0.12.0 — 2026-11-09
|
|
8
58
|
|
|
9
59
|
### Added
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ cascade . # reopen it later
|
|
|
36
36
|
cascade run index.cascade --frames 1 --json # render headlessly
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The npm package is at 0.
|
|
39
|
+
The npm package is at 0.13.0 and includes every command below. 0.13 adds the chrome-free player, read-only roots and named writes in `cascade.json`, a keyed node cache and module-level constants in node files; see the [changelog](CHANGELOG.md).
|
|
40
40
|
|
|
41
41
|
## New in 0.9: shareable editors
|
|
42
42
|
|
|
@@ -3,14 +3,14 @@ import {
|
|
|
3
3
|
moduleId,
|
|
4
4
|
projectModuleFile,
|
|
5
5
|
validateNodeModuleArchitecture
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-QKL7CCRS.js";
|
|
7
7
|
import {
|
|
8
8
|
builtinNodeRegistration
|
|
9
9
|
} from "./chunk-MI5JLQYA.js";
|
|
10
10
|
import {
|
|
11
11
|
ProjectRoot,
|
|
12
12
|
extractNodeDefinition
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
14
14
|
|
|
15
15
|
// src/cli/webBuild.ts
|
|
16
16
|
import fs from "node:fs/promises";
|
|
@@ -264,4 +264,4 @@ export {
|
|
|
264
264
|
inside,
|
|
265
265
|
exists
|
|
266
266
|
};
|
|
267
|
-
//# sourceMappingURL=chunk-
|
|
267
|
+
//# sourceMappingURL=chunk-2L34ERS2.js.map
|
|
@@ -5,21 +5,21 @@ import {
|
|
|
5
5
|
inside,
|
|
6
6
|
playerFile,
|
|
7
7
|
staticRuntimePlugin
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-2L34ERS2.js";
|
|
9
9
|
import {
|
|
10
10
|
PREFLIGHT_WARNING_CODES,
|
|
11
11
|
createRuntime,
|
|
12
12
|
moduleId,
|
|
13
13
|
projectModuleFile,
|
|
14
14
|
validateNodeModuleArchitecture
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-QKL7CCRS.js";
|
|
16
16
|
import {
|
|
17
17
|
builtinNodeRegistration
|
|
18
18
|
} from "./chunk-MI5JLQYA.js";
|
|
19
19
|
import {
|
|
20
20
|
ProjectRoot,
|
|
21
21
|
extractNodeDefinition
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
23
23
|
|
|
24
24
|
// packages/runtime/src/editor/index.ts
|
|
25
25
|
var SCENE_FORMAT = "cascade.scene";
|
|
@@ -813,4 +813,4 @@ export {
|
|
|
813
813
|
scanForLeaks,
|
|
814
814
|
deployVercel
|
|
815
815
|
};
|
|
816
|
-
//# sourceMappingURL=chunk-
|
|
816
|
+
//# sourceMappingURL=chunk-JEW5MPGI.js.map
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
runDeterministicProjectGraph,
|
|
28
28
|
saveStillImages,
|
|
29
29
|
validateProjectGraph
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-QKL7CCRS.js";
|
|
31
31
|
import {
|
|
32
32
|
graphSeed
|
|
33
33
|
} from "./chunk-L6B6FUQP.js";
|
|
@@ -37,14 +37,14 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
compileEmbedded,
|
|
39
39
|
compileProjectModule
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-W7OUAT7J.js";
|
|
41
41
|
import {
|
|
42
42
|
cascadeVersion,
|
|
43
43
|
formatStageTrace
|
|
44
44
|
} from "./chunk-5ORUQ6ES.js";
|
|
45
45
|
import {
|
|
46
46
|
ProjectRoot
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
48
48
|
|
|
49
49
|
// src/cli/runner.ts
|
|
50
50
|
import * as fs from "fs/promises";
|
|
@@ -264,4 +264,4 @@ export {
|
|
|
264
264
|
renderSuggestion,
|
|
265
265
|
runGraph
|
|
266
266
|
};
|
|
267
|
-
//# sourceMappingURL=chunk-
|
|
267
|
+
//# sourceMappingURL=chunk-OBZAP3GG.js.map
|
|
@@ -7,14 +7,14 @@ import {
|
|
|
7
7
|
formatPlan,
|
|
8
8
|
fromBase64,
|
|
9
9
|
parseScene
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JEW5MPGI.js";
|
|
11
11
|
import {
|
|
12
12
|
cookProjectGraph,
|
|
13
13
|
installProjectIo
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-QKL7CCRS.js";
|
|
15
15
|
import {
|
|
16
16
|
ProjectRoot
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
18
18
|
|
|
19
19
|
// src/cli/editorCommands.ts
|
|
20
20
|
import fs from "node:fs/promises";
|
|
@@ -34,7 +34,7 @@ Shareable editors (see doc/EDITOR_EXPORT.md):
|
|
|
34
34
|
async function runEditorCommand(args) {
|
|
35
35
|
const [command, graph] = args;
|
|
36
36
|
if (command === "share") {
|
|
37
|
-
const { runShareCommand } = await import("./share-
|
|
37
|
+
const { runShareCommand } = await import("./share-B7HTCOJX.js");
|
|
38
38
|
return runShareCommand(args);
|
|
39
39
|
}
|
|
40
40
|
if (!graph || graph.startsWith("-")) throw new Error(`cascade ${command} requires a graph file`);
|
|
@@ -333,4 +333,4 @@ export {
|
|
|
333
333
|
runEditorCommand,
|
|
334
334
|
sceneDocument
|
|
335
335
|
};
|
|
336
|
-
//# sourceMappingURL=chunk-
|
|
336
|
+
//# sourceMappingURL=chunk-QAGATJGE.js.map
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
import {
|
|
63
63
|
ProjectRoot,
|
|
64
64
|
extractNodeDefinition
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
66
66
|
import {
|
|
67
67
|
CAPABILITIES_BY_ENVIRONMENT,
|
|
68
68
|
validateNodeDefinition
|
|
@@ -2237,6 +2237,9 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
|
|
|
2237
2237
|
(statement) => ts2.isVariableStatement(statement) && isDefinition(statement)
|
|
2238
2238
|
);
|
|
2239
2239
|
for (const statement of file.statements) {
|
|
2240
|
+
if (ts2.isVariableStatement(statement) && !isDefinition(statement) && !supersededByDefinition(statement) && isPureConstant(statement)) {
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2240
2243
|
if (ts2.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
2241
2244
|
const superseded = supersededByDefinition(statement);
|
|
2242
2245
|
add(
|
|
@@ -2301,6 +2304,48 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
|
|
|
2301
2304
|
if (!declaration || !ts2.isIdentifier(declaration.name)) return null;
|
|
2302
2305
|
return declaration.name.text === "icon" || declaration.name.text === "runsOn" ? declaration.name.text : null;
|
|
2303
2306
|
}
|
|
2307
|
+
function isPureConstant(statement) {
|
|
2308
|
+
if (!(statement.declarationList.flags & ts2.NodeFlags.Const)) return false;
|
|
2309
|
+
return statement.declarationList.declarations.every(
|
|
2310
|
+
(declaration) => ts2.isIdentifier(declaration.name) && declaration.initializer !== void 0 && isPure(declaration.initializer, false)
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
function isPure(node, frozen) {
|
|
2314
|
+
if (ts2.isNumericLiteral(node) || ts2.isStringLiteral(node) || ts2.isBigIntLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node) || node.kind === ts2.SyntaxKind.TrueKeyword || node.kind === ts2.SyntaxKind.FalseKeyword || node.kind === ts2.SyntaxKind.NullKeyword) return true;
|
|
2315
|
+
if (ts2.isIdentifier(node)) return true;
|
|
2316
|
+
if (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node)) return true;
|
|
2317
|
+
if (ts2.isParenthesizedExpression(node)) return isPure(node.expression, frozen);
|
|
2318
|
+
if (ts2.isAsExpression(node))
|
|
2319
|
+
return isPure(node.expression, frozen || isConstAssertion(node.type));
|
|
2320
|
+
if (ts2.isSatisfiesExpression(node) || ts2.isNonNullExpression(node) || ts2.isTypeAssertionExpression(node))
|
|
2321
|
+
return isPure(node.expression, frozen);
|
|
2322
|
+
if (ts2.isPrefixUnaryExpression(node))
|
|
2323
|
+
return node.operator !== ts2.SyntaxKind.PlusPlusToken && node.operator !== ts2.SyntaxKind.MinusMinusToken && isPure(node.operand, false);
|
|
2324
|
+
if (ts2.isBinaryExpression(node))
|
|
2325
|
+
return !isAssignment(node.operatorToken.kind) && isPure(node.left, false) && isPure(node.right, false);
|
|
2326
|
+
if (ts2.isConditionalExpression(node))
|
|
2327
|
+
return isPure(node.condition, false) && isPure(node.whenTrue, frozen) && isPure(node.whenFalse, frozen);
|
|
2328
|
+
if (ts2.isTemplateExpression(node))
|
|
2329
|
+
return node.templateSpans.every((span) => isPure(span.expression, false));
|
|
2330
|
+
if (ts2.isPropertyAccessExpression(node)) return isPure(node.expression, false);
|
|
2331
|
+
if (ts2.isElementAccessExpression(node))
|
|
2332
|
+
return isPure(node.expression, false) && isPure(node.argumentExpression, false);
|
|
2333
|
+
if (ts2.isArrayLiteralExpression(node))
|
|
2334
|
+
return frozen && node.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, true));
|
|
2335
|
+
if (ts2.isObjectLiteralExpression(node))
|
|
2336
|
+
return frozen && node.properties.every((property) => ts2.isPropertyAssignment(property) ? !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, true) : ts2.isShorthandPropertyAssignment(property));
|
|
2337
|
+
if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression) && ts2.isIdentifier(node.expression.expression) && node.expression.expression.text === "Object" && node.expression.name.text === "freeze" && node.arguments.length === 1) {
|
|
2338
|
+
const [argument] = node.arguments;
|
|
2339
|
+
return ts2.isArrayLiteralExpression(argument) ? argument.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, false)) : ts2.isObjectLiteralExpression(argument) ? argument.properties.every((property) => ts2.isPropertyAssignment(property) && !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, false)) : false;
|
|
2340
|
+
}
|
|
2341
|
+
return false;
|
|
2342
|
+
}
|
|
2343
|
+
function isConstAssertion(type) {
|
|
2344
|
+
return ts2.isTypeReferenceNode(type) && ts2.isIdentifier(type.typeName) && type.typeName.text === "const";
|
|
2345
|
+
}
|
|
2346
|
+
function isAssignment(kind) {
|
|
2347
|
+
return kind >= ts2.SyntaxKind.FirstAssignment && kind <= ts2.SyntaxKind.LastAssignment;
|
|
2348
|
+
}
|
|
2304
2349
|
function isDefinition(statement) {
|
|
2305
2350
|
return Boolean(statement.declarationList.flags & ts2.NodeFlags.Const) && Boolean(statement.modifiers?.some((item) => item.kind === ts2.SyntaxKind.ExportKeyword)) && statement.declarationList.declarations.length === 1 && ts2.isIdentifier(statement.declarationList.declarations[0].name) && statement.declarationList.declarations[0].name.text === "definition";
|
|
2306
2351
|
}
|
|
@@ -2337,25 +2382,41 @@ function normalize(value) {
|
|
|
2337
2382
|
}
|
|
2338
2383
|
|
|
2339
2384
|
// src/cli/nodeFiles.ts
|
|
2385
|
+
import { randomBytes } from "node:crypto";
|
|
2340
2386
|
import fs from "node:fs/promises";
|
|
2341
2387
|
import path from "node:path";
|
|
2388
|
+
var KEYED_DIR = ".cascade-cache/keyed";
|
|
2389
|
+
var KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
|
|
2342
2390
|
function createNodeFileCapability(project) {
|
|
2343
2391
|
const within2 = (relative, what) => {
|
|
2344
2392
|
try {
|
|
2345
|
-
|
|
2393
|
+
const clean = relative.replace(/^\.?\//, "");
|
|
2394
|
+
return what === "writing" ? project.resolve(clean) : project.resolveMedia(clean);
|
|
2346
2395
|
} catch (error) {
|
|
2347
2396
|
throw new Error(
|
|
2348
2397
|
`files: ${what} outside the project is refused \u2014 ${relative} (${error instanceof Error ? error.message : String(error)})`
|
|
2349
2398
|
);
|
|
2350
2399
|
}
|
|
2351
2400
|
};
|
|
2352
|
-
const
|
|
2401
|
+
const writable = (relative) => {
|
|
2353
2402
|
const clean = relative.replace(/^\.?\//, "");
|
|
2354
|
-
|
|
2403
|
+
const full = within2(clean, "writing");
|
|
2404
|
+
if (!project.writeFolderOf(clean))
|
|
2355
2405
|
throw new Error(
|
|
2356
|
-
`files: writes are confined to .cascade-cache/ \u2014 got ${clean}. A node's output is derived data
|
|
2406
|
+
`files: writes are confined to .cascade-cache/ and the folders cascade.json "writes" declares \u2014 got ${clean}. A node's output is derived data; declare an export folder such as "writes": ["renders"] to write named files.`
|
|
2357
2407
|
);
|
|
2358
|
-
return
|
|
2408
|
+
return full;
|
|
2409
|
+
};
|
|
2410
|
+
const writeWhole2 = async (full, data) => {
|
|
2411
|
+
await fs.mkdir(path.dirname(full), { recursive: true });
|
|
2412
|
+
const staging = `${full}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
2413
|
+
await fs.writeFile(staging, data);
|
|
2414
|
+
await fs.rename(staging, full);
|
|
2415
|
+
};
|
|
2416
|
+
const keyed = (key) => {
|
|
2417
|
+
if (typeof key !== "string" || key.length > 512 || !KEY.test(key))
|
|
2418
|
+
throw new Error(`files.cache: key ${JSON.stringify(key)} must be letters, digits, ".", "_" and "-" in "/"-separated parts, each starting with a letter or digit`);
|
|
2419
|
+
return within2(`${KEYED_DIR}/${key}`, "writing");
|
|
2359
2420
|
};
|
|
2360
2421
|
const live = (options) => {
|
|
2361
2422
|
if (options.signal.aborted) throw new Error("files: cancelled");
|
|
@@ -2367,9 +2428,22 @@ function createNodeFileCapability(project) {
|
|
|
2367
2428
|
},
|
|
2368
2429
|
async write(relative, data, options) {
|
|
2369
2430
|
live(options);
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2431
|
+
await writeWhole2(writable(relative), data);
|
|
2432
|
+
},
|
|
2433
|
+
cache: {
|
|
2434
|
+
async get(key, options) {
|
|
2435
|
+
live(options);
|
|
2436
|
+
try {
|
|
2437
|
+
return new Uint8Array(await fs.readFile(keyed(key)));
|
|
2438
|
+
} catch (error) {
|
|
2439
|
+
if (error.code === "ENOENT") return null;
|
|
2440
|
+
throw error;
|
|
2441
|
+
}
|
|
2442
|
+
},
|
|
2443
|
+
async put(key, data, options) {
|
|
2444
|
+
live(options);
|
|
2445
|
+
await writeWhole2(keyed(key), data);
|
|
2446
|
+
}
|
|
2373
2447
|
},
|
|
2374
2448
|
async list(relative, options) {
|
|
2375
2449
|
live(options);
|
|
@@ -3763,4 +3837,4 @@ export {
|
|
|
3763
3837
|
builtinModuleIds,
|
|
3764
3838
|
diagnoseGraph
|
|
3765
3839
|
};
|
|
3766
|
-
//# sourceMappingURL=chunk-
|
|
3840
|
+
//# sourceMappingURL=chunk-QKL7CCRS.js.map
|
|
@@ -39,6 +39,19 @@ function resolveWithinRoot(root, ...segments) {
|
|
|
39
39
|
}
|
|
40
40
|
return full;
|
|
41
41
|
}
|
|
42
|
+
function canonicalPath(full) {
|
|
43
|
+
let existing = full;
|
|
44
|
+
while (!existsIncludingSymlink(existing)) {
|
|
45
|
+
const parent = path.dirname(existing);
|
|
46
|
+
if (parent === existing) return full;
|
|
47
|
+
existing = parent;
|
|
48
|
+
}
|
|
49
|
+
return path.join(canonical(existing, `path is not accessible: ${full}`), path.relative(existing, full));
|
|
50
|
+
}
|
|
51
|
+
function isWithin(base, candidate) {
|
|
52
|
+
const rel = path.relative(base, candidate);
|
|
53
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
54
|
+
}
|
|
42
55
|
function existsIncludingSymlink(candidate) {
|
|
43
56
|
try {
|
|
44
57
|
fs.lstatSync(candidate);
|
|
@@ -56,6 +69,101 @@ function canonical(candidate, message) {
|
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
// server/src/projectConfig.ts
|
|
73
|
+
import fs2 from "node:fs";
|
|
74
|
+
import path2 from "node:path";
|
|
75
|
+
var CREDENTIAL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
76
|
+
var SECRET_KEY = /^(?:api[_-]?key|apiKey|secret|password|access[_-]?token|refresh[_-]?token|credential|credentials)$/i;
|
|
77
|
+
function readProjectManifest(root) {
|
|
78
|
+
const raw = readRaw(root);
|
|
79
|
+
return normalizeManifest(raw);
|
|
80
|
+
}
|
|
81
|
+
function writeProjectManifest(root, input) {
|
|
82
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("project manifest must be an object");
|
|
83
|
+
const value = input;
|
|
84
|
+
const current = readRaw(root);
|
|
85
|
+
const next = {
|
|
86
|
+
...current,
|
|
87
|
+
name: typeof value.name === "string" ? value.name.trim() : "",
|
|
88
|
+
commands: stringRecord(value.commands, "commands"),
|
|
89
|
+
credentials: credentialNames(value.credentials),
|
|
90
|
+
settings: settingsRecord(value.settings)
|
|
91
|
+
};
|
|
92
|
+
fs2.writeFileSync(path2.join(root, "cascade.json"), `${JSON.stringify(next, null, 2)}
|
|
93
|
+
`, "utf8");
|
|
94
|
+
return normalizeManifest(next);
|
|
95
|
+
}
|
|
96
|
+
function readRaw(root) {
|
|
97
|
+
try {
|
|
98
|
+
const value = JSON.parse(fs2.readFileSync(path2.join(root, "cascade.json"), "utf8"));
|
|
99
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("root must be an object");
|
|
100
|
+
return value;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.code === "ENOENT") return {};
|
|
103
|
+
throw new Error(`Cannot read cascade.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function normalizeManifest(raw) {
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
name: typeof raw.name === "string" ? raw.name : "",
|
|
109
|
+
commands: Object.freeze(stringRecord(raw.commands, "commands")),
|
|
110
|
+
credentials: Object.freeze(credentialNames(raw.credentials)),
|
|
111
|
+
settings: Object.freeze(settingsRecord(raw.settings)),
|
|
112
|
+
reads: Object.freeze(relativeFolders(raw.reads, "reads", true)),
|
|
113
|
+
writes: Object.freeze(relativeFolders(raw.writes, "writes", false))
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function relativeFolders(input, label, mayLeave) {
|
|
117
|
+
if (input === void 0) return [];
|
|
118
|
+
const shape = mayLeave ? '"../other/data"' : '"renders"';
|
|
119
|
+
if (!Array.isArray(input)) throw new Error(`cascade.json ${label} must be an array of relative folders such as [${shape}]`);
|
|
120
|
+
const folders = input.map((entry) => {
|
|
121
|
+
if (typeof entry !== "string" || !entry.trim() || entry.includes("\0") || path2.isAbsolute(entry) || /^~/.test(entry))
|
|
122
|
+
throw new Error(`cascade.json ${label}: ${JSON.stringify(entry)} is not a relative folder such as ${shape}`);
|
|
123
|
+
const folder = path2.posix.normalize(entry.split("\\").join("/")).replace(/\/+$/, "");
|
|
124
|
+
const leaves = folder === ".." || folder.startsWith("../");
|
|
125
|
+
if (!mayLeave && (leaves || folder === "." || /^(?:\.git|nodes|\.cascade)(?:\/|$)/.test(folder)))
|
|
126
|
+
throw new Error(`cascade.json writes: ${JSON.stringify(entry)} must be a folder inside the project, not its root, .git, .cascade or nodes`);
|
|
127
|
+
if (mayLeave && folder === ".") throw new Error('cascade.json reads: "." is the project itself, which is always readable');
|
|
128
|
+
return folder;
|
|
129
|
+
});
|
|
130
|
+
return [...new Set(folders)];
|
|
131
|
+
}
|
|
132
|
+
function stringRecord(input, label) {
|
|
133
|
+
if (input === void 0) return {};
|
|
134
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`${label} must be an object of strings`);
|
|
135
|
+
const entries = Object.entries(input);
|
|
136
|
+
if (entries.some(([key, value]) => !CREDENTIAL_NAME.test(key) || typeof value !== "string" || value.includes("\0"))) {
|
|
137
|
+
throw new Error(`${label} must contain safe names and string values`);
|
|
138
|
+
}
|
|
139
|
+
return Object.fromEntries(entries);
|
|
140
|
+
}
|
|
141
|
+
function credentialNames(input) {
|
|
142
|
+
if (input === void 0) return [];
|
|
143
|
+
if (!Array.isArray(input) || input.some((name) => typeof name !== "string" || !CREDENTIAL_NAME.test(name))) {
|
|
144
|
+
throw new Error("credentials must be an array of safe names");
|
|
145
|
+
}
|
|
146
|
+
return [...new Set(input)];
|
|
147
|
+
}
|
|
148
|
+
function settingsRecord(input) {
|
|
149
|
+
if (input === void 0) return {};
|
|
150
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("settings must be an object");
|
|
151
|
+
assertSafeSettings(input, 0);
|
|
152
|
+
return structuredClone(input);
|
|
153
|
+
}
|
|
154
|
+
function assertSafeSettings(value, depth) {
|
|
155
|
+
if (depth > 12) throw new Error("settings nesting is too deep");
|
|
156
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return;
|
|
157
|
+
if (Array.isArray(value)) return value.forEach((item) => assertSafeSettings(item, depth + 1));
|
|
158
|
+
if (!value || typeof value !== "object") throw new Error("settings must contain JSON values");
|
|
159
|
+
for (const [key, item] of Object.entries(value)) {
|
|
160
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype" || SECRET_KEY.test(key)) {
|
|
161
|
+
throw new Error(`settings key ${JSON.stringify(key)} looks secret or unsafe; credentials belong outside the project`);
|
|
162
|
+
}
|
|
163
|
+
assertSafeSettings(item, depth + 1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
59
167
|
// server/src/shell/service.ts
|
|
60
168
|
import fs4 from "node:fs";
|
|
61
169
|
import os2 from "node:os";
|
|
@@ -64,11 +172,11 @@ import { execFile, spawn } from "node:child_process";
|
|
|
64
172
|
import { randomUUID } from "node:crypto";
|
|
65
173
|
|
|
66
174
|
// server/src/credentials.ts
|
|
67
|
-
import
|
|
175
|
+
import fs3 from "node:fs";
|
|
68
176
|
import os from "node:os";
|
|
69
|
-
import
|
|
177
|
+
import path3 from "node:path";
|
|
70
178
|
var CredentialStore = class {
|
|
71
|
-
constructor(file = process.env.CASCADE_CREDENTIALS ||
|
|
179
|
+
constructor(file = process.env.CASCADE_CREDENTIALS || path3.join(os.homedir(), ".cascade", "credentials.yaml")) {
|
|
72
180
|
this.file = file;
|
|
73
181
|
}
|
|
74
182
|
has(name) {
|
|
@@ -96,7 +204,7 @@ var CredentialStore = class {
|
|
|
96
204
|
read() {
|
|
97
205
|
let source;
|
|
98
206
|
try {
|
|
99
|
-
source =
|
|
207
|
+
source = fs3.readFileSync(this.file, "utf8");
|
|
100
208
|
} catch (error) {
|
|
101
209
|
if (error.code === "ENOENT") return /* @__PURE__ */ new Map();
|
|
102
210
|
throw error;
|
|
@@ -170,83 +278,6 @@ function safeEnvName(value) {
|
|
|
170
278
|
return value && /^[A-Za-z_][A-Za-z0-9_]{0,127}$/.test(value) && !/^(?:PATH|NODE_OPTIONS|PYTHONPATH|LD_.*|DYLD_.*)$/i.test(value) ? value : null;
|
|
171
279
|
}
|
|
172
280
|
|
|
173
|
-
// server/src/projectConfig.ts
|
|
174
|
-
import fs3 from "node:fs";
|
|
175
|
-
import path3 from "node:path";
|
|
176
|
-
var CREDENTIAL_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
177
|
-
var SECRET_KEY = /^(?:api[_-]?key|apiKey|secret|password|access[_-]?token|refresh[_-]?token|credential|credentials)$/i;
|
|
178
|
-
function readProjectManifest(root) {
|
|
179
|
-
const raw = readRaw(root);
|
|
180
|
-
return normalizeManifest(raw);
|
|
181
|
-
}
|
|
182
|
-
function writeProjectManifest(root, input) {
|
|
183
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("project manifest must be an object");
|
|
184
|
-
const value = input;
|
|
185
|
-
const current = readRaw(root);
|
|
186
|
-
const next = {
|
|
187
|
-
...current,
|
|
188
|
-
name: typeof value.name === "string" ? value.name.trim() : "",
|
|
189
|
-
commands: stringRecord(value.commands, "commands"),
|
|
190
|
-
credentials: credentialNames(value.credentials),
|
|
191
|
-
settings: settingsRecord(value.settings)
|
|
192
|
-
};
|
|
193
|
-
fs3.writeFileSync(path3.join(root, "cascade.json"), `${JSON.stringify(next, null, 2)}
|
|
194
|
-
`, "utf8");
|
|
195
|
-
return normalizeManifest(next);
|
|
196
|
-
}
|
|
197
|
-
function readRaw(root) {
|
|
198
|
-
try {
|
|
199
|
-
const value = JSON.parse(fs3.readFileSync(path3.join(root, "cascade.json"), "utf8"));
|
|
200
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("root must be an object");
|
|
201
|
-
return value;
|
|
202
|
-
} catch (error) {
|
|
203
|
-
if (error.code === "ENOENT") return {};
|
|
204
|
-
throw new Error(`Cannot read cascade.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
function normalizeManifest(raw) {
|
|
208
|
-
return Object.freeze({
|
|
209
|
-
name: typeof raw.name === "string" ? raw.name : "",
|
|
210
|
-
commands: Object.freeze(stringRecord(raw.commands, "commands")),
|
|
211
|
-
credentials: Object.freeze(credentialNames(raw.credentials)),
|
|
212
|
-
settings: Object.freeze(settingsRecord(raw.settings))
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
function stringRecord(input, label) {
|
|
216
|
-
if (input === void 0) return {};
|
|
217
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error(`${label} must be an object of strings`);
|
|
218
|
-
const entries = Object.entries(input);
|
|
219
|
-
if (entries.some(([key, value]) => !CREDENTIAL_NAME.test(key) || typeof value !== "string" || value.includes("\0"))) {
|
|
220
|
-
throw new Error(`${label} must contain safe names and string values`);
|
|
221
|
-
}
|
|
222
|
-
return Object.fromEntries(entries);
|
|
223
|
-
}
|
|
224
|
-
function credentialNames(input) {
|
|
225
|
-
if (input === void 0) return [];
|
|
226
|
-
if (!Array.isArray(input) || input.some((name) => typeof name !== "string" || !CREDENTIAL_NAME.test(name))) {
|
|
227
|
-
throw new Error("credentials must be an array of safe names");
|
|
228
|
-
}
|
|
229
|
-
return [...new Set(input)];
|
|
230
|
-
}
|
|
231
|
-
function settingsRecord(input) {
|
|
232
|
-
if (input === void 0) return {};
|
|
233
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("settings must be an object");
|
|
234
|
-
assertSafeSettings(input, 0);
|
|
235
|
-
return structuredClone(input);
|
|
236
|
-
}
|
|
237
|
-
function assertSafeSettings(value, depth) {
|
|
238
|
-
if (depth > 12) throw new Error("settings nesting is too deep");
|
|
239
|
-
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return;
|
|
240
|
-
if (Array.isArray(value)) return value.forEach((item) => assertSafeSettings(item, depth + 1));
|
|
241
|
-
if (!value || typeof value !== "object") throw new Error("settings must contain JSON values");
|
|
242
|
-
for (const [key, item] of Object.entries(value)) {
|
|
243
|
-
if (key === "__proto__" || key === "constructor" || key === "prototype" || SECRET_KEY.test(key)) {
|
|
244
|
-
throw new Error(`settings key ${JSON.stringify(key)} looks secret or unsafe; credentials belong outside the project`);
|
|
245
|
-
}
|
|
246
|
-
assertSafeSettings(item, depth + 1);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
281
|
// server/src/shell/types.ts
|
|
251
282
|
var ShellProcessError = class extends Error {
|
|
252
283
|
constructor(kind, result, message) {
|
|
@@ -847,7 +878,48 @@ var ProjectRoot = class _ProjectRoot {
|
|
|
847
878
|
if (normalized === "shared" || normalized.startsWith("shared/")) {
|
|
848
879
|
return this.resolveShared(...normalized.split("/").slice(1));
|
|
849
880
|
}
|
|
850
|
-
|
|
881
|
+
try {
|
|
882
|
+
return this.resolve(relativePath);
|
|
883
|
+
} catch (error) {
|
|
884
|
+
if (!(error instanceof PathSafetyError)) throw error;
|
|
885
|
+
const declared = this.resolveDeclaredRead(relativePath);
|
|
886
|
+
if (declared) return declared;
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* A read outside the project, allowed only into a root the project declares
|
|
892
|
+
* in cascade.json `"reads"` (`["../other/data"]`). Reached by `..` or by a
|
|
893
|
+
* symlink alike: the canonical target must lie under the canonical read
|
|
894
|
+
* root. Never used for writes — `resolve()` still refuses those.
|
|
895
|
+
*/
|
|
896
|
+
resolveDeclaredRead(relativePath) {
|
|
897
|
+
if (path5.isAbsolute(relativePath)) return null;
|
|
898
|
+
const reads = readProjectManifest(this.root).reads;
|
|
899
|
+
if (!reads.length) return null;
|
|
900
|
+
const target = canonicalPath(path5.resolve(this.root, relativePath));
|
|
901
|
+
for (const folder of reads) {
|
|
902
|
+
let base;
|
|
903
|
+
try {
|
|
904
|
+
base = fssync.realpathSync(path5.resolve(this.root, folder));
|
|
905
|
+
} catch {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (isWithin(base, target)) return target;
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Where a node may write a file under its own name: `.cascade-cache/`, or a
|
|
914
|
+
* folder the project declares in cascade.json `"writes"` (`["renders"]`).
|
|
915
|
+
* Returns the folder the normalised path falls in, or null. Containment is
|
|
916
|
+
* still `resolve()`'s, so a symlink out of a write folder is refused.
|
|
917
|
+
*/
|
|
918
|
+
writeFolderOf(relativePath) {
|
|
919
|
+
const clean = path5.posix.normalize(relativePath.split("\\").join("/").replace(/^\.?\//, ""));
|
|
920
|
+
for (const folder of [".cascade-cache", ...readProjectManifest(this.root).writes])
|
|
921
|
+
if (clean.startsWith(`${folder}/`)) return folder;
|
|
922
|
+
return null;
|
|
851
923
|
}
|
|
852
924
|
/**
|
|
853
925
|
* Whether this project is under version control, walking **up** rather than
|
|
@@ -1122,4 +1194,4 @@ export {
|
|
|
1122
1194
|
extractNodeDefinition,
|
|
1123
1195
|
ProjectRoot
|
|
1124
1196
|
};
|
|
1125
|
-
//# sourceMappingURL=chunk-
|
|
1197
|
+
//# sourceMappingURL=chunk-W5PJ5RMP.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
readProjectManifest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
4
4
|
|
|
5
5
|
// server/src/compile.ts
|
|
6
6
|
import * as esbuild from "esbuild";
|
|
@@ -119,4 +119,4 @@ export {
|
|
|
119
119
|
compileProjectPanel,
|
|
120
120
|
compileEmbedded
|
|
121
121
|
};
|
|
122
|
-
//# sourceMappingURL=chunk-
|
|
122
|
+
//# sourceMappingURL=chunk-W7OUAT7J.js.map
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
compileEmbedded,
|
|
6
6
|
compileProjectModule,
|
|
7
7
|
compileProjectPanel
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-W7OUAT7J.js";
|
|
9
9
|
import {
|
|
10
10
|
PythonWorker,
|
|
11
11
|
StageCancelledError,
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
resolveWithinRoot,
|
|
35
35
|
secretValue,
|
|
36
36
|
writeProjectManifest
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-W5PJ5RMP.js";
|
|
38
38
|
import "./chunk-NJFMDXHR.js";
|
|
39
39
|
import "./chunk-J7VK7MNF.js";
|
|
40
40
|
import "./chunk-FWMSGKEH.js";
|
|
@@ -3467,4 +3467,4 @@ export {
|
|
|
3467
3467
|
resolveProjectTarget,
|
|
3468
3468
|
runStudioCli
|
|
3469
3469
|
};
|
|
3470
|
-
//# sourceMappingURL=cliCommands-
|
|
3470
|
+
//# sourceMappingURL=cliCommands-XVFRD7CF.js.map
|