zyrk 0.6.8 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,8 +104,12 @@ Offline installs use locked verified sources and fail if an input is missing.
104
104
  Manifest, lock, and projection changes are journaled for process-crash recovery;
105
105
  a failed resolution, verification, or build-policy check keeps the prior graph.
106
106
 
107
- Dependency install scripts and implicit `binding.gyp` builds require explicit
108
- approval. Review first, trust the selected sources, then install:
107
+ Dependency `preinstall`, `install`, and `postinstall` scripts (plus implicit
108
+ `binding.gyp` builds) are supported. In an interactive terminal, `zyrk install`
109
+ shows the exact candidates and lets you select which scripts to run. Press Enter
110
+ to skip all pending scripts and continue with an intentionally unbuilt install.
111
+ Headless installs also skip unapproved scripts and report that readiness state.
112
+ Review candidates separately with:
109
113
 
110
114
  ```bash
111
115
  zyrk approve-builds # inspect exact sources and commands; executes no package code
@@ -115,8 +119,12 @@ zyrk rebuild --offline # rebuild the complete approved locked graph
115
119
  ```
116
120
 
117
121
  Approvals are local to the exact graph, sources, patch contents, and script
118
- policy. Mandatory user denies override project approvals. Approved scripts run
119
- with your account's permissions; permission is not a sandbox. Packages that need
122
+ policy. Interactive selections are written to the checked-in
123
+ `zyrk-workspace.json` file with exact package source identities, so collaborators
124
+ can reuse the review alongside the lockfile. Manual `zyrk approve-builds
125
+ <package>` approvals are recorded there too. Mandatory user denies override
126
+ project approvals. Approved scripts run with your account's permissions;
127
+ permission is not a sandbox. Packages that need
120
128
  writes use a complete private dependency generation under `.zyrk-local/`, with
121
129
  independent writable files and dependency links confined to that generation.
122
130
  Build failure keeps the previous project graph active. `--ignore-scripts`
@@ -125,6 +133,24 @@ hooks never run automatically. A project can request runtime writable packages
125
133
  with `"zyrk": { "mutablePackages": ["package-name"] }`; this also uses a complete
126
134
  private graph.
127
135
 
136
+ `zyrk-workspace.json` is portable review state, not a machine-specific store
137
+ manifest. It records each approved package name, version, and exact source ID;
138
+ the file can be committed with `package-lock.json` so collaborators do not have
139
+ to repeat the same approval for the same verified source. User-level denials
140
+ still take precedence.
141
+
142
+ ### Run project scripts
143
+
144
+ Use `zyrk run <script>` or the package-manager shortcut `zyrk <script>`; both
145
+ use the same project-script policy, installed-generation checks, and local
146
+ binary path. Arguments after `--` are passed to the script:
147
+
148
+ ```bash
149
+ zyrk dev
150
+ zyrk test -- --watch
151
+ zyrk run build
152
+ ```
153
+
128
154
  Default registry and authentication settings are loaded from user/project
129
155
  `.npmrc` for installation, dependency edits, `dlx`, and `create`. Environment
130
156
  token placeholders are supported. Credentials are restricted to their configured
@@ -136,7 +162,10 @@ registry; private names never fall back to a public endpoint. Disabled TLS
136
162
  verification is rejected before lookup. Git sources and non-workspace local sources remain unsupported;
137
163
  they fail explicitly. Optional dependencies incompatible with the selected platform
138
164
  are omitted; missing packages, integrity failures, and other optional failures
139
- remain errors. Locks retain exact platform-qualified optional variants; each machine
165
+ remain errors. Platform-specific optional packages that do not match this machine
166
+ are summarized once after installation; these are normal variants for packages
167
+ supporting multiple operating systems and CPUs. Locks retain exact
168
+ platform-qualified optional variants; each machine
140
169
  selects its compatible local closure without rewriting the lock or recording store
141
170
  paths. Older platform-pruned locks need an intentional non-frozen refresh; incomplete
142
171
  npm locks must be freshly resolved before portable adoption. macOS, Windows, and
@@ -396,11 +425,11 @@ The commit hook runs the full check gate, increments the version in
396
425
  `package.json`, `package-lock.json`, and `VERSION`, and creates a matching version
397
426
  tag after the commit. Keep changes focused and never bypass the hooks.
398
427
 
