@dev-mainsequence/codex-plugin 0.0.0-stage → 0.4.2

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 (57) hide show
  1. package/.codex-plugin/plugin.json +22 -0
  2. package/.mcp.json +16 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +13 -0
  5. package/README.md +155 -2
  6. package/assets/mainsequence-icon.png +0 -0
  7. package/assets/mainsequence-icon.svg +454 -0
  8. package/build-info.json +10 -0
  9. package/dist/hosts/codex.js +62 -0
  10. package/dist/hosts/codexPresentation.js +165 -0
  11. package/dist/hosts/discovery.js +138 -0
  12. package/dist/hosts/panel/panel.html +165 -0
  13. package/dist/package.json +1 -0
  14. package/dist/panel-server.js +135 -0
  15. package/dist/runner.js +71 -0
  16. package/dist/vendor/devtools/LICENSE +21 -0
  17. package/dist/vendor/devtools/NOTICE +12 -0
  18. package/dist/vendor/devtools/index.js +72 -0
  19. package/dist/vendor/devtools/package.json +5 -0
  20. package/dist/vendor/devtools/shared/accountScope.js +19 -0
  21. package/dist/vendor/devtools/shared/actions.js +164 -0
  22. package/dist/vendor/devtools/shared/agentReadiness.js +71 -0
  23. package/dist/vendor/devtools/shared/agents.js +240 -0
  24. package/dist/vendor/devtools/shared/arguments.js +142 -0
  25. package/dist/vendor/devtools/shared/cli.js +144 -0
  26. package/dist/vendor/devtools/shared/config.js +64 -0
  27. package/dist/vendor/devtools/shared/devcontainerJsonc.js +153 -0
  28. package/dist/vendor/devtools/shared/envfile.js +128 -0
  29. package/dist/vendor/devtools/shared/environments.js +38 -0
  30. package/dist/vendor/devtools/shared/hostSession.js +324 -0
  31. package/dist/vendor/devtools/shared/machineSession.js +338 -0
  32. package/dist/vendor/devtools/shared/mcp.js +421 -0
  33. package/dist/vendor/devtools/shared/mcpTransport.js +177 -0
  34. package/dist/vendor/devtools/shared/models.js +12 -0
  35. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +300 -0
  36. package/dist/vendor/devtools/shared/organizationProjects.js +134 -0
  37. package/dist/vendor/devtools/shared/process.js +61 -0
  38. package/dist/vendor/devtools/shared/projectOperations.js +279 -0
  39. package/dist/vendor/devtools/shared/projects.js +390 -0
  40. package/dist/vendor/devtools/shared/pythonProjectStatus.js +86 -0
  41. package/dist/vendor/devtools/shared/pythonSdkStatus.js +26 -0
  42. package/dist/vendor/devtools/shared/repositories.js +215 -0
  43. package/dist/vendor/devtools/shared/repositoryClient.js +108 -0
  44. package/dist/vendor/devtools/shared/repositoryIdentity.js +116 -0
  45. package/dist/vendor/devtools/shared/repositoryModels.js +72 -0
  46. package/dist/vendor/devtools/shared/session.js +532 -0
  47. package/docs/adr/0002-conversational-project-mcp-client.md +59 -0
  48. package/docs/agents-tools-panel.md +63 -0
  49. package/docs/codex-verification.md +161 -0
  50. package/docs/conversational-mcp.md +54 -0
  51. package/hooks/hooks.json +17 -0
  52. package/package.json +50 -4
  53. package/skills/mainsequence-mcp/SKILL.md +40 -0
  54. package/skills/ms/SKILL.md +8 -0
  55. package/skills/ms/agents/openai.yaml +7 -0
  56. package/skills/ms/assets/mainsequence-icon.png +0 -0
  57. package/skills/ms/assets/mainsequence-icon.svg +454 -0
