@mp-lb/arcnet 0.0.0-stage → 0.2.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 (239) hide show
  1. package/README.md +48 -2
  2. package/dist/addDependency.d.ts +3 -0
  3. package/dist/addDependency.d.ts.map +1 -0
  4. package/dist/addDependency.js +29 -0
  5. package/dist/addDependency.js.map +1 -0
  6. package/dist/apiClient.d.ts +3 -0
  7. package/dist/apiClient.d.ts.map +1 -0
  8. package/dist/apiClient.js +39 -0
  9. package/dist/apiClient.js.map +1 -0
  10. package/dist/checkIdentity.d.ts +3 -0
  11. package/dist/checkIdentity.d.ts.map +1 -0
  12. package/dist/checkIdentity.js +9 -0
  13. package/dist/checkIdentity.js.map +1 -0
  14. package/dist/ciOutputs.d.ts +2 -0
  15. package/dist/ciOutputs.d.ts.map +1 -0
  16. package/dist/ciOutputs.js +31 -0
  17. package/dist/ciOutputs.js.map +1 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +9 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/createCli.d.ts +3 -0
  23. package/dist/createCli.d.ts.map +1 -0
  24. package/dist/createCli.js +90 -0
  25. package/dist/createCli.js.map +1 -0
  26. package/dist/createProject.d.ts +26 -0
  27. package/dist/createProject.d.ts.map +1 -0
  28. package/dist/createProject.js +35 -0
  29. package/dist/createProject.js.map +1 -0
  30. package/dist/createSetupCommand.d.ts +3 -0
  31. package/dist/createSetupCommand.d.ts.map +1 -0
  32. package/dist/createSetupCommand.js +11 -0
  33. package/dist/createSetupCommand.js.map +1 -0
  34. package/dist/deploy.d.ts +3 -0
  35. package/dist/deploy.d.ts.map +1 -0
  36. package/dist/deploy.js +47 -0
  37. package/dist/deploy.js.map +1 -0
  38. package/dist/fail.d.ts +2 -0
  39. package/dist/fail.d.ts.map +1 -0
  40. package/dist/fail.js +4 -0
  41. package/dist/fail.js.map +1 -0
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +3 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/inspectReferences.d.ts +3 -0
  47. package/dist/inspectReferences.d.ts.map +1 -0
  48. package/dist/inspectReferences.js +10 -0
  49. package/dist/inspectReferences.js.map +1 -0
  50. package/dist/inspectStaticTree.d.ts +2 -0
  51. package/dist/inspectStaticTree.d.ts.map +1 -0
  52. package/dist/inspectStaticTree.js +17 -0
  53. package/dist/inspectStaticTree.js.map +1 -0
  54. package/dist/packageInfo.d.ts +3 -0
  55. package/dist/packageInfo.d.ts.map +1 -0
  56. package/dist/packageInfo.js +9 -0
  57. package/dist/packageInfo.js.map +1 -0
  58. package/dist/parseSelection.d.ts +3 -0
  59. package/dist/parseSelection.d.ts.map +1 -0
  60. package/dist/parseSelection.js +13 -0
  61. package/dist/parseSelection.js.map +1 -0
  62. package/dist/parseSetupInput.d.ts +16 -0
  63. package/dist/parseSetupInput.d.ts.map +1 -0
  64. package/dist/parseSetupInput.js +34 -0
  65. package/dist/parseSetupInput.js.map +1 -0
  66. package/dist/readSelection.d.ts +15 -0
  67. package/dist/readSelection.d.ts.map +1 -0
  68. package/dist/readSelection.js +36 -0
  69. package/dist/readSelection.js.map +1 -0
  70. package/dist/renderPlan.d.ts +27 -0
  71. package/dist/renderPlan.d.ts.map +1 -0
  72. package/dist/renderPlan.js +25 -0
  73. package/dist/renderPlan.js.map +1 -0
  74. package/dist/renderResult.d.ts +5 -0
  75. package/dist/renderResult.d.ts.map +1 -0
  76. package/dist/renderResult.js +15 -0
  77. package/dist/renderResult.js.map +1 -0
  78. package/dist/resolveRepository.d.ts +2 -0
  79. package/dist/resolveRepository.d.ts.map +1 -0
  80. package/dist/resolveRepository.js +24 -0
  81. package/dist/resolveRepository.js.map +1 -0
  82. package/dist/runProcess.d.ts +2 -0
  83. package/dist/runProcess.d.ts.map +1 -0
  84. package/dist/runProcess.js +16 -0
  85. package/dist/runProcess.js.map +1 -0
  86. package/dist/runTerraform.d.ts +8 -0
  87. package/dist/runTerraform.d.ts.map +1 -0
  88. package/dist/runTerraform.js +111 -0
  89. package/dist/runTerraform.js.map +1 -0
  90. package/dist/runVercel.d.ts +34 -0
  91. package/dist/runVercel.d.ts.map +1 -0
  92. package/dist/runVercel.js +52 -0
  93. package/dist/runVercel.js.map +1 -0
  94. package/dist/safePath.d.ts +2 -0
  95. package/dist/safePath.d.ts.map +1 -0
  96. package/dist/safePath.js +35 -0
  97. package/dist/safePath.js.map +1 -0
  98. package/dist/safeTargetPath.d.ts +2 -0
  99. package/dist/safeTargetPath.d.ts.map +1 -0
  100. package/dist/safeTargetPath.js +29 -0
  101. package/dist/safeTargetPath.js.map +1 -0
  102. package/dist/schemas.d.ts +106 -0
  103. package/dist/schemas.d.ts.map +1 -0
  104. package/dist/schemas.js +85 -0
  105. package/dist/schemas.js.map +1 -0
  106. package/dist/secrets/assertNewSource.d.ts +3 -0
  107. package/dist/secrets/assertNewSource.d.ts.map +1 -0
  108. package/dist/secrets/assertNewSource.js +32 -0
  109. package/dist/secrets/assertNewSource.js.map +1 -0
  110. package/dist/secrets/config.d.ts +37 -0
  111. package/dist/secrets/config.d.ts.map +1 -0
  112. package/dist/secrets/config.js +67 -0
  113. package/dist/secrets/config.js.map +1 -0
  114. package/dist/secrets/createSecretsCommands.d.ts +3 -0
  115. package/dist/secrets/createSecretsCommands.d.ts.map +1 -0
  116. package/dist/secrets/createSecretsCommands.js +93 -0
  117. package/dist/secrets/createSecretsCommands.js.map +1 -0
  118. package/dist/secrets/loadSecrets.d.ts +3 -0
  119. package/dist/secrets/loadSecrets.d.ts.map +1 -0
  120. package/dist/secrets/loadSecrets.js +35 -0
  121. package/dist/secrets/loadSecrets.js.map +1 -0
  122. package/dist/secrets/promptPassword.d.ts +2 -0
  123. package/dist/secrets/promptPassword.d.ts.map +1 -0
  124. package/dist/secrets/promptPassword.js +42 -0
  125. package/dist/secrets/promptPassword.js.map +1 -0
  126. package/dist/secrets/provisionSource.d.ts +25 -0
  127. package/dist/secrets/provisionSource.d.ts.map +1 -0
  128. package/dist/secrets/provisionSource.js +141 -0
  129. package/dist/secrets/provisionSource.js.map +1 -0
  130. package/dist/secrets/readRemoteSource.d.ts +7 -0
  131. package/dist/secrets/readRemoteSource.d.ts.map +1 -0
  132. package/dist/secrets/readRemoteSource.js +32 -0
  133. package/dist/secrets/readRemoteSource.js.map +1 -0
  134. package/dist/secrets/runWithSecrets.d.ts +6 -0
  135. package/dist/secrets/runWithSecrets.d.ts.map +1 -0
  136. package/dist/secrets/runWithSecrets.js +59 -0
  137. package/dist/secrets/runWithSecrets.js.map +1 -0
  138. package/dist/secrets/secretCommand.d.ts +2 -0
  139. package/dist/secrets/secretCommand.d.ts.map +1 -0
  140. package/dist/secrets/secretCommand.js +17 -0
  141. package/dist/secrets/secretCommand.js.map +1 -0
  142. package/dist/secrets/selectSecrets.d.ts +3 -0
  143. package/dist/secrets/selectSecrets.d.ts.map +1 -0
  144. package/dist/secrets/selectSecrets.js +18 -0
  145. package/dist/secrets/selectSecrets.js.map +1 -0
  146. package/dist/secrets/syncSecrets.d.ts +10 -0
  147. package/dist/secrets/syncSecrets.d.ts.map +1 -0
  148. package/dist/secrets/syncSecrets.js +43 -0
  149. package/dist/secrets/syncSecrets.js.map +1 -0
  150. package/dist/secrets/wireSecretsWorkflows.d.ts +2 -0
  151. package/dist/secrets/wireSecretsWorkflows.d.ts.map +1 -0
  152. package/dist/secrets/wireSecretsWorkflows.js +48 -0
  153. package/dist/secrets/wireSecretsWorkflows.js.map +1 -0
  154. package/dist/secrets/withDeploymentSecrets.d.ts +2 -0
  155. package/dist/secrets/withDeploymentSecrets.d.ts.map +1 -0
  156. package/dist/secrets/withDeploymentSecrets.js +58 -0
  157. package/dist/secrets/withDeploymentSecrets.js.map +1 -0
  158. package/dist/secrets/writeSecretsWorkflow.d.ts +3 -0
  159. package/dist/secrets/writeSecretsWorkflow.d.ts.map +1 -0
  160. package/dist/secrets/writeSecretsWorkflow.js +60 -0
  161. package/dist/secrets/writeSecretsWorkflow.js.map +1 -0
  162. package/dist/selectNetwork.d.ts +26 -0
  163. package/dist/selectNetwork.d.ts.map +1 -0
  164. package/dist/selectNetwork.js +48 -0
  165. package/dist/selectNetwork.js.map +1 -0
  166. package/dist/setup.d.ts +2 -0
  167. package/dist/setup.d.ts.map +1 -0
  168. package/dist/setup.js +192 -0
  169. package/dist/setup.js.map +1 -0
  170. package/dist/setupReport.d.ts +8 -0
  171. package/dist/setupReport.d.ts.map +1 -0
  172. package/dist/setupReport.js +2 -0
  173. package/dist/setupReport.js.map +1 -0
  174. package/dist/setupVercel.d.ts +9 -0
  175. package/dist/setupVercel.d.ts.map +1 -0
  176. package/dist/setupVercel.js +72 -0
  177. package/dist/setupVercel.js.map +1 -0
  178. package/dist/stageSite.d.ts +3 -0
  179. package/dist/stageSite.d.ts.map +1 -0
  180. package/dist/stageSite.js +31 -0
  181. package/dist/stageSite.js.map +1 -0
  182. package/dist/validateConfig.d.ts +3 -0
  183. package/dist/validateConfig.d.ts.map +1 -0
  184. package/dist/validateConfig.js +17 -0
  185. package/dist/validateConfig.js.map +1 -0
  186. package/docs/public/commands.md +132 -0
  187. package/docs/public/llms-full.txt +182 -0
  188. package/package.json +50 -3
  189. package/src/addDependency.ts +41 -0
  190. package/src/apiClient.ts +51 -0
  191. package/src/checkIdentity.ts +18 -0
  192. package/src/ciOutputs.ts +47 -0
  193. package/src/cli.ts +11 -0
  194. package/src/createCli.ts +128 -0
  195. package/src/createProject.ts +66 -0
  196. package/src/createSetupCommand.ts +16 -0
  197. package/src/deploy.ts +71 -0
  198. package/src/fail.ts +3 -0
  199. package/src/index.ts +2 -0
  200. package/src/inspectReferences.ts +20 -0
  201. package/src/inspectStaticTree.ts +19 -0
  202. package/src/packageInfo.ts +10 -0
  203. package/src/parseSelection.ts +22 -0
  204. package/src/parseSetupInput.ts +40 -0
  205. package/src/readSelection.ts +47 -0
  206. package/src/renderPlan.ts +27 -0
  207. package/src/renderResult.ts +18 -0
  208. package/src/resolveRepository.ts +38 -0
  209. package/src/runProcess.ts +26 -0
  210. package/src/runTerraform.ts +182 -0
  211. package/src/runVercel.ts +77 -0
  212. package/src/safePath.ts +43 -0
  213. package/src/safeTargetPath.ts +32 -0
  214. package/src/schemas.ts +126 -0
  215. package/src/secrets/assertNewSource.ts +40 -0
  216. package/src/secrets/config.ts +91 -0
  217. package/src/secrets/createSecretsCommands.ts +189 -0
  218. package/src/secrets/loadSecrets.ts +53 -0
  219. package/src/secrets/promptPassword.ts +46 -0
  220. package/src/secrets/provisionSource.ts +238 -0
  221. package/src/secrets/readRemoteSource.ts +73 -0
  222. package/src/secrets/runWithSecrets.ts +71 -0
  223. package/src/secrets/secretCommand.ts +26 -0
  224. package/src/secrets/selectSecrets.ts +33 -0
  225. package/src/secrets/syncSecrets.ts +60 -0
  226. package/src/secrets/wireSecretsWorkflows.ts +57 -0
  227. package/src/secrets/withDeploymentSecrets.ts +77 -0
  228. package/src/secrets/writeSecretsWorkflow.ts +83 -0
  229. package/src/selectNetwork.ts +73 -0
  230. package/src/setup.ts +277 -0
  231. package/src/setupReport.ts +11 -0
  232. package/src/setupVercel.ts +126 -0
  233. package/src/stageSite.ts +54 -0
  234. package/src/validateConfig.ts +22 -0
  235. package/templates/deploy-arc.yaml +73 -0
  236. package/templates/deploy-vercel.yaml +43 -0
  237. package/templates/network.yaml +105 -0
  238. package/templates/vercel-static/main.tf +128 -0
  239. package/templates/vercel-static/module.yaml +51 -0
