canopycms 0.0.67-int.88 → 0.0.67-int.90

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 (55) hide show
  1. package/dist/ai/resolve-branch.d.ts +2 -2
  2. package/dist/ai/resolve-branch.js +5 -5
  3. package/dist/api/assets.js +1 -1
  4. package/dist/api/branch.js +3 -2
  5. package/dist/assets/pipeline.d.ts +4 -3
  6. package/dist/assets/pipeline.js +11 -15
  7. package/dist/assets/sharp-loader.d.ts +28 -0
  8. package/dist/assets/sharp-loader.js +44 -0
  9. package/dist/assets/transform-directives.d.ts +1 -1
  10. package/dist/assets/transform-directives.js +1 -1
  11. package/dist/assets/transform.d.ts +14 -2
  12. package/dist/assets/transform.js +23 -3
  13. package/dist/auth/plugin.js +1 -1
  14. package/dist/branch-workspace.d.ts +4 -2
  15. package/dist/branch-workspace.js +16 -10
  16. package/dist/build-mode.d.ts +32 -4
  17. package/dist/build-mode.js +32 -4
  18. package/dist/cli/cli.d.ts +11 -1
  19. package/dist/cli/cli.js +1260 -73
  20. package/dist/cli/generate-ai-content.js +5 -2
  21. package/dist/cli/init-github-app.d.ts +519 -0
  22. package/dist/cli/init-github-app.js +1551 -0
  23. package/dist/cli/init.js +71 -8
  24. package/dist/cli/project-detect.js +9 -1
  25. package/dist/cli/template-files/Dockerfile.cms.template +22 -25
  26. package/dist/cli/template-files/canopycms.config.ts.template +8 -6
  27. package/dist/cli/template-files/cdk-app.ts.template +48 -1
  28. package/dist/cli/template-files/cdk-tsconfig.json.template +41 -0
  29. package/dist/cli/template-files/cms-stack.ts.template +82 -19
  30. package/dist/cli/template-files/deploy-cms.yml.template +84 -4
  31. package/dist/cli/template-files/dockerignore.template +6 -0
  32. package/dist/cli/template-files/middleware-clerk.ts.template +17 -0
  33. package/dist/cli/template-files/middleware.ts.template +11 -7
  34. package/dist/cli/templates.d.ts +1 -0
  35. package/dist/cli/templates.js +8 -4
  36. package/dist/content-reader.js +5 -4
  37. package/dist/context.d.ts +5 -5
  38. package/dist/git-manager.js +3 -3
  39. package/dist/github-service.d.ts +41 -3
  40. package/dist/github-service.js +11 -1
  41. package/dist/operating-mode/mode-env.d.ts +9 -4
  42. package/dist/operating-mode/mode-env.js +9 -4
  43. package/dist/services.js +7 -6
  44. package/dist/utils/error.js +36 -0
  45. package/dist/utils/provisioning-lock.d.ts +4 -4
  46. package/dist/utils/provisioning-lock.js +7 -7
  47. package/dist/worker/cms-worker.d.ts +153 -3
  48. package/dist/worker/cms-worker.js +228 -6
  49. package/dist/worker/git-sync.js +8 -2
  50. package/dist/worker/github-auth.d.ts +268 -0
  51. package/dist/worker/github-auth.js +413 -0
  52. package/dist/worker/task-runner.d.ts +8 -1
  53. package/dist/worker/task-runner.js +42 -3
  54. package/dist/worker/worker-context.d.ts +26 -3
  55. package/package.json +2 -1
@@ -6,8 +6,8 @@ import type { CanopyConfig } from '../config/index.js';
6
6
  /**
7
7
  * Resolve the branch root directory for reading content.
8
8
  *
9
- * - Static deployment: current working directory (content is in the checkout)
10
- * - Server (prod/dev): load or create the default active branch workspace
9
+ * - Static deployment, or any build: current working directory (content is in the checkout)
10
+ * - Server (prod/dev) at run time: load or create the default active branch workspace
11
11
  *
12
12
  * Branch resolution priority (mirrors createActiveBranchDetector in services.ts):
13
13
  * 1. Explicit `defaultActiveBranch` in config
@@ -3,13 +3,13 @@
3
3
  * Used by both the route handler and build utility.
4
4
  */