399
- CI checks pushed commits and pull requests. The separate
400
- [npm publishing action](.github/workflows/npm-publish.yml) is triggered when a
401
- GitHub release is published. It verifies the release tag, formatting, typecheck,
402
- build, and tests, then publishes that exact commit. Ordinary commits do not
403
- publish packages. Dependabot and a scheduled advisory check supplement CI; an
428
+ CI checks pushed commits and pull requests. The
429
+ [npm publishing action](.github/workflows/npm-publish.yml) runs when the commit
430
+ hook's version tag is pushed, verifies formatting, typecheck, build, tests, and
431
+ the tag/version match, then publishes that exact commit. A manual workflow
432
+ dispatch can publish a prior version tag for release backfills. Dependabot and a scheduled advisory check supplement CI; an
404
433
  unavailable advisory service fails separately from a clean result.
405
434
 
406
435
  The contributor guide is [AGENTS.md](AGENTS.md). Bug reports and reproducible
package/VERSION CHANGED
@@ -1 +1 @@
1
- v0.6.8
1
+ v0.7.2
package/bin/zyrk.js CHANGED
@@ -12,6 +12,7 @@ const { runCli } = await import(
12
12
  process.exitCode = await runCli({
13
13
  argv: process.argv.slice(2),
14
14
  env: process.env,
15
+ stdin: process.stdin,
15
16
  stdout: process.stdout,
16
17
  stderr: process.stderr,
17
18
  });
@@ -25,6 +25,7 @@ export type ToolEnvironment = {
25
25
  export type RunCliOptions = {
26
26
  argv: readonly string[];
27
27
  env: NodeJS.ProcessEnv;
28
+ stdin?: NodeJS.ReadStream;
28
29
  stdout: {
29
30
  write(chunk: string): unknown;
30
31
  };
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AA4BA,OAAO,EAAe,SAAS,EAAe,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,OAAO,EAAoB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAM3C,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AA6DzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyBhD;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AA6EF;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,GACnD,SAAS,CAgDX;AAmBD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC3D,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;CACxC,CAAC;AAEF,gFAAgF;AAChF,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA8bpE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AA+BA,OAAO,EAAe,SAAS,EAAe,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,CAAC;AACrB,OAAO,EAAoB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAM3C,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEhF,eAAO,MAAM,uBAAuB,IAAI,CAAC;AA6DzC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BhD;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AA6EF;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAA;CAAE,GACnD,SAAS,CAgDX;AAmBD,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAC1B,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAC1C,MAAM,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC3D,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;CACxC,CAAC;AAuDF,gFAAgF;AAChF,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CA2fpE"}
package/dist/cli/main.js CHANGED
@@ -12,6 +12,8 @@ import { sanitizeDiagnostic, redactSecrets } from "../log.js";
12
12
  import { installProject } from "../install/run.js";
13
13
  import { installRegistry } from "../install/registry.js";
14
14
  import { readFileSync } from "node:fs";
15
+ import { readFile } from "node:fs/promises";
16
+ import { createInterface } from "node:readline/promises";
15
17
  import * as path from "node:path";
16
18
  import { fileURLToPath } from "node:url";
17
19
  import { runCreate, starterSpecFor } from "../dlx/create.js";
@@ -22,7 +24,7 @@ import { runStoreCommand } from "../commands/store.js";
22
24
  import { exitCodeFor, ZyrkError, toZyrkError } from "../errors.js";
23
25
  export { ZyrkError };
24
26
  import { DEFAULT_REGISTRY, RegistryClient } from "../fetch/registry.js";
25
- import { resolveStoreRoot } from "../config/paths.js";
27
+ import { findProjectBoundary, resolveStoreRoot } from "../config/paths.js";
26
28
  import { Store } from "../store/layout.js";
27
29
  import { delegateToNpm, delegatedHelpLines, isDelegatedCommand, } from "./npm-delegate.js";
28
30
  import { isGlobalInstallRequest, parseArgs } from "./parse.js";
@@ -89,6 +91,7 @@ export function helpText(version) {
89
91
  `zyrk v${version} — one verified store per user, shared across projects through links.`,
90
92
  "",
91
93
  "Usage: zyrk <command> [args] [--json]",
94
+ " zyrk <script> [args] (shortcut for zyrk run <script> [args])",
92
95
  "",
93
96
  "Dependency commands:",
94
97
  ];
@@ -212,6 +215,39 @@ function readCliVersion() {
212
215
  return "unknown";
213
216
  }
214
217
  }
218
+ async function promptForBuildApproval(builds, input, output) {
219
+ output.write("\nDependency lifecycle scripts need approval. Review these exact package sources:\n");
220
+ builds.forEach((build, index) => {
221
+ const stages = build.stages
222
+ .map((stage) => `${stage.name}: ${redactSecrets(stage.command)
223
+ .replace(/[\x00-\x1f\x7f-\x9f]/g, "")
224
+ .slice(0, 180)}`)
225
+ .join("; ");
226
+ output.write(` ${index + 1}. ${build.name}@${build.version} (${build.sourceId}) — ${stages}\n`);
227
+ });
228
+ output.write("Enter numbers separated by commas to approve selected sources, `all` to approve all listed sources, or press Enter to skip their scripts and continue unbuilt.\n");
229
+ const prompt = createInterface({
230
+ input,
231
+ output: output,
232
+ terminal: true,
233
+ });
234
+ try {
235
+ const answer = (await prompt.question("Approve builds> ")).trim();
236
+ if (!answer)
237
+ return [];
238
+ if (answer.toLowerCase() === "all")
239
+ return ["*"];
240
+ const numbers = answer.split(/[ ,]+/).map((value) => Number(value));
241
+ if (numbers.some((number) => !Number.isInteger(number) || number < 1 || number > builds.length))
242
+ throw new ZyrkError("ZYRK_USAGE", "Build selection contains an invalid candidate number.", {
243
+ remedy: `Select numbers from 1 to ${builds.length}, enter all, or press Enter to skip scripts.`,
244
+ });
245
+ return [...new Set(numbers)].map((number) => builds[number - 1].sourceId);
246
+ }
247
+ finally {
248
+ prompt.close();
249
+ }
250
+ }
215
251
  /** Parse, dispatch, delegate, or run a tool, and emit. The only impure seam. */
216
252
  export async function runCli(options) {
217
253
  const parsed = parseArgs(options.argv);
@@ -464,6 +500,11 @@ export async function runCli(options) {
464
500
  env: options.env,
465
501
  specs,
466
502
  projectDir: cwd,
503
+ ...(options.stdin?.isTTY && options.stderr.isTTY && !parsed.json
504
+ ? {
505
+ requestBuildApproval: (builds) => promptForBuildApproval(builds, options.stdin, options.stderr),
506
+ }
507
+ : {}),
467
508
  ...tools,
468
509
  });
469
510
  progress.close();
@@ -520,6 +561,11 @@ export async function runCli(options) {
520
561
  projectDir: options.cwd ?? process.cwd(),
521
562
  store: tools.store,
522
563
  registry: tools.registry,
564
+ ...(options.stdin?.isTTY && options.stderr.isTTY && !parsed.json
565
+ ? {
566
+ requestBuildApproval: (builds) => promptForBuildApproval(builds, options.stdin, options.stderr),
567
+ }
568
+ : {}),
523
569
  });
524
570
  progress.close();
525
571
  if (parsed.json) {
@@ -602,6 +648,48 @@ export async function runCli(options) {
602
648
  return emit(fail(toZyrkError(error, `zyrk ${parsed.command} failed.`), parsed));
603
649
  }
604
650
  }
651
+ // Package scripts are accepted as ergonomic aliases for `zyrk run <name>`.
652
+ // Route through the same project runner so policy, generation leases,
653
+ // local-bin PATH and lifecycle environment stay identical.
654
+ if (!parsed.help &&
655
+ !parsed.version &&
656
+ parsed.command !== undefined &&
657
+ !KNOWN_COMMANDS.has(parsed.command) &&
658
+ parsed.argsBeforeCommand.length === 0) {
659
+ const cwd = options.cwd ?? process.cwd();
660
+ const boundary = await findProjectBoundary(cwd);
661
+ if (boundary) {
662
+ let declared = false;
663
+ try {
664
+ const manifest = JSON.parse(await readFile(path.join(boundary, "package.json"), "utf8"));
665
+ if (manifest.scripts !== null &&
666
+ typeof manifest.scripts === "object" &&
667
+ !Array.isArray(manifest.scripts) &&
668
+ typeof manifest.scripts[parsed.command] === "string")
669
+ declared = true;
670
+ }
671
+ catch {
672
+ // Preserve the normal unknown-command diagnostic when no valid
673
+ // project script can be identified.
674
+ }
675
+ if (declared) {
676
+ try {
677
+ return await runProjectCommand({
678
+ command: "run",
679
+ args: [parsed.command, ...parsed.commandArgs],
680
+ cwd,
681
+ env: options.env,
682
+ stdout: options.stdout,
683
+ stderr: options.stderr,
684
+ json: parsed.json,
685
+ });
686
+ }
687
+ catch (error) {
688
+ return emit(fail(toZyrkError(error, `zyrk ${parsed.command} failed.`), parsed));
689
+ }
690
+ }
691
+ }
692
+ }
605
693
  return emit(dispatch(parsed, { version, env: options.env }));
606
694
  }
