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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.codex-plugin/plugin.json +21 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +13 -0
  4. package/README.md +143 -2
  5. package/assets/mainsequence-icon.png +0 -0
  6. package/assets/mainsequence-icon.svg +454 -0
  7. package/build-info.json +10 -0
  8. package/dist/hosts/codex.js +62 -0
  9. package/dist/hosts/codexPresentation.js +164 -0
  10. package/dist/package.json +1 -0
  11. package/dist/runner.js +71 -0
  12. package/dist/vendor/devtools/LICENSE +21 -0
  13. package/dist/vendor/devtools/NOTICE +12 -0
  14. package/dist/vendor/devtools/index.js +68 -0
  15. package/dist/vendor/devtools/package.json +5 -0
  16. package/dist/vendor/devtools/shared/actions.js +156 -0
  17. package/dist/vendor/devtools/shared/agentReadiness.js +71 -0
  18. package/dist/vendor/devtools/shared/agents.js +236 -0
  19. package/dist/vendor/devtools/shared/arguments.js +142 -0
  20. package/dist/vendor/devtools/shared/cli.js +144 -0
  21. package/dist/vendor/devtools/shared/config.js +64 -0
  22. package/dist/vendor/devtools/shared/devcontainerJsonc.js +153 -0
  23. package/dist/vendor/devtools/shared/envfile.js +128 -0
  24. package/dist/vendor/devtools/shared/hostSession.js +324 -0
  25. package/dist/vendor/devtools/shared/machineSession.js +338 -0
  26. package/dist/vendor/devtools/shared/mcp.js +329 -0
  27. package/dist/vendor/devtools/shared/mcpTransport.js +164 -0
  28. package/dist/vendor/devtools/shared/models.js +12 -0
  29. package/dist/vendor/devtools/shared/organizationProjectWorkspace.js +300 -0
  30. package/dist/vendor/devtools/shared/organizationProjects.js +145 -0
  31. package/dist/vendor/devtools/shared/process.js +61 -0
  32. package/dist/vendor/devtools/shared/projectOperations.js +279 -0
  33. package/dist/vendor/devtools/shared/projects.js +390 -0
  34. package/dist/vendor/devtools/shared/pythonProjectStatus.js +86 -0
  35. package/dist/vendor/devtools/shared/pythonSdkStatus.js +26 -0
  36. package/dist/vendor/devtools/shared/repositories.js +215 -0
  37. package/dist/vendor/devtools/shared/repositoryClient.js +108 -0
  38. package/dist/vendor/devtools/shared/repositoryIdentity.js +116 -0
  39. package/dist/vendor/devtools/shared/repositoryModels.js +72 -0
  40. package/dist/vendor/devtools/shared/session.js +444 -0
  41. package/docs/adr/0002-conversational-project-mcp-client.md +59 -0
  42. package/docs/codex-verification.md +124 -0
  43. package/docs/conversational-mcp.md +54 -0
  44. package/hooks/hooks.json +17 -0
  45. package/package.json +46 -4
  46. package/skills/mainsequence-mcp/SKILL.md +40 -0
  47. package/skills/ms/SKILL.md +8 -0
  48. package/skills/ms/agents/openai.yaml +7 -0
  49. package/skills/ms/assets/mainsequence-icon.png +0 -0
  50. package/skills/ms/assets/mainsequence-icon.svg +454 -0
