zyrk 0.6.9 → 0.7.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/README.md CHANGED
@@ -105,9 +105,11 @@ 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
107
  Dependency `preinstall`, `install`, and `postinstall` scripts (plus implicit
108
- `binding.gyp` builds) are supported and run during `zyrk install` after you
109
- approve the exact reviewed candidates. Unreviewed package code stays blocked.
110
- Review first, trust the selected sources, then install:
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:
111
113
 
112
114
  ```bash
113
115
  zyrk approve-builds # inspect exact sources and commands; executes no package code
@@ -117,8 +119,12 @@ zyrk rebuild --offline # rebuild the complete approved locked graph
117
119
  ```
118
120
 
119
121
  Approvals are local to the exact graph, sources, patch contents, and script
120
- policy. Mandatory user denies override project approvals. Approved scripts run
121
- 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
122
128
  writes use a complete private dependency generation under `.zyrk-local/`, with
123
129
  independent writable files and dependency links confined to that generation.
124
130
  Build failure keeps the previous project graph active. `--ignore-scripts`
@@ -127,6 +133,12 @@ hooks never run automatically. A project can request runtime writable packages
127
133
  with `"zyrk": { "mutablePackages": ["package-name"] }`; this also uses a complete
128
134
  private graph.
129
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
+
130
142
  ### Run project scripts
131
143
 
132
144
  Use `zyrk run <script>` or the package-manager shortcut `zyrk <script>`; both
@@ -413,11 +425,11 @@ The commit hook runs the full check gate, increments the version in
413
425
  `package.json`, `package-lock.json`, and `VERSION`, and creates a matching version
414
426
  tag after the commit. Keep changes focused and never bypass the hooks.
415
427
 
416
- CI checks pushed commits and pull requests. The separate
417
- [npm publishing action](.github/workflows/npm-publish.yml) is triggered when a
418
- GitHub release is published. It verifies the release tag, formatting, typecheck,
419
- build, and tests, then publishes that exact commit. Ordinary commits do not
420
- 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
421
433
  unavailable advisory service fails separately from a clean result.
422
434
 
423
435
  The contributor guide is [AGENTS.md](AGENTS.md). Bug reports and reproducible
package/VERSION CHANGED
@@ -1 +1 @@
1
- v0.6.9
1
+ v0.7.0
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":"AA6BA,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,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,CA+epE"}
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
@@ -13,6 +13,7 @@ import { installProject } from "../install/run.js";
13
13
  import { installRegistry } from "../install/registry.js";
14
14
  import { readFileSync } from "node:fs";
15
15
  import { readFile } from "node:fs/promises";
16
+ import { createInterface } from "node:readline/promises";
16
17
  import * as path from "node:path";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import { runCreate, starterSpecFor } from "../dlx/create.js";
@@ -214,6 +215,39 @@ function readCliVersion() {
214
215
  return "unknown";
215
216
  }
216
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
+ }
217
251
  /** Parse, dispatch, delegate, or run a tool, and emit. The only impure seam. */
218
252
  export async function runCli(options) {
219
253
  const parsed = parseArgs(options.argv);
@@ -466,6 +500,11 @@ export async function runCli(options) {
466
500
  env: options.env,
467
501
  specs,
468
502
  projectDir: cwd,
503
+ ...(options.stdin?.isTTY && options.stderr.isTTY && !parsed.json
504
+ ? {
505
+ requestBuildApproval: (builds) => promptForBuildApproval(builds, options.stdin, options.stderr),
506
+ }
507
+ : {}),
469
508
  ...tools,
470
509
  });
471
510
  progress.close();
@@ -522,6 +561,11 @@ export async function runCli(options) {
522
561
  projectDir: options.cwd ?? process.cwd(),
523
562
  store: tools.store,
524
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
+ : {}),
525
569
  });
526
570
  progress.close();
527
571
  if (parsed.json) {
@@ -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;
@@ -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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"}
@@ -4,7 +4,7 @@ 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";
@@ -556,6 +556,7 @@ export async function installProject(options) {
556
556
  selected: options.approveBuilds,
557
557
  env: options.env ?? process.env,
558
558
  policy: scriptPolicy(manifest),
559
+ portable: true,
559
560
  });
560
561
  return {
561
562
  readiness: "build-review",
@@ -566,15 +567,37 @@ export async function installProject(options) {
566
567
  builds,
567
568
  };
568
569
  }
569
- const unbuilt = builds.length > 0 && options.ignoreScripts === true;
570
+ let approvedBuilds = [];
570
571
  if (builds.length && !options.ignoreScripts) {
571
- await assertApproved({
572
+ const approvalOptions = {
572
573
  project,
573
574
  env: options.env ?? process.env,
574
575
  digest: reviewedDigest,
575
576
  builds,
576
577
  policy: scriptPolicy(manifest),
577
- });
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) {
578
601
  if (!privateGraph) {
579
602
  realized = await materializePrivate({
580
603
  project,
@@ -589,7 +612,7 @@ export async function installProject(options) {
589
612
  project,
590
613
  generationId,
591
614
  nodes: activeNodes,
592
- builds,
615
+ builds: approvedBuilds,
593
616
  realized,
594
617
  env: options.env ?? process.env,
595
618
  });
@@ -613,9 +636,10 @@ export async function installProject(options) {
613
636
  privateGraph = true;
614
637
  }
615
638
  }
616
- if (unbuilt)
617
- for (const build of builds)
618
- 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
+ }
619
643
  if (privateGraph)
620
644
  warnings.push("Using a complete project-private dependency graph; build/runtime writes do not change shared source.");
621
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":"AAmBA,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"}
@@ -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, {
@@ -444,6 +444,7 @@ export async function installWorkspaceProject(options) {
444
444
  selected: options.approveBuilds,
445
445
  env: options.env ?? process.env,
446
446
  policy,
447
+ portable: true,
447
448
  });
448
449
  return {
449
450
  readiness: "build-review",
@@ -454,14 +455,38 @@ export async function installWorkspaceProject(options) {
454
455
  builds,
455
456
  };
456
457
  }
458
+ let approvedBuilds = [];
457
459
  if (builds.length && !options.ignoreScripts) {
458
- await assertApproved({
460
+ const approvalOptions = {
459
461
  project: root,
460
462
  env: options.env ?? process.env,
461
463
  digest,
462
464
  builds,
463
465
  policy,
464
- });
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) {
465
490
  // Approved writes need a coherent fully private registry closure.
466
491
  await allocatePrivate(new Set(nodes.map((n) => n.key)));
467
492
  options.onProgress?.({ phase: "building" });
@@ -469,12 +494,12 @@ export async function installWorkspaceProject(options) {
469
494
  project: root,
470
495
  generationId,
471
496
  nodes,
472
- builds,
497
+ builds: approvedBuilds,
473
498
  realized,
474
499
  env: options.env ?? process.env,
475
500
  });
476
501
  }
477
- const readiness = builds.length && options.ignoreScripts
502
+ const readiness = builds.length && (options.ignoreScripts || skippedBuilds > 0)
478
503
  ? "intentionally-unbuilt"
479
504
  : "ready";
480
505
  registration = await registerGeneration({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyrk",
3
- "version": "0.6.9",
3
+ "version": "0.7.0",
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",