@starci/hfs 4.0.9 → 4.1.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.
Files changed (247) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/LICENSE +21 -0
  3. package/emit/operations.mjs +1 -1
  4. package/emit/type-schema.mjs +2 -1
  5. package/lint/run.mjs +82 -8
  6. package/package.json +3 -2
  7. package/report/order.mjs +2 -0
  8. package/runtime/config.example.yaml +187 -0
  9. package/runtime/engine/by-code-unit.mjs +12 -0
  10. package/runtime/engine/config.mjs +247 -273
  11. package/runtime/engine/invalid-config.mjs +5 -5
  12. package/runtime/engine/model-config.mjs +90 -0
  13. package/runtime/engine/orca-config.mjs +3 -1
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +134 -0
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +102 -69
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -1
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  30. package/runtime/modules/models/registry.yaml +25 -397
  31. package/runtime/modules/models/runtimes.yaml +92 -262
  32. package/runtime/modules/models/tiers.yaml +65 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/lib.mjs +6 -0
  37. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  38. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  39. package/runtime/scripts/api/git/lib.mjs +36 -1
  40. package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
  41. package/runtime/scripts/api/process/run-program.mjs +8 -0
  42. package/runtime/scripts/api/sops/decrypt.mjs +9 -16
  43. package/runtime/scripts/api/sops/lib.mjs +309 -11
  44. package/runtime/scripts/api/sops/seal.mjs +8 -4
  45. package/runtime/scripts/hfs/allows.mjs +3 -3
  46. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  47. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  48. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  49. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  50. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  51. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  52. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  53. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  54. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  55. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  56. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  57. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  58. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  59. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  60. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  61. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  62. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  63. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  64. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  65. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  66. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  67. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  69. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  70. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  71. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
  72. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  73. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  74. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  75. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  76. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  77. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  78. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  79. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  80. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  81. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  82. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  83. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  84. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  85. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  86. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  87. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  88. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  89. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  90. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  91. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  92. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  93. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  94. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  95. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  96. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  97. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  98. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  99. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  100. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  101. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  102. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  103. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  104. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  105. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  106. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  107. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  108. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  109. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  110. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  111. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  112. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  113. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  114. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  115. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  116. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  117. package/runtime/scripts/hfs/architecture.mjs +3 -4
  118. package/runtime/scripts/hfs/check.mjs +61 -70
  119. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  120. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  121. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  122. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  123. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  124. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  125. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  126. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  127. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  128. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  129. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  130. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  131. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  132. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  133. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  134. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  135. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  136. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  137. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  138. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  139. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  140. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  141. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  142. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  143. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  144. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  145. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  146. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  147. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  148. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  149. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  150. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  151. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  152. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  153. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  154. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  155. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  156. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  157. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  158. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  159. package/runtime/scripts/hfs/secret.mjs +90 -45
  160. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  161. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  162. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  163. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  165. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  166. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  167. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  168. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  169. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  170. package/runtime/scripts/hfs/slots.mjs +72 -693
  171. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  172. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  173. package/runtime/scripts/hfs/tree.mjs +19 -14
  174. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  175. package/runtime/scripts/hfs/view.mjs +1 -1
  176. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  177. package/runtime/scripts/lib/event-contract.mjs +17 -13
  178. package/runtime/scripts/lib/fs-kind.mjs +14 -4
  179. package/runtime/scripts/lib/git.mjs +2 -2
  180. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  181. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  182. package/runtime/scripts/lib/i18n.mjs +3 -2
  183. package/runtime/scripts/lib/in-order.mjs +71 -0
  184. package/runtime/scripts/lib/language.mjs +3 -3
  185. package/runtime/scripts/lib/list.mjs +8 -1
  186. package/runtime/scripts/lib/mutation-fence.mjs +15 -0
  187. package/runtime/scripts/lib/path-key.mjs +42 -9
  188. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  189. package/runtime/scripts/lib/regex.mjs +1 -1
  190. package/runtime/scripts/lib/same-text.mjs +1 -1
  191. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  192. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  193. package/runtime/scripts/lib/sops-envelope.mjs +103 -2
  194. package/runtime/scripts/lib/stack-services.mjs +1 -2
  195. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  196. package/runtime/scripts/lib/walk.mjs +6 -1
  197. package/scaffold/add-cli-lite.mjs +2 -1
  198. package/scaffold/add-table.mjs +2 -2
  199. package/scaffold/add.mjs +3 -2
  200. package/scaffold/app.mjs +4 -2
  201. package/scaffold/edition-gate.mjs +25 -25
  202. package/scaffold/lite-exports.mjs +2 -1
  203. package/scaffold/service.mjs +5 -4
  204. package/sync/hygiene.mjs +15 -9
  205. package/sync/index.mjs +7 -3
  206. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  207. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  208. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  209. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  210. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  211. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  212. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  213. package/templates/app/starciwork.gitignore +1 -1
  214. package/templates/be/image/api/Dockerfile +1 -1
  215. package/templates/be/image/cli/Dockerfile +1 -1
  216. package/templates/be/image/worker/Dockerfile +1 -1
  217. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  218. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  219. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  220. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  221. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  222. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  223. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  224. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  225. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  226. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  228. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  229. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  230. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  231. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  232. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  233. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  234. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  235. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  236. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  237. package/templates/fe/image/next/Dockerfile +2 -1
  238. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  239. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  240. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  242. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  243. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  245. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  246. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  247. package/upgrade/index.mjs +9 -2