5
5
  import { loadOrCreateBranchContext } from '../branch-workspace.js';
6
- import { isDeployedStatic } from '../build-mode.js';
6
+ import { readsFromCheckout } from '../build-mode.js';
7
7
  import { detectHeadBranch } from '../utils/git.js';
8
8
  /**
9
9
  * Resolve the branch root directory for reading content.
10
10
  *
11
- * - Static deployment: current working directory (content is in the checkout)
12
- * - Server (prod/dev): load or create the default active branch workspace
11
+ * - Static deployment, or any build: current working directory (content is in the checkout)
12
+ * - Server (prod/dev) at run time: load or create the default active branch workspace
13
13
  *
14
14
  * Branch resolution priority (mirrors createActiveBranchDetector in services.ts):
15
15
  * 1. Explicit `defaultActiveBranch` in config
@@ -17,8 +17,8 @@ import { detectHeadBranch } from '../utils/git.js';
17
17
  * 3. Fall back to `defaultBaseBranch` or 'main'
18
18
  */
19
19
  export async function resolveBranchRoot(config) {
20
- // Static deployments read content directly from the checkout — no branch workspace needed
21
- if (isDeployedStatic(config)) {
20
+ // Static deployments and builds read content directly from the checkout — no branch workspace needed
21
+ if (readsFromCheckout(config)) {
22
22
  return process.cwd();
23
23
  }
24
24
  let activeBranch;
@@ -232,7 +232,7 @@ const deleteAssetHandler = async (ctx, req, params) => {
232
232
  /** Cache-Control applied to every transform output this route writes/serves - matches finalize.ts's PUBLIC_CACHE_CONTROL for static public objects. */
233
233
  const TRANSFORM_CACHE_CONTROL = 'public, max-age=31536000, immutable';
234
234
  /**
235
- * Lazy dev-mode emulation of the prod transform Lambda (PR 7 reuses
235
+ * Lazy dev-mode emulation of the prod transform Lambda (which reuses
236
236
  * `parseTransformPath`/`formatDirectives`/`applyTransform` unchanged): parse
237
237
  * the request, load the original, transform it, write the result back under
238
238
  * its CANONICAL key (so a non-canonically-ordered directive string still
@@ -106,8 +106,9 @@ export const createBranchHandler = async (ctx, req, body) => {
106
106
  // reserved canopycms-settings- prefix, and the remote-mirror check
107
107
  // further down) apply ONLY to this user-facing creation path.
108
108
  // http/handler.ts's auto-create (base/active/settings branches) and
109
- // branch-workspace.ts's loadOrCreateBranchContext (reached from content
110
- // reads and the AI pipeline) provision system/known branch names, not
109
+ // branch-workspace.ts's loadOrCreateBranchContext (reached from run-time
110
+ // content reads and the AI pipeline; build-time reads return the checkout
111
+ // and never provision) provision system/known branch names, not
111
112
  // user-chosen ones, and deliberately stay uncovered -- intentional, not
112
113
  // an oversight.
113
114
  // Prevent git branch name collision with the settings branch. Settings
@@ -6,9 +6,10 @@
6
6
  *
7
7
  * Server-only - never import this module from client/editor code. It pulls
8
8
  * in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
9
- * in a browser bundle. `sharp` is loaded dynamically (see
10
- * `rasterIsDecodable` below) rather than statically imported, but is just as
11
- * server-only - it is never reachable from a browser bundle either way.
9
+ * in a browser bundle. `sharp` is loaded on first use through `loadSharp()`
10
+ * (sharp-loader.ts; see `rasterIsDecodable` below) rather than statically
11
+ * imported, but is just as server-only - it is never reachable from a browser
12
+ * bundle either way.
12
13
  */
13
14
  import type { AssetMeta } from './types.js';
14
15
  /** Client-declared content types accepted at presign time (UX only - never trusted at finalize). */
@@ -6,15 +6,17 @@
6
6
  *
7
7
  * Server-only - never import this module from client/editor code. It pulls
8
8
  * in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
9
- * in a browser bundle. `sharp` is loaded dynamically (see
10
- * `rasterIsDecodable` below) rather than statically imported, but is just as
11
- * server-only - it is never reachable from a browser bundle either way.
9
+ * in a browser bundle. `sharp` is loaded on first use through `loadSharp()`
10
+ * (sharp-loader.ts; see `rasterIsDecodable` below) rather than statically
11
+ * imported, but is just as server-only - it is never reachable from a browser
12
+ * bundle either way.
12
13
  */
13
14
  import { fileTypeFromBuffer } from 'file-type';
14
15
  import { imageSize } from 'image-size';
15
16
  import { create as createContentDisposition } from 'content-disposition';
16
17
  import { getErrorMessage } from '../utils/error.js';
17
18
  import { hashBytes, publicKey, slugifyFilename } from './keys.js';
19
+ import { loadSharp } from './sharp-loader.js';
18
20
  import { sanitizeSvg } from './svg-sanitizer.js';
19
21
  import { MAX_ANIMATED_FRAMES, MAX_INPUT_PIXELS } from './transform-directives.js';
20
22
  const RASTER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
@@ -175,26 +177,20 @@ const UNDECODABLE_RASTER_ERROR = 'This image could not be decoded - it may be co
175
177
  * same bytes from its own probe with the same 422.
176
178
  */
177
179
  async function rasterIsDecodable(data) {
178
- // Typed as the whole module (not just its callable default export) and
179
- // dereferenced via `.default` below - sharp's own types ship an
180
- // `export const sharp: SharpConstructor; export default sharp;` pair for
181
- // the ESM entry point resolved by `import()`, so `typeof import('sharp')`
182
- // is the two-property namespace object, not the callable itself.
183
- let sharpModule;
180
+ // `loadSharp()` has already logged the load failure once for the process;
181
+ // this warning is per upload, and names what the failure costs here.
182
+ let sharp;
184
183
  try {
185
- sharpModule = await import('sharp');
184
+ sharp = await loadSharp();
186
185
  }
187
186
  catch (err) {
188
187
  console.warn(`[canopycms] sharp could not be loaded - skipping raster decode validation at finalize: ${getErrorMessage(err)}`);
189
188
  return true;
190
189
  }
191
190
  try {
192
- const probeMeta = await sharpModule
193
- .default(data, { limitInputPixels: MAX_INPUT_PIXELS })
194
- .metadata();
191
+ const probeMeta = await sharp(data, { limitInputPixels: MAX_INPUT_PIXELS }).metadata();
195
192
  const pagesToRead = Math.min(probeMeta.pages ?? 1, MAX_ANIMATED_FRAMES);
196
- await sharpModule
197
- .default(data, { pages: pagesToRead, limitInputPixels: MAX_INPUT_PIXELS })
193
+ await sharp(data, { pages: pagesToRead, limitInputPixels: MAX_INPUT_PIXELS })
198
194
  .resize({ width: DECODE_CHECK_SIZE, height: DECODE_CHECK_SIZE, fit: 'fill' })
199
195
  .toBuffer();
200
196
  return true;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The one place this package loads `sharp` at runtime. Everything else
3
+ * reaches it through `loadSharp()` and imports only its types:
4
+ * `@typescript-eslint/no-restricted-imports` in eslint.config.mjs rejects a
5
+ * static value import of `sharp` anywhere under `src/` outside tests.
6
+ *
7
+ * Why lazily. sharp is a native addon, and loading it dlopens libvips. A
8
+ * static `import sharp from 'sharp'` makes importing the MODULE GRAPH load
9
+ * libvips. With sharp external, as in an adopter's Next 16 standalone build,
10
+ * Turbopack wraps it in an async module that awaits the load when the graph is
11
+ * evaluated (cms-image-build-epic.md in `.claude/future-tasks/`, "Every route
12
+ * loads sharp"). transform.ts is reachable from `canopycms/server` and, through
13
+ * api/assets.ts, from `canopycms/http` and so from `canopycms-next`, which a
14
+ * host app's root layout can import (that adopter's did). When the libvips
15
+ * `.so` was missing from its standalone output, that one static import turned
16
+ * it into a 500 on every on-demand route, 404s included, and defeated
17
+ * pipeline.ts's deliberate fail-open. Loaded on first use, a missing binary
18
+ * affects only the image operations that need it.
19
+ *
20
+ * Memoized INCLUDING a rejection: a failed dlopen does not heal inside a
21
+ * running process, so a retry would only pay for the failure again and log
22
+ * it again. The failure is logged once, here, and every later caller gets the
23
+ * same rejected promise to handle as its operation requires: transform.ts
24
+ * lets it propagate as a server error, pipeline.ts fails open.
25
+ */
26
+ import type { SharpConstructor } from 'sharp';
27
+ /** Resolve sharp's callable constructor, loading the native module on the first call only. */
28
+ export declare function loadSharp(): Promise<SharpConstructor>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The one place this package loads `sharp` at runtime. Everything else
3
+ * reaches it through `loadSharp()` and imports only its types:
4
+ * `@typescript-eslint/no-restricted-imports` in eslint.config.mjs rejects a
5
+ * static value import of `sharp` anywhere under `src/` outside tests.
6
+ *
7
+ * Why lazily. sharp is a native addon, and loading it dlopens libvips. A
8
+ * static `import sharp from 'sharp'` makes importing the MODULE GRAPH load
9
+ * libvips. With sharp external, as in an adopter's Next 16 standalone build,
10
+ * Turbopack wraps it in an async module that awaits the load when the graph is
11
+ * evaluated (cms-image-build-epic.md in `.claude/future-tasks/`, "Every route
12
+ * loads sharp"). transform.ts is reachable from `canopycms/server` and, through
13
+ * api/assets.ts, from `canopycms/http` and so from `canopycms-next`, which a
14
+ * host app's root layout can import (that adopter's did). When the libvips
15
+ * `.so` was missing from its standalone output, that one static import turned
16
+ * it into a 500 on every on-demand route, 404s included, and defeated
17
+ * pipeline.ts's deliberate fail-open. Loaded on first use, a missing binary
18
+ * affects only the image operations that need it.
19
+ *
20
+ * Memoized INCLUDING a rejection: a failed dlopen does not heal inside a
21
+ * running process, so a retry would only pay for the failure again and log
22
+ * it again. The failure is logged once, here, and every later caller gets the
23
+ * same rejected promise to handle as its operation requires: transform.ts
24
+ * lets it propagate as a server error, pipeline.ts fails open.
25
+ */
26
+ import { getErrorMessage } from '../utils/error.js';
27
+ import { canopyLogError } from '../utils/logger.js';
28
+ let loading;
29
+ /** Resolve sharp's callable constructor, loading the native module on the first call only. */
30
+ export function loadSharp() {
31
+ if (!loading) {
32
+ loading = import('sharp').then((mod) => mod.default, (err) => {
33
+ canopyLogError(`[canopycms] sharp failed to load - image transforms and upload decode validation are unavailable in this process: ${getErrorMessage(err)}`);
34
+ throw err;
35
+ });
36
+ // Marks the stored promise handled; every caller still gets the rejection.
37
+ // Without it, a caller that does not await - a warm-up `void loadSharp()` -
38
+ // leaves a rejected promise with no handler, which Node treats as fatal by
39
+ // default: a whole-process outage, the shape this module exists to prevent.
40
+ // The failure is already logged above.
41
+ loading.catch(() => undefined);
42
+ }
43
+ return loading;
44
+ }
@@ -4,7 +4,7 @@
4
4
  * NO imports (not even other files in this directory) so it can be imported
5
5
  * from client bundles (via `assetUrl`/`assetSrcSet` in asset-url.ts, exported
6
6
  * off the package's main entry) as well as from the server-only transform
7
- * engine (transform.ts) and the future prod transform Lambda (PR 7), without
7
+ * engine (transform.ts) and the prod transform Lambda, without
8
8
  * ever pulling in node:crypto, sharp, or any other server-only dependency.
9
9
  *
10
10
  * `{directives}` is either the literal identity token (`orig`) or a
@@ -4,7 +4,7 @@
4
4
  * NO imports (not even other files in this directory) so it can be imported
5
5
  * from client bundles (via `assetUrl`/`assetSrcSet` in asset-url.ts, exported
6
6
  * off the package's main entry) as well as from the server-only transform
7
- * engine (transform.ts) and the future prod transform Lambda (PR 7), without
7
+ * engine (transform.ts) and the prod transform Lambda, without
8
8
  * ever pulling in node:crypto, sharp, or any other server-only dependency.
9
9
  *
10
10
  * `{directives}` is either the literal identity token (`orig`) or a
@@ -3,8 +3,15 @@
3
3
  * source image bytes with sharp. Server-only - never import this from
4
4
  * client/editor code (this is why it lives in its own file, separate from
5
5
  * the dependency-free transform-directives.ts). Used by the dev-mode lazy
6
- * `/assets/t/*` emulation in api/assets.ts today, and will be reused
7
- * unchanged by the prod transform Lambda (PR 7).
6
+ * `/assets/t/*` emulation in api/assets.ts and, unchanged, by the prod
7
+ * transform Lambda (packages/canopycms-cdk/lambda/asset-transform).
8
+ *
9
+ * sharp is imported for its TYPES only and loaded on first use through
10
+ * `loadSharp()` (sharp-loader.ts), so importing this module never loads
11
+ * libvips. That matters because this module sits in the import graph of
12
+ * `canopycms/server` and `canopycms/http`: see sharp-loader.ts for the
13
+ * adopter outage a static import caused, and eslint.config.mjs for the rule
14
+ * that keeps it from coming back.
8
15
  *
9
16
  * Pipeline: a cheap metadata-only probe (`limitInputPixels: MAX_INPUT_PIXELS`,
10
17
  * decompression-bomb defense #1) learns the real page count, so animated
@@ -47,4 +54,9 @@ export interface TransformRejection {
47
54
  error: string;
48
55
  }
49
56
  export type TransformResult = TransformSuccess | TransformRejection;
57
+ /**
58
+ * Resolves a `TransformRejection` for anything wrong with the input or the
59
+ * output. REJECTS (throws) when sharp itself cannot be loaded in this process
60
+ * - see the comment at the `loadSharp()` call below.
61
+ */
50
62
  export declare function applyTransform(input: ApplyTransformInput, directives: TransformDirectives): Promise<TransformResult>;
@@ -3,8 +3,15 @@
3
3
  * source image bytes with sharp. Server-only - never import this from
4
4
  * client/editor code (this is why it lives in its own file, separate from
5
5
  * the dependency-free transform-directives.ts). Used by the dev-mode lazy
6
- * `/assets/t/*` emulation in api/assets.ts today, and will be reused
7
- * unchanged by the prod transform Lambda (PR 7).
6
+ * `/assets/t/*` emulation in api/assets.ts and, unchanged, by the prod
7
+ * transform Lambda (packages/canopycms-cdk/lambda/asset-transform).
8
+ *
9
+ * sharp is imported for its TYPES only and loaded on first use through
10
+ * `loadSharp()` (sharp-loader.ts), so importing this module never loads
11
+ * libvips. That matters because this module sits in the import graph of
12
+ * `canopycms/server` and `canopycms/http`: see sharp-loader.ts for the
13
+ * adopter outage a static import caused, and eslint.config.mjs for the rule
14
+ * that keeps it from coming back.
8
15
  *
9
16
  * Pipeline: a cheap metadata-only probe (`limitInputPixels: MAX_INPUT_PIXELS`,
10
17
  * decompression-bomb defense #1) learns the real page count, so animated
@@ -28,8 +35,8 @@
28
35
  * path with no extra native dependency) purely for pipeline uniformity, not
29
36
  * because GIF needs stripping.
30
37
  */
31
- import sharp from 'sharp';
32
38
  import { getErrorMessage } from '../utils/error.js';
39
+ import { loadSharp } from './sharp-loader.js';
33
40
  import { MAX_ANIMATED_FRAMES, MAX_INPUT_PIXELS, } from './transform-directives.js';
34
41
  /** Raster formats the transform engine accepts as input. svg/pdf never reach here - they're served statically. */
35
42
  const ALLOWED_INPUT_EXTS = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif']);
@@ -126,6 +133,11 @@ function encodeSourceFormat(pipeline, sourceExt, quality) {
126
133
  return pipeline.png(options);
127
134
  }
128
135
  }
136
+ /**
137
+ * Resolves a `TransformRejection` for anything wrong with the input or the
138
+ * output. REJECTS (throws) when sharp itself cannot be loaded in this process
139
+ * - see the comment at the `loadSharp()` call below.
140
+ */
129
141
  export async function applyTransform(input, directives) {
130
142
  const sourceExt = input.ext.toLowerCase();
131
143
  if (!ALLOWED_INPUT_EXTS.has(sourceExt)) {
@@ -135,6 +147,14 @@ export async function applyTransform(input, directives) {
135
147
  error: `Unsupported input format for transform: '${sourceExt}'`,
136
148
  };
137
149
  }
150
+ // After the format check and OUTSIDE the try below, both on purpose. A load
151
+ // failure is a fault in this environment (the native binary is missing or
152
+ // built for another platform), not a fact about these bytes, so it must
153
+ // reach the caller as a thrown error - a 500 from http/handler.ts's
154
+ // top-level catch or from the transform Lambda's - and never the 422 the
155
+ // catch below means "sharp could not process this input". The 400 above
156
+ // needs no decoder, so it keeps working where sharp cannot load.
157
+ const sharp = await loadSharp();
138
158
  const resize = directives.identity ? undefined : directives;
139
159
  const format = resize?.format;
140
160
  const quality = resize?.quality;
@@ -22,6 +22,6 @@ export function assertAuthPluginAllowedForMode(plugin, mode) {
22
22
  '`verifiesCredentials: true`. This plugin performs no real credential verification, ' +
23
23
  'so anyone could impersonate any user (including admins). Configure a verifying auth ' +
24
24
  "plugin for production (e.g. createClerkAuthPlugin from 'canopycms-auth-clerk' with " +
25
- "CLERK_SECRET_KEY set), or run with mode: 'dev' for local development.");
25
+ "CLERK_JWT_KEY set), or run with mode: 'dev' for local development.");
26
26
  }
27
27
  }
@@ -25,8 +25,10 @@ export { loadBranchContext } from './branch-metadata.js';
25
25
  /**
26
26
  * Load an existing branch context, or create the workspace if it doesn't exist yet.
27
27
  *
28
- * Static deployments skip all git/branch workspace operations and return
29
- * a synthetic context pointing at the current working directory.
28
+ * When content is read from the checkout (`readsFromCheckout`: a static
29
+ * deployment, or any build) this skips every git and branch-workspace
30
+ * operation and returns a synthetic context rooted at the current working
31
+ * directory. `branchName` is echoed back on that context but selects nothing.
30
32
  */
31
33
  export declare function loadOrCreateBranchContext(options: {
32
34
  config: CanopyConfig;
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { ensureBranchRoot } from './paths/index.js';
3
3
  import { getBranchMetadataFileManager, loadBranchContext } from './branch-metadata.js';
4
- import { isDeployedStatic } from './build-mode.js';
4
+ import { readsFromCheckout } from './build-mode.js';
5
5
  import { operatingStrategy } from './operating-mode/index.js';
6
6
  import { GitManager } from './git-manager.js';
7
7
  import { createDebugLogger } from './utils/debug.js';
@@ -28,11 +28,15 @@ export class BranchWorkspaceManager {
28
28
  }
29
29
  // Create new lock promise
30
30
  const lockPromise = (async () => {
31
- // The in-memory lock above only serializes within one process. Parallel
32
- // build workers (separate processes) could otherwise both clone into the
33
- // same branch workspace ("destination path already exists"), so guard the
34
- // workspace init with a cross-process lock too. initializeWorkspace is
35
- // idempotent, so the waiter simply finds the workspace already cloned.
31
+ // The in-memory lock above only serializes within one process. Separate
32
+ // processes can provision the same branch workspace at once -- several
33
+ // Lambda containers sharing one EFS workspace root in prod, or the dev
34
+ // server beside a content-reading script run outside a build in dev --
35
+ // and would otherwise both clone into it ("destination path already
36
+ // exists"), so guard the workspace init with a cross-process lock too.
37
+ // initializeWorkspace is idempotent, so the waiter simply finds the
38
+ // workspace already cloned. (A build's content reads never get here:
39
+ // loadOrCreateBranchContext returns the checkout before provisioning.)
36
40
  let releaseLock;
37
41
  try {
38
42
  log.debug('workspace', 'Ensuring git workspace', {
@@ -122,12 +126,14 @@ export { loadBranchContext } from './branch-metadata.js';
122
126
  /**
123
127
  * Load an existing branch context, or create the workspace if it doesn't exist yet.
124
128
  *
125
- * Static deployments skip all git/branch workspace operations and return
126
- * a synthetic context pointing at the current working directory.
129
+ * When content is read from the checkout (`readsFromCheckout`: a static
130
+ * deployment, or any build) this skips every git and branch-workspace
131
+ * operation and returns a synthetic context rooted at the current working
132
+ * directory. `branchName` is echoed back on that context but selects nothing.
127
133
  */
128
134
  export async function loadOrCreateBranchContext(options) {
129
- // Static deployments read content directly from the checkout — no git ops needed
130
- if (isDeployedStatic(options.config)) {
135
+ // Static deployments and builds read content directly from the checkout — no git ops
136
+ if (readsFromCheckout(options.config)) {
131
137
  const cwd = process.cwd();
132
138
  return {
133
139
  branch: {
@@ -8,12 +8,40 @@ export declare const isDeployedStatic: (config: {
8
8
  deployedAs?: string;
9
9
  }) => boolean;
10
10
  /**
11
- * Safety net: detect build phase where auth is unavailable.
12
- * Covers edge cases like getCanopy() called from generateStaticParams
13
- * in server deployments. For static deployments, isDeployedStatic()
14
- * is the primary check.
11
+ * Detect a build, where there is no request and no auth.
12
+ *
13
+ * Under Next.js this is `NEXT_PHASE === 'phase-production-build'`, which
14
+ * `next build` sets itself: after compiling, and immediately before it creates
15
+ * the static worker that collects page data and prerenders, whose processes
16
+ * inherit it. So it is true in page modules, `generateStaticParams` and
17
+ * prerendering, but NOT yet set when `next build` loads `next.config.*`, which
18
+ * it does first. `next dev`, `next start` and the standalone server never set
19
+ * it. Verified in Next 15.5.21 and 16.1.7, where `build/index.js` (in both
20
+ * `dist/` and `dist/esm/`) holds the only assignment -- re-check on every Next
21
+ * major.
22
+ *
23
+ * `CANOPY_BUILD_MODE=true` is the framework-neutral switch: for builds Next
24
+ * does not drive, and for scripts run alongside one (the generated
25
+ * `Dockerfile.cms` sets it in its builder stage, ahead of the build command).
15
26
  */
16
27
  export declare const isBuildMode: () => boolean;
28
+ /**
29
+ * Is content read straight from the checkout, rather than from a branch
30
+ * workspace?
31
+ *
32
+ * This decides WHERE content is read. `isDeployedStatic` and `isBuildMode`
33
+ * used on their own decide WHO reads it (`STATIC_DEPLOY_USER`, no ACLs).
34
+ *
35
+ * True for a static deployment, and for every build in either mode and either
36
+ * deployment type. A build reads the working tree at `process.cwd()` and
37
+ * never touches git, `.canopy-dev` or a branch clone: CI builds exactly the
38
+ * checked-out commit, and a local build reads what is on disk, uncommitted
39
+ * files included. Editor saves not yet copied out of `.canopy-dev`
40
+ * (`canopycms sync pull`) are not part of a build.
41
+ */
42
+ export declare const readsFromCheckout: (config: {
43
+ deployedAs?: string;
44
+ }) => boolean;
17
45
  /**
18
46
  * Synthetic user with full access for static deployments and build phase.
19
47
  * Has Admin privileges — all content is readable, permissions are skipped.
@@ -7,10 +7,21 @@ export const isDeployedStatic = (config) => {
7
7
  return config.deployedAs === 'static';
8
8
  };
9
9
  /**
10
- * Safety net: detect build phase where auth is unavailable.
11
- * Covers edge cases like getCanopy() called from generateStaticParams
12
- * in server deployments. For static deployments, isDeployedStatic()
13
- * is the primary check.
10
+ * Detect a build, where there is no request and no auth.
11
+ *
12
+ * Under Next.js this is `NEXT_PHASE === 'phase-production-build'`, which
13
+ * `next build` sets itself: after compiling, and immediately before it creates
14
+ * the static worker that collects page data and prerenders, whose processes
15
+ * inherit it. So it is true in page modules, `generateStaticParams` and
16
+ * prerendering, but NOT yet set when `next build` loads `next.config.*`, which
17
+ * it does first. `next dev`, `next start` and the standalone server never set
18
+ * it. Verified in Next 15.5.21 and 16.1.7, where `build/index.js` (in both
19
+ * `dist/` and `dist/esm/`) holds the only assignment -- re-check on every Next
20
+ * major.
21
+ *
22
+ * `CANOPY_BUILD_MODE=true` is the framework-neutral switch: for builds Next
23
+ * does not drive, and for scripts run alongside one (the generated
24
+ * `Dockerfile.cms` sets it in its builder stage, ahead of the build command).
14
25
  */
15
26
  export const isBuildMode = () => {
16
27
  // Next.js build phase
@@ -21,6 +32,23 @@ export const isBuildMode = () => {
21
32
  return true;
22
33
  return false;
23
34
  };
35
+ /**
36
+ * Is content read straight from the checkout, rather than from a branch
37
+ * workspace?
38
+ *
39
+ * This decides WHERE content is read. `isDeployedStatic` and `isBuildMode`
40
+ * used on their own decide WHO reads it (`STATIC_DEPLOY_USER`, no ACLs).
41
+ *
42
+ * True for a static deployment, and for every build in either mode and either
43
+ * deployment type. A build reads the working tree at `process.cwd()` and
44
+ * never touches git, `.canopy-dev` or a branch clone: CI builds exactly the
45
+ * checked-out commit, and a local build reads what is on disk, uncommitted
46
+ * files included. Editor saves not yet copied out of `.canopy-dev`
47
+ * (`canopycms sync pull`) are not part of a build.
48
+ */
49
+ export const readsFromCheckout = (config) => {
50
+ return isDeployedStatic(config) || isBuildMode();
51
+ };
24
52
  /**
25
53
  * Synthetic user with full access for static deployments and build phase.
26
54
  * Has Admin privileges — all content is readable, permissions are skipped.
package/dist/cli/cli.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * CanopyCMS CLI entrypoint.
4
4
  *
5
5
  * Routes commands to their implementations:
6
- * init, init-deploy, worker, generate-ai-content, sync, migrate
6
+ * init, init-deploy, init-github-app, worker, generate-ai-content, sync, migrate
7
7
  *
8
8
  * Command implementations live in separate files (init.ts, sync.ts, etc.)
9
9
  * and are dynamically imported to keep startup fast.
@@ -16,6 +16,16 @@ export declare function parseArgs(rawArgs: string[]): {
16
16
  flags: Record<string, string | boolean>;
17
17
  command: string | undefined;
18
18
  };
19
+ /**
20
+ * The argv after a literal `--`, as a real string array.
21
+ *
22
+ * minimist types its parsed object with an `any` index signature, so reading
23
+ * `argv['--']` directly would hand an `any` straight to spawn(). Narrowed here
24
+ * through `unknown` instead, and returned as `[]` when absent so callers can
25
+ * treat "no passthrough" and "empty passthrough" the same way. Exported for
26
+ * testing.
27
+ */
28
+ export declare function passthroughArgs(argv: Record<string, unknown>): string[];
19
29
  /**
20
30
  * Validate the --auth flag value for `init`. Returns undefined when the flag
21
31
  * was not provided (caller should fall through to interactive prompt / default).