akm-cli 0.10.261012-beta → 0.10.261013-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/SECURITY.md +2 -2
  3. package/STABILITY.md +2 -2
  4. package/dist/assets/hints/cli-hints-full.md +6 -7
  5. package/dist/assets/hints/cli-hints-short.md +3 -4
  6. package/dist/commands/command/execution-source-loader.js +10 -14
  7. package/dist/commands/env/env-binding.js +47 -58
  8. package/dist/commands/env/env-cli.js +20 -22
  9. package/dist/commands/env/env.js +10 -58
  10. package/dist/commands/env/secret-cli.js +9 -27
  11. package/dist/commands/env/secret.js +3 -10
  12. package/dist/commands/health/scheduler-binary.js +1 -1
  13. package/dist/commands/improve/distill.js +0 -2
  14. package/dist/commands/improve/eligibility.js +0 -2
  15. package/dist/commands/lint/env-key-rules.js +7 -6
  16. package/dist/commands/lint/index.js +1 -1
  17. package/dist/commands/proposal/validators/proposal-validators.js +1 -4
  18. package/dist/commands/read/show.js +4 -25
  19. package/dist/commands/sources/add-cli.js +8 -63
  20. package/dist/commands/sources/bundle-cli.js +8 -9
  21. package/dist/commands/sources/installed-stashes.js +117 -61
  22. package/dist/commands/sources/schema-repair.js +0 -2
  23. package/dist/commands/sources/source-clone.js +31 -7
  24. package/dist/commands/tasks/explain.js +1 -1
  25. package/dist/commands/tasks/tasks.js +38 -56
  26. package/dist/commands/ui/redact.js +4 -4
  27. package/dist/commands/ui/workspace-actions.js +1 -1
  28. package/dist/commands/workflow/plan.js +47 -8
  29. package/dist/commands/workflow-cli.js +37 -41
  30. package/dist/core/adapter/adapters/akm-adapter.js +3 -3
  31. package/dist/core/adapter/adapters/akm-lint.js +3 -4
  32. package/dist/core/adapter/adapters/akm-metadata.js +2 -2
  33. package/dist/core/adapter/adapters/akm-task-adapter.js +5 -6
  34. package/dist/core/adapter/adapters/dotenv-adapter.js +4 -3
  35. package/dist/core/asset/resolve-ref.js +5 -0
  36. package/dist/core/bundle-rename.js +12 -4
  37. package/dist/core/common.js +4 -52
  38. package/dist/core/config/config-walker.js +1 -1
  39. package/dist/core/config/config.js +55 -18
  40. package/dist/core/config/schema/primitives.js +12 -6
  41. package/dist/core/config/schema/sources-bundles.js +2 -2
  42. package/dist/core/env-secret-ref.js +84 -73
  43. package/dist/core/errors.js +1 -1
  44. package/dist/core/paths.js +1 -6
  45. package/dist/core/redaction.js +16 -3
  46. package/dist/core/ref-token.js +82 -0
  47. package/dist/core/secret-value.js +14 -0
  48. package/dist/core/write-source.js +20 -4
  49. package/dist/indexer/indexer.js +27 -36
  50. package/dist/indexer/installations.js +1 -8
  51. package/dist/indexer/lookup/adapter-concept-owner.js +40 -14
  52. package/dist/indexer/materialize-embeddings.js +1 -1
  53. package/dist/indexer/scan/drain-dir.js +3 -3
  54. package/dist/indexer/search/search-source.js +4 -2
  55. package/dist/indexer/walk/path-resolver.js +13 -68
  56. package/dist/indexer/walk/walker.js +18 -22
  57. package/dist/integrations/agent/engine-resolution.js +9 -7
  58. package/dist/integrations/agent/runner-dispatch.js +4 -5
  59. package/dist/integrations/agent/runner.js +2 -0
  60. package/dist/llm/client.js +1 -1
  61. package/dist/llm/embedders/remote.js +4 -3
  62. package/dist/output/renderers.js +3 -4
  63. package/dist/output/shapes/helpers.js +0 -2
  64. package/dist/output/text/command-format.js +12 -2
  65. package/dist/output/text/show-directives.js +16 -28
  66. package/dist/output/text/workflow-format.js +37 -7
  67. package/dist/scripts/akm-migrate-node.js +45865 -44692
  68. package/dist/scripts/akm-migrate.js +45804 -44630
  69. package/dist/setup/steps/tasks.js +1 -2
  70. package/dist/sources/providers/provider-utils.js +12 -0
  71. package/dist/sources/resolve.js +12 -16
  72. package/dist/sources/snapshot-fetchers/secret-seam.js +2 -4
  73. package/dist/sources/snapshot-fetchers/x.js +1 -1
  74. package/dist/storage/repositories/proposals-repository.js +2 -2
  75. package/dist/storage/repositories/task-history-repository.js +2 -2
  76. package/dist/storage/repositories/workflow-runs-repository.js +38 -116
  77. package/dist/tasks/embedded.js +1 -1
  78. package/dist/tasks/prepare/prepare-support.js +3 -2
  79. package/dist/tasks/prepare/prepare.js +3 -4
  80. package/dist/tasks/prepare/script-capture.js +8 -22
  81. package/dist/tasks/run/load-task.js +46 -14
  82. package/dist/tasks/run/run-native-task.js +0 -8
  83. package/dist/tasks/run/run-workflow-task.js +2 -2
  84. package/dist/tasks/scheduler-sync.js +0 -1
  85. package/dist/tasks/source/bounded-document.js +0 -41
  86. package/dist/tasks/source/parse-task-source.js +0 -1
  87. package/dist/tasks/source/task-source-v4.js +1 -4
  88. package/dist/workflows/compile.js +1 -15
  89. package/dist/workflows/exec/child-workflow.js +1 -2
  90. package/dist/workflows/exec/dispatch-redaction.js +24 -6
  91. package/dist/workflows/exec/environment.js +11 -74
  92. package/dist/workflows/exec/exec-unit.js +31 -23
  93. package/dist/workflows/exec/frozen-judge.js +17 -3
  94. package/dist/workflows/exec/native-executor.js +70 -26
  95. package/dist/workflows/exec/run-workflow.js +31 -18
  96. package/dist/workflows/exec/step-work.js +4 -3
  97. package/dist/workflows/exec/unit-dispatch.js +1 -8
  98. package/dist/workflows/freeze/environment.js +14 -119
  99. package/dist/workflows/freeze/freeze.js +0 -1
  100. package/dist/workflows/freeze/step-values.js +1 -2
  101. package/dist/workflows/freeze/targets/child-workflow.js +3 -5
  102. package/dist/workflows/freeze/targets/command.js +6 -9
  103. package/dist/workflows/freeze/targets/shell.js +0 -3
  104. package/dist/workflows/parser.js +6 -25
  105. package/dist/workflows/plan.js +6 -2
  106. package/dist/workflows/renderer.js +77 -4
  107. package/dist/workflows/runtime/run-plan.js +37 -23
  108. package/dist/workflows/runtime/runs.js +41 -84
  109. package/dist/workflows/runtime/workflow-asset-loader.js +55 -86
  110. package/dist/workflows/source-files.js +7 -19
  111. package/docs/architecture/0.10-process-rules.md +6 -6
  112. package/docs/architecture/architecture.md +6 -6
  113. package/docs/architecture/comparisons/claude-code-vs-akm-workflows-full.md +12 -18
  114. package/docs/architecture/decisions/0003-child-env-allowlist-and-provenance.md +6 -8
  115. package/docs/architecture/internals/storage-locations.md +4 -4
  116. package/docs/architecture/persisted-data-compat.md +3 -2
  117. package/docs/architecture/specs/0.9.0-public-api-issue-backlog.md +1 -1
  118. package/docs/architecture/specs/akm-format-neutral-bundle-workspace-spec.md +3 -3
  119. package/docs/architecture/testing/manual-testing-checklist.md +46 -78
  120. package/docs/architecture/testing/task-workflow-manual-tests.md +22 -40
  121. package/docs/architecture/testing/testing-workflow.md +9 -11
  122. package/docs/architecture/workflow-engine.md +41 -27
  123. package/docs/guides/author-bundles.md +14 -17
  124. package/docs/guides/author-workflows.md +43 -9
  125. package/docs/guides/bundles.md +16 -11
  126. package/docs/guides/claude-code-vs-akm-workflows.md +1 -1
  127. package/docs/guides/run-workflows.md +25 -13
  128. package/docs/guides/scheduling.md +1 -1
  129. package/docs/maintainers/README.md +2 -3
  130. package/docs/maintainers/release-checklist.md +73 -126
  131. package/docs/migration/v0.9-to-v0.10.md +5 -1
  132. package/docs/reference/cli.md +124 -112
  133. package/docs/reference/configuration.md +16 -9
  134. package/docs/reference/data-and-telemetry.md +1 -1
  135. package/docs/reference/env-and-secrets.md +66 -15
  136. package/docs/reference/registry.md +6 -11
  137. package/docs/reference/tasks.md +11 -3
  138. package/docs/reference/ui-api.md +3 -3
  139. package/docs/reference/workflow-schema.md +109 -36
  140. package/package.json +1 -1
  141. package/schemas/akm-task.json +2 -4
  142. package/dist/commands/sources/dangerous-env-audit.js +0 -134
  143. package/dist/core/activation-policy.js +0 -67
  144. package/dist/execution/directory-identity.js +0 -78
  145. package/dist/workflows/authoring/scope-key.js +0 -79
  146. package/dist/workflows/source-semantics.js +0 -110