@@ -0,0 +1,27 @@
1
+ // ensure-temp-root.mjs — the temp root exists and takes writes before anything is placed in it or a child is started with TEMP/TMP/TMPDIR pointing at it.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { tempRoot, TEMP_ROOT_ENV } from '../../../engine/temp-root.mjs';
5
+
6
+ const TEMP_ROOT_UNUSABLE = 'TEMP_ROOT_UNUSABLE';
7
+ const writable = new Set();
8
+
9
+ const unusable = (root, reason) => Object.assign(new Error(`${TEMP_ROOT_UNUSABLE}: the temp root ${root} ${reason}; set roots.temp in config.yaml (or ${TEMP_ROOT_ENV}) to a directory the runtime can create and write - it is not replaced by the operating-system temp directory`), { code: TEMP_ROOT_UNUSABLE, root });
10
+
11
+ /** Writes and removes one probe file in `root`: a directory that exists can still refuse writes. */
12
+ function probeWritable(root) {
13
+ const probe = path.join(root, `.starci-write-probe-${process.pid}`);
14
+ fs.writeFileSync(probe, '');
15
+ fs.rmSync(probe, { force: true });
16
+ }
17
+
18
+ /** The temp root of `env`, created when it is missing. Throws an Error with code TEMP_ROOT_UNUSABLE when it cannot be created or written. */
19
+ export function ensureTempRoot({ env = process.env } = {}) {
20
+ const root = tempRoot({ env });
21
+ try {
22
+ fs.mkdirSync(root, { recursive: true });
23
+ if (!writable.has(root)) probeWritable(root);
24
+ } catch (error) { throw unusable(root, `cannot be created or written (${error.code ?? error.message})`); }
25
+ writable.add(root);
26
+ return root;
27
+ }
@@ -6,6 +6,7 @@ import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { samePath } from '../../lib/path-key.mjs';
8
8
  import { realpathOr } from '../../lib/fs-kind.mjs';
9
+ import { tempRoot } from '../../../engine/temp-root.mjs';
9
10
 
10
11
  const SKILL_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
11
12
 
