@jr2/cli 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/build.ts +50 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jr2/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The jr2 CLI: init, up, run — the interface to a jr2 Instance.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -37,7 +37,7 @@
37
37
  "types": "./src/index.ts",
38
38
  "dependencies": {
39
39
  "ts-blank-space": "^0.9.0",
40
- "@jr2/orchestrator": "0.1.1"
40
+ "@jr2/orchestrator": "0.1.2"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.0.1",
package/src/build.ts CHANGED
@@ -998,30 +998,47 @@ const execCommand: RunCommand = async (command, args, cwd) => {
998
998
 
999
999
  /**
1000
1000
  * Materialize the Instance into `outDir` (ADR-0043, as amended there). Two shapes, and the key is
1001
- * the INSTANCE's own: walk up for a `pnpm-workspace.yaml`, because that — not
1002
- * {@link detectKitCheckout} — is what says whether `pnpm deploy` can run at all. A checkout CLI can
1003
- * legitimately drive a standalone instance (the developer's `/tmp` folder), and keying on the CLI's
1004
- * own provenance would send that instance down a path whose job — materializing workspace symlinks
1005
- * — only exists in a workspace. The mirror holds too: an INSTALLED kit driving an instance nested
1006
- * in the user's own pnpm monorepo takes `pnpm deploy`, because that instance carries no lockfile of
1007
- * its own — the workspace root holds it.
1001
+ * the INSTANCE's own, asked in this order:
1008
1002
  *
1009
- * - Workspace member (the kit checkout's `templates/*` and `features/kind-instance`): `pnpm deploy
1010
- * --legacy`, unchanged. pnpm is a contributor prerequisite, like go for the operator, never a
1011
- * product dependency.
1012
- * - Standalone: stage a copy ({@link BUNDLE_STAGE_EXCLUDE}) and run a frozen production install
1013
- * from the committed lockfile ({@link lockfileInstall}) inside it.
1003
+ * 1. Its own lockfile. An instance that carries one is self-contained the lockfile IS its
1004
+ * dependency graph, wherever the folder sits so it is staged as a copy
1005
+ * ({@link BUNDLE_STAGE_EXCLUDE}) and installed frozen and production-only by the lockfile's own
1006
+ * package manager ({@link lockfileInstall}). This is how a standalone instance committed INSIDE
1007
+ * a pnpm monorepo bundles: the kit's own `docs/intro` sits under the checkout's
1008
+ * `pnpm-workspace.yaml` and is deliberately not one of its packages.
1009
+ * 2. A `pnpm-workspace.yaml` above it. A workspace member carries no lockfile of its own — the
1010
+ * workspace root holds it — so it is the ABSENCE of one that sends the instance to `pnpm deploy
1011
+ * --legacy`, unchanged. pnpm is a contributor prerequisite, like go for the operator, never a
1012
+ * product dependency.
1013
+ * 3. Neither: {@link lockfileInstall}'s named refusal.
1014
+ *
1015
+ * Neither key is {@link detectKitCheckout}: a checkout CLI legitimately drives a standalone
1016
+ * instance (the developer's `/tmp` folder), and an installed kit legitimately drives a member of
1017
+ * the user's own monorepo. Asking "is there a workspace file above me" FIRST — the previous rule —
1018
+ * answered a question about an ancestor, and sent shape (1) down `pnpm deploy`, which pnpm answers
1019
+ * for a non-member with "No projects matched the filters" and exit 0: no bundle, and the failure
1020
+ * surfaced later as an ENOENT from the seal. That silent success is why the deploy branch checks
1021
+ * that the bundle exists, and names the shape when it does not.
1014
1022
  */
1015
1023
  export async function bundleInstance(
1016
1024
  instanceDir: string,
1017
1025
  outDir: string,
1018
1026
  run: RunCommand = execCommand,
1019
1027
  ): Promise<void> {
1020
- if (await pnpmWorkspaceRoot(instanceDir)) {
1028
+ const root = (await hasOwnLockfile(instanceDir)) ? undefined : await pnpmWorkspaceRoot(instanceDir);
1029
+ if (root) {
1021
1030
  const pkg = JSON.parse(await readFile(join(instanceDir, "package.json"), "utf8")) as { name?: string };
1022
1031
  if (!pkg.name) throw new Error(`${instanceDir}/package.json has no "name" — needed to bundle the instance`);
1023
1032
  // --legacy: materialize (copy) workspace deps into the bundle rather than linking them.
1024
1033
  await run("pnpm", ["--filter", pkg.name, "--prod", "deploy", "--legacy", outDir], instanceDir);
1034
+ // pnpm exits 0 — "No projects matched the filters" — for a `--filter` that names no member.
1035
+ if (!(await exists(outDir))) {
1036
+ throw new Error(
1037
+ `pnpm deploy wrote no bundle for ${instanceDir}: it sits under the pnpm workspace at ${root} but is not ` +
1038
+ `one of its packages, and it has no lockfile of its own — add it to the workspace's \`packages:\`, ` +
1039
+ `or install and commit a lockfile so it bundles as a standalone instance`,
1040
+ );
1041
+ }
1025
1042
  return;
1026
1043
  }
1027
1044
  // Asked before the copy: an instance with no lockfile must fail on the cheap half.
@@ -1033,9 +1050,26 @@ export async function bundleInstance(
1033
1050
  await run(command, args, outDir);
1034
1051
  }
1035
1052
 
1036
- /** The nearest `pnpm-workspace.yaml` at or above `dir`, i.e. "is this instance a workspace member".
1037
- * A file test, not a manifest parse: pnpm's own membership rule starts here, and a `packages:` glob
1038
- * that excluded this directory would leave `pnpm deploy --filter` failing loudly by name. */
1053
+ /** Whether the instance carries a lockfile of ANY manager yarn included, so a yarn instance under
1054
+ * a workspace reaches {@link lockfileInstall}'s named refusal rather than `pnpm deploy`. */
1055
+ async function hasOwnLockfile(instanceDir: string): Promise<boolean> {
1056
+ const present = new Set(await readdir(instanceDir));
1057
+ return [...LOCKFILE_INSTALLS.flatMap((row) => row.lockfiles), YARN_LOCKFILE].some((f) => present.has(f));
1058
+ }
1059
+
1060
+ async function exists(path: string): Promise<boolean> {
1061
+ try {
1062
+ await stat(path);
1063
+ return true;
1064
+ } catch {
1065
+ return false;
1066
+ }
1067
+ }
1068
+
1069
+ /** The nearest `pnpm-workspace.yaml` at or above `dir`. A file test, not a manifest parse:
1070
+ * membership proper is pnpm's to decide, and this is asked only for an instance with no lockfile of
1071
+ * its own ({@link bundleInstance}). A `packages:` glob that excludes the directory does NOT fail
1072
+ * `pnpm deploy --filter` — pnpm exits 0 having deployed nothing — which the caller checks. */
1039
1073
  async function pnpmWorkspaceRoot(dir: string): Promise<string | undefined> {
1040
1074
  let d = resolve(dir);
1041
1075
  for (;;) {