607
695
  async function defaultTools(env, version, cwd = process.cwd()) {
@@ -10,5 +10,6 @@ export declare function runDependencyCommand(options: {
10
10
  registry: RegistryClient;
11
11
  env?: NodeJS.ProcessEnv;
12
12
  onProgress?: InstallOptions["onProgress"];
13
+ requestBuildApproval?: InstallOptions["requestBuildApproval"];
13
14
  }): Promise<InstallResult>;
14
15
  //# sourceMappingURL=dependencies.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dependencies.d.ts","sourceRoot":"","sources":["../../src/commands/dependencies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAavE,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,OAAO,EAAE,iBAAiB,CAAC;IAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,aAAa,CAAC,CA4HzB"}
1
+ {"version":3,"file":"dependencies.d.ts","sourceRoot":"","sources":["../../src/commands/dependencies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,iBAAiB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAavE,wBAAsB,oBAAoB,CAAC,OAAO,EAAE;IAClD,OAAO,EAAE,iBAAiB,CAAC;IAC3B,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,cAAc,CAAC,YAAY,CAAC,CAAC;IAC1C,oBAAoB,CAAC,EAAE,cAAc,CAAC,sBAAsB,CAAC,CAAC;CAC/D,GAAG,OAAO,CAAC,aAAa,CAAC,CA+HzB"}
@@ -16,6 +16,9 @@ export async function runDependencyCommand(options) {
16
16
  registry: options.registry,
17
17
  ...(options.env ? { env: options.env } : {}),
18
18
  ...(options.onProgress ? { onProgress: options.onProgress } : {}),
19
+ ...(options.requestBuildApproval
20
+ ? { requestBuildApproval: options.requestBuildApproval }
21
+ : {}),
19
22
  };
20
23
  const names = [];
21
24
  let from;
@@ -1,5 +1,9 @@
1
1
  import { type RuntimePlatform, type FinalizedGraph } from "../resolve/resolver.ts";
2
2
  import type { WorkspaceGraph } from "./workspace-graph.ts";
3
+ /** Summarize expected platform filtering without emitting one warning per variant. */
4
+ export declare function platformOmissionSummary(omissions: readonly {
5
+ name: string;
6
+ }[]): string | undefined;
3
7
  export declare function selectPlatformGraph(graph: FinalizedGraph, optionalRoots: ReadonlySet<string>, platform?: RuntimePlatform): FinalizedGraph;
4
8
  export declare function selectWorkspacePlatform(graph: WorkspaceGraph, platform?: RuntimePlatform, omitDev?: boolean): WorkspaceGraph;
5
9
  //# sourceMappingURL=platform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/install/platform.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAuG3D,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,EAClC,QAAQ,kBAAoB,GAC3B,cAAc,CAqBhB;AACD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,EACrB,QAAQ,kBAAoB,EAC5B,OAAO,UAAQ,GACd,cAAc,CA+BhB"}
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/install/platform.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,sFAAsF;AACtF,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GACrC,MAAM,GAAG,SAAS,CAQpB;AAsGD,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,EAClC,QAAQ,kBAAoB,GAC3B,cAAc,CAqBhB;AACD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,cAAc,EACrB,QAAQ,kBAAoB,EAC5B,OAAO,UAAQ,GACd,cAAc,CA+BhB"}
@@ -3,6 +3,17 @@ import { satisfies } from "semver";
3
3
  import { ZyrkError } from "../errors.js";
4
4
  import { currentPlatform, } from "../resolve/resolver.js";
5
5
  import { lockMismatch } from "./lock.js";
6
+ /** Summarize expected platform filtering without emitting one warning per variant. */
7
+ export function platformOmissionSummary(omissions) {
8
+ if (omissions.length === 0)
9
+ return undefined;
10
+ const packages = new Set(omissions.map((item) => item.name));
11
+ const entries = omissions.length;
12
+ const variants = packages.size;
13
+ const entryLabel = `${entries} platform-specific optional ${entries === 1 ? "dependency" : "dependencies"}`;
14
+ const packageLabel = `${variants} ${variants === 1 ? "package variant" : "package variants"}`;
15
+ return `Skipped ${entryLabel} across ${packageLabel}; this is expected when selecting dependencies for the current platform.`;
16
+ }
6
17
  function compatible(node, platform) {
7
18
  return ([
8
19
  [node.platform?.os, platform.os],
@@ -12,6 +12,7 @@ export type BuildCandidate = {
12
12
  command: string;
13
13
  }>;
14
14
  };
15
+ export declare const WORKSPACE_POLICY_FILE = "zyrk-workspace.json";
15
16
  export declare function candidates(nodes: readonly ResolvedOccurrence[], dirs: ReadonlyMap<string, string>): Promise<BuildCandidate[]>;
16
17
  export declare function candidateDigest(graph: GraphDoc, builds: readonly BuildCandidate[], policy: unknown, patches: string): string;
17
18
  export declare function approveCandidate(options: {
@@ -21,7 +22,15 @@ export declare function approveCandidate(options: {
21
22
  selected: readonly string[];
22
23
  env?: NodeJS.ProcessEnv;
23
24
  policy?: unknown;
25
+ portable?: boolean;
24
26
  }): Promise<void>;
27
+ export declare function approvedCandidates(options: {
28
+ project: string;
29
+ env: NodeJS.ProcessEnv;
30
+ digest: string;
31
+ builds: readonly BuildCandidate[];
32
+ policy: unknown;
33
+ }): Promise<BuildCandidate[]>;
25
34
  export declare function assertApproved(options: {
26
35
  project: string;
27
36
  env: NodeJS.ProcessEnv;
@@ -1 +1 @@
1
- {"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/install/private.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAOjE,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,CAAC;AAUF,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,OAAO,CAAC,cAAc,EAAE,CAAC,CA8C3B;AAyBD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,GACd,MAAM,CASR;AACD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBhB;AAcD,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChB;AACD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,QAAQ,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGzB;AAID,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,QAAQ,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFhB"}
1
+ {"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/install/private.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAOjE,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAClD,CAAC;AACF,eAAO,MAAM,qBAAqB,wBAAwB,CAAC;AAe3D,wBAAsB,UAAU,CAC9B,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,OAAO,CAAC,cAAc,EAAE,CAAC,CA8C3B;AAyBD,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,MAAM,GACd,MAAM,CASR;AACD,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgChB;AA0ED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAgC5B;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AACD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,QAAQ,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,aAAa,CAAC,CAmGzB;AAID,wBAAsB,YAAY,CAAC,OAAO,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAClC,QAAQ,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFhB"}
@@ -13,6 +13,7 @@ import { canonicalJson } from "../util/json.js";
13
13
  import { isErrno, pathExists, writeFileAtomic } from "../util/fsx.js";
14
14
  import { superviseChild } from "../util/proc.js";
15
15
  import { redactSecrets } from "../log.js";
16
+ export const WORKSPACE_POLICY_FILE = "zyrk-workspace.json";
16
17
  function fail(message) {
17
18
  throw new ZyrkError("ZYRK_SCRIPT_BLOCKED", message, {
18
19
  remedy: "Review the exact candidate with zyrk approve-builds, then explicitly approve its package names. Mandatory denials cannot be overridden.",
@@ -93,16 +94,62 @@ export async function approveCandidate(options) {
93
94
  const selected = new Set(options.selected);
94
95
  const all = selected.has("*");
95
96
  for (const name of selected)
96
- if (name !== "*" && !options.builds.some((b) => b.name === name))
97
+ if (name !== "*" &&
98
+ !options.builds.some((b) => b.name === name || b.sourceId === name))
97
99
  fail("An approval selector does not match a reviewed build candidate.");
98
100
  await writeFileAtomic(path.join(options.project, ".zyrk-local", "approvals.json"), JSON.stringify({
99
101
  schemaVersion: 1,
100
102
  candidate: options.digest,
101
103
  sources: options.builds
102
- .filter((b) => all || selected.has(b.name))
104
+ .filter((b) => all || selected.has(b.name) || selected.has(b.sourceId))
103
105
  .map((b) => b.sourceId)
104
106
  .sort(),
105
107
  }), { mode: 0o600 });
108
+ if (options.portable) {
109
+ const selectedBuilds = options.builds.filter((build) => all || selected.has(build.name) || selected.has(build.sourceId));
110
+ await writePortableApprovals(options.project, selectedBuilds);
111
+ }
112
+ }
113
+ async function readPortableApprovals(project) {
114
+ const file = path.join(project, WORKSPACE_POLICY_FILE);
115
+ let raw;
116
+ try {
117
+ const stat = await lstat(file);
118
+ if (!stat.isFile() || stat.isSymbolicLink())
119
+ fail("The workspace build-approval file must be a regular file.");
120
+ raw = JSON.parse(await readFile(file, "utf8"));
121
+ }
122
+ catch (cause) {
123
+ if (cause instanceof ZyrkError)
124
+ throw cause;
125
+ if (isErrno(cause, "ENOENT"))
126
+ return [];
127
+ fail("The workspace build-approval file is unreadable or malformed.");
128
+ }
129
+ if (!object(raw) ||
130
+ raw.schemaVersion !== 1 ||
131
+ !Array.isArray(raw.approvedBuilds) ||
132
+ raw.approvedBuilds.some((item) => !object(item) ||
133
+ typeof item.name !== "string" ||
134
+ typeof item.version !== "string" ||
135
+ typeof item.sourceId !== "string" ||
136
+ !/^src1_[a-f0-9]{40}$/.test(item.sourceId)))
137
+ fail("The workspace build-approval file has an unsupported format.");
138
+ return raw.approvedBuilds;
139
+ }
140
+ async function writePortableApprovals(project, selected) {
141
+ const current = await readPortableApprovals(project);
142
+ const bySource = new Map(current.map((item) => [item.sourceId, item]));
143
+ for (const build of selected)
144
+ bySource.set(build.sourceId, {
145
+ name: build.name,
146
+ version: build.version,
147
+ sourceId: build.sourceId,
148
+ });
149
+ await writeFileAtomic(path.join(project, WORKSPACE_POLICY_FILE), `${JSON.stringify({
150
+ schemaVersion: 1,
151
+ approvedBuilds: [...bySource.values()].sort((a, b) => a.name.localeCompare(b.name)),
152
+ }, null, 2)}\n`, { mode: 0o644 });
106
153
  }
107
154
  async function readUserPolicy(env) {
108
155
  let user = {};
@@ -117,9 +164,9 @@ async function readUserPolicy(env) {
117
164
  fail("Mandatory user build policy is malformed.");
118
165
  return user;
119
166
  }
120
- export async function assertApproved(options) {
167
+ export async function approvedCandidates(options) {
121
168
  if (!options.builds.length)
122
- return;
169
+ return [];
123
170
  const user = await readUserPolicy(options.env);
124
171
  let local;
125
172
  try {
@@ -133,19 +180,29 @@ export async function assertApproved(options) {
133
180
  throw cause;
134
181
  local = undefined;
135
182
  }
136
- for (const build of options.builds) {
183
+ const portable = await readPortableApprovals(options.project);
184
+ return options.builds.filter((build) => {
137
185
  if (denied(user.scripts, build) || denied(options.policy, build))
138
- fail(`Build of ${build.name}@${build.version} is denied by policy.`);
139
- const portable = object(options.policy) &&
186
+ return false;
187
+ const manifestAllowed = object(options.policy) &&
140
188
  Array.isArray(options.policy.allow) &&
141
189
  options.policy.allow.includes(build.sourceId);
142
- if (!portable &&
143
- (!object(local) ||
144
- local.schemaVersion !== 1 ||
145
- local.candidate !== options.digest ||
146
- !Array.isArray(local.sources) ||
147
- !local.sources.includes(build.sourceId)))
148
- fail(`Build of ${build.name}@${build.version} has not been approved for this exact candidate.`);
190
+ const workspaceAllowed = portable.some((approval) => approval.sourceId === build.sourceId);
191
+ const localAllowed = object(local) &&
192
+ local.schemaVersion === 1 &&
193
+ local.candidate === options.digest &&
194
+ Array.isArray(local.sources) &&
195
+ local.sources.includes(build.sourceId);
196
+ return manifestAllowed || workspaceAllowed || localAllowed;
197
+ });
198
+ }
199
+ export async function assertApproved(options) {
200
+ const approved = await approvedCandidates(options);
201
+ if (approved.length !== options.builds.length) {
202
+ const pending = options.builds.find((build) => !approved.some((item) => item.sourceId === build.sourceId));
203
+ fail(pending
204
+ ? `Build of ${pending.name}@${pending.version} has not been approved for this exact source.`
205
+ : "Not every dependency build candidate is approved.");
149
206
  }
150
207
  }
151
208
  export async function materializePrivate(options) {
@@ -43,6 +43,7 @@ export type InstallOptions = {
43
43
  env?: NodeJS.ProcessEnv;
44
44
  reviewBuilds?: boolean;
45
45
  approveBuilds?: readonly string[];
46
+ requestBuildApproval?: (builds: readonly BuildCandidate[]) => Promise<readonly string[]>;
46
47
  rebuild?: boolean;
47
48
  repair?: boolean;
48
49
  resolveBudget?: Partial<ResolverLimits>;
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/install/run.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/install/run.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  import { sourceProgress, reportLinked } from "./progress.js";
2
- import { selectPlatformGraph } from "./platform.js";
2
+ import { platformOmissionSummary, selectPlatformGraph } from "./platform.js";
3
3
  import { findWorkspaceRoot } from "./workspaces.js";
4
4
  import { assertSupportedSourcePayload } from "./source-policy.js";
5
5
  import { prepareRepair } from "./repair.js";
6
6
  import { createFetchOperation } from "../fetch/package.js";
7
- import { candidates, candidateDigest, approveCandidate, assertApproved, materializePrivate, buildPrivate, } from "./private.js";
7
+ import { candidates, candidateDigest, approveCandidate, approvedCandidates, materializePrivate, buildPrivate, } from "./private.js";
8
8
  import { loadProjectPatches, applyProjectPatches } from "../commands/patch.js";
9
9
  import { registerGeneration } from "./metadata.js";
10
10
  import { randomId } from "../util/ids.js";
@@ -420,7 +420,10 @@ export async function installProject(options) {
420
420
  name: "package.json",
421
421
  contents: `${JSON.stringify(manifest, null, 2)}\n`,
422
422
  });
423
- const warnings = (activeResolution.omissions ?? []).map((item) => `${item.name}: omitted optional dependency incompatible with the selected platform.`);
423
+ const warnings = [];
424
+ const omissionSummary = platformOmissionSummary(activeResolution.omissions ?? []);
425
+ if (omissionSummary)
426
+ warnings.push(omissionSummary);
424
427
  for (const stage of ["preinstall", "install", "postinstall"])
425
428
  if (manifest.scripts?.[stage])
426
429
  warnings.push(`Root ${stage} hook was not run; invoke it explicitly after installation.`);
@@ -553,6 +556,7 @@ export async function installProject(options) {
553
556
  selected: options.approveBuilds,
554
557
  env: options.env ?? process.env,
555
558
  policy: scriptPolicy(manifest),
559
+ portable: true,
556
560
  });
557
561
  return {
558
562
  readiness: "build-review",
@@ -563,15 +567,37 @@ export async function installProject(options) {
563
567
  builds,
564
568
  };
565
569
  }
566
- const unbuilt = builds.length > 0 && options.ignoreScripts === true;
570
+ let approvedBuilds = [];
567
571
  if (builds.length && !options.ignoreScripts) {
568
- await assertApproved({
572
+ const approvalOptions = {
569
573
  project,
570
574
  env: options.env ?? process.env,
571
575
  digest: reviewedDigest,
572
576
  builds,
573
577
  policy: scriptPolicy(manifest),
574
- });
578
+ };
579
+ approvedBuilds = await approvedCandidates(approvalOptions);
580
+ if (approvedBuilds.length < builds.length &&
581
+ options.requestBuildApproval) {
582
+ const pending = builds.filter((build) => !approvedBuilds.some((item) => item.sourceId === build.sourceId));
583
+ const selected = await options.requestBuildApproval(pending);
584
+ if (selected.length) {
585
+ await approveCandidate({
586
+ project,
587
+ digest: reviewedDigest,
588
+ builds: pending,
589
+ selected,
590
+ env: options.env ?? process.env,
591
+ policy: scriptPolicy(manifest),
592
+ portable: true,
593
+ });
594
+ approvedBuilds = await approvedCandidates(approvalOptions);
595
+ }
596
+ }
597
+ }
598
+ const unbuilt = builds.length > 0 &&
599
+ (options.ignoreScripts === true || approvedBuilds.length < builds.length);
600
+ if (approvedBuilds.length && !options.ignoreScripts) {
575
601
  if (!privateGraph) {
576
602
  realized = await materializePrivate({
577
603
  project,
@@ -586,7 +612,7 @@ export async function installProject(options) {
586
612
  project,
587
613
  generationId,
588
614
  nodes: activeNodes,
589
- builds,
615
+ builds: approvedBuilds,
590
616
  realized,
591
617
  env: options.env ?? process.env,
592
618
  });
@@ -610,9 +636,10 @@ export async function installProject(options) {
610
636
  privateGraph = true;
611
637
  }
612
638
  }
613
- if (unbuilt)
614
- for (const build of builds)
615
- warnings.push(`${build.name}@${build.version}: skipped ${build.stages.map((s) => s.name).join(", ")}; intentionally unbuilt.`);
639
+ if (unbuilt) {
640
+ const skipped = builds.length - approvedBuilds.length;
641
+ warnings.push(`${skipped} dependency build ${skipped === 1 ? "script was" : "scripts were"} skipped because approval was not granted; readiness is intentionally unbuilt. Run zyrk approve-builds to review and approve sources.`);
642
+ }
616
643
  if (privateGraph)
617
644
  warnings.push("Using a complete project-private dependency graph; build/runtime writes do not change shared source.");
618
645
  const entries = selectedRoots.map((r) => ({
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-install.d.ts","sourceRoot":"","sources":["../../src/install/workspace-install.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAuH9D,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAqrBxB"}
1
+ {"version":3,"file":"workspace-install.d.ts","sourceRoot":"","sources":["../../src/install/workspace-install.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAuH9D,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,aAAa,CAAC,CAqtBxB"}
@@ -1,5 +1,5 @@
1
1
  import { sourceProgress, reportLinked } from "./progress.js";
2
- import { selectWorkspacePlatform } from "./platform.js";
2
+ import { platformOmissionSummary, selectWorkspacePlatform, } from "./platform.js";
3
3
  /** Multi-importer live workspace installation with one publication journal. */
4
4
  import { chmod, copyFile, lstat, mkdir, readFile, realpath, rename, rm, symlink, } from "node:fs/promises";
5
5
  import { constants } from "node:fs";
@@ -32,7 +32,7 @@ import { loadProjectPatches, applyProjectPatches } from "../commands/patch.js";
32
32
  import { prepareRepair } from "./repair.js";
33
33
  import { assertSupportedSourcePayload } from "./source-policy.js";
34
34
  import { registerGeneration } from "./metadata.js";
35
- import { candidates, candidateDigest, approveCandidate, assertApproved, buildPrivate, } from "./private.js";
35
+ import { candidates, candidateDigest, approveCandidate, approvedCandidates, buildPrivate, } from "./private.js";
36
36
  const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
37
37
  const unsupported = (message) => {
38
38
  throw new ZyrkError("ZYRK_NOT_IMPLEMENTED", message, {
@@ -191,7 +191,10 @@ export async function installWorkspaceProject(options) {
191
191
  ? await readFile(path.join(root, "package-lock.json"), "utf8")
192
192
  : JSON.stringify(doc, null, 2) + "\n";
193
193
  graph = selectWorkspacePlatform(graph, options.platform, options.omitDev);
194
- const warnings = graph.omissions.map((o) => `${o.name}: omitted incompatible optional workspace dependency.`);
194
+ const warnings = [];
195
+ const omissionSummary = platformOmissionSummary(graph.omissions);
196
+ if (omissionSummary)
197
+ warnings.push(omissionSummary);
195
198
  for (const importer of project.importers)
196
199
  for (const stage of ["preinstall", "install", "postinstall"])
197
200
  if (importer.manifest.scripts?.[stage])
@@ -441,6 +444,7 @@ export async function installWorkspaceProject(options) {
441
444
  selected: options.approveBuilds,
442
445
  env: options.env ?? process.env,
443
446
  policy,
447
+ portable: true,
444
448
  });
445
449
  return {
446
450
  readiness: "build-review",
@@ -451,14 +455,38 @@ export async function installWorkspaceProject(options) {
451
455
  builds,
452
456
  };
453
457
  }
458
+ let approvedBuilds = [];
454
459
  if (builds.length && !options.ignoreScripts) {
455
- await assertApproved({
460
+ const approvalOptions = {
456
461
  project: root,
457
462
  env: options.env ?? process.env,
458
463
  digest,
459
464
  builds,
460
465
  policy,
461
- });
466
+ };
467
+ approvedBuilds = await approvedCandidates(approvalOptions);
468
+ if (approvedBuilds.length < builds.length &&
469
+ options.requestBuildApproval) {
470
+ const pending = builds.filter((build) => !approvedBuilds.some((item) => item.sourceId === build.sourceId));
471
+ const selected = await options.requestBuildApproval(pending);
472
+ if (selected.length) {
473
+ await approveCandidate({
474
+ project: root,
475
+ digest,
476
+ builds: pending,
477
+ selected,
478
+ env: options.env ?? process.env,
479
+ policy,
480
+ portable: true,
481
+ });
482
+ approvedBuilds = await approvedCandidates(approvalOptions);
483
+ }
484
+ }
485
+ }
486
+ const skippedBuilds = builds.length - approvedBuilds.length;
487
+ if (skippedBuilds > 0)
488
+ warnings.push(`${skippedBuilds} dependency build ${skippedBuilds === 1 ? "script was" : "scripts were"} skipped because approval was not granted; readiness is intentionally unbuilt. Run zyrk approve-builds to review and approve sources.`);
489
+ if (approvedBuilds.length && !options.ignoreScripts) {
462
490
  // Approved writes need a coherent fully private registry closure.
463
491
  await allocatePrivate(new Set(nodes.map((n) => n.key)));
464
492
  options.onProgress?.({ phase: "building" });
@@ -466,12 +494,12 @@ export async function installWorkspaceProject(options) {
466
494
  project: root,
467
495
  generationId,
468
496
  nodes,
469
- builds,
497
+ builds: approvedBuilds,
470
498
  realized,
471
499
  env: options.env ?? process.env,
472
500
  });
473
501
  }
474
- const readiness = builds.length && options.ignoreScripts
502
+ const readiness = builds.length && (options.ignoreScripts || skippedBuilds > 0)
475
503
  ? "intentionally-unbuilt"
476
504
  : "ready";
477
505
  registration = await registerGeneration({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyrk",
3
- "version": "0.6.8",
3
+ "version": "0.7.2",
4
4
  "description": "Storage-efficient Node package manager: one verified store per user, shared across projects through links.",
5
5
  "license": "MIT",
6
6
  "type": "module",