@@ -37,7 +38,7 @@ function strictlyInsideReal(p, root) {
37
38
  export function forbiddenRoot(p, { checkoutsUnder = null, hold } = {}) {
38
39
  const resolved = path.resolve(p);
39
40
  if (path.parse(resolved).root === resolved || samePath(path.dirname(resolved), resolved)) return 'a filesystem root';
40
- for (const [name, dir] of [['the home directory', os.homedir()], ['the temp directory', os.tmpdir()], ['the runtime', SKILL_ROOT],
41
+ for (const [name, dir] of [['the home directory', os.homedir()], ['the temp directory', os.tmpdir()], ['the configured temp root', tempRoot()], ['the runtime', SKILL_ROOT],
41
42
  ['the repository hosting the runtime', path.dirname(SKILL_ROOT)], ['the repositories root', path.dirname(path.dirname(SKILL_ROOT))]]) {
42
43
  if (dir && samePath(path.resolve(dir), resolved)) return name;
43
44
  }
@@ -12,3 +12,9 @@ export function unlinkOnly(p) {
12
12
  try { fs.unlinkSync(p); } catch { try { fs.rmdirSync(p); } catch { /* verified below */ } }
13
13
  try { fs.lstatSync(p); return false; } catch (error) { return error?.code === 'ENOENT'; }
14
14
  }
15
+
16
+ /** Construct an archive-reader refusal with its existing error code. */
17
+ export const zipRefuse = (message, code = 'zip-corrupt') => Object.assign(new Error(message), { code });
18
+
19
+ /** Relative ZIP entry names exclude traversal, drive roots and control characters. */
20
+ export const validZipName=name=>Boolean(name)&&!name.startsWith('/')&&!/^[A-Za-z]:/.test(name)&&!/[\0\r\n\\]/.test(name)&&!name.split('/').includes('..');
@@ -0,0 +1,11 @@
1
+ // make-temp-dir.mjs — a fresh temporary directory under the temp root (engine/temp-root.mjs tempRoot).
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ensureTempRoot } from './ensure-temp-root.mjs';
5
+
6
+ /** A new directory `<prefix><random>` under `parent` (default the temp root, ensureTempRoot: TEMP_ROOT_UNUSABLE when it cannot be made), which is created when it is missing. */
7
+ export function makeTempDir(prefix, { env = process.env, parent = undefined } = {}) {
8
+ const root = parent ?? ensureTempRoot({ env });
9
+ fs.mkdirSync(root, { recursive: true });
10
+ return fs.mkdtempSync(path.join(root, prefix));
11
+ }
@@ -41,7 +41,7 @@ const retrying = (fn, retries) => {
41
41
  };
42
42
  const removeFile = (p, retries) => retrying(() => {
43
43
  try { fs.unlinkSync(p); } catch (error) {
44
- if (WIN && (error?.code === 'EPERM' || error?.code === 'EACCES')) { fs.chmodSync(p, 0o666); fs.unlinkSync(p); } else throw error;
44
+ if (WIN && (error?.code === 'EPERM' || error?.code === 'EACCES')) { fs.chmodSync(p, 0o600); fs.unlinkSync(p); } else throw error;
45
45
  }
46
46
  }, retries);
47
47
 
@@ -65,40 +65,57 @@ export function safeRemove(root, { retries = 5, checkoutsUnder = null, hold } =
65
65
  // the root itself or strictly under the root's real path. A link is unlinked above (the link only); any other
66
66
  // entry that resolves outside the tree is refused, whatever made it look like a plain entry.
67
67
  const rootReal = realpathOr(target);
68
- const insideRoot = (p) => { const real = realpathOr(p); if (!real || !rootReal) return false; if (same(real, rootReal)) return true;
69
- const rel = path.relative(rootReal, real); return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel); };
70
- const walk = (dir, st, parentReal) => {
71
- if (isLinkLike(dir, { parentReal, stat: st })) {
72
- if (unlinkOnly(dir)) { out.removed.links += 1; return true; }
73
- fail(dir, { code: 'LINK_STUCK', message: 'a link could not be unlinked; nothing above it is removed' });
74
- return false;
75
- }
76
- if (!insideRoot(dir)) {
77
- fail(dir, { code: 'OUTSIDE_ROOT', message: `refusing to delete ${dir}: its real path ${realpathOr(dir) ?? '?'} is outside ${rootReal ?? target}` });
78
- return false;
79
- }
80
- if (!st.isDirectory()) {
81
- const error = removeFile(dir, retries);
82
- if (error) { fail(dir, error); return false; }
83
- out.removed.files += 1; return true;
84
- }
85
- const real = realpathOr(dir);
86
- if (!real) { fail(dir, { code: 'REALPATH', message: 'cannot resolve the directory' }); return false; }
87
- let entries;
88
- try { entries = fs.readdirSync(dir); } catch (error) { fail(dir, error); return false; }
89
- let clear = true;
90
- for (const name of entries) {
91
- const child = path.join(dir, name);
92
- let childStat;
93
- try { childStat = fs.lstatSync(child); } catch (error) { if (error?.code !== 'ENOENT') { fail(child, error); clear = false; } continue; }
94
- if (!walk(child, childStat, real)) clear = false;
95
- }
96
- if (!clear) return false;
97
- const error = retrying(() => fs.rmdirSync(dir), retries);
98
- if (error) { fail(dir, error); return false; }
99
- out.removed.dirs += 1; return true;
100
- };
101
- walk(target, st, null);
68
+ const ctx = { out, fail, retries, rootReal, target };
69
+ walk(ctx, target, st, null);
102
70
  out.ok = !fs.existsSync(target) && (() => { try { fs.lstatSync(target); return false; } catch { return true; } })();
103
71
  return out;
104
72
  }
73
+
74
+ function insideRoot({ rootReal }, p) {
75
+ const real = realpathOr(p);
76
+ if (!real || !rootReal) return false;
77
+ if (same(real, rootReal)) return true;
78
+ const rel = path.relative(rootReal, real);
79
+ return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
80
+ }
81
+
82
+ // Removes each child of `dir` (real path `real`); false when any child stays.
83
+ function removeChildren(ctx, dir, entries, real) {
84
+ let clear = true;
85
+ for (const name of entries) {
86
+ const child = path.join(dir, name);
87
+ let childStat;
88
+ try { childStat = fs.lstatSync(child); } catch (error) { if (error?.code !== 'ENOENT') { ctx.fail(child, error); clear = false; } continue; }
89
+ if (!walk(ctx, child, childStat, real)) clear = false;
90
+ }
91
+ return clear;
92
+ }
93
+
94
+ function walkDirectory(ctx, dir) {
95
+ const { out, fail, retries } = ctx;
96
+ const real = realpathOr(dir);
97
+ if (!real) { fail(dir, { code: 'REALPATH', message: 'cannot resolve the directory' }); return false; }
98
+ let entries;
99
+ try { entries = fs.readdirSync(dir); } catch (error) { fail(dir, error); return false; }
100
+ if (!removeChildren(ctx, dir, entries, real)) return false;
101
+ const error = retrying(() => fs.rmdirSync(dir), retries);
102
+ if (error) { fail(dir, error); return false; }
103
+ out.removed.dirs += 1; return true;
104
+ }
105
+
106
+ function walk(ctx, dir, st, parentReal) {
107
+ const { out, fail, retries, rootReal, target } = ctx;
108
+ if (isLinkLike(dir, { parentReal, stat: st })) {
109
+ if (unlinkOnly(dir)) { out.removed.links += 1; return true; }
110
+ fail(dir, { code: 'LINK_STUCK', message: 'a link could not be unlinked; nothing above it is removed' });
111
+ return false;
112
+ }
113
+ if (!insideRoot(ctx, dir)) {
114
+ fail(dir, { code: 'OUTSIDE_ROOT', message: `refusing to delete ${dir}: its real path ${realpathOr(dir) ?? '?'} is outside ${rootReal ?? target}` });
115
+ return false;
116
+ }
117
+ if (st.isDirectory()) return walkDirectory(ctx, dir);
118
+ const error = removeFile(dir, retries);
119
+ if (error) { fail(dir, error); return false; }
120
+ out.removed.files += 1; return true;
121
+ }
@@ -6,8 +6,13 @@
6
6
  // caller's runner into {ok, stdout, stderr}. Only the call files beside this one import it: each names one git verb
7
7
  // (diff.mjs, ls-files.mjs, worktree-*.mjs, ...), and a caller folds the spawn result it gets back with the pure
8
8
  // helpers of scripts/lib/git.mjs (gitResultOf, gitOutputOf).
9
+ import { assertMutationFence } from '../../lib/mutation-fence.mjs';
10
+ import fs from 'node:fs';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
9
13
  import { spawnSync } from 'node:child_process';
10
14
  import { withoutGitLocalEnv } from '../../lib/git.mjs';
15
+ import { withTempEnv } from '../../../engine/temp-root.mjs';
11
16
 
12
17
  /**
13
18
  * Spawn `file` (the git binary) once with `args`: utf8 text, a hidden window, never a shell.
@@ -16,9 +21,11 @@ import { withoutGitLocalEnv } from '../../lib/git.mjs';
16
21
  * ERR_UNKNOWN_ENCODING, so it reaches spawnSync as encoding:null (Buffer output either way).
17
22
  */
18
23
  export const gitSpawn = (file, args, options = {}) => {
24
+ assertMutationFence({ kind: 'git-effect', args });
19
25
  const spawn = { encoding: 'utf8', windowsHide: true, ...options };
20
26
  if (spawn.encoding === 'buffer') spawn.encoding = null;
21
- return spawnSync(file, args, spawn);
27
+ // tempChildEnv copies the env (node adds NODE_V8_COVERAGE to the env object it is given: a frozen caller env must not throw under coverage) and points TEMP/TMP/TMPDIR at the temp root
28
+ return spawnSync(file, args, withTempEnv(spawn));
22
29
  };
23
30
 
24
31
  /**
@@ -40,3 +47,31 @@ export const gitRunner = (git = null) => (args, opts = {}) => {
40
47
  const r = git ? git(args, opts) : runGit(args, { timeout: 300_000, maxBuffer: 64 * 1024 * 1024, ...opts });
41
48
  return { ok: r?.ok ?? (!r?.error && r?.status === 0), stdout: String(r?.stdout ?? r?.out ?? '').trim(), stderr: String(r?.stderr ?? r?.err ?? r?.error?.message ?? r?.error ?? '').trim() };
42
49
  };
50
+
51
+ let cachedGit;
52
+ const gitPathOf = ({ env, platform, home, exists }) => {
53
+ const p = platform === 'win32' ? path.win32 : path.posix;
54
+ const pathKey = Object.keys(env).find((name) => name.toLowerCase() === 'path');
55
+ const shim = p.join(home, '.starci', 'bin');
56
+ const same = (a, b) => (platform === 'win32' ? p.resolve(a).toLowerCase() === p.resolve(b).toLowerCase() : p.resolve(a) === p.resolve(b));
57
+ const binary = platform === 'win32' ? 'git.exe' : 'git';
58
+ for (const raw of String(pathKey ? env[pathKey] : '').split(platform === 'win32' ? ';' : ':')) {
59
+ const dir = raw.replace(/^"(.*)"$/, '$1');
60
+ const file = dir && p.isAbsolute(dir) && !same(dir, shim) ? p.join(dir, binary) : null;
61
+ if (file && exists(file)) return file;
62
+ }
63
+ return null;
64
+ };
65
+ /**
66
+ * The absolute path of the real git binary, or null: the first PATH directory holding git (git.exe on Windows) that is not
67
+ * the StarCi shim directory <home>/.starci/bin (a seat's guard shim is never the git the runtime itself runs). A bare name is never
68
+ * spawned, so a writable directory cannot be reached by name lookup at spawn time. Resolved once per process; options (env,
69
+ * platform, home, exists) are for a spec and bypass the cache.
70
+ */
71
+ export const gitExecutable = (options = null) => {
72
+ if (!options && cachedGit !== undefined) return cachedGit;
73
+ const { env = process.env, platform = process.platform, home = os.homedir(), exists = (file) => fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true } = options ?? {};
74
+ const found = gitPathOf({ env, platform, home, exists });
75
+ if (!options) cachedGit = found;
76
+ return found;
77
+ };
@@ -0,0 +1,52 @@
1
+ // resolve-real-tool.mjs - find the host executable behind a StarCi PATH wrapper without ever selecting that wrapper again.
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const pathValue = (env) => {
7
+ const key = Object.keys(env ?? {}).find((name) => name.toLowerCase() === 'path');
8
+ return key ? String(env[key] ?? '') : '';
9
+ };
10
+
11
+ const canonical = (value, { platform, realpath }) => {
12
+ let resolved = path.resolve(value);
13
+ try { resolved = realpath(resolved); } catch { /* a missing PATH entry stays comparable by its resolved spelling */ }
14
+ return platform === 'win32' ? resolved.toLowerCase() : resolved;
15
+ };
16
+
17
+ const executableNames = (program, { env, platform }) => {
18
+ if (platform !== 'win32' || path.extname(program)) return [program];
19
+ const extensions = String(env?.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean);
20
+ const names = [program];
21
+ for (const extension of extensions) names.push(`${program}${extension}`, `${program}${extension.toLowerCase()}`);
22
+ return [...new Set(names)];
23
+ };
24
+
25
+ const firstExecutableHit = (entry, names, stat, realpath) => {
26
+ for (const executable of names) {
27
+ const candidate = path.join(entry, executable);
28
+ try {
29
+ if (stat(candidate).isFile()) {
30
+ try { return realpath(candidate); } catch { return path.resolve(candidate); }
31
+ }
32
+ } catch { /* try the next executable spelling or PATH entry */ }
33
+ }
34
+ return null;
35
+ };
36
+
37
+ /** Resolve `program` from PATH while excluding <home>/.starci/bin by canonical directory identity. */
38
+ export function resolveRealTool(program, { env = process.env, home = os.homedir(), platform = process.platform,
39
+ stat = fs.statSync, realpath = fs.realpathSync.native } = {}) {
40
+ const name = String(program ?? '');
41
+ if (!name || path.basename(name) !== name) return null;
42
+ const delimiter = platform === 'win32' ? ';' : ':';
43
+ const shimKey = canonical(path.join(home, '.starci', 'bin'), { platform, realpath });
44
+ const names = executableNames(name, { env, platform });
45
+ for (const rawEntry of pathValue(env).split(delimiter).filter(Boolean)) {
46
+ const entry = rawEntry.replace(/^"(.*)"$/, '$1');
47
+ if (canonical(entry, { platform, realpath }) === shimKey) continue;
48
+ const hit = firstExecutableHit(entry, names, stat, realpath);
49
+ if (hit) return hit;
50
+ }
51
+ return null;
52
+ }
@@ -0,0 +1,8 @@
1
+ // run-program.mjs — run one declared program argv (`file args`, never a shell), waited for: a UAT policy command of an
2
+ // assisted run (scripts/uat/assisted-runner.mjs), resolved to its file by scripts/uat/launch.mjs launchFor. Returns the
3
+ // spawnSync result {status, stdout, stderr, error, signal} (utf8 text, a hidden window).
4
+ import { spawnSync } from 'node:child_process';
5
+ import { withTempEnv } from '../../../engine/temp-root.mjs';
6
+
7
+ /** Runs `file args`; options (cwd, env, timeout, stdio, input) pass through. */
8
+ export const runProgram = (file, args, options = {}) => spawnSync(file, args, withTempEnv({ encoding: 'utf8', windowsHide: true, ...options }));
@@ -1,20 +1,13 @@
1
- // decrypt.mjs — one `sops` decryption: the custody reads (scripts/api/sops/exec-env.mjs execEnv) and the test
2
- // secrets (scripts/uat/test-secret.mjs testSecret). The plaintext stays in this process's memory (stdout over a pipe).
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { spawnSync } from 'node:child_process';
6
- import { resolveSops } from './lib.mjs';
1
+ // decrypt.mjs — the public SOPS file-decrypt call.
2
+ import { runSopsFile } from './lib.mjs';
7
3
 
8
4
  /**
9
- * Run `<bin> <args>` (bin null: the sops resolveSops finds) with SOPS_AGE_KEY_FILE defaulted to ~/.starci/master.identity.
10
- * {status, stdout, stderr, error}; a missing sops is {status: null, error: SOPS_MISSING}.
5
+ * Decrypt one custody file from caller-supplied SOPS arguments; the shared file owner requires the caller's original identity selection.
6
+ * @param {string|null} bin Explicit SOPS executable, or null to use the selected environment's resolver.
7
+ * @param {string[]} args SOPS arguments forwarded unchanged to the file operation.
8
+ * @param {object} options Identity, invocation, environment and capture options owned by runSopsFile.
9
+ * @returns {{status: number|null, stdout: string|null, stderr: string|null, error: Error|null}} The observed child result or typed identity refusal.
11
10
  */
12
- export function decrypt(bin, args, { env = process.env, cwd = undefined, maxBuffer = 16 * 1024 * 1024, timeout = undefined } = {}) {
13
- const exe = bin ?? resolveSops(env);
14
- if (!exe) return { status: null, stdout: '', stderr: '', error: Object.assign(new Error('sops is not installed (Windows: winget install Mozilla.SOPS)'), { code: 'SOPS_MISSING' }) };
15
- const r = spawnSync(exe, args, {
16
- cwd, encoding: 'utf8', windowsHide: true, maxBuffer, timeout,
17
- env: { ...env, SOPS_AGE_KEY_FILE: env.SOPS_AGE_KEY_FILE || path.join(os.homedir(), '.starci', 'master.identity') },
18
- });
19
- return { status: r.status, stdout: r.stdout, stderr: r.stderr, error: r.error ?? null };
11
+ export function decrypt(bin, args, options = {}) {
12
+ return runSopsFile(bin, args, 'decrypt', options);
20
13
  }