@@ -0,0 +1,300 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readOrganizationProjectWorkspace = readOrganizationProjectWorkspace;
37
+ exports.setupOrganizationProject = setupOrganizationProject;
38
+ const fs = __importStar(require("node:fs/promises"));
39
+ const path = __importStar(require("node:path"));
40
+ const node_crypto_1 = require("node:crypto");
41
+ const models_1 = require("./models.js");
42
+ const organizationProjects_1 = require("./organizationProjects.js");
43
+ const repositories_1 = require("./repositories.js");
44
+ const projects_1 = require("./projects.js");
45
+ const repositoryIdentity_1 = require("./repositoryIdentity.js");
46
+ const session_1 = require("./session.js");
47
+ function workspaceName(name) {
48
+ // Claude displays the folder basename. Keep the actual name, including case and spaces.
49
+ if (!name || name.trim() !== name || /[<>:"/\\|?*\x00-\x1f]/.test(name) || /[. ]$/.test(name)
50
+ || name === '.' || name === '..' || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name)
51
+ || Buffer.byteLength(name) > 200 || ['repositories', '.mainsequence-project.json'].includes(name.toLowerCase())) {
52
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The project name must be usable as a local folder name. Rename the project before setting it up.');
53
+ }
54
+ return name;
55
+ }
56
+ async function readManifest(file) {
57
+ const stat = await fs.lstat(file);
58
+ if (!stat.isFile() || stat.isSymbolicLink())
59
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The project workspace manifest must be an ordinary file.');
60
+ try {
61
+ return JSON.parse(await fs.readFile(file, 'utf8'));
62
+ }
63
+ catch {
64
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The project workspace manifest is unreadable.');
65
+ }
66
+ }
67
+ /** Read prepared membership, not a claim that any running Claude session has attached it. */
68
+ async function readOrganizationProjectWorkspace(root) {
69
+ if (!path.isAbsolute(root))
70
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Choose an absolute project workspace folder.');
71
+ root = path.resolve(root);
72
+ await ordinaryDirectory(root);
73
+ const manifest = await readManifest(path.join(root, '.mainsequence-project.json'));
74
+ const container = path.dirname(root);
75
+ const canonical = await readManifest(path.join(container, '.mainsequence-project.json'));
76
+ if (manifest.schemaVersion !== 2 || manifest.status !== 'ready' || manifest.root !== root
77
+ || path.basename(root) !== workspaceName(manifest.name) || JSON.stringify(manifest) !== JSON.stringify(canonical)
78
+ || !Array.isArray(manifest.repositories) || !manifest.repositories.length) {
79
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Set up this project again before opening its workspace.');
80
+ }
81
+ (0, organizationProjects_1.projectUid)(manifest.projectUid);
82
+ (0, organizationProjects_1.projectUid)(manifest.environmentUid);
83
+ (0, session_1.requireBackend)(manifest.backend);
84
+ const paths = new Set(), branches = new Set();
85
+ for (const row of manifest.repositories) {
86
+ (0, organizationProjects_1.projectUid)(row.repositoryUid);
87
+ (0, organizationProjects_1.projectUid)(row.branchUid);
88
+ const relative = typeof row.path === 'string' ? path.relative(path.join(container, 'repositories'), row.path) : '';
89
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
90
+ || !path.isAbsolute(row.path) || paths.has(row.path) || branches.has(row.branchUid)
91
+ || typeof row.branch !== 'string' || !row.branch || /[\r\n\0]/.test(row.branch)) {
92
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The project repository membership is invalid. Set up this project again.');
93
+ }
94
+ paths.add(row.path);
95
+ branches.add(row.branchUid);
96
+ await ordinaryDirectory(row.path);
97
+ await ordinaryDirectory(path.join(row.path, '.git'));
98
+ if (!await exists(path.join(row.path, '.git')))
99
+ throw new models_1.ToolingError('INVALID_REQUEST', 'A project checkout is missing. Set up this project again.');
100
+ }
101
+ const url = new URL('claude://code/new');
102
+ url.searchParams.set('folder', root);
103
+ return { root, name: manifest.name, backend: manifest.backend, projectUid: manifest.projectUid,
104
+ environmentUid: manifest.environmentUid, repositories: manifest.repositories, claudeUrl: url.href };
105
+ }
106
+ async function exists(file) { return fs.lstat(file).then(() => true).catch((error) => { if (error.code === 'ENOENT')
107
+ return false; throw error; }); }
108
+ async function ordinaryDirectory(directory, create = false) {
109
+ const resolved = path.resolve(directory), parent = path.dirname(resolved);
110
+ if (parent !== resolved)
111
+ await ordinaryDirectory(parent, create);
112
+ const stat = await fs.lstat(resolved).catch((error) => { if (error.code === 'ENOENT')
113
+ return undefined; throw error; });
114
+ if (stat && (!stat.isDirectory() || stat.isSymbolicLink()))
115
+ throw new models_1.ToolingError('INVALID_REQUEST', `Use ordinary directories for the project workspace: ${resolved}`);
116
+ if (!stat && create) {
117
+ await fs.mkdir(resolved).catch((error) => { if (error.code !== 'EEXIST')
118
+ throw error; });
119
+ await ordinaryDirectory(resolved);
120
+ }
121
+ }
122
+ async function atomicJson(file, value) {
123
+ const temp = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
124
+ try {
125
+ await fs.writeFile(temp, JSON.stringify(value, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
126
+ await fs.rename(temp, file);
127
+ }
128
+ finally {
129
+ await fs.rm(temp, { force: true });
130
+ }
131
+ }
132
+ async function namedDirectory(parent, name, uid) {
133
+ await ordinaryDirectory(parent);
134
+ const names = await exists(parent) ? (await fs.readdir(parent)).filter(value => value === uid || value.endsWith(`-${uid}`)) : [];
135
+ if (names.length > 1)
136
+ throw new models_1.ToolingError('INVALID_REQUEST', `Multiple workspace folders match ${uid}. Resolve the duplicate folders first.`);
137
+ return path.join(parent, names[0] || `${(0, repositories_1.safeSlug)(name)}-${uid}`);
138
+ }
139
+ /** Stable, independent branch checkouts. No reset, stash, force checkout, deletion or automatic pull. */
140
+ async function setupOrganizationProject(input, options) {
141
+ const { session, signal, state } = options;
142
+ const backend = (0, session_1.requireBackend)(input.backend), environmentUid = (0, organizationProjects_1.projectUid)(input.environmentUid), uid = (0, organizationProjects_1.projectUid)(input.projectUid);
143
+ const env = (0, projects_1.projectEnvironment)({ backend }, options.env);
144
+ // Apply the same environment to nested Git helpers, including context and .env setup.
145
+ for (const key of Object.keys(env))
146
+ if (key.startsWith('GIT_') && key !== 'GIT_SSH_COMMAND')
147
+ delete env[key];
148
+ const host = { ...options.host, runProcess: spec => options.host.runProcess({ ...spec, env: spec.env ?? env }) };
149
+ if (typeof input.baseDir !== 'string' || !path.isAbsolute(input.baseDir) || /[\r\n\0]/.test(input.baseDir))
150
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Choose an absolute repositories base folder in General.');
151
+ // Resolve the base once (macOS /tmp and home aliases are legitimate). Children must be ordinary directories.
152
+ let base = path.resolve(input.baseDir);
153
+ if (await exists(base))
154
+ base = await fs.realpath(base);
155
+ const client = new organizationProjects_1.OrganizationProjectClient(session, backend, signal);
156
+ const { project, branches } = await client.getProject(environmentUid, uid);
157
+ const displayName = workspaceName(project.name);
158
+ if (!branches.length)
159
+ throw new models_1.ToolingError('INVALID_REQUEST', 'This project has no accessible branches to set up.');
160
+ if (branches.length !== project.code_repository_branch_uids.length)
161
+ throw new models_1.ToolingError('API_ERROR', 'Project membership changed. Refresh it before setting up locally.');
162
+ const infos = [];
163
+ for (const branch of branches) {
164
+ signal?.throwIfAborted();
165
+ const info = await (0, repositories_1.repositorySetupInfo)(session, backend, branch.code_repository_uid, branch.repository_branch);
166
+ if (info.branch.uid !== branch.uid)
167
+ throw new models_1.ToolingError('API_ERROR', 'The selected project branch identity changed.');
168
+ infos.push(info);
169
+ }
170
+ const org = infos[0].orgSlug;
171
+ if (infos.some(info => info.orgSlug !== org))
172
+ throw new models_1.ToolingError('API_ERROR', 'The project repositories belong to different organizations.');
173
+ // The UID identifies the storage container, never the folder opened/displayed by Claude.
174
+ // Repositories are siblings of the named workspace: /add-dir must register each one,
175
+ // instead of treating nested checkouts as already covered by the working directory.
176
+ const container = await namedDirectory(path.join(base, org, 'organization-projects', (0, node_crypto_1.createHash)('sha256').update(backend).digest('hex').slice(0, 16)), project.name, uid);
177
+ const root = path.join(container, displayName);
178
+ const manifestPath = path.join(container, '.mainsequence-project.json');
179
+ await ordinaryDirectory(container);
180
+ let prior;
181
+ if (await exists(manifestPath)) {
182
+ prior = await readManifest(manifestPath);
183
+ if (![1, 2].includes(prior.schemaVersion) || prior.backend !== backend || prior.projectUid !== uid || prior.environmentUid !== environmentUid)
184
+ throw new models_1.ToolingError('INVALID_REQUEST', 'This workspace belongs to another project or backend.');
185
+ }
186
+ else if (await exists(container) && (await fs.readdir(container)).length)
187
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The workspace folder already contains unmanaged files. Choose another base folder.');
188
+ await ordinaryDirectory(root);
189
+ if (await exists(root) && (await fs.readdir(root)).length && prior?.root !== root)
190
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The named workspace folder already contains files. Resolve the folder collision before retrying.');
191
+ const run = async (cwd, args, mutation = false) => {
192
+ signal?.throwIfAborted();
193
+ if (mutation)
194
+ state.mutationStarted = true;
195
+ const result = await host.runProcess({ executable: 'git', args, cwd, env, signal, timeoutMs: 120_000 });
196
+ if (result.cancelled)
197
+ throw new models_1.ToolingError('CANCELLED', 'Project setup was cancelled. Completed checkouts are retained.');
198
+ if (result.exitCode !== 0)
199
+ throw new models_1.ToolingError('PROCESS_FAILED', `Git could not ${args[0]} in ${cwd}. Existing files were preserved; inspect the checkout before retrying.`);
200
+ return result.stdout.trim();
201
+ };
202
+ const repositories = [];
203
+ for (const info of infos) {
204
+ const branch = info.branch;
205
+ let sourcePath = (0, repositories_1.canonicalRepositoryDir)(base, org, info.repository.uid, info.repository.code_repository_name);
206
+ const sourceParent = path.dirname(sourcePath);
207
+ await ordinaryDirectory(sourceParent);
208
+ if (!await exists(sourcePath) && await exists(sourceParent)) {
209
+ const matches = (await fs.readdir(sourceParent)).filter(name => name.endsWith(`-${info.repository.uid}`));
210
+ if (matches.length > 1)
211
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Multiple local repositories match this repository UID. Resolve the duplicate folders first.');
212
+ if (matches.length === 1)
213
+ sourcePath = path.join(sourceParent, matches[0]);
214
+ }
215
+ const repositoryFolder = await namedDirectory(path.join(container, 'repositories'), info.repository.code_repository_name, info.repository.uid);
216
+ const target = await namedDirectory(repositoryFolder, branch.repository_branch, branch.uid);
217
+ for (const candidate of [sourcePath, target]) {
218
+ await ordinaryDirectory(candidate);
219
+ if (!await exists(candidate))
220
+ continue;
221
+ const gitDir = path.join(candidate, '.git');
222
+ if (!await exists(gitDir) || (await fs.lstat(gitDir)).isSymbolicLink() || !(await fs.lstat(gitDir)).isDirectory())
223
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Project setup requires standalone repository checkouts.');
224
+ const origin = await run(candidate, ['remote', 'get-url', 'origin']);
225
+ if ((0, repositoryIdentity_1.repositorySshKeyIdentity)(origin).identity !== (0, repositoryIdentity_1.repositorySshKeyIdentity)(info.binding.git_ssh_url || '').identity)
226
+ throw new models_1.ToolingError('INVALID_GIT_CONTEXT', `The checkout origin does not match ${info.repository.code_repository_name}.`);
227
+ if (candidate === target && await run(candidate, ['symbolic-ref', '--short', 'HEAD']) !== branch.repository_branch && await run(candidate, ['status', '--porcelain']))
228
+ throw new models_1.ToolingError('INVALID_REQUEST', `Commit or stash changes in ${candidate} before switching its branch.`);
229
+ }
230
+ repositories.push({ repositoryUid: info.repository.uid, branchUid: branch.uid, name: info.repository.code_repository_name, branch: branch.repository_branch, path: target, sourcePath });
231
+ }
232
+ // Phase 1: ensure the ordinary repository exists, without switching existing source checkouts.
233
+ for (const row of repositories) {
234
+ if (!await exists(row.sourcePath)) {
235
+ host.progress?.(`Preparing repository ${row.name}.`);
236
+ await (0, projects_1.setupRepository)(host, session, { backend, repositoryUid: row.repositoryUid, branch: row.branch, baseDir: base }, { env, signal, state });
237
+ }
238
+ }
239
+ signal?.throwIfAborted();
240
+ state.mutationStarted = true;
241
+ await ordinaryDirectory(container, true);
242
+ return (0, projects_1.withProjectLock)(container, async () => {
243
+ if (prior?.schemaVersion === 2 && prior.root !== root) {
244
+ const previousRoot = path.join(container, workspaceName(prior.name));
245
+ if (prior.root !== previousRoot)
246
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The previous workspace location is invalid.');
247
+ await ordinaryDirectory(previousRoot);
248
+ if (await exists(previousRoot)) {
249
+ if (await exists(root))
250
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The renamed workspace folder already exists.');
251
+ await fs.rename(previousRoot, root);
252
+ }
253
+ }
254
+ await ordinaryDirectory(root, true);
255
+ const manifest = { schemaVersion: 2, backend, environmentUid, projectUid: uid, name: project.name, root, repositories, status: 'preparing' };
256
+ // Phase 2: persist the stable Claude workspace before preparing its individual checkouts.
257
+ await atomicJson(manifestPath, manifest);
258
+ await atomicJson(path.join(root, '.mainsequence-project.json'), manifest);
259
+ // Phase 3: each membership gets its own attached branch, including several branches of one repository.
260
+ for (const row of repositories) {
261
+ signal?.throwIfAborted();
262
+ host.progress?.(`Preparing ${row.name} on ${row.branch}.`);
263
+ if (!await exists(row.path)) {
264
+ await (0, projects_1.setupRepository)(host, session, { backend, repositoryUid: row.repositoryUid, branch: row.branch, baseDir: container }, { env, signal, state, destination: row.path, reference: row.sourcePath });
265
+ }
266
+ else {
267
+ const context = await (0, repositories_1.projectContext)(host, row.path, backend, undefined, env, signal);
268
+ await (0, projects_1.withProjectLock)(context.gitCommonDir, async () => {
269
+ if (context.branch === row.branch)
270
+ return;
271
+ if (await run(row.path, ['status', '--porcelain']))
272
+ throw new models_1.ToolingError('INVALID_REQUEST', `Commit or stash changes in ${row.path} before switching its branch.`);
273
+ await run(row.path, ['check-ref-format', '--branch', row.branch]);
274
+ await run(row.path, ['fetch', '--', 'origin', `refs/heads/${row.branch}:refs/remotes/origin/${row.branch}`], true);
275
+ const local = await host.runProcess({ executable: 'git', args: ['show-ref', '--verify', '--quiet', `refs/heads/${row.branch}`], cwd: row.path, env, signal });
276
+ if (local.cancelled)
277
+ throw new models_1.ToolingError('CANCELLED', 'Project setup was cancelled.');
278
+ if (![0, 1].includes(local.exitCode ?? -1))
279
+ throw new models_1.ToolingError('INVALID_GIT_CONTEXT', 'Could not inspect the local branch.');
280
+ await run(row.path, local.exitCode === 0 ? ['switch', '--no-guess', '--', row.branch] : ['switch', '--create', row.branch, '--track', `origin/${row.branch}`], true);
281
+ });
282
+ }
283
+ if (await run(row.path, ['symbolic-ref', '--short', 'HEAD']) !== row.branch)
284
+ throw new models_1.ToolingError('INVALID_GIT_CONTEXT', 'The checkout did not select the project branch.');
285
+ }
286
+ const instructions = path.join(root, 'CLAUDE.md');
287
+ if (!await exists(instructions)) {
288
+ const legacyInstructions = path.join(container, 'CLAUDE.md');
289
+ const content = prior?.schemaVersion === 1 && await exists(legacyInstructions)
290
+ ? await fs.readFile(legacyInstructions, 'utf8')
291
+ : '# MainSequence workspace\n\nThe project repositories and their assigned branches are listed in `.mainsequence-project.json`.\nThe MainSequence plugin adds each repository to this Claude session with `/add-dir`.\nWork inside the appropriate repository directory; each checkout has its own Git history and instructions.\n';
292
+ await fs.writeFile(instructions, content, { flag: 'wx' });
293
+ }
294
+ await atomicJson(manifestPath, { ...manifest, status: 'ready' });
295
+ await atomicJson(path.join(root, '.mainsequence-project.json'), { ...manifest, status: 'ready' });
296
+ const claudeUrl = new URL('claude://code/new');
297
+ claudeUrl.searchParams.set('folder', root);
298
+ return { root, name: project.name, projectUid: uid, environmentUid, repositories, claudeUrl: claudeUrl.href };
299
+ });
300
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrganizationProjectClient = void 0;
4
+ exports.projectUid = projectUid;
5
+ const models_1 = require("./models.js");
6
+ const session_1 = require("./session.js");
7
+ const repositoryModels_1 = require("./repositoryModels.js");
8
+ const repositoryClient_1 = require("./repositoryClient.js");
9
+ const environments_1 = require("./environments.js");
10
+ function projectUid(value, label = 'UID') {
11
+ if (typeof value !== 'string' || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value))
12
+ throw new models_1.ToolingError('INVALID_REQUEST', `${label} must be a UUID.`);
13
+ return value.toLowerCase();
14
+ }
15
+ function object(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ throw new models_1.ToolingError('API_ERROR', 'The project service returned an invalid object.');
18
+ return value;
19
+ }
20
+ function line(value, label) {
21
+ if (typeof value !== 'string' || !value.trim() || /[\r\n\0]/.test(value))
22
+ throw new models_1.ToolingError('API_ERROR', `Invalid ${label}.`);
23
+ return value;
24
+ }
25
+ function project(value, environmentUid) {
26
+ const row = object(value);
27
+ if (projectUid(row.organization_environment_uid) !== environmentUid || !Array.isArray(row.code_repository_branch_uids) || typeof row.description !== 'string')
28
+ throw new models_1.ToolingError('API_ERROR', 'The project response does not match the selected environment.');
29
+ return { uid: projectUid(row.uid), organization_environment_uid: environmentUid, name: line(row.name, 'project name'), description: row.description,
30
+ code_repository_branch_uids: [...new Set(row.code_repository_branch_uids.map((uid) => projectUid(uid, 'Branch UID')))] };
31
+ }
32
+ function pageValues(value) {
33
+ const payload = object(value);
34
+ if (!Array.isArray(payload.results) || !Number.isSafeInteger(payload.count) || payload.count < 0)
35
+ throw new models_1.ToolingError('API_ERROR', 'The project service returned an invalid page.');
36
+ return { results: payload.results, count: payload.count };
37
+ }
38
+ function webUrl(value) {
39
+ let url;
40
+ try {
41
+ url = new URL(value);
42
+ }
43
+ catch {
44
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Enter an absolute Command Center or repository web URL.');
45
+ }
46
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password)
47
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Links must use HTTP(S) without embedded credentials.');
48
+ return url;
49
+ }
50
+ /** Human desktop clients: exact environment scope, branch membership and permission-filtered data. */
51
+ class OrganizationProjectClient {
52
+ session;
53
+ signal;
54
+ backend;
55
+ constructor(session, backend, signal) {
56
+ this.session = session;
57
+ this.signal = signal;
58
+ this.backend = (0, session_1.requireBackend)(backend);
59
+ }
60
+ async get(apiPath) { this.signal?.throwIfAborted(); return this.session.request(this.backend, 'GET', apiPath); }
61
+ async environments() {
62
+ return (await (0, environments_1.listEnvironments)(this.session, this.backend, this.signal)).map(({ uid, name }) => ({ uid, name }));
63
+ }
64
+ async list(environment, options = {}) {
65
+ const environmentUid = projectUid(environment, 'Environment UID'), limit = options.limit ?? 25, offset = options.offset ?? 0;
66
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0 || (options.search !== undefined && typeof options.search !== 'string'))
67
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Invalid project pagination or search.');
68
+ const query = new URLSearchParams({ organization_environment_uid: environmentUid, limit: String(limit), offset: String(offset), ordering: 'name' });
69
+ if (options.search?.trim())
70
+ query.set('search', options.search.trim());
71
+ const payload = pageValues(await this.get(`/api/v1/organization-projects/?${query}`));
72
+ return { results: payload.results.map(row => project(row, environmentUid)), count: payload.count, limit, offset };
73
+ }
74
+ async detail(environment, uid) {
75
+ const environmentUid = projectUid(environment, 'Environment UID'), selectedUid = projectUid(uid, 'Project UID');
76
+ const selected = project(await this.get(`/api/v1/organization-projects/${selectedUid}/`), environmentUid);
77
+ if (selected.uid !== selectedUid)
78
+ throw new models_1.ToolingError('API_ERROR', 'The backend returned another project.');
79
+ return selected;
80
+ }
81
+ async branches(selected) {
82
+ const payload = await this.get(`/api/v1/organization-projects/${projectUid(selected.uid)}/code-repository-branches/`);
83
+ if (!Array.isArray(payload))
84
+ throw new models_1.ToolingError('API_ERROR', 'The project branch response must be an array.');
85
+ const seen = new Set();
86
+ return payload.map(value => {
87
+ const branch = (0, repositoryModels_1.codeRepositoryBranchFromResponse)(value, 'Project branch');
88
+ const uid = projectUid(branch.uid), environment = projectUid(value.organization_environment_uid);
89
+ if (seen.has(uid) || environment !== selected.organization_environment_uid || !selected.code_repository_branch_uids.includes(uid))
90
+ throw new models_1.ToolingError('API_ERROR', 'Project membership changed or contains an invalid branch. Refresh the project.');
91
+ seen.add(uid);
92
+ return { ...branch, uid, code_repository_uid: projectUid(branch.code_repository_uid), organization_environment_uid: environment, code_repository_type: line(value.code_repository_type, 'repository type') };
93
+ });
94
+ }
95
+ async getProject(environment, uid) { const selected = await this.detail(environment, uid); return { project: selected, branches: await this.branches(selected) }; }
96
+ async link(input) {
97
+ const selected = await this.detail(input.environmentUid, input.projectUid);
98
+ if (!['project', 'repository', 'branch'].includes(input.target))
99
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Choose a project, repository or branch link.');
100
+ const branch = input.target === 'project' ? undefined : (await this.branches(selected)).find(row => row.uid === projectUid(input.branchUid, 'Branch UID'));
101
+ if (input.target !== 'project' && !branch)
102
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The selected branch is no longer a member of this project.');
103
+ if (input.commandCenterUrl?.trim()) {
104
+ const url = webUrl(input.commandCenterUrl.trim());
105
+ url.pathname = `/app/main-sequence-foundry/${branch ? 'code-repositories' : 'projects'}`;
106
+ url.search = '';
107
+ url.hash = '';
108
+ if (branch) {
109
+ url.searchParams.set('msCodeRepositoryUid', branch.code_repository_uid);
110
+ if (input.target === 'branch')
111
+ url.searchParams.set('msCodeRepositoryBranchUid', branch.uid);
112
+ url.searchParams.set('msTab', 'code');
113
+ }
114
+ else
115
+ url.searchParams.set('msProjectUid', selected.uid);
116
+ return url.href;
117
+ }
118
+ if (!branch?.github_repository_binding_uid)
119
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Set the Command Center URL in General to open this resource.');
120
+ const binding = await new repositoryClient_1.RepositoryClient(this.session, this.backend).getGitHubRepositoryBinding(branch.github_repository_binding_uid);
121
+ if (binding.uid !== branch.github_repository_binding_uid || binding.code_repository_uid !== branch.code_repository_uid)
122
+ throw new models_1.ToolingError('API_ERROR', 'The repository link does not match the selected branch.');
123
+ if (!binding.git_repo_url)
124
+ throw new models_1.ToolingError('INVALID_REQUEST', 'No repository web link is available. Set the Command Center URL in General.');
125
+ const url = webUrl(binding.git_repo_url);
126
+ url.search = '';
127
+ url.hash = '';
128
+ url.pathname = url.pathname.replace(/\.git\/?$/, '').replace(/\/$/, '');
129
+ if (input.target === 'branch')
130
+ url.pathname += '/tree/' + encodeURIComponent(branch.repository_branch);
131
+ return url.href;
132
+ }
133
+ }
134
+ exports.OrganizationProjectClient = OrganizationProjectClient;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runProcess = runProcess;
4
+ exports.createNodeHost = createNodeHost;
5
+ const child_process_1 = require("child_process");
6
+ /** Never prints arguments, environment, or child output. Consumers receive private captured data. */
7
+ function runProcess(spec) {
8
+ if (spec.signal?.aborted)
9
+ return Promise.resolve({ exitCode: null, stdout: '', stderr: '', cancelled: true });
10
+ return new Promise(resolve => {
11
+ let stdout = '', stderr = '', cancelled = false, timedOut = false;
12
+ let killTimer;
13
+ let settled = false;
14
+ const child = (0, child_process_1.spawn)(spec.executable, spec.args, {
15
+ cwd: spec.cwd, env: spec.env ?? process.env,
16
+ shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe']
17
+ });
18
+ const kill = (signal) => {
19
+ try {
20
+ if (process.platform !== 'win32' && child.pid)
21
+ process.kill(-child.pid, signal);
22
+ else
23
+ child.kill(signal);
24
+ }
25
+ catch { /* exited */ }
26
+ };
27
+ const stop = () => {
28
+ kill('SIGTERM');
29
+ killTimer ??= setTimeout(() => kill('SIGKILL'), 1500);
30
+ };
31
+ const abort = () => { cancelled = true; stop(); };
32
+ spec.signal?.addEventListener('abort', abort, { once: true });
33
+ const timer = setTimeout(() => { timedOut = true; stop(); }, spec.timeoutMs ?? 600_000);
34
+ const finish = (exitCode, unavailable = false) => {
35
+ if (settled)
36
+ return;
37
+ settled = true;
38
+ clearTimeout(timer);
39
+ if (killTimer)
40
+ clearTimeout(killTimer);
41
+ spec.signal?.removeEventListener('abort', abort);
42
+ resolve({ exitCode, stdout, stderr, cancelled, timedOut, unavailable });
43
+ };
44
+ // Bounded private buffers; stdout never goes to hooks, progress, or ordinary logs.
45
+ child.stdout?.on('data', chunk => { stdout = (stdout + chunk.toString()).slice(-2_000_000); });
46
+ child.stderr?.on('data', chunk => { stderr = (stderr + chunk.toString()).slice(-2_000_000); });
47
+ child.once('error', () => finish(null, true));
48
+ child.once('close', code => finish(code));
49
+ });
50
+ }
51
+ function createNodeHost(progress) {
52
+ return {
53
+ runProcess, progress,
54
+ async openBrowser(url) {
55
+ const program = process.platform === 'darwin' ? '/usr/bin/open' : process.platform === 'win32' ? 'rundll32.exe' : 'xdg-open';
56
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url];
57
+ const result = await runProcess({ executable: program, args, cwd: process.cwd(), timeoutMs: 15_000 });
58
+ return result.exitCode === 0;
59
+ }
60
+ };
61
+ }