@@ -0,0 +1,66 @@
1
+ import { checkIdentity } from "./checkIdentity.js";
2
+ import { fail } from "./fail.js";
3
+ import type { VercelConfig, VercelRequest } from "./schemas.js";
4
+
5
+ export const createProject = async (
6
+ config: VercelConfig,
7
+ name: string,
8
+ request: VercelRequest,
9
+ ) => {
10
+ const site = config.sites[name];
11
+ if (!site) fail("Choose a configured site.");
12
+
13
+ if (site.mode === "existing") {
14
+ checkIdentity(
15
+ await request("GET", `/v9/projects/${site.projectId}`, config.account),
16
+ site,
17
+ config.account,
18
+ );
19
+
20
+ return config;
21
+ }
22
+
23
+ if (site.creationApproved !== true)
24
+ fail(
25
+ "Project creation disabled. Obtain explicit target/cost approval and set creationApproved=true for this site.",
26
+ );
27
+
28
+ if (config.account.type === "personal") {
29
+ const who = await request("GET", "/v2/user", config.account);
30
+ if (who?.user?.id !== config.account.id)
31
+ fail(
32
+ "Token personal account does not match configured account; no project created.",
33
+ );
34
+ }
35
+
36
+ if (
37
+ await request(
38
+ "GET",
39
+ `/v9/projects/${encodeURIComponent(site.projectName)}`,
40
+ config.account,
41
+ )
42
+ )
43
+ fail(
44
+ `Project name ${site.projectName} already exists. Set an explicit existing reference after verifying its ID; no duplicate project was created.`,
45
+ );
46
+
47
+ const created = await request("POST", "/v11/projects", config.account, {
48
+ name: site.projectName,
49
+ framework: null,
50
+ });
51
+
52
+ if (
53
+ !created ||
54
+ !/^prj_[A-Za-z0-9]+$/.test(created.id ?? "") ||
55
+ created.accountId !== config.account.id ||
56
+ created.name !== site.projectName
57
+ )
58
+ fail(
59
+ "Creation response identity mismatch. Inspect the account before retrying.",
60
+ );
61
+ const next = structuredClone(config);
62
+ next.approved = false;
63
+ next.sites[name] = { ...site, mode: "existing", projectId: created.id! };
64
+ delete next.sites[name]!.creationApproved;
65
+ return next;
66
+ };
@@ -0,0 +1,16 @@
1
+ import { Command } from "commander";
2
+ export const createSetupCommand = () =>
3
+ new Command("setup")
4
+ .description(
5
+ "Add ArcNet configuration and workflow; pin the CLI in the project manifest.",
6
+ )
7
+ .argument("<slug>", "Project slug matching zap.yaml")
8
+ .option("--dry-run", "Report generated files without writing")
9
+ .option("--service <name>", "Default container service", "backend")
10
+ .option("--dockerfile <path>", "Container Dockerfile", ".arcnet/Dockerfile")
11
+ .option(
12
+ "--repository <owner/repo>",
13
+ "GitHub repository (defaults to origin)",
14
+ )
15
+ .option("--services <path>", "Explicit service selection JSON")
16
+ .option("--vercel <path>", "Static Vercel references JSON");
package/src/deploy.ts ADDED
@@ -0,0 +1,71 @@
1
+ import { rmSync } from "node:fs";
2
+ import { fail } from "./fail.js";
3
+ import { inspectReferences } from "./inspectReferences.js";
4
+ import type {
5
+ RunProcess,
6
+ VercelConfig,
7
+ VercelRequest,
8
+ VercelSite,
9
+ } from "./schemas.js";
10
+ import { stageSite } from "./stageSite.js";
11
+ import { validateConfig } from "./validateConfig.js";
12
+
13
+ export const deploy = async (
14
+ config: VercelConfig,
15
+ root: string,
16
+ request: VercelRequest,
17
+ run: RunProcess,
18
+ environment: NodeJS.ProcessEnv,
19
+ target = "production",
20
+ ) => {
21
+ validateConfig(config, root);
22
+ if (config.approved !== true)
23
+ fail(
24
+ "Deployment disabled. Review account, project references, routing and production-trigger ownership before setting approved=true.",
25
+ );
26
+ if (!["production", "preview"].includes(target))
27
+ fail("Target must be production or preview.");
28
+ if (
29
+ environment.GITHUB_ACTIONS === "true" &&
30
+ (environment.GITHUB_REPOSITORY !== config.repository ||
31
+ environment.GITHUB_REF !== "refs/heads/main")
32
+ )
33
+ fail(
34
+ "CI deployment is restricted to the configured repository main branch.",
35
+ );
36
+ await inspectReferences(config, request); // Check every identity before building or uploading any site.
37
+ const stages: Array<[VercelSite, string]> = [];
38
+
39
+ try {
40
+ for (const site of Object.values(config.sites)) {
41
+ const buildEnv = Object.fromEntries(
42
+ Object.entries(environment).filter(([k]) => !k.startsWith("VERCEL_")),
43
+ );
44
+
45
+ await run(site.build[0]!, site.build.slice(1), root, buildEnv);
46
+ stages.push([site, stageSite(root, site, config.account)]);
47
+ }
48
+
49
+ for (const [site, stage] of stages) {
50
+ const args = [
51
+ "--yes",
52
+ "vercel@63.1.2",
53
+ "deploy",
54
+ "--prebuilt",
55
+ "--yes",
56
+ ...(target === "production" ? ["--prod"] : []),
57
+ "--token",
58
+ environment.VERCEL_TOKEN!,
59
+ ];
60
+
61
+ await run("npx", args, stage, {
62
+ ...environment,
63
+ VERCEL_PROJECT_ID: site.projectId,
64
+ VERCEL_ORG_ID: config.account.id,
65
+ });
66
+ }
67
+ } finally {
68
+ for (const [, stage] of stages)
69
+ rmSync(stage, { recursive: true, force: true });
70
+ }
71
+ };
package/src/fail.ts ADDED
@@ -0,0 +1,3 @@
1
+ export const fail: (message: string) => never = (message) => {
2
+ throw new Error(message);
3
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createCli } from "./createCli.js";
2
+ export { setup } from "./setup.js";
@@ -0,0 +1,20 @@
1
+ import { checkIdentity } from "./checkIdentity.js";
2
+ import { fail } from "./fail.js";
3
+ import type { VercelConfig, VercelRequest } from "./schemas.js";
4
+
5
+ export const inspectReferences = async (
6
+ config: VercelConfig,
7
+ request: VercelRequest,
8
+ ) => {
9
+ for (const site of Object.values(config.sites)) {
10
+ if (site.mode !== "existing")
11
+ fail(
12
+ `Project ${site.projectName} is new. Create it explicitly, review and commit the returned reference before deployment.`,
13
+ );
14
+ checkIdentity(
15
+ await request("GET", `/v9/projects/${site.projectId}`, config.account),
16
+ site,
17
+ config.account,
18
+ );
19
+ }
20
+ };
@@ -0,0 +1,19 @@
1
+ import { lstatSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { fail } from "./fail.js";
4
+
5
+ export const inspectStaticTree = (path: string) => {
6
+ for (const name of readdirSync(path)) {
7
+ const p = join(path, name),
8
+ stat = lstatSync(p);
9
+
10
+ if (
11
+ stat.isSymbolicLink() ||
12
+ [".git", ".env", "node_modules"].includes(name) ||
13
+ name.startsWith(".env.")
14
+ )
15
+ fail(`Refusing unsafe static artifact entry: ${name}`);
16
+ if (stat.isDirectory()) inspectStaticTree(p);
17
+ else if (!stat.isFile()) fail("Static output contains a non-regular file.");
18
+ }
19
+ };
@@ -0,0 +1,10 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+ export const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
6
+ export const packageVersion = z
7
+ .object({ version: z.string() })
8
+ .parse(
9
+ JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")),
10
+ ).version;
@@ -0,0 +1,22 @@
1
+ import {
2
+ serviceDeploySchema,
3
+ type ServiceDeploy,
4
+ } from "@mp-lb/zapper/dist/arc/schemas.js";
5
+
6
+ export const parseSelection = (
7
+ input: unknown,
8
+ ): Record<string, ServiceDeploy> => {
9
+ if (
10
+ !input ||
11
+ typeof input !== "object" ||
12
+ Array.isArray(input) ||
13
+ Object.keys(input).length === 0
14
+ )
15
+ throw new Error("Services file must be a nonempty JSON object.");
16
+ return Object.fromEntries(
17
+ Object.entries(input).map(([name, block]) => [
18
+ name,
19
+ serviceDeploySchema.parse(block),
20
+ ]),
21
+ );
22
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { createSetupCommand } from "./createSetupCommand.js";
3
+
4
+ const schema = z.object({
5
+ service: z.string(),
6
+ dockerfile: z.string(),
7
+ repository: z.string().optional(),
8
+ services: z.string().optional(),
9
+ vercel: z.string().optional(),
10
+ dryRun: z.boolean().default(false),
11
+ });
12
+
13
+ export const parseSetupInput = (args: string[]) => {
14
+ const command = createSetupCommand()
15
+ .exitOverride()
16
+ .configureOutput({ writeErr: () => {} });
17
+
18
+ // Commander owns argument parsing; reject duplicate flags instead of silently taking the last value.
19
+ const flags = args.filter((value) => value.startsWith("--"));
20
+ if (new Set(flags).size !== flags.length)
21
+ throw new Error("Duplicate setup option.");
22
+ command.parse(args, { from: "user" });
23
+
24
+ const slug = z
25
+ .string()
26
+ .max(23)
27
+ .regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/)
28
+ .parse(command.args[0]);
29
+
30
+ const input = schema.parse(command.opts());
31
+
32
+ const options = {
33
+ ...input,
34
+ "seen-service": command.getOptionValueSource("service") === "cli",
35
+ "seen-dockerfile": command.getOptionValueSource("dockerfile") === "cli",
36
+ "seen-repository": command.getOptionValueSource("repository") === "cli",
37
+ };
38
+
39
+ return { slug, options, dry: input.dryRun };
40
+ };
@@ -0,0 +1,47 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { fail } from "./fail.js";
3
+ import { parseSelection } from "./parseSelection.js";
4
+ const read = (path: string) => readFileSync(path, "utf8");
5
+
6
+ export const readSelection = (file: string, safe: (name: string) => string) => {
7
+ const input: unknown = JSON.parse(read(safe(file)));
8
+ const selection = parseSelection(input);
9
+
10
+ const supported = [
11
+ "cloud-run-web",
12
+ "gce-worker",
13
+ "vercel-static",
14
+ "shared-mongo-atlas",
15
+ "upstash-redis",
16
+ "aws-s3",
17
+ ];
18
+
19
+ for (const [name, block] of Object.entries(selection)) {
20
+ if (
21
+ !/^[a-z][a-z0-9-]*$/.test(name) ||
22
+ ["gcp", "registry", "ci", "myshop"].includes(name) ||
23
+ /myshop/i.test(JSON.stringify(block))
24
+ )
25
+ fail(
26
+ "Invalid service selection: MyShop is local-only and project module names are reserved.",
27
+ );
28
+ if (!supported.includes(block.module))
29
+ fail(
30
+ `Unsupported starter module: ${block.module}. Use a reviewed custom Arc manifest.`,
31
+ );
32
+ if (
33
+ ["cloud-run-web", "gce-worker"].includes(block.module) &&
34
+ (!block.dockerfile || !existsSync(safe(block.dockerfile)))
35
+ )
36
+ fail(`Service ${name} requires an existing explicit Dockerfile.`);
37
+ if (
38
+ ["cloud-run-web", "vercel-static"].includes(block.module) &&
39
+ (!block.domain || !/^[a-z0-9.-]+$/.test(block.domain))
40
+ )
41
+ fail(`Service ${name} requires an explicit domain.`);
42
+ if (block["deploy-path"]) safe(block["deploy-path"]);
43
+ if (block["local-config"]) safe(block["local-config"]);
44
+ }
45
+
46
+ return selection;
47
+ };
@@ -0,0 +1,27 @@
1
+ import type { VercelConfig } from "./schemas.js";
2
+ import { validateConfig } from "./validateConfig.js";
3
+
4
+ export const renderPlan = (config: VercelConfig, root: string) => {
5
+ validateConfig(config, root);
6
+ return {
7
+ mode: "vercel",
8
+ account: config.account,
9
+ approved: config.approved,
10
+ terraform: false,
11
+ changesProjectSettings: false,
12
+ changesDNS: false,
13
+ sites: Object.entries(config.sites).map(([name, site]) => ({
14
+ name,
15
+ operation:
16
+ site.mode === "new"
17
+ ? "explicit-create-required-before-deploy"
18
+ : "reference-existing",
19
+ projectId: site.projectId ?? null,
20
+ projectName: site.projectName,
21
+ build: site.build,
22
+ output: site.output,
23
+ outputConfig: site.outputConfig,
24
+ deployment: "prebuilt-static",
25
+ })),
26
+ };
27
+ };
@@ -0,0 +1,18 @@
1
+ export const renderResult = (
2
+ value: unknown,
3
+ options: { json?: boolean; jsonl?: boolean },
4
+ ) => {
5
+ if (options.json && options.jsonl)
6
+ throw new Error("Choose either --json or --jsonl.");
7
+ if (options.jsonl)
8
+ return JSON.stringify({ type: "command.completed", result: value });
9
+ if (options.json) return JSON.stringify(value);
10
+ if (
11
+ value &&
12
+ typeof value === "object" &&
13
+ "message" in value &&
14
+ typeof value.message === "string"
15
+ )
16
+ return value.message;
17
+ return JSON.stringify(value, null, 2);
18
+ };
@@ -0,0 +1,38 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { fail } from "./fail.js";
3
+
4
+ export const resolveRepository = (
5
+ root: string,
6
+ configured?: string,
7
+ ): string => {
8
+ let repository = configured;
9
+
10
+ if (!repository) {
11
+ let origin;
12
+
13
+ try {
14
+ origin = execFileSync("git", ["remote", "get-url", "origin"], {
15
+ cwd: root,
16
+ encoding: "utf8",
17
+ stdio: ["ignore", "pipe", "ignore"],
18
+ }).trim();
19
+ } catch {
20
+ fail("Cannot read Git origin. Pass --repository owner/repo.");
21
+ }
22
+
23
+ repository =
24
+ /^(?:git@github\.com:|https:\/\/github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/.exec(
25
+ origin!,
26
+ )?.[1];
27
+ }
28
+
29
+ if (
30
+ !/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(
31
+ repository ?? "",
32
+ )
33
+ )
34
+ fail(
35
+ "Repository must be an explicit GitHub owner/repo (or a GitHub origin).",
36
+ );
37
+ return repository!;
38
+ };
@@ -0,0 +1,26 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export const runProcess = (
4
+ command: string,
5
+ args: string[],
6
+ cwd: string,
7
+ env: NodeJS.ProcessEnv,
8
+ machine = false,
9
+ input?: string,
10
+ ) => {
11
+ const result = spawnSync(command, args, {
12
+ cwd,
13
+ env,
14
+ input,
15
+ stdio: [
16
+ input === undefined ? "ignore" : "pipe",
17
+ machine ? 2 : "inherit",
18
+ "inherit",
19
+ ],
20
+ });
21
+
22
+ if (result.error || result.status !== 0)
23
+ throw new Error(
24
+ `${command} failed (${result.status ?? "could not start"}). No automatic retry; earlier cloud changes may have succeeded.`,
25
+ );
26
+ };
@@ -0,0 +1,182 @@
1
+ import { loadManifest } from "@mp-lb/zapper/dist/arc/manifest.js";
2
+ import { resolveModules } from "@mp-lb/zapper/dist/arc/modules.js";
3
+ import {
4
+ deployBlockSchema,
5
+ networkConfigSchema,
6
+ } from "@mp-lb/zapper/dist/arc/schemas.js";
7
+ import { spawnSync } from "node:child_process";
8
+ import {
9
+ existsSync,
10
+ mkdtempSync,
11
+ readFileSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { createRequire } from "node:module";
16
+ import { devNull, tmpdir } from "node:os";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { parse } from "yaml";
19
+ import { runProcess } from "./runProcess.js";
20
+ import { envPoolSchema, terraformConfigSchema } from "./schemas.js";
21
+ const require = createRequire(import.meta.url);
22
+
23
+ export const runTerraform = (
24
+ command: "check" | "plan" | "deploy",
25
+ root: string,
26
+ machine = false,
27
+ ) => {
28
+ const here = join(root, ".arcnet");
29
+ const read = (name: string) => readFileSync(join(here, name), "utf8");
30
+ const config = terraformConfigSchema.parse(JSON.parse(read("project.json")));
31
+ const zap = parse(readFileSync(join(root, "zap.yaml"), "utf8"));
32
+ const network = networkConfigSchema.parse(parse(read("network.yaml")));
33
+ const deployment = deployBlockSchema.parse(zap.deploy);
34
+ if (config.version !== 1 || zap.project !== config.slug)
35
+ throw new Error("Project config does not match zap.yaml.");
36
+ const services = Object.keys(deployment?.services ?? {});
37
+ if (
38
+ JSON.stringify(services.sort()) !==
39
+ JSON.stringify([...config.services].sort()) ||
40
+ /myshop/i.test(JSON.stringify(deployment.services))
41
+ )
42
+ throw new Error(
43
+ "Service selection differs or includes local-only MyShop. Review scope before deployment.",
44
+ );
45
+ const blocks = Object.values(deployment.services);
46
+ for (const service of blocks)
47
+ if (
48
+ ["cloud-run-web", "gce-worker"].includes(service.module) &&
49
+ (!service.dockerfile || !existsSync(join(root, service.dockerfile)))
50
+ )
51
+ throw new Error("Container Dockerfile is missing.");
52
+ if (deployment.project?.ci?.params?.repository !== config.repository)
53
+ throw new Error(
54
+ "Deployment manifest repository differs from project config.",
55
+ );
56
+
57
+ const publicEnv: unknown = existsSync(join(here, "env.production.json"))
58
+ ? JSON.parse(read("env.production.json"))
59
+ : {};
60
+
61
+ let secretEnv;
62
+
63
+ try {
64
+ secretEnv = JSON.parse(process.env.ARCNET_ENV_JSON || "{}");
65
+ } catch {
66
+ throw new Error("ARCNET_ENV_JSON must be valid JSON (value omitted).");
67
+ }
68
+
69
+ for (const pool of [publicEnv, secretEnv])
70
+ if (
71
+ !pool ||
72
+ Array.isArray(pool) ||
73
+ typeof pool !== "object" ||
74
+ Object.entries(pool).some(
75
+ ([k, v]) => !/^[A-Z_][A-Z0-9_]*$/.test(k) || typeof v !== "string",
76
+ )
77
+ )
78
+ throw new Error(
79
+ "Environment pools must be JSON objects mapping environment names to strings.",
80
+ );
81
+
82
+ const envPool = {
83
+ ...envPoolSchema.parse(publicEnv),
84
+ ...envPoolSchema.parse(secretEnv),
85
+ };
86
+
87
+ if (command === "check") {
88
+ return {
89
+ operation: command,
90
+ message:
91
+ "Offline config check passed. Cloud access, image runtime and live deployment have NOT been verified.",
92
+ };
93
+ }
94
+
95
+ if (command === "deploy" && config.approved !== true)
96
+ throw new Error(
97
+ "Deployment disabled. Complete prerequisites and obtain target/cost/IAM/DNS approval before committing approved=true.",
98
+ );
99
+
100
+ if (process.env.GITHUB_ACTIONS === "true") {
101
+ if (
102
+ process.env.GITHUB_REPOSITORY !== config.repository ||
103
+ process.env.GITHUB_REF !== "refs/heads/main"
104
+ )
105
+ throw new Error(
106
+ "CI deployment is restricted to the configured repository main branch.",
107
+ );
108
+ if (
109
+ !/^projects\/\d+\/locations\/global\/workloadIdentityPools\/github\/providers\/github-actions$/.test(
110
+ config.workloadIdentityProvider,
111
+ )
112
+ )
113
+ throw new Error(
114
+ "Set workloadIdentityProvider from the approved first deployment.",
115
+ );
116
+ }
117
+
118
+ const modulesDir = resolve(
119
+ dirname(require.resolve("@mp-lb/zapper")),
120
+ "../arc",
121
+ );
122
+
123
+ const instances = resolveModules(loadManifest(root), modulesDir, root);
124
+ for (const instance of instances)
125
+ for (const credential of instance.manifest.credentials)
126
+ if (!process.env[credential.name])
127
+ throw new Error(
128
+ `${credential.name} is required for ${instance.key}; supply a scoped operator credential through environment, never commit it.`,
129
+ );
130
+ if (JSON.stringify(network).includes("REPLACE_"))
131
+ throw new Error(
132
+ "Network contains REPLACE_ placeholders. Commit reviewed network/billing settings before planning or deployment.",
133
+ );
134
+
135
+ const missing = blocks
136
+ .flatMap((service) => service.env ?? [])
137
+ .filter((key) => !key.includes("=") && !Object.hasOwn(envPool, key));
138
+
139
+ if (missing.length)
140
+ throw new Error(
141
+ `Missing app environment keys: ${missing.join(", ")}. Supply ARCNET_ENV_JSON or committed public env.`,
142
+ );
143
+ for (const tool of [
144
+ "terraform",
145
+ "gcloud",
146
+ ...(blocks.some((s) => ["cloud-run-web", "gce-worker"].includes(s.module))
147
+ ? ["docker"]
148
+ : []),
149
+ ])
150
+ if (spawnSync(tool, ["--version"], { stdio: "ignore" }).status !== 0)
151
+ throw new Error(
152
+ `Required executable unavailable: ${tool}. See StackForge ArcNet prerequisites.`,
153
+ );
154
+ const executable = require.resolve("@mp-lb/zapper");
155
+ // The engine requires a file for operator configuration. Keep this transient,
156
+ // and explicitly disable its machine-global credentials fallback.
157
+ const temporary = mkdtempSync(join(tmpdir(), "arcnet-operator-"));
158
+
159
+ try {
160
+ const operator = join(temporary, "operator.json");
161
+ writeFileSync(operator, JSON.stringify({ credentials: devNull }), {
162
+ mode: 0o600,
163
+ });
164
+
165
+ runProcess(
166
+ process.execPath,
167
+ [executable, "arc", command],
168
+ root,
169
+ {
170
+ ...process.env,
171
+ ZAP_ARC_NETWORK_FILE: join(here, "network.yaml"),
172
+ ZAP_ARC_CONFIG: operator,
173
+ },
174
+ machine,
175
+ JSON.stringify(envPool),
176
+ );
177
+
178
+ return { operation: command, message: `Zap Arc ${command} completed.` };
179
+ } finally {
180
+ rmSync(temporary, { recursive: true, force: true });
181
+ }
182
+ };