@@ -1,67 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /**
5
- * `EDITOR`/`VISUAL`/`PAGER` are flagged by `isDangerousEnvKey` for a
6
- * documented RCE vector (many tools invoke them to launch an editor/pager),
7
- * but env-key-rules.ts's own module doc calls out their "high FP rate" in
8
- * the same breath — an installed bundle's env file can supply a value, but
9
- * nothing in akm's own env-injection path ever *invokes* EDITOR/VISUAL/PAGER
10
- * with that value, so the RCE vector these three describe cannot fire from
11
- * an injected env the way LD_PRELOAD or GIT_SSH_COMMAND can. Blocking a
12
- * third-party install over them protects nothing while making "the operator
13
- * legitimately wants to set their editor" the common case that eats the
14
- * refusal. Kept as a name-level literal set (not an import of
15
- * `commands/lint/env-key-rules.ts`) so this module stays the pure leaf its
16
- * own doc comment promises — no new import edges into the rest of the tree.
17
- */
18
- const INTERACTIVE_TOOL_ENV_KEYS = new Set(["EDITOR", "VISUAL", "PAGER"]);
19
- /**
20
- * Decide whether injecting an env with the given dangerous keys is allowed,
21
- * warned, or blocked. Third-party (registry-installed) stashes hard-block a
22
- * genuine RCE-class key; first-party stashes warn. The interactive-tool
23
- * group (see {@link INTERACTIVE_TOOL_ENV_KEYS}) only ever warns, since akm's
24
- * own env-injection path never invokes those keys as a command. An explicit
25
- * `--allow-dangerous-env-keys` (threaded through by the caller, same override
26
- * `decideDangerousKeyInstall`'s `"warn-allow"` already honors for rule 2)
27
- * downgrades a remaining block to a warning too — the operator is not racing
28
- * themselves. See rule 1 above.
29
- *
30
- * @param dangerousKeys The subset of injected keys flagged as process-hijacking
31
- * (already filtered by the caller via `isDangerousEnvKey`).
32
- * @param thirdParty `true` when the env's source is a third-party stash — i.e.
33
- * its origin carries a `registryId`.
34
- * @param allowDangerousEnvKeys `true` when the operator passed `--allow-dangerous-env-keys` (or
35
- * its equivalent) for this injection. Defaults to `false`.
36
- */
37
- export function decideDangerousEnvInjection(input) {
38
- if (input.dangerousKeys.length === 0)
39
- return "allow";
40
- if (!input.thirdParty)
41
- return "warn";
42
- if (input.allowDangerousEnvKeys)
43
- return "warn";
44
- const onlyInteractiveTool = input.dangerousKeys.every((key) => INTERACTIVE_TOOL_ENV_KEYS.has(key));
45
- return onlyInteractiveTool ? "warn" : "block";
46
- }
47
- /**
48
- * Decide the baseline install stance for a freshly-installed stash's
49
- * dangerous-key scan. The interactive confirm / rollback UX is applied by the
50
- * caller on top of the `"gate"` stance — this predicate only fixes the policy.
51
- * See rule 2 above.
52
- */
53
- export function decideDangerousKeyInstall(input) {
54
- if (!input.findingsPresent)
55
- return "allow";
56
- return input.allowDangerousEnvKeys ? "warn-allow" : "gate";
57
- }
58
- // ── Rule 3: write activation (search-source.ts, installations.ts) ────────────
59
- /**
60
- * Whether a resolved source is write-activated. Only the primary stash and
61
- * sources explicitly marked `writable: true` are writable; registry-cached
62
- * (installed, read-only) sources are never written in place because
63
- * `akm update` overwrites them. See rule 3 above.
64
- */
65
- export function isSourceWriteActivated(source) {
66
- return source.writable === true;
67
- }
@@ -1,78 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { UsageError } from "../core/errors.js";
7
- export function captureFrozenDirectoryIdentity(rootInput, relativeCwd) {
8
- const requestedRoot = path.resolve(rootInput);
9
- const requestedCwd = path.resolve(requestedRoot, relativeCwd ?? ".");
10
- requireContained(requestedRoot, requestedCwd);
11
- const realRoot = fs.realpathSync(requestedRoot);
12
- const realCwd = fs.realpathSync(requestedCwd);
13
- requireContained(realRoot, realCwd);
14
- const rootStat = fs.statSync(realRoot, { bigint: true });
15
- const cwdStat = fs.statSync(realCwd, { bigint: true });
16
- if (!rootStat.isDirectory() || !cwdStat.isDirectory()) {
17
- throw new UsageError("Execution root and cwd must both be directories.", "INVALID_FLAG_VALUE");
18
- }
19
- return Object.freeze({
20
- requestedRoot,
21
- realRoot,
22
- rootDevice: rootStat.dev.toString(),
23
- rootInode: rootStat.ino.toString(),
24
- requestedCwd,
25
- realCwd,
26
- cwdDevice: cwdStat.dev.toString(),
27
- cwdInode: cwdStat.ino.toString(),
28
- });
29
- }
30
- /**
31
- * Re-verify, immediately before spawn, that a frozen cwd still resolves
32
- * inside its execution root — path containment and resolved-path identity,
33
- * NOT device/inode identity. Unlike the removed assertFrozenDirectoryIdentity,
34
- * this does not compare device/inode numbers against the frozen snapshot: a
35
- * remount, an rsync, or a container rebuild that leaves the same real
36
- * content at the same real PATH must not abort a dispatch. What still
37
- * throws is anything that changes the resolved real PATH itself — a
38
- * symlink swap, an ancestor swap, a file replacing the directory, or the
39
- * root itself being replaced by a symlink elsewhere (root-vs-cwd
40
- * containment alone can't catch that last one, since both resolve together
41
- * under the new root; comparing the root's own resolved path to its frozen
42
- * value catches it).
43
- */
44
- export function assertFrozenDirectoryContained(identity) {
45
- let realRoot;
46
- let realCwd;
47
- try {
48
- realRoot = fs.realpathSync(identity.requestedRoot);
49
- realCwd = fs.realpathSync(identity.requestedCwd);
50
- }
51
- catch {
52
- throw changed(identity.requestedCwd);
53
- }
54
- if (realRoot !== identity.realRoot)
55
- throw changed(identity.requestedCwd);
56
- const relative = path.relative(realRoot, realCwd);
57
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
58
- throw changed(identity.requestedCwd);
59
- }
60
- let cwdStat;
61
- try {
62
- cwdStat = fs.statSync(realCwd, { bigint: true });
63
- }
64
- catch {
65
- throw changed(identity.requestedCwd);
66
- }
67
- if (!cwdStat.isDirectory())
68
- throw changed(identity.requestedCwd);
69
- }
70
- function changed(cwd) {
71
- return new UsageError(`Frozen execution cwd ${cwd} changed and no longer resolves inside its root.`, "PATH_ESCAPE_VIOLATION");
72
- }
73
- function requireContained(root, candidate) {
74
- const relative = path.relative(root, candidate);
75
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
76
- throw new UsageError(`${candidate} escapes its execution root.`, "PATH_ESCAPE_VIOLATION");
77
- }
78
- }
@@ -1,79 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- import { createHash } from "node:crypto";
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- import { isWithin, resolveStashDir, safeRealpath, toPosix } from "../../core/common.js";
8
- const PROJECT_CONFIG_RELATIVE_PATH = path.join(".akm", "config.json");
9
- export function getCurrentWorkflowScopeKey() {
10
- const anchor = resolveWorkflowScopeAnchor(process.cwd());
11
- const normalized = normalizeScopePath(anchor);
12
- const digest = createHash("sha256").update(normalized).digest("hex");
13
- return `dir:v1:${digest}`;
14
- }
15
- export function resolveWorkflowScopeAnchor(startDir) {
16
- const cwd = safeRealpath(startDir);
17
- const projectRoot = findNearestProjectConfigRoot(cwd);
18
- if (projectRoot)
19
- return projectRoot;
20
- const gitRoot = findNearestGitRoot(cwd);
21
- if (gitRoot)
22
- return gitRoot;
23
- try {
24
- const stashDir = safeRealpath(resolveStashDir());
25
- if (isWithin(cwd, stashDir))
26
- return stashDir;
27
- }
28
- catch {
29
- // Ignore stash resolution failures and fall back to cwd.
30
- }
31
- return cwd;
32
- }
33
- function findNearestProjectConfigRoot(startDir) {
34
- let currentDir = startDir;
35
- while (true) {
36
- const configPath = path.join(currentDir, PROJECT_CONFIG_RELATIVE_PATH);
37
- if (isFile(configPath)) {
38
- return safeRealpath(currentDir);
39
- }
40
- const parentDir = path.dirname(currentDir);
41
- if (parentDir === currentDir)
42
- return null;
43
- currentDir = parentDir;
44
- }
45
- }
46
- function findNearestGitRoot(startDir) {
47
- let currentDir = startDir;
48
- while (true) {
49
- const gitPath = path.join(currentDir, ".git");
50
- if (exists(gitPath)) {
51
- return safeRealpath(currentDir);
52
- }
53
- const parentDir = path.dirname(currentDir);
54
- if (parentDir === currentDir)
55
- return null;
56
- currentDir = parentDir;
57
- }
58
- }
59
- function exists(candidate) {
60
- try {
61
- fs.accessSync(candidate, fs.constants.F_OK);
62
- return true;
63
- }
64
- catch {
65
- return false;
66
- }
67
- }
68
- function isFile(candidate) {
69
- try {
70
- return fs.statSync(candidate).isFile();
71
- }
72
- catch {
73
- return false;
74
- }
75
- }
76
- function normalizeScopePath(value) {
77
- const posix = toPosix(value);
78
- return process.platform === "win32" ? posix.toLowerCase() : posix;
79
- }
@@ -1,110 +0,0 @@
1
- // This Source Code Form is subject to the terms of the Mozilla Public
2
- // License, v. 2.0. If a copy of the MPL was not distributed with this
3
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
- /** Semantic checks for the Markdown workflow grammar (`parser.ts`). */
5
- import fs from "node:fs";
6
- import path from "node:path";
7
- export class WorkflowSourceSemanticError extends Error {
8
- code;
9
- constructor(code, message) {
10
- super(message);
11
- this.code = code;
12
- this.name = "WorkflowSourceSemanticError";
13
- }
14
- }
15
- export function canonicalizeWorkflowWorkingDirectory(value, workspaceRoot) {
16
- if (hasControlCharacter(value)) {
17
- throw new WorkflowSourceSemanticError("working-directory-control-character", "working-directory may not contain NUL or control characters.");
18
- }
19
- if (value === "" || value.trim() === "") {
20
- throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory must be a non-empty relative contained path.");
21
- }
22
- const portable = value.replaceAll("\\", "/");
23
- const segments = portable.split("/");
24
- if (path.posix.isAbsolute(portable) ||
25
- path.win32.isAbsolute(value) ||
26
- portable.startsWith("~") ||
27
- segments.some((segment) => segment === "" || segment === "..")) {
28
- throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory must be relative and contained.");
29
- }
30
- const withoutDots = segments.filter((segment) => segment !== ".");
31
- const canonical = withoutDots.length === 0 ? "." : withoutDots.join("/");
32
- if (workspaceRoot !== undefined)
33
- verifyPhysicalContainment(workspaceRoot, canonical);
34
- return canonical;
35
- }
36
- function hasControlCharacter(value) {
37
- for (const character of value) {
38
- const codePoint = character.codePointAt(0) ?? 0;
39
- if (codePoint <= 0x1f || codePoint === 0x7f)
40
- return true;
41
- }
42
- return false;
43
- }
44
- function verifyPhysicalContainment(workspaceRoot, relative) {
45
- let root;
46
- try {
47
- root = fs.realpathSync(workspaceRoot);
48
- }
49
- catch {
50
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "Workspace root cannot be physically verified.");
51
- }
52
- const candidate = path.resolve(root, ...relative.split("/"));
53
- if (!contained(root, candidate)) {
54
- throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory escapes the workspace.");
55
- }
56
- let current = root;
57
- for (const segment of relative === "." ? [] : relative.split("/")) {
58
- current = path.join(current, segment);
59
- try {
60
- const entry = fs.lstatSync(current);
61
- if (entry.isSymbolicLink()) {
62
- let physical;
63
- try {
64
- physical = fs.realpathSync(current);
65
- }
66
- catch {
67
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory contains a dangling or unresolvable symlink.");
68
- }
69
- if (!contained(root, physical)) {
70
- throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory resolves through a symlink outside the workspace.");
71
- }
72
- let target;
73
- try {
74
- target = fs.statSync(current);
75
- }
76
- catch {
77
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory symlink target cannot be physically verified.");
78
- }
79
- if (!target.isDirectory()) {
80
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory must resolve through directories.");
81
- }
82
- continue;
83
- }
84
- if (!entry.isDirectory()) {
85
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory must resolve through directories.");
86
- }
87
- const physical = fs.realpathSync(current);
88
- if (!contained(root, physical)) {
89
- throw new WorkflowSourceSemanticError("working-directory-escape", "working-directory resolves through a symlink outside the workspace.");
90
- }
91
- }
92
- catch (cause) {
93
- if (cause instanceof WorkflowSourceSemanticError)
94
- throw cause;
95
- const code = cause?.code;
96
- if (code === "ENOENT") {
97
- // A genuinely absent lexical component is allowed at source time; the
98
- // runtime dispatch boundary revalidates containment when it appears.
99
- // `lstat` distinguishes this from a dangling symlink, which fails
100
- // closed above even before its target can appear.
101
- return;
102
- }
103
- throw new WorkflowSourceSemanticError("working-directory-unverifiable", "working-directory cannot be physically verified.");
104
- }
105
- }
106
- }
107
- function contained(root, candidate) {
108
- const relative = path.relative(root, candidate);
109
- return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
110
- }