@dharta/cli 0.6.77

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 (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/dist/agent.d.ts +17 -0
  4. package/dist/agent.js +134 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/api-error.d.ts +3 -0
  7. package/dist/api-error.js +80 -0
  8. package/dist/api-error.js.map +1 -0
  9. package/dist/browser.d.ts +1 -0
  10. package/dist/browser.js +19 -0
  11. package/dist/browser.js.map +1 -0
  12. package/dist/cli.d.ts +5 -0
  13. package/dist/cli.js +1978 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/commands/agents.d.ts +131 -0
  16. package/dist/commands/agents.js +439 -0
  17. package/dist/commands/agents.js.map +1 -0
  18. package/dist/commands/analytics.d.ts +25 -0
  19. package/dist/commands/analytics.js +166 -0
  20. package/dist/commands/analytics.js.map +1 -0
  21. package/dist/commands/api.d.ts +19 -0
  22. package/dist/commands/api.js +278 -0
  23. package/dist/commands/api.js.map +1 -0
  24. package/dist/commands/appearance.d.ts +19 -0
  25. package/dist/commands/appearance.js +63 -0
  26. package/dist/commands/appearance.js.map +1 -0
  27. package/dist/commands/artifact.d.ts +33 -0
  28. package/dist/commands/artifact.js +235 -0
  29. package/dist/commands/artifact.js.map +1 -0
  30. package/dist/commands/audit-log.d.ts +32 -0
  31. package/dist/commands/audit-log.js +102 -0
  32. package/dist/commands/audit-log.js.map +1 -0
  33. package/dist/commands/auth.d.ts +47 -0
  34. package/dist/commands/auth.js +117 -0
  35. package/dist/commands/auth.js.map +1 -0
  36. package/dist/commands/caps.d.ts +82 -0
  37. package/dist/commands/caps.js +213 -0
  38. package/dist/commands/caps.js.map +1 -0
  39. package/dist/commands/create.d.ts +22 -0
  40. package/dist/commands/create.js +76 -0
  41. package/dist/commands/create.js.map +1 -0
  42. package/dist/commands/deploy.d.ts +45 -0
  43. package/dist/commands/deploy.js +892 -0
  44. package/dist/commands/deploy.js.map +1 -0
  45. package/dist/commands/dev.d.ts +33 -0
  46. package/dist/commands/dev.js +655 -0
  47. package/dist/commands/dev.js.map +1 -0
  48. package/dist/commands/directories.d.ts +60 -0
  49. package/dist/commands/directories.js +83 -0
  50. package/dist/commands/directories.js.map +1 -0
  51. package/dist/commands/doctor.d.ts +13 -0
  52. package/dist/commands/doctor.js +123 -0
  53. package/dist/commands/doctor.js.map +1 -0
  54. package/dist/commands/embed-keys.d.ts +61 -0
  55. package/dist/commands/embed-keys.js +165 -0
  56. package/dist/commands/embed-keys.js.map +1 -0
  57. package/dist/commands/env.d.ts +59 -0
  58. package/dist/commands/env.js +300 -0
  59. package/dist/commands/env.js.map +1 -0
  60. package/dist/commands/files.d.ts +14 -0
  61. package/dist/commands/files.js +84 -0
  62. package/dist/commands/files.js.map +1 -0
  63. package/dist/commands/git-credential.d.ts +1 -0
  64. package/dist/commands/git-credential.js +70 -0
  65. package/dist/commands/git-credential.js.map +1 -0
  66. package/dist/commands/kartas.d.ts +101 -0
  67. package/dist/commands/kartas.js +289 -0
  68. package/dist/commands/kartas.js.map +1 -0
  69. package/dist/commands/keys.d.ts +33 -0
  70. package/dist/commands/keys.js +141 -0
  71. package/dist/commands/keys.js.map +1 -0
  72. package/dist/commands/login.d.ts +22 -0
  73. package/dist/commands/login.js +78 -0
  74. package/dist/commands/login.js.map +1 -0
  75. package/dist/commands/logs.d.ts +14 -0
  76. package/dist/commands/logs.js +89 -0
  77. package/dist/commands/logs.js.map +1 -0
  78. package/dist/commands/members.d.ts +32 -0
  79. package/dist/commands/members.js +77 -0
  80. package/dist/commands/members.js.map +1 -0
  81. package/dist/commands/model-keys.d.ts +22 -0
  82. package/dist/commands/model-keys.js +222 -0
  83. package/dist/commands/model-keys.js.map +1 -0
  84. package/dist/commands/open.d.ts +29 -0
  85. package/dist/commands/open.js +119 -0
  86. package/dist/commands/open.js.map +1 -0
  87. package/dist/commands/parity.d.ts +15 -0
  88. package/dist/commands/parity.js +73 -0
  89. package/dist/commands/parity.js.map +1 -0
  90. package/dist/commands/rollback.d.ts +21 -0
  91. package/dist/commands/rollback.js +26 -0
  92. package/dist/commands/rollback.js.map +1 -0
  93. package/dist/commands/schedules.d.ts +52 -0
  94. package/dist/commands/schedules.js +231 -0
  95. package/dist/commands/schedules.js.map +1 -0
  96. package/dist/commands/scopes.d.ts +3 -0
  97. package/dist/commands/scopes.js +21 -0
  98. package/dist/commands/scopes.js.map +1 -0
  99. package/dist/commands/sessions.d.ts +53 -0
  100. package/dist/commands/sessions.js +142 -0
  101. package/dist/commands/sessions.js.map +1 -0
  102. package/dist/commands/setup.d.ts +28 -0
  103. package/dist/commands/setup.js +949 -0
  104. package/dist/commands/setup.js.map +1 -0
  105. package/dist/commands/status.d.ts +19 -0
  106. package/dist/commands/status.js +63 -0
  107. package/dist/commands/status.js.map +1 -0
  108. package/dist/commands/update.d.ts +14 -0
  109. package/dist/commands/update.js +120 -0
  110. package/dist/commands/update.js.map +1 -0
  111. package/dist/commands/usage.d.ts +3 -0
  112. package/dist/commands/usage.js +37 -0
  113. package/dist/commands/usage.js.map +1 -0
  114. package/dist/commands/webhooks.d.ts +47 -0
  115. package/dist/commands/webhooks.js +156 -0
  116. package/dist/commands/webhooks.js.map +1 -0
  117. package/dist/commands/whoami.d.ts +7 -0
  118. package/dist/commands/whoami.js +17 -0
  119. package/dist/commands/whoami.js.map +1 -0
  120. package/dist/completions.d.ts +13 -0
  121. package/dist/completions.js +120 -0
  122. package/dist/completions.js.map +1 -0
  123. package/dist/config.d.ts +20 -0
  124. package/dist/config.js +98 -0
  125. package/dist/config.js.map +1 -0
  126. package/dist/context.d.ts +14 -0
  127. package/dist/context.js +38 -0
  128. package/dist/context.js.map +1 -0
  129. package/dist/device-flow.d.ts +9 -0
  130. package/dist/device-flow.js +127 -0
  131. package/dist/device-flow.js.map +1 -0
  132. package/dist/harness-preflight.d.ts +24 -0
  133. package/dist/harness-preflight.js +252 -0
  134. package/dist/harness-preflight.js.map +1 -0
  135. package/dist/harnesses.d.ts +11 -0
  136. package/dist/harnesses.js +112 -0
  137. package/dist/harnesses.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.js +3 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/login-offer.d.ts +9 -0
  142. package/dist/login-offer.js +36 -0
  143. package/dist/login-offer.js.map +1 -0
  144. package/dist/manifest.d.ts +15 -0
  145. package/dist/manifest.js +142 -0
  146. package/dist/manifest.js.map +1 -0
  147. package/dist/money.d.ts +1 -0
  148. package/dist/money.js +16 -0
  149. package/dist/money.js.map +1 -0
  150. package/dist/output.d.ts +2 -0
  151. package/dist/output.js +20 -0
  152. package/dist/output.js.map +1 -0
  153. package/dist/runtime.d.ts +19 -0
  154. package/dist/runtime.js +95 -0
  155. package/dist/runtime.js.map +1 -0
  156. package/dist/scaffold.d.ts +16 -0
  157. package/dist/scaffold.js +74 -0
  158. package/dist/scaffold.js.map +1 -0
  159. package/dist/setup-scan.d.ts +21 -0
  160. package/dist/setup-scan.js +127 -0
  161. package/dist/setup-scan.js.map +1 -0
  162. package/dist/templates.d.ts +14 -0
  163. package/dist/templates.js +65 -0
  164. package/dist/templates.js.map +1 -0
  165. package/dist/validators.d.ts +10 -0
  166. package/dist/validators.js +116 -0
  167. package/dist/validators.js.map +1 -0
  168. package/dist/version-check.d.ts +25 -0
  169. package/dist/version-check.js +68 -0
  170. package/dist/version-check.js.map +1 -0
  171. package/node_modules/@dharta/sdk/LICENSE +21 -0
  172. package/node_modules/@dharta/sdk/README.md +87 -0
  173. package/node_modules/@dharta/sdk/dist/errors.d.ts +20 -0
  174. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -0
  175. package/node_modules/@dharta/sdk/dist/errors.js +51 -0
  176. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -0
  177. package/node_modules/@dharta/sdk/dist/http.d.ts +26 -0
  178. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -0
  179. package/node_modules/@dharta/sdk/dist/http.js +136 -0
  180. package/node_modules/@dharta/sdk/dist/http.js.map +1 -0
  181. package/node_modules/@dharta/sdk/dist/index.d.ts +15 -0
  182. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -0
  183. package/node_modules/@dharta/sdk/dist/index.js +21 -0
  184. package/node_modules/@dharta/sdk/dist/index.js.map +1 -0
  185. package/node_modules/@dharta/sdk/dist/resources.d.ts +48 -0
  186. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -0
  187. package/node_modules/@dharta/sdk/dist/resources.js +150 -0
  188. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -0
  189. package/node_modules/@dharta/sdk/dist/types.d.ts +94 -0
  190. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -0
  191. package/node_modules/@dharta/sdk/dist/types.js +59 -0
  192. package/node_modules/@dharta/sdk/dist/types.js.map +1 -0
  193. package/node_modules/@dharta/sdk/package.json +32 -0
  194. package/node_modules/@dharta/sdk/src/errors.ts +58 -0
  195. package/node_modules/@dharta/sdk/src/http.ts +166 -0
  196. package/node_modules/@dharta/sdk/src/index.ts +36 -0
  197. package/node_modules/@dharta/sdk/src/resources.ts +197 -0
  198. package/node_modules/@dharta/sdk/src/types.ts +179 -0
  199. package/package.json +38 -0
  200. package/src/agent.ts +147 -0
  201. package/src/api-error.ts +88 -0
  202. package/src/browser.ts +20 -0
  203. package/src/cli.ts +2339 -0
  204. package/src/commands/agents.ts +635 -0
  205. package/src/commands/analytics.ts +181 -0
  206. package/src/commands/api.ts +297 -0
  207. package/src/commands/appearance.ts +79 -0
  208. package/src/commands/artifact.ts +331 -0
  209. package/src/commands/audit-log.ts +125 -0
  210. package/src/commands/auth.ts +186 -0
  211. package/src/commands/caps.ts +302 -0
  212. package/src/commands/create.ts +114 -0
  213. package/src/commands/deploy.ts +1122 -0
  214. package/src/commands/dev.ts +792 -0
  215. package/src/commands/directories.ts +136 -0
  216. package/src/commands/doctor.ts +138 -0
  217. package/src/commands/embed-keys.ts +237 -0
  218. package/src/commands/env.ts +369 -0
  219. package/src/commands/files.ts +124 -0
  220. package/src/commands/git-credential.ts +78 -0
  221. package/src/commands/kartas.ts +439 -0
  222. package/src/commands/keys.ts +206 -0
  223. package/src/commands/login.ts +115 -0
  224. package/src/commands/logs.ts +111 -0
  225. package/src/commands/members.ts +109 -0
  226. package/src/commands/model-keys.ts +278 -0
  227. package/src/commands/open.ts +173 -0
  228. package/src/commands/parity.ts +96 -0
  229. package/src/commands/rollback.ts +45 -0
  230. package/src/commands/schedules.ts +290 -0
  231. package/src/commands/scopes.ts +22 -0
  232. package/src/commands/sessions.ts +225 -0
  233. package/src/commands/setup.ts +1176 -0
  234. package/src/commands/status.ts +94 -0
  235. package/src/commands/update.ts +157 -0
  236. package/src/commands/usage.ts +41 -0
  237. package/src/commands/webhooks.ts +200 -0
  238. package/src/commands/whoami.ts +24 -0
  239. package/src/completions.ts +168 -0
  240. package/src/config.ts +143 -0
  241. package/src/context.ts +59 -0
  242. package/src/device-flow.ts +174 -0
  243. package/src/harness-preflight.ts +293 -0
  244. package/src/harnesses.ts +117 -0
  245. package/src/index.ts +3 -0
  246. package/src/login-offer.ts +49 -0
  247. package/src/manifest.ts +153 -0
  248. package/src/money.ts +15 -0
  249. package/src/output.ts +22 -0
  250. package/src/runtime.ts +127 -0
  251. package/src/scaffold.ts +102 -0
  252. package/src/setup-scan.ts +154 -0
  253. package/src/templates.ts +85 -0
  254. package/src/validators.ts +124 -0
  255. package/src/version-check.ts +75 -0
  256. package/templates/blank/CLAUDE.md +21 -0
  257. package/templates/blank/dharta.toml +11 -0
  258. package/templates/faq-agent/CLAUDE.md +51 -0
  259. package/templates/faq-agent/dharta.toml +11 -0
@@ -0,0 +1,949 @@
1
+ import { constants, existsSync, promises as fs, readFileSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import * as path from "node:path";
4
+ import { findGitRoot, scanAgentFolders, renderTable, describeFolder } from "../setup-scan.js";
5
+ import { renderManifest, patchManifest } from "../manifest.js";
6
+ import { deriveSlug, validateAgentName } from "../agent.js";
7
+ import { defaultGitRunner, rejectOverlappingAgentRoots } from "./deploy.js";
8
+ import { closestChoice, normalizeNonBlank } from "../validators.js";
9
+ import { DEFAULT_CREATE_HARNESS, hasAgentMarker } from "../harnesses.js";
10
+ const METHODS = ["git", "folder"];
11
+ const METHOD_INPUTS = [...METHODS, "github"];
12
+ const CI_PROVIDERS = ["github"];
13
+ const MANAGED_WORKFLOW_MARKER = "# Dharta managed workflow. Generated by dharta setup (format v1).";
14
+ const CLI_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
15
+ export async function setup(pathArg, opts = {}) {
16
+ const cwd = path.resolve(opts.cwd ?? ".");
17
+ const git = opts.gitRunner ?? defaultGitRunner;
18
+ const log = opts.log ?? ((line) => console.error(line));
19
+ const ask = opts.yes ? undefined : opts.ask;
20
+ const pathInput = pathArg === undefined ? undefined : normalizeNonBlank(pathArg, "path");
21
+ const parsedMethod = parseMethod(opts.method); // validate configuration up front
22
+ const forcedMethod = parsedMethod.method;
23
+ const ci = parseCi(opts.ci) ?? parsedMethod.ci;
24
+ if (opts.enable && opts.disable)
25
+ throw new Error("Use either --enable or --disable, not both.");
26
+ if (opts.name !== undefined)
27
+ validateAgentName(opts.name, "--name");
28
+ // Resolve and validate an explicit target before setup can initialize git or
29
+ // write anything. Besides avoiding a stray .git on typos, this establishes a
30
+ // lexical + realpath boundary that a symlink cannot redirect later writes
31
+ // through.
32
+ let gitRoot = findGitRoot(cwd);
33
+ await validateExplicitSetupPath(cwd, pathInput, gitRoot ?? cwd);
34
+ if (parsedMethod.legacyGithub) {
35
+ log("Compatibility: --method github now means --method folder plus --ci github. " +
36
+ "Prefer the two explicit options in scripts.");
37
+ }
38
+ // --- 1. git context (needed by git transport and GitHub automation) -----
39
+ const containingPlainAgent = !gitRoot && !pathInput ? nearestAgentMarkerDir(cwd) : undefined;
40
+ const localSetupRoot = containingPlainAgent ?? cwd;
41
+ let gitInitialized = false;
42
+ const existingMethod = !gitRoot && !forcedMethod
43
+ ? await inferExistingMethodWithoutGit(localSetupRoot, pathInput)
44
+ : undefined;
45
+ const needsGit = ci === "github" || (forcedMethod ?? existingMethod) !== "folder";
46
+ if (!gitRoot && needsGit) {
47
+ const rel = relLabel(localSetupRoot);
48
+ const create = ask
49
+ ? yesNoCancel(await ask(`No git here. Create git in ${rel} ? [Yes] / [no] / [cancel] `))
50
+ : "yes"; // non-interactive default takes the [Yes] (or pass --method folder)
51
+ if (create === "cancel")
52
+ throw new Error("setup canceled.");
53
+ if (create === "no" && ci === "github") {
54
+ throw new Error("--ci github requires a git repo. Initialize git, then run setup again.");
55
+ }
56
+ if (create === "yes") {
57
+ await run(git, ["-C", localSetupRoot, "init", "-b", "main"], "git init");
58
+ gitRoot = localSetupRoot;
59
+ gitInitialized = true;
60
+ log(`Initialized a git repo in ${rel}.`);
61
+ }
62
+ }
63
+ const scanRoot = gitRoot ?? containingPlainAgent ?? cwd;
64
+ const hasGit = !!gitRoot;
65
+ // --- 2. scan for agent folders -----------------------------------------
66
+ const folders = await scanAgentFolders(scanRoot, { isGitRepo: hasGit });
67
+ const shared = { cwd, git, hasGit, scanRoot, gitInitialized, forcedMethod, ci, ask, log };
68
+ // The nearest containing agent always wins, even if it is deep or ignored
69
+ // and therefore absent from the repo-wide discovery list. Setup from inside
70
+ // an agent must never fall through to a sibling selection.
71
+ if (!pathInput) {
72
+ const containingDir = nearestAgentMarkerDir(cwd, gitRoot ?? undefined);
73
+ if (containingDir) {
74
+ const target = folders.find((folder) => folder.dir === containingDir) ??
75
+ await describeFolder(containingDir, path.relative(scanRoot, containingDir));
76
+ requireAgent(target, pathInput);
77
+ return configureAgent(target, shared, opts);
78
+ }
79
+ }
80
+ // --- 3. interactive + several agents: select exactly one ----------------
81
+ // Setup is agent-scoped, just like deploy. Standing inside an agent selects
82
+ // it immediately. At an ambiguous repo root, show one menu and configure the
83
+ // single choice; configuring siblings requires explicit repeat invocations.
84
+ if (ask && !pathInput && folders.length >= 2) {
85
+ const target = await pickOneAgent(folders, shared);
86
+ if (!target)
87
+ return snapshotResult(folders[0], shared);
88
+ return configureAgent(target, shared, opts);
89
+ }
90
+ // --- single agent / explicit path / non-interactive --------------------
91
+ if (ask && folders.length > 0) {
92
+ log("");
93
+ log(renderTable(folders));
94
+ log("");
95
+ }
96
+ const target = await pickFolder(pathInput, folders, scanRoot, cwd, ask);
97
+ requireAgent(target, pathInput);
98
+ return configureAgent(target, shared, opts);
99
+ }
100
+ function nearestAgentMarkerDir(start, boundary) {
101
+ let dir = path.resolve(start);
102
+ const stop = boundary === undefined ? undefined : path.resolve(boundary);
103
+ for (;;) {
104
+ if (hasAgentMarker(dir))
105
+ return dir;
106
+ if (stop && dir === stop)
107
+ return undefined;
108
+ const parent = path.dirname(dir);
109
+ if (parent === dir)
110
+ return undefined;
111
+ dir = parent;
112
+ }
113
+ }
114
+ // setup configures an EXISTING agent; scaffolding a brand-new folder is
115
+ // `dharta create`'s job (RFC 0024). A folder with no harness or karta marker
116
+ // has no agent to configure - send them to create.
117
+ function requireAgent(target, pathArg) {
118
+ if (hasAgentMarker(target.dir))
119
+ return;
120
+ const here = target.rel === "." ? "this folder" : target.rel;
121
+ throw new Error(`No agent to configure in ${here}. Create one first with \`dharta create${pathArg ? ` ${pathArg}` : ""}\`.`);
122
+ }
123
+ // Configure ONE agent: name, deploy gate, delivery method, then write the
124
+ // self-documenting dharta.toml and apply the method action.
125
+ async function configureAgent(target, s, opts) {
126
+ const { cwd, hasGit, scanRoot, gitInitialized, ask, log } = s;
127
+ // Discovery is read-only and may encounter paths supplied by the working
128
+ // tree. Re-establish the selected agent boundary before prompts or writes so
129
+ // an explicit or discovered symlink can never become a write target.
130
+ await validateSelectedAgentRoot(scanRoot, target.dir, target.rel);
131
+ const defaultName = target.name ?? deriveSlug(path.basename(target.dir));
132
+ const name = validateAgentName(opts.name === undefined
133
+ ? ((ask ? (await ask(`Agent name [${defaultName}]: `)).trim() : "") || defaultName)
134
+ : opts.name, "agent name");
135
+ // Default to the EXISTING gate so re-running setup on an enabled agent does
136
+ // not silently disable it; a brand-new agent has deployEnabled = false, so
137
+ // D6's fail-safe default still holds.
138
+ let deploy;
139
+ if (opts.enable)
140
+ deploy = true;
141
+ else if (opts.disable)
142
+ deploy = false;
143
+ else if (ask) {
144
+ const defaultDeploy = target.configured ? target.deployEnabled : false;
145
+ const hint = defaultDeploy ? "Y/n" : "y/N";
146
+ deploy = yesNo(await ask(`Enable deploy for this agent? [${hint}] `), defaultDeploy);
147
+ }
148
+ else
149
+ deploy = target.deployEnabled;
150
+ // An explicit option changes the transport; otherwise preserve the method
151
+ // already configured for this agent. Only an unconfigured agent falls back
152
+ // to the repo-shaped default / interactive choice.
153
+ let method = s.forcedMethod ?? target.deployMethod;
154
+ if (!method)
155
+ method = ask ? await askMethod(ask, hasGit) : hasGit ? "git" : "folder";
156
+ if (!hasGit)
157
+ method = "folder"; // folder is forced without a git repo
158
+ // Enforce the same one-manifest-per-release-root invariant as deploy before
159
+ // either local artifact is written. A parent and child dharta.toml would make
160
+ // ownership of the child's files ambiguous regardless of transport or CI.
161
+ await rejectOverlappingAgentRoots(target.dir, s.git);
162
+ await rejectDuplicateAgentSlug(scanRoot, target.dir, name, hasGit);
163
+ const workflow = s.ci === "github"
164
+ ? githubWorkflowTarget(scanRoot, target.dir, name, method, await currentGitBranch(s.git, scanRoot))
165
+ : undefined;
166
+ let workflowState;
167
+ if (workflow) {
168
+ if (!hasGit)
169
+ throw new Error("--ci github requires a git repo.");
170
+ await validateWorkflowTarget(workflow);
171
+ workflowState = await existingWorkflowState(workflow, log);
172
+ if (workflowState !== "custom") {
173
+ await rejectGitignoredSetupFile(s.git, scanRoot, workflow.dest, workflow.rel);
174
+ }
175
+ }
176
+ const manifestPath = path.join(target.dir, "dharta.toml");
177
+ await validateSafeWriteTarget(target.dir, manifestPath, "dharta.toml", false);
178
+ if (hasGit) {
179
+ await rejectGitignoredSetupFile(s.git, scanRoot, manifestPath, path.relative(scanRoot, manifestPath));
180
+ }
181
+ const existingManifest = await lstatIfPresent(manifestPath);
182
+ const scaffolded = existingManifest === undefined;
183
+ if (existingManifest) {
184
+ const patched = patchManifest(await fs.readFile(manifestPath, "utf8"), {
185
+ name,
186
+ deploy,
187
+ deployMethod: method,
188
+ });
189
+ await safeAtomicWriteFile(target.dir, manifestPath, "dharta.toml", patched);
190
+ }
191
+ else {
192
+ await safeAtomicWriteFile(target.dir, manifestPath, "dharta.toml", renderManifest({
193
+ name,
194
+ deploy,
195
+ deployMethod: method,
196
+ harness: target.harnessType ?? DEFAULT_CREATE_HARNESS,
197
+ }));
198
+ }
199
+ log(`${scaffolded ? "Wrote" : "Updated"} ${path.relative(cwd, manifestPath) || "dharta.toml"} ` +
200
+ `(name = ${name}, deploy = ${deploy}, deploy_method = ${method}).`);
201
+ applyMethod(method, { deploy, log });
202
+ const workflowScaffolded = workflow
203
+ ? await scaffoldGithubWorkflow(workflow, workflowState, log)
204
+ : false;
205
+ return {
206
+ folder: target.rel,
207
+ name,
208
+ deploy,
209
+ method,
210
+ ci: s.ci,
211
+ manifestPath,
212
+ scaffolded,
213
+ workflowScaffolded,
214
+ gitInitialized,
215
+ };
216
+ }
217
+ async function pickOneAgent(initial, s) {
218
+ const { scanRoot, hasGit, ask, log } = s;
219
+ const view = await Promise.all(initial.map((folder) => describeFolder(folder.dir, folder.rel)));
220
+ log("");
221
+ log(`Agents in ${hasGit ? "this repo" : "this folder"} (${scanRoot}):`);
222
+ log("");
223
+ log(renderMenu(view, null));
224
+ log("");
225
+ return selectAgent(ask, view, log);
226
+ }
227
+ // The agent folder the user is standing in: an exact cwd match, else the
228
+ // deepest agent that contains cwd. null when cwd is outside every agent.
229
+ function currentAgentDir(folders, cwd) {
230
+ const containing = folders
231
+ .filter((f) => f.dir === cwd || cwd.startsWith(f.dir + path.sep))
232
+ .sort((a, b) => b.dir.length - a.dir.length);
233
+ return containing[0]?.dir ?? null;
234
+ }
235
+ // Numbered agent menu: the current folder is prefixed [current], and a final
236
+ // `q` row cancels this invocation.
237
+ function renderMenu(folders, currentDir) {
238
+ const headers = ["#", "AGENT FOLDER", "NAME", "HARNESS", "CONFIGURED", "DEPLOY", "METHOD"];
239
+ const rows = folders.map((f, i) => [
240
+ String(i + 1),
241
+ `${f.dir === currentDir ? "[current] " : ""}${f.rel === "." ? "./ (repo root)" : `./${f.rel}`}`,
242
+ f.name ?? "-",
243
+ f.harnessType ?? "-",
244
+ f.configured ? "yes" : "no",
245
+ f.configured ? (f.deployEnabled ? "enabled" : "disabled") : "-",
246
+ f.deployMethod ?? "-",
247
+ ]);
248
+ const quit = ["q", "quit", "", "", "", "", ""];
249
+ const widths = headers.map((_, i) => Math.max(...[headers, ...rows, quit].map((r) => r[i].length)));
250
+ const fmt = (cols) => cols.map((c, i) => c.padEnd(widths[i])).join(" ").trimEnd();
251
+ return [fmt(headers), ...rows.map(fmt), fmt(quit)].join("\n");
252
+ }
253
+ // Read one menu choice: a number, or 'q' to quit. An empty answer quits because
254
+ // an ambiguous repository root has no safe implicit agent selection.
255
+ async function selectAgent(ask, folders, log) {
256
+ for (;;) {
257
+ const choice = (await ask("Select one agent to set up, or q to quit [q]: ")).trim().toLowerCase() || "q";
258
+ if (choice === "q" || choice === "quit")
259
+ return null;
260
+ const n = Number(choice);
261
+ if (Number.isInteger(n) && n >= 1 && n <= folders.length)
262
+ return folders[n - 1];
263
+ log(`Enter a number 1-${folders.length}, or q to quit.`);
264
+ }
265
+ }
266
+ // A read-only SetupResult for "quit before configuring anything" - only used to
267
+ // satisfy the return type; the interactive caller never reads it.
268
+ function snapshotResult(f, s) {
269
+ return {
270
+ folder: f.rel,
271
+ name: f.name ?? deriveSlug(path.basename(f.dir)),
272
+ deploy: f.deployEnabled,
273
+ method: f.deployMethod ?? (s.hasGit ? "git" : "folder"),
274
+ ci: s.ci,
275
+ manifestPath: path.join(f.dir, "dharta.toml"),
276
+ scaffolded: false,
277
+ workflowScaffolded: false,
278
+ gitInitialized: s.gitInitialized,
279
+ };
280
+ }
281
+ function parseMethod(raw) {
282
+ if (raw === undefined)
283
+ return { legacyGithub: false };
284
+ const normalized = normalizeNonBlank(raw, "--method");
285
+ // Backward compatibility for the old three-way transport switch. GitHub is
286
+ // automation, and the workflow it historically generated used folder
287
+ // upload, so preserve that behavior while making the two decisions explicit.
288
+ if (normalized === "github") {
289
+ return { method: "folder", ci: "github", legacyGithub: true };
290
+ }
291
+ if (!METHODS.includes(normalized)) {
292
+ const suggestion = closestChoice(normalized, METHOD_INPUTS);
293
+ throw new Error(`Unknown --method '${normalized}'. Use git or folder.` +
294
+ (suggestion ? ` Did you mean ${suggestion}?` : ""));
295
+ }
296
+ return { method: normalized, legacyGithub: false };
297
+ }
298
+ function parseCi(raw) {
299
+ if (raw === undefined)
300
+ return undefined;
301
+ const normalized = normalizeNonBlank(raw, "--ci");
302
+ if (!CI_PROVIDERS.includes(normalized)) {
303
+ const suggestion = closestChoice(normalized, CI_PROVIDERS);
304
+ throw new Error(`Unknown --ci '${normalized}'. Use github.` +
305
+ (suggestion ? ` Did you mean ${suggestion}?` : ""));
306
+ }
307
+ return normalized;
308
+ }
309
+ // Before setup decides whether to initialize git, honor a persisted folder
310
+ // method for the obvious single-agent / explicit-path cases. This prevents a
311
+ // routine setup re-run from changing a plain-folder project's repo shape.
312
+ async function inferExistingMethodWithoutGit(cwd, pathArg) {
313
+ if (pathArg) {
314
+ return (await describeFolder(path.resolve(cwd, pathArg), pathArg)).deployMethod;
315
+ }
316
+ const folders = await scanAgentFolders(cwd, { isGitRepo: false });
317
+ if (folders.length === 1)
318
+ return folders[0].deployMethod;
319
+ if (folders.length > 1 && folders.every((folder) => folder.deployMethod === "folder")) {
320
+ return "folder";
321
+ }
322
+ return undefined;
323
+ }
324
+ async function validateExplicitSetupPath(cwd, pathArg, boundary) {
325
+ if (!pathArg)
326
+ return;
327
+ const target = path.resolve(cwd, pathArg);
328
+ try {
329
+ await validateExistingDirectoryTree(boundary, target, `Cannot set up ${pathArg}`);
330
+ }
331
+ catch (error) {
332
+ const code = error.code;
333
+ if (code === "ENOENT") {
334
+ throw new Error(`Cannot set up ${pathArg}: that directory does not exist. ` +
335
+ `Create an agent first with \`dharta create ${pathArg}\`.`);
336
+ }
337
+ if (code === "ENOTDIR") {
338
+ throw new Error(`Cannot set up ${pathArg}: a parent path is not a directory.`);
339
+ }
340
+ throw error;
341
+ }
342
+ if (!hasAgentMarker(target)) {
343
+ throw new Error(`Cannot set up ${pathArg}: no agent was found there. ` +
344
+ `Create one first with \`dharta create ${pathArg}\`.`);
345
+ }
346
+ }
347
+ async function validateSelectedAgentRoot(scanRoot, agentDir, rel) {
348
+ const label = rel === "." ? "selected agent root" : `selected agent root '${rel}'`;
349
+ await validateExistingDirectoryTree(scanRoot, agentDir, `Cannot use ${label}`);
350
+ }
351
+ // Resolve the agent folder: an explicit path, the sole candidate, an
352
+ // interactive pick, or (non-interactive + ambiguous) a loud error.
353
+ async function pickFolder(pathArg, folders, scanRoot, cwd, ask) {
354
+ if (pathArg) {
355
+ const requested = path.resolve(cwd, pathArg);
356
+ const [scanRootReal, requestedReal] = await Promise.all([fs.realpath(scanRoot), fs.realpath(requested)]);
357
+ const realRelative = containedRelativePath(scanRootReal, requestedReal);
358
+ // Use the scan root's spelling for all later reads/writes. This preserves
359
+ // harmless platform aliases such as macOS /var -> /private/var while
360
+ // removing any caller-supplied symlink component from the writable path.
361
+ const dir = realRelative === undefined ? requested : path.resolve(scanRoot, realRelative);
362
+ const match = folders.find((f) => f.dir === dir);
363
+ if (match)
364
+ return match;
365
+ // Not surfaced by the scan (deep / untracked): describe it directly so an
366
+ // existing dharta.toml's name + deploy are read, not silently reset to the
367
+ // folder slug - which would rename the agent and change its deploy slug.
368
+ return describeFolder(dir, path.relative(scanRoot, dir));
369
+ }
370
+ const current = currentAgentDir(folders, cwd);
371
+ if (current)
372
+ return folders.find((folder) => folder.dir === current);
373
+ if (folders.length === 1)
374
+ return folders[0];
375
+ if (folders.length === 0) {
376
+ // No marker folder found - configure the scan root itself.
377
+ return { dir: scanRoot, rel: ".", configured: false, deployEnabled: false };
378
+ }
379
+ if (!ask) {
380
+ throw new Error("Multiple agent folders here. Pass a [path] to pick one, e.g. `dharta setup ./support-bot`.");
381
+ }
382
+ const answer = (await ask("Configure which folder? (path) ")).trim();
383
+ const dir = path.resolve(scanRoot, answer);
384
+ const match = folders.find((f) => f.dir === dir || f.rel === answer);
385
+ if (!match)
386
+ throw new Error(`No agent folder matching '${answer}'.`);
387
+ return match;
388
+ }
389
+ async function askMethod(ask, hasGit) {
390
+ const def = hasGit ? "git" : "folder";
391
+ const a = (await ask(`Deploy method - git (push) / folder (upload) [${def}]: `)).trim().toLowerCase();
392
+ return METHODS.includes(a) ? a : def;
393
+ }
394
+ function applyMethod(method, o) {
395
+ const deployHint = o.deploy ? "`dharta deploy`" : "enable it (deploy = true), then `dharta deploy`";
396
+ switch (method) {
397
+ case "git":
398
+ // The hosted repo + the `dharta` remote are provisioned at deploy time
399
+ // (dharta deploy self-provisions), keeping setup destination-agnostic.
400
+ o.log(`Method: git. Deploy with ${deployHint} - it provisions the hosted repo and wires the 'karta' remote on first run. ` +
401
+ "After that, 'dharta deploy' remains the agent-scoped path. A direct deploy-branch push (for example, 'git push dharta main') is a compatibility shortcut only for repos with one enabled agent.");
402
+ return;
403
+ case "folder":
404
+ o.log(`Method: folder. Deploy with ${deployHint} - it uploads only this agent folder as a tarball.`);
405
+ return;
406
+ }
407
+ }
408
+ function githubWorkflowTarget(repoRoot, agentDirectory, name, method, branch) {
409
+ const relative = path.relative(repoRoot, agentDirectory);
410
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) {
411
+ throw new Error("--ci github requires the agent folder to be inside the git repo.");
412
+ }
413
+ const agentDir = relative === "" ? "." : relative.split(path.sep).join("/");
414
+ if (agentDir.includes("${{")) {
415
+ throw new Error("--ci github cannot generate a safe workflow for an agent path containing '${{'. " +
416
+ "Rename the folder, then run setup again.");
417
+ }
418
+ const slug = deriveSlug(name);
419
+ const rel = `.github/workflows/dharta-${slug}.yml`;
420
+ const secretName = `DHARTA_API_KEY_${slug.replaceAll("-", "_").toUpperCase()}`;
421
+ return { repoRoot, slug, method, branch, secretName, agentDir, rel, dest: path.join(repoRoot, rel) };
422
+ }
423
+ async function currentGitBranch(git, repoRoot) {
424
+ const result = await git(["-C", repoRoot, "symbolic-ref", "--quiet", "--short", "HEAD"]);
425
+ const branch = result.stdout.trim();
426
+ if (result.code !== 0 || branch.length === 0 || branch.match(/[\0\r\n]/)) {
427
+ throw new Error("--ci github needs a checked-out branch so setup can generate the correct push trigger. " +
428
+ "Check out the deploy branch, then run setup again.");
429
+ }
430
+ if (branch.includes("${{")) {
431
+ throw new Error("--ci github cannot generate a safe workflow for a branch containing '${{'. " +
432
+ "Check out a different deploy branch, then run setup again.");
433
+ }
434
+ return branch;
435
+ }
436
+ async function rejectDuplicateAgentSlug(scanRoot, targetDir, name, isGitRepo) {
437
+ const slug = deriveSlug(name);
438
+ const folders = isGitRepo
439
+ ? await scanAgentFolders(scanRoot, { isGitRepo: true })
440
+ : await recursiveConfiguredFolders(scanRoot);
441
+ const conflict = folders.find((folder) => path.resolve(folder.dir) !== path.resolve(targetDir) &&
442
+ folder.name !== undefined && deriveSlug(folder.name) === slug);
443
+ if (!conflict)
444
+ return;
445
+ throw new Error(`Agent name '${name}' resolves to slug '${slug}', already used by ${conflict.rel}. ` +
446
+ "Choose unique agent names before setup; Dharta deploy identity is the derived slug.");
447
+ }
448
+ async function recursiveConfiguredFolders(root) {
449
+ const folders = [];
450
+ const walk = async (dir) => {
451
+ if (existsSync(path.join(dir, "dharta.toml"))) {
452
+ folders.push(await describeFolder(dir, path.relative(root, dir) || "."));
453
+ }
454
+ let entries;
455
+ try {
456
+ entries = await fs.readdir(dir, { withFileTypes: true });
457
+ }
458
+ catch {
459
+ return;
460
+ }
461
+ for (const entry of entries) {
462
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith("."))
463
+ continue;
464
+ await walk(path.join(dir, entry.name));
465
+ }
466
+ };
467
+ await walk(path.resolve(root));
468
+ return folders;
469
+ }
470
+ // GitHub is an automation layer, not a third persisted transport. Each agent
471
+ // owns one path-filtered workflow and one named secret, so changing support
472
+ // cannot trigger billing and concurrent runs for the same agent stay ordered.
473
+ // The job pins the method rendered by setup so its checkout, concurrency, and
474
+ // credential scope cannot drift from the actual command. Setup does not mint
475
+ // credentials; it explains the scope required by that method.
476
+ async function scaffoldGithubWorkflow(target, state, log) {
477
+ if (state === "custom")
478
+ return false;
479
+ await safeAtomicWriteFile(target.repoRoot, target.dest, target.rel, renderDeployWorkflow(target));
480
+ log(`CI: github. ${state === "generated" ? "Updated" : "Wrote"} ${target.rel} for ${target.agentDir}.`);
481
+ log(` This workflow is pinned to --mode ${target.method}. To change methods, rerun setup with ` +
482
+ "--ci github and rotate the agent's CI secret for the new scope.");
483
+ const scope = target.method === "folder" ? "releases:write" : "source_repos:write";
484
+ log(` To let CI deploy, add a ${scope} key as the ${target.secretName} repo secret:`);
485
+ log(` dharta keys create ci-${target.slug} --scope ${scope}`);
486
+ log(" (requires an active key with keys:write; dashboard creation also works)");
487
+ log(` gh secret set ${target.secretName} # or add it in repo Settings > Secrets`);
488
+ log(" Then push a change to this agent folder, or run the workflow manually.");
489
+ return true;
490
+ }
491
+ async function validateWorkflowTarget(target) {
492
+ // `target.dest` is always repo/.github/workflows/file. Derive the workflow
493
+ // directory without trusting a caller-supplied relative path.
494
+ const workflowsDir = path.dirname(target.dest);
495
+ await validateSafeWriteTarget(target.repoRoot, target.dest, target.rel, false);
496
+ await rejectLegacyRepoWorkflow(workflowsDir, target.dest);
497
+ await rejectConflictingGeneratedWorkflow(workflowsDir, target);
498
+ }
499
+ async function rejectConflictingGeneratedWorkflow(workflowsDir, target) {
500
+ let entries;
501
+ try {
502
+ entries = await fs.readdir(workflowsDir, { withFileTypes: true });
503
+ }
504
+ catch (error) {
505
+ if (error.code === "ENOENT")
506
+ return;
507
+ throw error;
508
+ }
509
+ const agentMarker = `# Agent directory: ${JSON.stringify(target.agentDir)}.`;
510
+ const unpinnedCommand = `dharta deploy --dir ${shellQuote(target.agentDir)}`;
511
+ for (const entry of entries) {
512
+ if (!entry.isFile() || !(entry.name.endsWith(".yml") || entry.name.endsWith(".yaml")))
513
+ continue;
514
+ const dest = path.join(workflowsDir, entry.name);
515
+ const content = await fs.readFile(dest, "utf8");
516
+ if (!isManagedWorkflow(content))
517
+ continue;
518
+ const deploysTarget = content.includes(agentMarker) || content.includes(unpinnedCommand);
519
+ if (path.resolve(dest) === path.resolve(target.dest) && !deploysTarget) {
520
+ throw new Error(`${target.rel} is a generated workflow for a different agent folder. ` +
521
+ `Agent slug '${target.slug}' is already in use; choose unique dharta.toml names before setup.`);
522
+ }
523
+ if (path.resolve(dest) !== path.resolve(target.dest) && deploysTarget) {
524
+ const rel = `.github/workflows/${entry.name}`;
525
+ throw new Error(`${rel} already deploys ${target.agentDir}. Remove or rename that generated workflow ` +
526
+ "before changing this agent's name, or both workflows could deploy the same folder.");
527
+ }
528
+ }
529
+ }
530
+ async function rejectLegacyRepoWorkflow(workflowsDir, targetDest) {
531
+ const rel = ".github/workflows/dharta-deploy.yml";
532
+ const dest = path.join(workflowsDir, "dharta-deploy.yml");
533
+ let content;
534
+ try {
535
+ content = await fs.readFile(dest, "utf8");
536
+ }
537
+ catch (error) {
538
+ if (error.code === "ENOENT" ||
539
+ error.code === "EISDIR")
540
+ return;
541
+ throw error;
542
+ }
543
+ const generatedLegacy = content.includes("# Deploy this repo's Dharta agents on every push (RFC 0024).") &&
544
+ content.includes("DHARTA_API_KEY: ${{ secrets.DHARTA_API_KEY }}") &&
545
+ content.includes("run: dharta deploy --mode folder");
546
+ if (generatedLegacy) {
547
+ throw new Error(`${rel} is Dharta's legacy repo-wide deploy workflow and can duplicate or fan out deploys. ` +
548
+ `Remove it, then rerun setup to create one path-scoped workflow per agent. No files were changed.`);
549
+ }
550
+ if (!isManagedWorkflow(content) && path.resolve(dest) !== path.resolve(targetDest)) {
551
+ throw new Error(`${rel} already exists but is not a current per-agent Dharta workflow. ` +
552
+ "Inspect, remove, or rename it before enabling per-agent CI; setup left every file unchanged.");
553
+ }
554
+ }
555
+ async function existingWorkflowState(target, log) {
556
+ const { dest, rel } = target;
557
+ try {
558
+ const stat = await fs.lstat(dest);
559
+ if (stat.isSymbolicLink()) {
560
+ throw new Error(`Cannot write ${rel}: the workflow file is a symbolic link.`);
561
+ }
562
+ if (stat.isDirectory()) {
563
+ throw new Error(`Cannot write ${rel}: expected a file, got a directory.`);
564
+ }
565
+ const content = await fs.readFile(dest, "utf8");
566
+ if (isManagedWorkflow(content))
567
+ return "generated";
568
+ log(`CI: github requested, but ${rel} is a custom workflow. Left it unchanged; ` +
569
+ "CI was not configured. Verify and maintain that workflow yourself.");
570
+ return "custom";
571
+ }
572
+ catch (error) {
573
+ if (error.code === "ENOENT")
574
+ return "missing";
575
+ throw error;
576
+ }
577
+ }
578
+ function isManagedWorkflow(content) {
579
+ const lines = content.split(/\r?\n/);
580
+ if (lines[0] === MANAGED_WORKFLOW_MARKER) {
581
+ return /^# Deploy only the [a-z0-9-]+ Dharta agent\.$/.test(lines[1] ?? "") &&
582
+ /^# Agent directory: ".*"\.$/.test(lines[2] ?? "");
583
+ }
584
+ // Compatibility for the per-agent workflow format emitted immediately
585
+ // before the explicit ownership marker existed. Its exact first two lines,
586
+ // pinned-method comment, and deploy command form a narrow signature; a
587
+ // custom workflow that merely mentions the old generator phrase is not
588
+ // adopted or overwritten.
589
+ return /^# Deploy only the [a-z0-9-]+ Dharta agent\. Generated by dharta setup\.$/.test(lines[0] ?? "") &&
590
+ /^# Agent directory: ".*"\.$/.test(lines[1] ?? "") &&
591
+ content.includes("# The command is pinned to the ") &&
592
+ /dharta deploy --mode (git|folder) --dir /.test(content);
593
+ }
594
+ // Validate an existing directory without following any repo-relative symlink.
595
+ // We deliberately start at the setup/repo boundary instead of walking system
596
+ // ancestors: platform paths such as /var may themselves be legitimate OS
597
+ // links, while every component controlled by the working tree must be real.
598
+ async function validateExistingDirectoryTree(root, target, label) {
599
+ const absoluteRoot = path.resolve(root);
600
+ const absoluteTarget = path.resolve(target);
601
+ const rootStat = await fs.lstat(absoluteRoot);
602
+ if (rootStat.isSymbolicLink()) {
603
+ throw new Error(`${label}: '.' is a symbolic link. ` +
604
+ "Dharta setup does not follow links when selecting an agent folder.");
605
+ }
606
+ if (!rootStat.isDirectory())
607
+ throw new Error(`${label}: a parent path is not a directory.`);
608
+ // Check the caller-visible leaf before realpath so a selected agent symlink
609
+ // is rejected even when it resolves back inside the repo.
610
+ const targetLeaf = await fs.lstat(absoluteTarget);
611
+ if (targetLeaf.isSymbolicLink()) {
612
+ throw new Error(`${label}: '${path.basename(absoluteTarget)}' is a symbolic link. ` +
613
+ "Dharta setup does not follow links when selecting an agent folder.");
614
+ }
615
+ if (!targetLeaf.isDirectory())
616
+ throw new Error(`${label}: expected a directory, got a file.`);
617
+ const [rootReal, targetReal] = await Promise.all([fs.realpath(absoluteRoot), fs.realpath(absoluteTarget)]);
618
+ const realRelative = containedRelativePath(rootReal, targetReal);
619
+ if (realRelative === undefined) {
620
+ throw new Error(`${label}: the resolved path escapes the current setup root.`);
621
+ }
622
+ // Prefer the caller-visible path when it is lexically below the boundary so
623
+ // repo-controlled intermediate symlinks are observable. macOS commonly
624
+ // exposes the same safe temp directory through /var and /private/var; when
625
+ // those lexical aliases differ, walk the already-contained canonical path.
626
+ const lexicalRelative = containedRelativePath(absoluteRoot, absoluteTarget);
627
+ const walkRoot = lexicalRelative === undefined ? rootReal : absoluteRoot;
628
+ const relativeTarget = lexicalRelative ?? realRelative;
629
+ const components = relativeTarget === "" ? [] : relativeTarget.split(path.sep);
630
+ let current = walkRoot;
631
+ for (const component of components) {
632
+ current = path.join(current, component);
633
+ const stat = await fs.lstat(current);
634
+ const display = path.relative(walkRoot, current) || ".";
635
+ if (stat.isSymbolicLink()) {
636
+ throw new Error(`${label}: '${display}' is a symbolic link. ` +
637
+ "Dharta setup does not follow links when selecting an agent folder.");
638
+ }
639
+ if (!stat.isDirectory()) {
640
+ throw new Error(`${label}: a parent path is not a directory.`);
641
+ }
642
+ }
643
+ }
644
+ // Validate (and optionally create) every parent below a trusted root. lstat is
645
+ // intentional: stat/readFile/writeFile would follow a final symlink. A realpath
646
+ // containment check provides a second invariant for platforms with links in
647
+ // unusual directory layouts.
648
+ async function validateSafeWriteTarget(root, destination, label, createParents) {
649
+ const absoluteRoot = path.resolve(root);
650
+ const absoluteDestination = path.resolve(destination);
651
+ const relativeDestination = containedRelativePath(absoluteRoot, absoluteDestination);
652
+ if (relativeDestination === undefined || relativeDestination === "") {
653
+ throw new Error(`Cannot write ${label}: destination is outside its intended root.`);
654
+ }
655
+ await validateExistingDirectoryTree(absoluteRoot, absoluteRoot, `Cannot write ${label}`);
656
+ const rootReal = await fs.realpath(absoluteRoot);
657
+ const parent = path.dirname(absoluteDestination);
658
+ const relativeParent = path.relative(absoluteRoot, parent);
659
+ const components = relativeParent === "" ? [] : relativeParent.split(path.sep);
660
+ let current = absoluteRoot;
661
+ let parentMissing = false;
662
+ for (const component of components) {
663
+ current = path.join(current, component);
664
+ let stat;
665
+ try {
666
+ stat = await fs.lstat(current);
667
+ }
668
+ catch (error) {
669
+ if (error.code !== "ENOENT")
670
+ throw error;
671
+ if (!createParents) {
672
+ parentMissing = true;
673
+ break;
674
+ }
675
+ try {
676
+ await fs.mkdir(current);
677
+ }
678
+ catch (mkdirError) {
679
+ if (mkdirError.code !== "EEXIST")
680
+ throw mkdirError;
681
+ }
682
+ stat = await fs.lstat(current);
683
+ }
684
+ const display = path.relative(absoluteRoot, current) || ".";
685
+ if (stat.isSymbolicLink()) {
686
+ throw new Error(`Cannot write ${label}: parent '${display}' is a symbolic link. ` +
687
+ "Dharta setup refuses to follow links for generated files.");
688
+ }
689
+ if (!stat.isDirectory()) {
690
+ throw new Error(`Cannot write ${label}: a parent path is not a directory.`);
691
+ }
692
+ const currentReal = await fs.realpath(current);
693
+ if (containedRelativePath(rootReal, currentReal) === undefined) {
694
+ throw new Error(`Cannot write ${label}: parent '${display}' resolves outside its intended root.`);
695
+ }
696
+ }
697
+ if (parentMissing)
698
+ return { rootReal };
699
+ let existing;
700
+ try {
701
+ existing = await fs.lstat(absoluteDestination);
702
+ }
703
+ catch (error) {
704
+ const code = error.code;
705
+ if (code === "ENOENT")
706
+ return { rootReal };
707
+ if (code === "ENOTDIR") {
708
+ throw new Error(`Cannot write ${label}: a parent path is not a directory.`);
709
+ }
710
+ throw error;
711
+ }
712
+ if (existing.isSymbolicLink()) {
713
+ throw new Error(`Cannot write ${label}: the destination is a symbolic link. ` +
714
+ "Dharta setup refuses to follow links for generated files.");
715
+ }
716
+ if (existing.isDirectory())
717
+ throw new Error(`Cannot write ${label}: expected a file, got a directory.`);
718
+ if (!existing.isFile())
719
+ throw new Error(`Cannot write ${label}: expected a regular file.`);
720
+ const destinationReal = await fs.realpath(absoluteDestination);
721
+ if (containedRelativePath(rootReal, destinationReal) === undefined) {
722
+ throw new Error(`Cannot write ${label}: destination resolves outside its intended root.`);
723
+ }
724
+ return { rootReal, existing };
725
+ }
726
+ // Write through a fresh same-directory file and atomically replace the final
727
+ // path. O_NOFOLLOW protects the temporary leaf; lstat rejects a linked final
728
+ // leaf; rename replaces rather than dereferences a link introduced after the
729
+ // check. This also avoids leaving half a manifest/workflow on interruption.
730
+ async function safeAtomicWriteFile(root, destination, label, content) {
731
+ const prepared = await validateSafeWriteTarget(root, destination, label, true);
732
+ const parent = path.dirname(destination);
733
+ const temporary = path.join(parent, `.${path.basename(destination)}.karta-${process.pid}-${randomBytes(12).toString("hex")}.tmp`);
734
+ const noFollow = constants.O_NOFOLLOW ?? 0;
735
+ const flags = constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | noFollow;
736
+ const finalMode = prepared.existing ? prepared.existing.mode & 0o777 : 0o644;
737
+ let handle;
738
+ let renamed = false;
739
+ try {
740
+ handle = await fs.open(temporary, flags, 0o600);
741
+ const opened = await handle.stat();
742
+ const temporaryReal = await fs.realpath(temporary);
743
+ if (containedRelativePath(prepared.rootReal, temporaryReal) === undefined) {
744
+ throw new Error(`Cannot write ${label}: temporary file escaped its intended root.`);
745
+ }
746
+ await handle.writeFile(content, "utf8");
747
+ await handle.sync();
748
+ await handle.chmod(finalMode);
749
+ await handle.close();
750
+ handle = undefined;
751
+ // Re-check after writing, immediately before the atomic replacement. The
752
+ // inode comparison ensures the lexical temporary path still names the file
753
+ // descriptor we created rather than a swapped link/file.
754
+ await validateSafeWriteTarget(root, destination, label, true);
755
+ const tempStat = await fs.lstat(temporary);
756
+ if (!tempStat.isFile() || tempStat.isSymbolicLink() ||
757
+ tempStat.dev !== opened.dev || tempStat.ino !== opened.ino) {
758
+ throw new Error(`Cannot write ${label}: temporary file changed during setup.`);
759
+ }
760
+ const checkedTemporaryReal = await fs.realpath(temporary);
761
+ if (containedRelativePath(prepared.rootReal, checkedTemporaryReal) === undefined) {
762
+ throw new Error(`Cannot write ${label}: temporary file escaped its intended root.`);
763
+ }
764
+ await fs.rename(temporary, destination);
765
+ renamed = true;
766
+ }
767
+ catch (error) {
768
+ if (error instanceof Error && error.message.startsWith(`Cannot write ${label}:`))
769
+ throw error;
770
+ throw new Error(`Cannot write ${label}: ${error.message}`);
771
+ }
772
+ finally {
773
+ await handle?.close().catch(() => { });
774
+ if (!renamed)
775
+ await fs.unlink(temporary).catch(() => { });
776
+ }
777
+ }
778
+ async function lstatIfPresent(file) {
779
+ try {
780
+ return await fs.lstat(file);
781
+ }
782
+ catch (error) {
783
+ if (error.code === "ENOENT")
784
+ return undefined;
785
+ throw error;
786
+ }
787
+ }
788
+ function containedRelativePath(root, candidate) {
789
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
790
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) {
791
+ return undefined;
792
+ }
793
+ return relative;
794
+ }
795
+ async function rejectGitignoredSetupFile(git, repoRoot, destination, label) {
796
+ const relative = containedRelativePath(repoRoot, destination);
797
+ if (relative === undefined || relative === "") {
798
+ throw new Error(`Cannot check ${label}: path is outside the git repo.`);
799
+ }
800
+ const gitPath = relative.split(path.sep).join("/");
801
+ // Do not pass --no-index: an already tracked generated file remains part of
802
+ // the repo even if a later ignore rule matches it. New/untracked generated
803
+ // files are the ones setup must reject as impossible to push.
804
+ const result = await git(["-C", repoRoot, "check-ignore", "-q", "--", gitPath]);
805
+ if (result.code === 1)
806
+ return;
807
+ if (result.code !== 0) {
808
+ throw new Error(`git could not check ignore rules for ${gitPath}: ${result.stderr.trim() || `exit ${result.code}`}`);
809
+ }
810
+ throw new Error(`Cannot configure ${label}: ${gitPath} is excluded by .gitignore. ` +
811
+ "Unignore it, then rerun setup; Dharta did not write the generated file or report it ready to push.");
812
+ }
813
+ function renderDeployWorkflow(target) {
814
+ const pushPaths = githubPushPaths(target);
815
+ const scope = target.method === "folder" ? "releases:write" : "source_repos:write";
816
+ const reconcileSource = target.method === "git" ? " --reconcile-source" : "";
817
+ const command = `dharta deploy --mode ${target.method}${reconcileSource} --dir ${shellQuote(target.agentDir)}`;
818
+ const concurrency = target.method === "git"
819
+ ? "dharta-git-deploy-${{ github.repository }}"
820
+ : `dharta-deploy-${target.slug}`;
821
+ return [
822
+ MANAGED_WORKFLOW_MARKER,
823
+ `# Deploy only the ${target.slug} Dharta agent.`,
824
+ `# Agent directory: ${JSON.stringify(target.agentDir)}.`,
825
+ "#",
826
+ `# Authentication comes from this agent's ${target.secretName} repo secret.`,
827
+ `# The command is pinned to the ${target.method} method used to render this workflow.`,
828
+ ...(target.method === "git" ? [
829
+ "# --reconcile-source is safe here only because the branch-tip guard below rejects stale runs.",
830
+ ] : []),
831
+ "# To change deploy_method, update dharta.toml by rerunning setup, regenerate this workflow,",
832
+ "# and rotate the secret so its operation scope matches the new method:",
833
+ `# rerun: dharta setup ${shellQuote(target.agentDir)} --ci github`,
834
+ "#",
835
+ "# Required repo secret:",
836
+ `# ${target.secretName} a dh_live_... key with \`${scope}\`:`,
837
+ `# dharta keys create ci-${target.slug} --scope ${scope}`,
838
+ "# (requires an active key with keys:write; dashboard creation also works)",
839
+ "# Optional repo variable:",
840
+ "# DHARTA_BASE_URL control-plane URL (defaults to https://dharta.ai).",
841
+ "",
842
+ `name: Deploy ${target.slug} to Dharta`,
843
+ "",
844
+ "on:",
845
+ " push:",
846
+ ` branches: [${JSON.stringify(githubLiteralPath(target.branch))}]`,
847
+ " paths:",
848
+ ...pushPaths.map((triggerPath) => ` - ${JSON.stringify(triggerPath)}`),
849
+ " workflow_dispatch:",
850
+ "",
851
+ "permissions:",
852
+ " contents: read",
853
+ "",
854
+ "concurrency:",
855
+ ` group: ${concurrency}`,
856
+ " queue: max",
857
+ " cancel-in-progress: false",
858
+ "",
859
+ "jobs:",
860
+ " deploy:",
861
+ " runs-on: ubuntu-latest",
862
+ " steps:",
863
+ " - uses: actions/checkout@v4",
864
+ ...(target.method === "git" ? [" with:", " fetch-depth: 0"] : []),
865
+ " - uses: actions/setup-node@v4",
866
+ ' with:',
867
+ ' node-version: "20"',
868
+ " - name: Install Dharta CLI",
869
+ ` run: npm install --global @dharta/cli@${CLI_VERSION}`,
870
+ " - name: Check current branch tip",
871
+ " id: freshness",
872
+ " env:",
873
+ ` DHARTA_DEPLOY_BRANCH: ${JSON.stringify(target.branch)}`,
874
+ " run: |",
875
+ " git fetch --no-tags --force origin \"refs/heads/${DHARTA_DEPLOY_BRANCH}:refs/remotes/origin/dharta-deploy-tip\"",
876
+ " tip=\"$(git rev-parse refs/remotes/origin/dharta-deploy-tip)\"",
877
+ " if [ \"$GITHUB_SHA\" = \"$tip\" ]; then",
878
+ " echo \"deploy=true\" >> \"$GITHUB_OUTPUT\"",
879
+ " else",
880
+ ` echo "Skipping stale ${target.method} deploy: workflow SHA $GITHUB_SHA is not current branch tip $tip."`,
881
+ " echo \"deploy=false\" >> \"$GITHUB_OUTPUT\"",
882
+ " fi",
883
+ " - name: Deploy",
884
+ " if: steps.freshness.outputs.deploy == 'true'",
885
+ " env:",
886
+ ` DHARTA_API_KEY: \${{ secrets.${target.secretName} }}`,
887
+ " DHARTA_BASE_URL: ${{ vars.DHARTA_BASE_URL || 'https://dharta.ai' }}",
888
+ ` run: ${JSON.stringify(command)}`,
889
+ "",
890
+ ].join("\n");
891
+ }
892
+ function githubPushPaths(target) {
893
+ // A root agent's `**` covers its own .gitignore and generated workflow.
894
+ if (target.agentDir === ".")
895
+ return ["**"];
896
+ const paths = new Set([`${githubLiteralPath(target.agentDir)}/**`]);
897
+ const parts = target.agentDir.split("/");
898
+ // Folder packaging applies ignore rules from the checkout root through the
899
+ // selected folder. Include every *potential* ancestor .gitignore, even when
900
+ // it does not exist yet, so adding one cannot silently change the package
901
+ // without triggering CI. The selected folder's own .gitignore is already
902
+ // covered by the subtree glob, hence the strict `< parts.length` bound.
903
+ for (let depth = 0; depth < parts.length; depth += 1) {
904
+ const prefix = parts.slice(0, depth).join("/");
905
+ const ignorePath = prefix ? `${prefix}/.gitignore` : ".gitignore";
906
+ paths.add(githubLiteralPath(ignorePath));
907
+ }
908
+ // Ordinarily a generated-workflow edit should trigger its deploy. Avoid a
909
+ // duplicate when an unusually placed agent subtree already covers it.
910
+ if (!target.rel.startsWith(`${target.agentDir}/`)) {
911
+ paths.add(githubLiteralPath(target.rel));
912
+ }
913
+ return [...paths];
914
+ }
915
+ function githubLiteralPath(value) {
916
+ // GitHub `paths` values are glob patterns even when YAML-quoted. Escape
917
+ // metacharacters that are legal in git directory names so a literal agent
918
+ // path cannot broaden, negate, or otherwise change its workflow trigger.
919
+ return value.replace(/[\\*?+!\[\]]/g, "\\$&");
920
+ }
921
+ function shellQuote(value) {
922
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
923
+ }
924
+ function relLabel(dir) {
925
+ const rel = path.relative(process.cwd(), dir);
926
+ return rel === "" ? "./" : `./${rel}`;
927
+ }
928
+ // (ensureRemote lives on the deploy command; setup stays destination-
929
+ // agnostic and lets `dharta deploy` provision + wire the remote.)
930
+ function yesNo(answer, fallback) {
931
+ const a = answer.trim().toLowerCase();
932
+ if (a === "")
933
+ return fallback;
934
+ return a === "y" || a === "yes";
935
+ }
936
+ function yesNoCancel(answer) {
937
+ const a = answer.trim().toLowerCase();
938
+ if (a === "n" || a === "no")
939
+ return "no";
940
+ if (a === "c" || a === "cancel")
941
+ return "cancel";
942
+ return "yes"; // empty (the [Yes] default) or y/yes
943
+ }
944
+ async function run(git, args, label) {
945
+ const r = await git(args);
946
+ if (r.code !== 0)
947
+ throw new Error(`${label} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
948
+ }
949
+ //# sourceMappingURL=setup.js.map