@@ -0,0 +1,390 @@
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.DRY_RUN_ACTIONS = void 0;
37
+ exports.projectCli = projectCli;
38
+ exports.projectEnvironment = projectEnvironment;
39
+ exports.projectCapabilities = projectCapabilities;
40
+ exports.npmCommand = npmCommand;
41
+ exports.withProjectLock = withProjectLock;
42
+ exports.runProjectAction = runProjectAction;
43
+ exports.setupRepository = setupRepository;
44
+ // Project-local CLI selection and argv contracts extracted from codeRepositoryCli.ts.
45
+ const fs = __importStar(require("fs/promises"));
46
+ const path = __importStar(require("path"));
47
+ const crypto = __importStar(require("crypto"));
48
+ const models_1 = require("./models.js");
49
+ const repositories_1 = require("./repositories.js");
50
+ const envfile_1 = require("./envfile.js");
51
+ const repositoryIdentity_1 = require("./repositoryIdentity.js");
52
+ const cli_1 = require("./cli.js");
53
+ const session_1 = require("./session.js");
54
+ const pythonSdkStatus_1 = require("./pythonSdkStatus.js");
55
+ async function projectCli(context) {
56
+ if (!context.projectType)
57
+ throw new models_1.ToolingError('PROJECT_TYPE_REQUIRED', 'Select --type python or --type vite_react for an ambiguous or unknown project.');
58
+ const cli = (0, cli_1.resolveCodeRepositoryCli)(context.root, context.projectType);
59
+ return { executable: cli.executable, prefix: cli.prefixArgs, available: cli.available };
60
+ }
61
+ async function isFile(value) { return fs.stat(value).then(s => s.isFile()).catch(() => false); }
62
+ /** The project actions with a read-only preview. Sync has none. */
63
+ exports.DRY_RUN_ACTIONS = ['update-sdk', 'install-skills'];
64
+ function commandArgs(context, action, dryRun) {
65
+ const python = context.projectType === 'python';
66
+ let args;
67
+ switch (action) {
68
+ // Sync only refreshes dependency files: Python runs uv lock, uv sync and the locked requirements.txt
69
+ // export; Node refreshes package-lock.json and runs npm ci. It takes no commit message and runs no
70
+ // Git command or backend request. The person commits and pushes.
71
+ case 'sync':
72
+ args = ['code-repository', 'sync', '--path', '.'];
73
+ break;
74
+ case 'sdk-status':
75
+ if (python)
76
+ return;
77
+ args = ['application', 'sdk-status', '--path', '.', '--json'];
78
+ break;
79
+ case 'update-sdk':
80
+ args = [python ? 'code-repository' : 'application', 'update-sdk', '--path', '.'];
81
+ break;
82
+ case 'update-agents':
83
+ if (!python)
84
+ return;
85
+ args = ['code-repository', 'update', 'AGENTS.md', '--path', '.'];
86
+ break;
87
+ case 'install-skills':
88
+ if (python)
89
+ return;
90
+ args = ['skills', 'install', '--path', '.'];
91
+ break;
92
+ default: return;
93
+ }
94
+ if (dryRun) {
95
+ if (!exports.DRY_RUN_ACTIONS.includes(action))
96
+ throw new models_1.ToolingError('INVALID_REQUEST', 'This action has no read-only dry-run mode.');
97
+ args.push('--dry-run');
98
+ }
99
+ return args;
100
+ }
101
+ /** SDK CLIs from before sync became a dependency refresh still advertise its commit and preview options. */
102
+ function advertisesReleaseSync(help, python) {
103
+ // The Node CLI prints one usage text for every command; only its sync line describes sync.
104
+ const text = python ? help : help.split(/\r?\n/).filter(line => line.includes('code-repository sync')).join('\n');
105
+ return /--dry-run|--message/.test(text);
106
+ }
107
+ async function probe(host, context, action, cli, env, signal) {
108
+ if (!cli.available)
109
+ return { action, state: 'unavailable', reason: 'The project-local SDK CLI is missing. Run install in this project.' };
110
+ const args = commandArgs(context, action);
111
+ if (!args)
112
+ return { action, state: 'unsupported', reason: 'This operation is not provided by this project SDK.' };
113
+ const command = args.slice(0, 2);
114
+ const help = await host.runProcess({ executable: cli.executable, args: [...cli.prefix, ...(context.projectType === 'python' ? command : []), '--help'], cwd: context.root, env, signal, timeoutMs: 20_000 });
115
+ if (help.cancelled)
116
+ throw new models_1.ToolingError('CANCELLED', 'The action was cancelled.');
117
+ if (help.exitCode !== 0 || !help.stdout.includes('--path') || (context.projectType !== 'python' && !help.stdout.includes(command.join(' ')))) {
118
+ return { action, state: 'unavailable', reason: 'The installed SDK CLI does not advertise this command. Update the project SDK or fix its environment.' };
119
+ }
120
+ // Never run the old sync: it commits, tags and pushes, and fails at a backend route that no longer exists.
121
+ if (action === 'sync' && advertisesReleaseSync(help.stdout, context.projectType === 'python')) {
122
+ const required = context.projectType === 'python' ? 'mainsequence 9.0.5' : '@dev-mainsequence/command-center-sdk 0.5.13';
123
+ return { action, state: 'unavailable', reason: `The installed SDK still has the retired release sync, which commits, tags and pushes. Run update-sdk: sync needs ${required} or later.` };
124
+ }
125
+ return { action, state: 'available' };
126
+ }
127
+ /** Scope inherited credentials to their explicit backend, then let the installed SDK read its store. */
128
+ function projectEnvironment(context, inherited) {
129
+ const env = { ...inherited };
130
+ const credentialKeys = ['MAINSEQUENCE_ACCESS_TOKEN', 'MAINSEQUENCE_REFRESH_TOKEN', 'MAINSEQUENCE_TOKEN', 'MAINSEQUENCE_RUNTIME_CREDENTIAL_ID', 'MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET'];
131
+ if (credentialKeys.some(key => env[key])) {
132
+ if (!env.MAINSEQUENCE_ENDPOINT || (0, session_1.requireBackend)(env.MAINSEQUENCE_ENDPOINT) !== context.backend) {
133
+ throw new models_1.ToolingError('BACKEND_MISMATCH', 'Inherited process credentials belong to another or unspecified backend.');
134
+ }
135
+ if (env.MAINSEQUENCE_TOKEN || env.MAINSEQUENCE_RUNTIME_CREDENTIAL_ID || env.MAINSEQUENCE_RUNTIME_CREDENTIAL_SECRET) {
136
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Unsupported or runtime credentials cannot be used for developer project actions.');
137
+ }
138
+ }
139
+ env.MAINSEQUENCE_ENDPOINT = context.backend;
140
+ return env;
141
+ }
142
+ async function projectCapabilities(host, context, env, signal, allowMcpSkills = false) {
143
+ const actions = ['sync', 'sdk-status', 'update-sdk', 'update-agents', 'install-skills', 'skills-sync', 'install', 'env-cleanup', 'open-folder', 'signed-terminal'];
144
+ if (!context.projectType)
145
+ return actions.map(action => ({ action, state: 'unavailable', reason: 'Project type is ambiguous. Select --type.' }));
146
+ const cli = await projectCli(context);
147
+ const scoped = projectEnvironment(context, env);
148
+ const results = [];
149
+ for (const action of actions) {
150
+ if (action === 'skills-sync')
151
+ results.push({ action, state: allowMcpSkills ? (cli.available ? 'available' : 'unavailable') : 'unsupported',
152
+ reason: !allowMcpSkills ? 'Backend-owned skills currently require MainSequence MCP.' : !cli.available ? 'Install the project SDK before synchronizing its skills.' : undefined });
153
+ else if (action === 'open-folder' || action === 'signed-terminal')
154
+ results.push({ action, state: (action === 'open-folder' ? host.openFolder : host.openTerminal) ? 'available' : 'unsupported', reason: (action === 'open-folder' ? host.openFolder : host.openTerminal) ? undefined : 'This host has no native folder/terminal capability.' });
155
+ else if (action === 'env-cleanup')
156
+ results.push({ action, state: 'available' });
157
+ else if (action === 'install') {
158
+ const command = context.projectType === 'python' ? { executable: 'uv', args: ['--version'] } : npmCommand('--version');
159
+ const result = await host.runProcess({ ...command, cwd: context.root, env: scoped, signal, timeoutMs: 10_000 });
160
+ if (result.cancelled)
161
+ throw new models_1.ToolingError('CANCELLED', 'The action was cancelled.');
162
+ results.push({ action, state: result.exitCode === 0 ? 'available' : 'unavailable', reason: result.exitCode === 0 ? undefined : `${context.projectType === 'python' ? 'uv' : 'npm'} is required.` });
163
+ }
164
+ else if (action === 'sdk-status' && context.projectType === 'python') {
165
+ results.push({ action, state: await isFile(pythonExecutable(context.root)) ? 'available' : 'unavailable', reason: await isFile(pythonExecutable(context.root)) ? undefined : 'Build the project .venv first.' });
166
+ }
167
+ else
168
+ results.push(await probe(host, context, action, cli, scoped, signal));
169
+ }
170
+ return results;
171
+ }
172
+ function npmCommand(command, platform = process.platform) {
173
+ // Windows npm.cmd needs cmd.exe. This is a fixed command, with no user arguments or secrets.
174
+ return platform === 'win32' ? { executable: 'cmd.exe', args: ['/d', '/s', '/c', `npm ${command}`] } : { executable: 'npm', args: [command] };
175
+ }
176
+ function pythonExecutable(root) { return path.join(root, '.venv', process.platform === 'win32' ? 'Scripts' : 'bin', process.platform === 'win32' ? 'python.exe' : 'python'); }
177
+ async function execute(host, spec, state, mutating) {
178
+ spec.signal?.throwIfAborted();
179
+ if (mutating)
180
+ state.mutationStarted = true;
181
+ const result = await host.runProcess(spec);
182
+ if (result.cancelled)
183
+ throw new models_1.ToolingError('CANCELLED', 'The action was cancelled; inspect the project before retrying.');
184
+ if (result.timedOut)
185
+ throw new models_1.ToolingError('PROCESS_TIMEOUT', 'The project command timed out; inspect the project before retrying.');
186
+ if (result.unavailable)
187
+ throw new models_1.ToolingError('MISSING_PREREQUISITE', 'The required project executable could not be started.');
188
+ if (result.exitCode !== 0)
189
+ throw new models_1.ToolingError('PROCESS_FAILED', `The project command failed (exit ${result.exitCode ?? 'unknown'}). No automatic retry or rollback was attempted.`);
190
+ return result.stdout;
191
+ }
192
+ async function withProjectLock(commonDir, work) {
193
+ return withFileLock(path.join(commonDir, 'mainsequence-devtools.lock'), work);
194
+ }
195
+ async function withFileLock(lock, work) {
196
+ let handle;
197
+ try {
198
+ handle = await fs.open(lock, 'wx', 0o600);
199
+ }
200
+ catch (error) {
201
+ if (error.code === 'EEXIST')
202
+ throw new models_1.ToolingError('PROJECT_BUSY', `Another tooling action holds this repository lock: ${lock}. After an interrupted run, verify no action is running before removing it.`);
203
+ throw new models_1.ToolingError('INVALID_GIT_CONTEXT', 'Could not acquire the repository action lock.');
204
+ }
205
+ try {
206
+ await handle.writeFile(JSON.stringify({ pid: process.pid, id: crypto.randomUUID() }));
207
+ return await work();
208
+ }
209
+ finally {
210
+ await handle.close();
211
+ await fs.unlink(lock);
212
+ }
213
+ }
214
+ async function runProjectAction(host, session, context, action, options) {
215
+ const env = projectEnvironment(context, options.env);
216
+ if (action === 'skills-sync') {
217
+ if (!options.allowMcpSkills)
218
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'Backend-owned skills require MainSequence MCP. This runner does not invoke it.');
219
+ if (!context.projectType)
220
+ throw new models_1.ToolingError('PROJECT_TYPE_REQUIRED', 'A supported project type is required.');
221
+ const cli = (0, cli_1.resolveCodeRepositoryCli)(context.root, context.projectType);
222
+ if (!cli.available)
223
+ throw new models_1.ToolingError('MISSING_PREREQUISITE', cli.missingMessage);
224
+ await session.status(context.backend, true);
225
+ for (const invocation of (0, cli_1.buildCodeRepositoryCliInvocations)(cli, 'skills')) {
226
+ await execute(host, { executable: invocation.executable, args: invocation.args, cwd: context.root, env, signal: options.signal }, options.state, true);
227
+ }
228
+ return { root: context.root, projectType: context.projectType };
229
+ }
230
+ if (action === 'open-folder') {
231
+ if (!host.openFolder)
232
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'This host does not expose a native folder opener. The project path is available through status.');
233
+ await host.openFolder(context.root);
234
+ return { root: context.root };
235
+ }
236
+ if (action === 'signed-terminal') {
237
+ if (!host.openTerminal)
238
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'This host does not expose a native terminal opener.');
239
+ await session.status(context.backend, true);
240
+ const { keyPath, pubPath } = (0, repositoryIdentity_1.repositorySshKeyPaths)(context.origin);
241
+ if (!(0, repositoryIdentity_1.repositorySshKeyIdentity)(context.origin).usesSsh || !(await isFile(keyPath)) || !(await isFile(pubPath)))
242
+ throw new models_1.ToolingError('MISSING_PREREQUISITE', 'A repository-specific SSH keypair is required. Set up this repository first.');
243
+ env.GIT_SSH_COMMAND = `ssh -i ${quoteSshPath(keyPath)} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`;
244
+ // Store-aware SDKs acquire access themselves. Never return or print a token-bearing shell command.
245
+ await host.openTerminal({ root: context.root, env });
246
+ return { root: context.root };
247
+ }
248
+ if (action === 'env-cleanup') {
249
+ options.state.mutationStarted = true;
250
+ await (0, envfile_1.writeMachineEnv)(host, context.root, context.backend);
251
+ return { root: context.root, endpointOnly: true };
252
+ }
253
+ if (!context.projectType)
254
+ throw new models_1.ToolingError('PROJECT_TYPE_REQUIRED', 'Select --type python or --type vite_react.');
255
+ if (action === 'install') {
256
+ if (options.dryRun)
257
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Install has no dry-run mode.');
258
+ host.progress?.(context.projectType === 'python' ? 'Building the project environment with uv sync.' : 'Installing locked npm dependencies.');
259
+ const command = context.projectType === 'python' ? { executable: 'uv', args: ['sync'] } : npmCommand('ci');
260
+ await execute(host, { ...command, cwd: context.root, env, signal: options.signal }, options.state, true);
261
+ return { root: context.root, projectType: context.projectType };
262
+ }
263
+ if (action === 'sdk-status' && context.projectType === 'python') {
264
+ const output = await execute(host, { executable: pythonExecutable(context.root), args: ['-I', '-c', "import importlib.metadata as m, json\ntry: v = m.version('mainsequence')\nexcept m.PackageNotFoundError: v = None\nprint(json.dumps({'installed': v}))"], cwd: context.root, env, signal: options.signal, timeoutMs: 10_000 }, options.state, false);
265
+ let installed;
266
+ try {
267
+ installed = JSON.parse(output).installed;
268
+ if (installed !== null && typeof installed !== 'string')
269
+ throw new Error();
270
+ }
271
+ catch {
272
+ throw new models_1.ToolingError('PROCESS_FAILED', 'The Python interpreter did not return valid distribution metadata.');
273
+ }
274
+ return (0, pythonSdkStatus_1.readPythonSdkStatus)(installed, options.fetch, options.signal);
275
+ }
276
+ const cli = await projectCli(context);
277
+ const capability = await probe(host, context, action, cli, env, options.signal);
278
+ if (capability.state !== 'available')
279
+ throw new models_1.ToolingError(capability.state === 'unsupported' ? 'UNSUPPORTED_ACTION' : 'MISSING_PREREQUISITE', capability.reason);
280
+ const args = commandArgs(context, action, options.dryRun);
281
+ if (options.dryRun) {
282
+ const help = await host.runProcess({ executable: cli.executable, args: [...cli.prefix, ...(context.projectType === 'python' ? args.slice(0, 2) : []), '--help'], cwd: context.root, env, signal: options.signal, timeoutMs: 20_000 });
283
+ if (!help.stdout.includes('--dry-run'))
284
+ throw new models_1.ToolingError('MISSING_PREREQUISITE', 'The installed CLI does not advertise dry-run support.');
285
+ }
286
+ // Old .env credentials would change the SDK's identity. Require explicit cleanup first.
287
+ if (!options.allowLegacyProjectCredentials && (0, envfile_1.hasProjectCredentials)(await (0, repositories_1.readProjectEnv)(context.root)))
288
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The project .env contains retired credentials. Run env-cleanup before dispatching SDK commands.');
289
+ host.progress?.(`Running ${action} with the project-installed ${context.projectType === 'python' ? 'Python' : 'Node'} SDK CLI.`);
290
+ const output = await execute(host, { executable: cli.executable, args: [...cli.prefix, ...args], cwd: context.root, env, signal: options.signal }, options.state, action !== 'sdk-status' && !options.dryRun);
291
+ if (action === 'sdk-status') {
292
+ let report;
293
+ try {
294
+ report = JSON.parse(output);
295
+ }
296
+ catch {
297
+ throw new models_1.ToolingError('PROCESS_FAILED', 'The SDK status command returned invalid JSON.');
298
+ }
299
+ const result = {};
300
+ for (const key of ['package', 'declared', 'locked', 'installed', 'wanted', 'latest', 'status', 'dependencyType', 'updateAvailable', 'updateSupported', 'hint']) {
301
+ if (report[key] === null || typeof report[key] === 'string' || typeof report[key] === 'boolean')
302
+ result[key] = report[key];
303
+ }
304
+ return result;
305
+ }
306
+ return { root: context.root, projectType: context.projectType, dryRun: Boolean(options.dryRun) };
307
+ }
308
+ function quoteSshPath(value) {
309
+ if (process.platform === 'win32') {
310
+ if (/["%\r\n]/.test(value))
311
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The SSH key path cannot be quoted safely on this platform.');
312
+ return `"${value}"`;
313
+ }
314
+ return "'" + value.replace(/'/g, "'\\''") + "'";
315
+ }
316
+ async function setupRepository(host, session, input, options) {
317
+ const { repository, binding, orgSlug } = await (0, repositories_1.repositorySetupInfo)(session, input.backend, input.repositoryUid, input.branch);
318
+ const type = repository.code_repository_type;
319
+ if (type !== 'python' && type !== 'vite_react')
320
+ throw new models_1.ToolingError('UNSUPPORTED_ACTION', 'This CodeRepository type is not supported.');
321
+ const cloneUrl = binding.git_ssh_url;
322
+ if (!cloneUrl || !(0, repositoryIdentity_1.repositorySshKeyIdentity)(cloneUrl).usesSsh)
323
+ throw new models_1.ToolingError('INVALID_REQUEST', 'Repository setup requires the existing SSH GitHub binding.');
324
+ const destination = options.destination ? path.resolve(options.destination) : (0, repositories_1.canonicalRepositoryDir)(input.baseDir, orgSlug, repository.uid, repository.code_repository_name);
325
+ const relative = path.relative(path.resolve(input.baseDir), destination);
326
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
327
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The checkout must be inside its selected base folder.');
328
+ const env = projectEnvironment({ backend: input.backend }, options.env);
329
+ // No replacement of existing folders. A failed clone is retained for inspection.
330
+ if (await fs.lstat(destination).then(() => true).catch((error) => { if (error.code === 'ENOENT')
331
+ return false; throw error; })) {
332
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The canonical checkout folder already exists. Select it with --path.');
333
+ }
334
+ options.signal?.throwIfAborted();
335
+ options.state.mutationStarted = true;
336
+ await fs.mkdir(path.resolve(input.baseDir), { recursive: true });
337
+ const baseReal = await fs.realpath(path.resolve(input.baseDir));
338
+ // Reject existing symlink components before creating folders or other setup effects.
339
+ const parents = [];
340
+ for (let current = path.dirname(destination); current !== path.resolve(input.baseDir); current = path.dirname(current))
341
+ parents.unshift(current);
342
+ for (const segment of parents) {
343
+ const stat = await fs.lstat(segment).catch((error) => { if (error.code === 'ENOENT')
344
+ return null; throw error; });
345
+ if (stat?.isSymbolicLink() || (stat && !stat.isDirectory()))
346
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The canonical checkout parent must contain ordinary directories, not symlinks.');
347
+ }
348
+ await fs.mkdir(path.dirname(destination), { recursive: true });
349
+ const parentReal = await fs.realpath(path.dirname(destination));
350
+ if (!parentReal.startsWith(baseReal + path.sep))
351
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The canonical checkout parent escapes the base directory through a symlink.');
352
+ const setupLock = path.join(parentReal, `.${path.basename(destination)}.mainsequence-devtools.lock`);
353
+ return withFileLock(setupLock, async () => {
354
+ if (await fs.lstat(destination).then(() => true).catch((error) => { if (error.code === 'ENOENT')
355
+ return false; throw error; }))
356
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The canonical checkout folder already exists. Select it with --path.');
357
+ const { keyPath, pubPath } = (0, repositoryIdentity_1.repositorySshKeyPaths)(cloneUrl);
358
+ const privateKey = await isFile(keyPath), publicKey = await isFile(pubPath);
359
+ if (privateKey !== publicKey)
360
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The repository SSH keypair is incomplete. Repair it before setup.');
361
+ await fs.mkdir(path.dirname(keyPath), { recursive: true, mode: 0o700 });
362
+ if (!privateKey) {
363
+ await execute(host, { executable: 'ssh-keygen', args: ['-t', 'ed25519', '-C', 'mainsequence@main-sequence.io', '-f', keyPath, '-N', ''], cwd: baseReal, env, signal: options.signal, timeoutMs: 30_000 }, options.state, true);
364
+ }
365
+ const key = (await fs.readFile(pubPath, 'utf8')).trim();
366
+ if (!key || /[\r\n]/.test(key))
367
+ throw new models_1.ToolingError('INVALID_REQUEST', 'The repository SSH public key is invalid.');
368
+ // Register only when access with the repository key is denied, matching VS Code setup.
369
+ env.GIT_SSH_COMMAND = `ssh -i ${quoteSshPath(keyPath)} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`;
370
+ const check = await host.runProcess({ executable: 'git', args: ['ls-remote', '--exit-code', cloneUrl, 'HEAD'], cwd: path.resolve(input.baseDir), env, signal: options.signal, timeoutMs: 15_000 });
371
+ if (check.cancelled)
372
+ throw new models_1.ToolingError('CANCELLED', 'Setup was cancelled.');
373
+ if (check.exitCode !== 0) {
374
+ await session.request(input.backend, 'POST', `/api/v1/code-repositories/${encodeURIComponent(repository.uid)}/add-deploy-key/`, { key_title: (0, repositoryIdentity_1.repositorySshKeyName)(cloneUrl), public_key: key });
375
+ await execute(host, { executable: 'git', args: ['ls-remote', '--exit-code', cloneUrl, 'HEAD'], cwd: path.resolve(input.baseDir), env, signal: options.signal, timeoutMs: 15_000 }, options.state, false);
376
+ }
377
+ host.progress?.('Cloning the registered repository branch into its canonical folder.');
378
+ await execute(host, { executable: 'git', args: ['clone', ...(options.reference ? ['--reference-if-able', options.reference, '--dissociate'] : []), '--branch', input.branch, '--single-branch', '--', cloneUrl, destination], cwd: baseReal, env, signal: options.signal }, options.state, true);
379
+ const context = await (0, repositories_1.projectContext)(host, destination, input.backend, type, env, options.signal);
380
+ if (context.repositoryIdentity !== (0, repositoryIdentity_1.repositorySshKeyIdentity)(cloneUrl).identity || context.branch !== input.branch)
381
+ throw new models_1.ToolingError('INVALID_GIT_CONTEXT', 'The cloned identity or branch does not match the selected repository.');
382
+ await withProjectLock(context.gitCommonDir, async () => {
383
+ await (0, repositories_1.git)(host, destination, ['config', 'core.sshCommand', env.GIT_SSH_COMMAND], options.signal);
384
+ await (0, envfile_1.writeMachineEnv)(host, destination, input.backend);
385
+ if (input.install)
386
+ await runProjectAction(host, session, context, 'install', options);
387
+ });
388
+ return { root: destination, repositoryUid: repository.uid, projectType: type, endpointOnly: true, dependenciesInstalled: Boolean(input.install) };
389
+ });
390
+ }
@@ -0,0 +1,86 @@
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.PYTHON_SDK_PROJECT_URL = void 0;
37
+ exports.pythonExecutable = pythonExecutable;
38
+ exports.hasPythonProject = hasPythonProject;
39
+ exports.projectVersion = projectVersion;
40
+ exports.readPythonSdkStatus = readPythonSdkStatus;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ exports.PYTHON_SDK_PROJECT_URL = 'https://raw.githubusercontent.com/mainsequence-sdk/mainsequence-sdk/main/pyproject.toml';
44
+ function pythonExecutable(root) {
45
+ return process.platform === 'win32'
46
+ ? path.join(root, '.venv', 'Scripts', 'python.exe')
47
+ : path.join(root, '.venv', 'bin', 'python');
48
+ }
49
+ function hasPythonProject(root) {
50
+ return fs.existsSync(path.join(root, 'pyproject.toml')) && !fs.existsSync(path.join(root, 'package.json'));
51
+ }
52
+ function projectVersion(content) {
53
+ let inProject = false;
54
+ for (const line of content.split(/\r?\n/)) {
55
+ if (/^\s*\[/.test(line))
56
+ inProject = /^\s*\[project\]\s*(?:#.*)?$/.test(line);
57
+ if (!inProject)
58
+ continue;
59
+ const match = line.match(/^\s*version\s*=\s*["']([^"']+)["']\s*(?:#.*)?$/);
60
+ if (match)
61
+ return match[1];
62
+ }
63
+ throw new Error('The public SDK pyproject.toml has no project version.');
64
+ }
65
+ async function publicProject() {
66
+ const response = await fetch(exports.PYTHON_SDK_PROJECT_URL, { headers: { Accept: 'text/plain' }, signal: AbortSignal.timeout(10_000) });
67
+ if (!response.ok)
68
+ throw new Error('The public SDK version could not be read.');
69
+ return response.text();
70
+ }
71
+ async function readPythonSdkStatus(root, capture, fetchProject = publicProject) {
72
+ const latest = projectVersion(await fetchProject());
73
+ // Import distribution metadata only: do not import the SDK or its auth bootstrap.
74
+ const code = [
75
+ 'import importlib.metadata as metadata, json, sys',
76
+ 'from packaging.version import Version',
77
+ 'try:',
78
+ " local = metadata.version('mainsequence')",
79
+ 'except metadata.PackageNotFoundError:',
80
+ ' local = None',
81
+ 'latest = sys.argv[1]',
82
+ "status = 'not_installed' if local is None else ('update_available' if Version(local) < Version(latest) else ('ahead' if Version(local) > Version(latest) else 'current'))",
83
+ "print(json.dumps(dict(latest=latest, installed=local, status=status, updateAvailable=status == 'update_available', updateSupported=False)))"
84
+ ].join('\n');
85
+ return { registryPublicationVerified: false, latestSourceVersion: latest, ...JSON.parse(await capture(pythonExecutable(root), ['-I', '-c', code, latest], root)) };
86
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PYTHON_SDK_PROJECT_URL = void 0;
4
+ exports.readPythonSdkStatus = readPythonSdkStatus;
5
+ // Metadata-only status adapted from VS Code's pythonSdkStatus.ts.
6
+ // main is the stable source; analyzing development never opts projects into prereleases.
7
+ exports.PYTHON_SDK_PROJECT_URL = 'https://raw.githubusercontent.com/mainsequence-sdk/mainsequence-sdk/main/pyproject.toml';
8
+ async function readPythonSdkStatus(installed, fetchProject = fetch, signal) {
9
+ const base = { package: 'mainsequence', installed, dependencyPolicy: 'project-declared', releaseChannel: 'project-declared', sourceUrl: exports.PYTHON_SDK_PROJECT_URL };
10
+ try {
11
+ const response = await fetchProject(exports.PYTHON_SDK_PROJECT_URL, { headers: { Accept: 'text/plain' }, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000) });
12
+ if (!response.ok)
13
+ throw new Error('unavailable');
14
+ const text = await response.text();
15
+ const project = text.split(/^\s*\[project\]\s*(?:#.*)?$/m)[1]?.split(/^\s*\[/m)[0];
16
+ const version = project?.match(/^\s*version\s*=\s*["']([^"']+)["']\s*(?:#.*)?$/m)?.[1];
17
+ if (!version)
18
+ throw new Error('invalid source');
19
+ return { ...base, latestSourceVersion: version, registryPublicationVerified: false,
20
+ hint: 'This compares installed metadata with the stable source tree. SDK updates preserve the project dependency policy; no prerelease channel is selected.' };
21
+ }
22
+ catch {
23
+ signal?.throwIfAborted();
24
+ return { ...base, sourceStatus: 'unavailable', hint: 'Installed metadata is available; the stable source version could not be read.' };
25
+ }
26
+ }