@infracraft/pulumi 1.16.3 → 1.16.4

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/dist/sandbox.cjs CHANGED
@@ -38,7 +38,13 @@ function buildSandboxScript(options) {
38
38
  const head = `REPO=$(git rev-parse --show-toplevel)`;
39
39
  const runSetupAndCli = [setup, cli].filter(Boolean).join("; ");
40
40
  if (!sandbox) return `${head}; cd "$REPO"; ${runSetupAndCli}`;
41
- const makeSandbox = [`SANDBOX=$(mktemp -d ${SANDBOX_ROOT}/${env ? `${env}-${appName}` : appName}.XXXXXX)`, `trap 'rm -rf "$SANDBOX"' EXIT`].join("; ");
41
+ const dirPrefix = env ? `${env}-${appName}` : appName;
42
+ const makeSandbox = [
43
+ `PROJECT_DIR="${SANDBOX_ROOT}/$(basename "$REPO")"`,
44
+ `mkdir -p "$PROJECT_DIR"`,
45
+ `SANDBOX=$(mktemp -d "$PROJECT_DIR/${dirPrefix}.XXXXXX")`,
46
+ `trap 'rm -rf "$SANDBOX"; rmdir "$PROJECT_DIR" 2>/dev/null || true' EXIT`
47
+ ].join("; ");
42
48
  if (gitGuard) return `${head}; ${makeSandbox}; ${`git -C "$REPO" ls-files | ${buildSandboxFileFilter(excludePaths)} | rsync -a --files-from=- "$REPO"/ "$SANDBOX"/`}; cd "\$SANDBOX" && git init -q && git add -A; ${runSetupAndCli}`;
43
49
  return `${head}; ${makeSandbox}; git -C "\$REPO" ls-files | rsync -a --files-from=- "\$REPO"/ "\$SANDBOX"/; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
44
50
  }
@@ -59,23 +65,40 @@ var DeploySandbox = class extends _pulumi_pulumi.ComponentResource {
59
65
  if (!_pulumi_pulumi.runtime.isDryRun()) this.prepareWorkspace();
60
66
  this.registerOutputs({});
61
67
  }
62
- /** mkdir the workspace root and GC stale sandboxes (best-effort). */
68
+ /**
69
+ * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are
70
+ * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),
71
+ * so this sweeps individual stale sandboxes and then drops any now-empty project
72
+ * dir (`rmdir` is a no-op on a project dir with live deploys).
73
+ */
63
74
  prepareWorkspace() {
64
75
  node_fs.mkdirSync(SANDBOX_ROOT, { recursive: true });
65
- let entries = [];
76
+ const now = Date.now();
77
+ let projects = [];
66
78
  try {
67
- entries = node_fs.readdirSync(SANDBOX_ROOT);
79
+ projects = node_fs.readdirSync(SANDBOX_ROOT);
68
80
  } catch {
69
81
  return;
70
82
  }
71
- const now = Date.now();
72
- for (const entry of entries) {
73
- const full = node_path.join(SANDBOX_ROOT, entry);
83
+ for (const project of projects) {
84
+ const projectDir = node_path.join(SANDBOX_ROOT, project);
85
+ let sandboxes = [];
86
+ try {
87
+ sandboxes = node_fs.readdirSync(projectDir);
88
+ } catch {
89
+ continue;
90
+ }
91
+ for (const sandbox of sandboxes) {
92
+ const full = node_path.join(projectDir, sandbox);
93
+ try {
94
+ if (now - node_fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) node_fs.rmSync(full, {
95
+ recursive: true,
96
+ force: true
97
+ });
98
+ } catch {}
99
+ }
74
100
  try {
75
- if (now - node_fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) node_fs.rmSync(full, {
76
- recursive: true,
77
- force: true
78
- });
101
+ node_fs.rmdirSync(projectDir);
79
102
  } catch {}
80
103
  }
81
104
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.cjs","names":["pulumi","fs","path"],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false → run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the env so leftovers/concurrent deploys are identifiable.\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`SANDBOX=$(mktemp -d ${SANDBOX_ROOT}/${dirPrefix}.XXXXXX)`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAMhC,MAAM,cAAc,CACnB,uBAAuB,aAAa,GAHnB,MAAM,GAAG,IAAI,GAAG,YAAY,QAGI,WACjD,+BACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;CAGA,AAAQ,mBAAyB;EAChC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAUC,QAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAOC,UAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,QAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
1
+ {"version":3,"file":"sandbox.cjs","names":["pulumi","fs","path"],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false → run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the env so leftovers/concurrent deploys are identifiable.\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\t// Group a repo's sandboxes under a per-project dir (named after the repo\n\t// folder) so concurrent deploys of different repos never mix. The project dir\n\t// is removed only once empty — `rmdir` fails on a non-empty dir, so it\n\t// survives until the project's last deploy has cleaned up its own sandbox.\n\tconst makeSandbox = [\n\t\t`PROJECT_DIR=\"${SANDBOX_ROOT}/$(basename \"$REPO\")\"`,\n\t\t`mkdir -p \"$PROJECT_DIR\"`,\n\t\t`SANDBOX=$(mktemp -d \"$PROJECT_DIR/${dirPrefix}.XXXXXX\")`,\n\t\t`trap 'rm -rf \"$SANDBOX\"; rmdir \"$PROJECT_DIR\" 2>/dev/null || true' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/**\n\t * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),\n\t * so this sweeps individual stale sandboxes and then drops any now-empty project\n\t * dir (`rmdir` is a no-op on a project dir with live deploys).\n\t */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tconst now = Date.now();\n\n\t\tlet projects: string[] = [];\n\n\t\ttry {\n\t\t\tprojects = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const project of projects) {\n\t\t\tconst projectDir = path.join(SANDBOX_ROOT, project);\n\n\t\t\tlet sandboxes: string[] = [];\n\n\t\t\ttry {\n\t\t\t\tsandboxes = fs.readdirSync(projectDir) as string[];\n\t\t\t} catch {\n\t\t\t\t// Not a directory (e.g. a stray file) — skip.\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const sandbox of sandboxes) {\n\t\t\t\tconst full = path.join(projectDir, sandbox);\n\n\t\t\t\ttry {\n\t\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Drop the project dir only if the sweep left it empty (best-effort).\n\t\t\ttry {\n\t\t\t\tfs.rmdirSync(projectDir);\n\t\t\t} catch {\n\t\t\t\t// Non-empty (live deploys) or already gone — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAIhC,MAAM,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY;CAM9C,MAAM,cAAc;EACnB,gBAAgB,aAAa;EAC7B;EACA,qCAAqC,UAAU;EAC/C;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmCA,eAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAACA,eAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;;;;CAQA,AAAQ,mBAAyB;EAChC,QAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,WAAqB,CAAC;EAE1B,IAAI;GACH,WAAWC,QAAG,YAAY,YAAY;EACvC,QAAQ;GACP;EACD;EAEA,KAAK,MAAM,WAAW,UAAU;GAC/B,MAAM,aAAaC,UAAK,KAAK,cAAc,OAAO;GAElD,IAAI,YAAsB,CAAC;GAE3B,IAAI;IACH,YAAYD,QAAG,YAAY,UAAU;GACtC,QAAQ;IAEP;GACD;GAEA,KAAK,MAAM,WAAW,WAAW;IAChC,MAAM,OAAOC,UAAK,KAAK,YAAY,OAAO;IAE1C,IAAI;KACH,IAAI,MAAMD,QAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,QAAG,OAAO,MAAM;MAAE,WAAW;MAAM,OAAO;KAAK,CAAC;IAElD,QAAQ,CAER;GACD;GAGA,IAAI;IACH,QAAG,UAAU,UAAU;GACxB,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
@@ -39,7 +39,12 @@ declare function buildSandboxScript(options: SandboxScriptOptions): string;
39
39
  */
40
40
  declare class DeploySandbox extends pulumi.ComponentResource {
41
41
  constructor(name: string, opts?: pulumi.ComponentResourceOptions);
42
- /** mkdir the workspace root and GC stale sandboxes (best-effort). */
42
+ /**
43
+ * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are
44
+ * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),
45
+ * so this sweeps individual stale sandboxes and then drops any now-empty project
46
+ * dir (`rmdir` is a no-op on a project dir with live deploys).
47
+ */
43
48
  private prepareWorkspace;
44
49
  }
45
50
  /** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.cts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AAoDhE;;;AApDgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cAoDnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAahC;EAAA,QAAA,gBAAA;AAAA;AA4BT;AAAA,iBAAgB,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
1
+ {"version":3,"file":"sandbox.d.cts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AA0DhE;;;AA1DgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cA0DnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAkBhC;;AAAgB;AAgDzB;;;EAhDS,QAAA,gBAAA;AAAA;;iBAgDO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
@@ -39,7 +39,12 @@ declare function buildSandboxScript(options: SandboxScriptOptions): string;
39
39
  */
40
40
  declare class DeploySandbox extends pulumi.ComponentResource {
41
41
  constructor(name: string, opts?: pulumi.ComponentResourceOptions);
42
- /** mkdir the workspace root and GC stale sandboxes (best-effort). */
42
+ /**
43
+ * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are
44
+ * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),
45
+ * so this sweeps individual stale sandboxes and then drops any now-empty project
46
+ * dir (`rmdir` is a no-op on a project dir with live deploys).
47
+ */
43
48
  private prepareWorkspace;
44
49
  }
45
50
  /** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.d.mts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AAoDhE;;;AApDgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cAoDnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAahC;EAAA,QAAA,gBAAA;AAAA;AA4BT;AAAA,iBAAgB,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
1
+ {"version":3,"file":"sandbox.d.mts","names":[],"sources":["../src/sandbox.ts"],"mappings":";;;;;;;AAgBA;;;;iBAAgB,sBAAA,CAAuB,YAA2B;AAAA,UAqBxD,oBAAA;EAAoB;EAE7B,OAAA;EAF6B;EAI7B,QAAA;EAAA;EAEA,OAAA;EAGA;;EAAA,GAAA;EAMA;EAJA,YAAA;EAIG;EAFH,KAAA;EASiC;EAPjC,GAAA;AAAA;AAO+D;AA0DhE;;;AA1DgE,iBAAhD,kBAAA,CAAmB,OAA6B,EAApB,oBAAoB;;;;;;cA0DnD,aAAA,SAAsB,MAAA,CAAO,iBAAiB;cAC9C,IAAA,UAAc,IAAA,GAAO,MAAA,CAAO,wBAAA;EAkBhC;;AAAgB;AAgDzB;;;EAhDS,QAAA,gBAAA;AAAA;;iBAgDO,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa"}
package/dist/sandbox.mjs CHANGED
@@ -34,7 +34,13 @@ function buildSandboxScript(options) {
34
34
  const head = `REPO=$(git rev-parse --show-toplevel)`;
35
35
  const runSetupAndCli = [setup, cli].filter(Boolean).join("; ");
36
36
  if (!sandbox) return `${head}; cd "$REPO"; ${runSetupAndCli}`;
37
- const makeSandbox = [`SANDBOX=$(mktemp -d ${SANDBOX_ROOT}/${env ? `${env}-${appName}` : appName}.XXXXXX)`, `trap 'rm -rf "$SANDBOX"' EXIT`].join("; ");
37
+ const dirPrefix = env ? `${env}-${appName}` : appName;
38
+ const makeSandbox = [
39
+ `PROJECT_DIR="${SANDBOX_ROOT}/$(basename "$REPO")"`,
40
+ `mkdir -p "$PROJECT_DIR"`,
41
+ `SANDBOX=$(mktemp -d "$PROJECT_DIR/${dirPrefix}.XXXXXX")`,
42
+ `trap 'rm -rf "$SANDBOX"; rmdir "$PROJECT_DIR" 2>/dev/null || true' EXIT`
43
+ ].join("; ");
38
44
  if (gitGuard) return `${head}; ${makeSandbox}; ${`git -C "$REPO" ls-files | ${buildSandboxFileFilter(excludePaths)} | rsync -a --files-from=- "$REPO"/ "$SANDBOX"/`}; cd "\$SANDBOX" && git init -q && git add -A; ${runSetupAndCli}`;
39
45
  return `${head}; ${makeSandbox}; git -C "\$REPO" ls-files | rsync -a --files-from=- "\$REPO"/ "\$SANDBOX"/; cp -c -R "\$REPO/.git" "\$SANDBOX/.git" 2>/dev/null || cp -R "\$REPO/.git" "\$SANDBOX/.git"; cd "$SANDBOX"; ${runSetupAndCli}`;
40
46
  }
@@ -55,23 +61,40 @@ var DeploySandbox = class extends pulumi.ComponentResource {
55
61
  if (!pulumi.runtime.isDryRun()) this.prepareWorkspace();
56
62
  this.registerOutputs({});
57
63
  }
58
- /** mkdir the workspace root and GC stale sandboxes (best-effort). */
64
+ /**
65
+ * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are
66
+ * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),
67
+ * so this sweeps individual stale sandboxes and then drops any now-empty project
68
+ * dir (`rmdir` is a no-op on a project dir with live deploys).
69
+ */
59
70
  prepareWorkspace() {
60
71
  fs.mkdirSync(SANDBOX_ROOT, { recursive: true });
61
- let entries = [];
72
+ const now = Date.now();
73
+ let projects = [];
62
74
  try {
63
- entries = fs.readdirSync(SANDBOX_ROOT);
75
+ projects = fs.readdirSync(SANDBOX_ROOT);
64
76
  } catch {
65
77
  return;
66
78
  }
67
- const now = Date.now();
68
- for (const entry of entries) {
69
- const full = path.join(SANDBOX_ROOT, entry);
79
+ for (const project of projects) {
80
+ const projectDir = path.join(SANDBOX_ROOT, project);
81
+ let sandboxes = [];
82
+ try {
83
+ sandboxes = fs.readdirSync(projectDir);
84
+ } catch {
85
+ continue;
86
+ }
87
+ for (const sandbox of sandboxes) {
88
+ const full = path.join(projectDir, sandbox);
89
+ try {
90
+ if (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {
91
+ recursive: true,
92
+ force: true
93
+ });
94
+ } catch {}
95
+ }
70
96
  try {
71
- if (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) fs.rmSync(full, {
72
- recursive: true,
73
- force: true
74
- });
97
+ fs.rmdirSync(projectDir);
75
98
  } catch {}
76
99
  }
77
100
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.mjs","names":[],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false → run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the env so leftovers/concurrent deploys are identifiable.\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\tconst makeSandbox = [\n\t\t`SANDBOX=$(mktemp -d ${SANDBOX_ROOT}/${dirPrefix}.XXXXXX)`,\n\t\t`trap 'rm -rf \"$SANDBOX\"' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/** mkdir the workspace root and GC stale sandboxes (best-effort). */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tlet entries: string[] = [];\n\n\t\ttry {\n\t\t\tentries = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tconst now = Date.now();\n\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(SANDBOX_ROOT, entry);\n\n\t\t\ttry {\n\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAMhC,MAAM,cAAc,CACnB,uBAAuB,aAAa,GAHnB,MAAM,GAAG,IAAI,GAAG,YAAY,QAGI,WACjD,+BACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;CAGA,AAAQ,mBAAyB;EAChC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,IAAI,UAAoB,CAAC;EAEzB,IAAI;GACH,UAAU,GAAG,YAAY,YAAY;EACtC,QAAQ;GACP;EACD;EAEA,MAAM,MAAM,KAAK,IAAI;EAErB,KAAK,MAAM,SAAS,SAAS;GAC5B,MAAM,OAAO,KAAK,KAAK,cAAc,KAAK;GAE1C,IAAI;IACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,GAAG,OAAO,MAAM;KAAE,WAAW;KAAM,OAAO;IAAK,CAAC;GAElD,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
1
+ {"version":3,"file":"sandbox.mjs","names":[],"sources":["../src/sandbox.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as pulumi from \"@pulumi/pulumi\";\n\n/** Escapes a path literal for embedding inside an awk ERE. */\nfunction escapeAwkRegex(path: string): string {\n\treturn path.replace(/[.[\\]{}()*+?^$|\\\\/]/g, \"\\\\$&\");\n}\n\n/**\n * Builds the shell filter applied to newline-delimited `git ls-files` output\n * before `rsync --files-from`. For `apps/<x>` it drops the app's files but keeps\n * `apps/<x>/package.json` (the monorepo workspace graph needs it during build);\n * any other entry drops the path and its subtree. Returns `cat` (passthrough)\n * when nothing is excluded. Uses awk so it is portable across macOS and Linux.\n */\nexport function buildSandboxFileFilter(excludePaths: string[] = []): string {\n\tif (excludePaths.length === 0) {\n\t\treturn \"cat\";\n\t}\n\n\tconst clauses = excludePaths.map((entry) => {\n\t\tconst escaped = escapeAwkRegex(entry);\n\n\t\tif (entry.startsWith(\"apps/\")) {\n\t\t\treturn `!(/^${escaped}\\\\// && !/^${escaped}\\\\/package\\\\.json$/)`;\n\t\t}\n\n\t\treturn `!/^${escaped}(\\\\/|$)/`;\n\t});\n\n\treturn `awk '${clauses.join(\" && \")}'`;\n}\n\n/** Root of the per-deploy sandbox tree. The DeploySandbox resource GCs this. */\nconst SANDBOX_ROOT = \"/tmp/infracraft\";\n\ninterface SandboxScriptOptions {\n\t/** Whether to isolate into a /tmp copy (false → run in the live tree). */\n\tsandbox: boolean;\n\t/** Whether the sandbox `.git` is a metadata-free stub (true) or the real one. */\n\tgitGuard: boolean;\n\t/** Resource-derived name, used in the sandbox dir prefix. */\n\tappName: string;\n\t/** Stack/environment name, prefixed to the sandbox dir so leftovers and\n\t * concurrent deploys are identifiable (e.g. `staging-vercel-deploy-nexus.XXXX`). */\n\tenv?: string;\n\t/** Upload-scoping excludes; applied only in stub mode (see design spec). */\n\texcludePaths?: string[];\n\t/** Shell run in the working dir before `cli` (e.g. write railpack.json). */\n\tsetup?: string;\n\t/** Fully-formed platform deploy command (its exit code becomes the script's). */\n\tcli: string;\n}\n\n/**\n * Builds the shell for a deploy's `command.local.Command.create`. See\n * docs/superpowers/specs/2026-06-05-deploy-sandbox-design.md for the modes.\n */\nexport function buildSandboxScript(options: SandboxScriptOptions): string {\n\tconst { sandbox, gitGuard, appName, env, excludePaths, setup, cli } = options;\n\n\tconst head = `REPO=$(git rev-parse --show-toplevel)`;\n\tconst runSetupAndCli = [setup, cli].filter(Boolean).join(\"; \");\n\n\tif (!sandbox) {\n\t\treturn `${head}; cd \"$REPO\"; ${runSetupAndCli}`;\n\t}\n\n\t// Prefix the dir with the env so leftovers/concurrent deploys are identifiable.\n\tconst dirPrefix = env ? `${env}-${appName}` : appName;\n\n\t// Group a repo's sandboxes under a per-project dir (named after the repo\n\t// folder) so concurrent deploys of different repos never mix. The project dir\n\t// is removed only once empty — `rmdir` fails on a non-empty dir, so it\n\t// survives until the project's last deploy has cleaned up its own sandbox.\n\tconst makeSandbox = [\n\t\t`PROJECT_DIR=\"${SANDBOX_ROOT}/$(basename \"$REPO\")\"`,\n\t\t`mkdir -p \"$PROJECT_DIR\"`,\n\t\t`SANDBOX=$(mktemp -d \"$PROJECT_DIR/${dirPrefix}.XXXXXX\")`,\n\t\t`trap 'rm -rf \"$SANDBOX\"; rmdir \"$PROJECT_DIR\" 2>/dev/null || true' EXIT`,\n\t].join(\"; \");\n\n\tif (gitGuard) {\n\t\tconst filter = buildSandboxFileFilter(excludePaths);\n\n\t\tconst copy =\n\t\t\t`git -C \"$REPO\" ls-files | ${filter} | ` +\n\t\t\t`rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\t\tconst stubGit = `cd \"$SANDBOX\" && git init -q && git add -A`;\n\n\t\treturn `${head}; ${makeSandbox}; ${copy}; ${stubGit}; ${runSetupAndCli}`;\n\t}\n\n\t// Original `.git`, full clean tree (no filter, no reconciliation needed).\n\tconst copy = `git -C \"$REPO\" ls-files | rsync -a --files-from=- \"$REPO\"/ \"$SANDBOX\"/`;\n\n\tconst copyGit =\n\t\t`cp -c -R \"$REPO/.git\" \"$SANDBOX/.git\" 2>/dev/null || ` +\n\t\t`cp -R \"$REPO/.git\" \"$SANDBOX/.git\"`;\n\n\treturn `${head}; ${makeSandbox}; ${copy}; ${copyGit}; cd \"$SANDBOX\"; ${runSetupAndCli}`;\n}\n\n/** Cross-bundle brand: `instanceof` is unreliable when the seam and the resource\n * come from different built entries, so the seam detects sandboxes by this. */\nconst DEPLOY_SANDBOX_BRAND = Symbol.for(\"@infracraft/pulumi/DeploySandbox\");\n\n/** Sweep sandboxes orphaned by a hard-killed run (older than this). */\nconst STALE_SANDBOX_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Isolation marker + workspace lifecycle. Listing it in a deploy's `dependsOn`\n * makes that deploy run in an isolated `/tmp/infracraft` copy. Carries no config;\n * the repo root is derived at runtime by the deploy command.\n */\nexport class DeploySandbox extends pulumi.ComponentResource {\n\tconstructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n\t\tsuper(\"infracraft:sandbox:DeploySandbox\", name, {}, opts);\n\n\t\t(this as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] = true;\n\n\t\tif (!pulumi.runtime.isDryRun()) {\n\t\t\tthis.prepareWorkspace();\n\t\t}\n\n\t\tthis.registerOutputs({});\n\t}\n\n\t/**\n\t * mkdir the workspace root and GC stale sandboxes (best-effort). Sandboxes are\n\t * nested one level under a per-project dir (`/tmp/infracraft/<project>/<sandbox>`),\n\t * so this sweeps individual stale sandboxes and then drops any now-empty project\n\t * dir (`rmdir` is a no-op on a project dir with live deploys).\n\t */\n\tprivate prepareWorkspace(): void {\n\t\tfs.mkdirSync(SANDBOX_ROOT, { recursive: true });\n\n\t\tconst now = Date.now();\n\n\t\tlet projects: string[] = [];\n\n\t\ttry {\n\t\t\tprojects = fs.readdirSync(SANDBOX_ROOT) as string[];\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\n\t\tfor (const project of projects) {\n\t\t\tconst projectDir = path.join(SANDBOX_ROOT, project);\n\n\t\t\tlet sandboxes: string[] = [];\n\n\t\t\ttry {\n\t\t\t\tsandboxes = fs.readdirSync(projectDir) as string[];\n\t\t\t} catch {\n\t\t\t\t// Not a directory (e.g. a stray file) — skip.\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const sandbox of sandboxes) {\n\t\t\t\tconst full = path.join(projectDir, sandbox);\n\n\t\t\t\ttry {\n\t\t\t\t\tif (now - fs.statSync(full).mtimeMs > STALE_SANDBOX_MS) {\n\t\t\t\t\t\tfs.rmSync(full, { recursive: true, force: true });\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Racing with an in-flight deploy's cleanup — ignore.\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Drop the project dir only if the sweep left it empty (best-effort).\n\t\t\ttry {\n\t\t\t\tfs.rmdirSync(projectDir);\n\t\t\t} catch {\n\t\t\t\t// Non-empty (live deploys) or already gone — ignore.\n\t\t\t}\n\t\t}\n\t}\n}\n\n/** Bundle-safe check for a `DeploySandbox` in a `dependsOn` array. */\nexport function isDeploySandbox(value: unknown): value is DeploySandbox {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\t(value as Record<symbol, unknown>)[DEPLOY_SANDBOX_BRAND] === true\n\t);\n}\n"],"mappings":";;;;;;;AAKA,SAAS,eAAe,MAAsB;CAC7C,OAAO,KAAK,QAAQ,wBAAwB,MAAM;AACnD;;;;;;;;AASA,SAAgB,uBAAuB,eAAyB,CAAC,GAAW;CAC3E,IAAI,aAAa,WAAW,GAC3B,OAAO;CAaR,OAAO,QAVS,aAAa,KAAK,UAAU;EAC3C,MAAM,UAAU,eAAe,KAAK;EAEpC,IAAI,MAAM,WAAW,OAAO,GAC3B,OAAO,OAAO,QAAQ,aAAa,QAAQ;EAG5C,OAAO,MAAM,QAAQ;CACtB,CAEqB,EAAE,KAAK,MAAM,EAAE;AACrC;;AAGA,MAAM,eAAe;;;;;AAwBrB,SAAgB,mBAAmB,SAAuC;CACzE,MAAM,EAAE,SAAS,UAAU,SAAS,KAAK,cAAc,OAAO,QAAQ;CAEtE,MAAM,OAAO;CACb,MAAM,iBAAiB,CAAC,OAAO,GAAG,EAAE,OAAO,OAAO,EAAE,KAAK,IAAI;CAE7D,IAAI,CAAC,SACJ,OAAO,GAAG,KAAK,gBAAgB;CAIhC,MAAM,YAAY,MAAM,GAAG,IAAI,GAAG,YAAY;CAM9C,MAAM,cAAc;EACnB,gBAAgB,aAAa;EAC7B;EACA,qCAAqC,UAAU;EAC/C;CACD,EAAE,KAAK,IAAI;CAEX,IAAI,UASH,OAAO,GAAG,KAAK,IAAI,YAAY,IAAI,6BARpB,uBAAuB,YAGH,EAAE,iDAKG,iDAAgB;CAUzD,OAAO,GAAG,KAAK,IAAI,YAAY,2LAAwC;AACxE;;;AAIA,MAAM,uBAAuB,OAAO,IAAI,kCAAkC;;AAG1E,MAAM,mBAAmB,OAAU,KAAK;;;;;;AAOxC,IAAa,gBAAb,cAAmC,OAAO,kBAAkB;CAC3D,YAAY,MAAc,MAAwC;EACjE,MAAM,oCAAoC,MAAM,CAAC,GAAG,IAAI;EAExD,AAAC,KAAiC,wBAAwB;EAE1D,IAAI,CAAC,OAAO,QAAQ,SAAS,GAC5B,KAAK,iBAAiB;EAGvB,KAAK,gBAAgB,CAAC,CAAC;CACxB;;;;;;;CAQA,AAAQ,mBAAyB;EAChC,GAAG,UAAU,cAAc,EAAE,WAAW,KAAK,CAAC;EAE9C,MAAM,MAAM,KAAK,IAAI;EAErB,IAAI,WAAqB,CAAC;EAE1B,IAAI;GACH,WAAW,GAAG,YAAY,YAAY;EACvC,QAAQ;GACP;EACD;EAEA,KAAK,MAAM,WAAW,UAAU;GAC/B,MAAM,aAAa,KAAK,KAAK,cAAc,OAAO;GAElD,IAAI,YAAsB,CAAC;GAE3B,IAAI;IACH,YAAY,GAAG,YAAY,UAAU;GACtC,QAAQ;IAEP;GACD;GAEA,KAAK,MAAM,WAAW,WAAW;IAChC,MAAM,OAAO,KAAK,KAAK,YAAY,OAAO;IAE1C,IAAI;KACH,IAAI,MAAM,GAAG,SAAS,IAAI,EAAE,UAAU,kBACrC,GAAG,OAAO,MAAM;MAAE,WAAW;MAAM,OAAO;KAAK,CAAC;IAElD,QAAQ,CAER;GACD;GAGA,IAAI;IACH,GAAG,UAAU,UAAU;GACxB,QAAQ,CAER;EACD;CACD;AACD;;AAGA,SAAgB,gBAAgB,OAAwC;CACvE,OACC,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,0BAA0B;AAE/D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infracraft/pulumi",
3
- "version": "1.16.3",
3
+ "version": "1.16.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {