pi-harness-runtime 1.1.158 → 1.1.159

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 (104) hide show
  1. package/harness/infisical-config.ts +395 -0
  2. package/harness/infisical-setup.ts +270 -0
  3. package/harness/key-loader.ts +129 -29
  4. package/harness/secret-provider.ts +437 -0
  5. package/harness/secret-schema.ts +163 -0
  6. package/harness/varlock-worker.ts +379 -0
  7. package/index.ts +12 -3
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/agent-policy/package.json +1 -1
  11. package/packages/architecture-generator/package.json +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/autonomous-refactor/package.json +1 -1
  14. package/packages/autonomous-runtime/package.json +1 -1
  15. package/packages/cache-strategy/package.json +1 -1
  16. package/packages/capability-registry/package.json +1 -1
  17. package/packages/checkpoint/package.json +1 -1
  18. package/packages/cli-plugin-sdk/package.json +1 -1
  19. package/packages/clipboard/package.json +1 -1
  20. package/packages/clipboard-plugin/package.json +1 -1
  21. package/packages/code-generation/package.json +1 -1
  22. package/packages/code-review/package.json +1 -1
  23. package/packages/codex-adapter/package.json +1 -1
  24. package/packages/codex-watcher/package.json +1 -1
  25. package/packages/config-capture/package.json +1 -1
  26. package/packages/context-compiler/package.json +1 -1
  27. package/packages/context-discovery/package.json +1 -1
  28. package/packages/context-manager/package.json +1 -1
  29. package/packages/cookie-sanitizer/package.json +1 -1
  30. package/packages/cost-optimizer/package.json +1 -1
  31. package/packages/dependency-analyzer/package.json +1 -1
  32. package/packages/django-plugin/package.json +1 -1
  33. package/packages/doc-generator/package.json +1 -1
  34. package/packages/evaluation-engine/package.json +1 -1
  35. package/packages/evaluation-runner/package.json +1 -1
  36. package/packages/event-bus/package.json +1 -1
  37. package/packages/event-store/package.json +1 -1
  38. package/packages/experience-replay/package.json +1 -1
  39. package/packages/feedback-collector/package.json +1 -1
  40. package/packages/file-copy-helper/package.json +1 -1
  41. package/packages/framework-detector/package.json +1 -1
  42. package/packages/framework-plugin-sdk/package.json +1 -1
  43. package/packages/frappe-plugin/package.json +1 -1
  44. package/packages/generic-web-plugin/package.json +1 -1
  45. package/packages/health-monitor/package.json +1 -1
  46. package/packages/intent-analyzer/package.json +1 -1
  47. package/packages/jev-judge/package.json +1 -1
  48. package/packages/knowledge-graph/package.json +1 -1
  49. package/packages/knowledge-retrieval/package.json +1 -1
  50. package/packages/laravel-plugin/package.json +1 -1
  51. package/packages/learning-engine/package.json +1 -1
  52. package/packages/mcp-adapter/package.json +1 -1
  53. package/packages/memory-engine/package.json +1 -1
  54. package/packages/milestone-manager/package.json +1 -1
  55. package/packages/model-registry/package.json +1 -1
  56. package/packages/nextjs-plugin/package.json +1 -1
  57. package/packages/notification/package.json +1 -1
  58. package/packages/observability/package.json +1 -1
  59. package/packages/okf-indexer/package.json +1 -1
  60. package/packages/paperclip-integration/package.json +1 -1
  61. package/packages/performance-optimizer/package.json +1 -1
  62. package/packages/privilege-broker/package.json +1 -1
  63. package/packages/project-analyzer/package.json +1 -1
  64. package/packages/project-bootstrap/package.json +1 -1
  65. package/packages/projection-engine/package.json +1 -1
  66. package/packages/prompt-compiler/package.json +1 -1
  67. package/packages/prompt-versioning/package.json +1 -1
  68. package/packages/provider-adapter-sdk/package.json +1 -1
  69. package/packages/provider-router/package.json +1 -1
  70. package/packages/provider-selector/package.json +1 -1
  71. package/packages/providers/package.json +1 -1
  72. package/packages/qdrant-skills/package.json +1 -1
  73. package/packages/quota-manager/package.json +1 -1
  74. package/packages/rate-limiter/package.json +1 -1
  75. package/packages/react-vite-plugin/package.json +1 -1
  76. package/packages/release-manager/package.json +1 -1
  77. package/packages/requirement-compiler/package.json +1 -1
  78. package/packages/runtime/package.json +1 -1
  79. package/packages/scheduler/package.json +1 -1
  80. package/packages/scheduler-adapter/package.json +1 -1
  81. package/packages/session/package.json +1 -1
  82. package/packages/session-api/package.json +1 -1
  83. package/packages/session-export/package.json +1 -1
  84. package/packages/shared-context/package.json +1 -1
  85. package/packages/skill-mcp-client/package.json +1 -1
  86. package/packages/skill-registry/package.json +1 -1
  87. package/packages/skills/package.json +1 -1
  88. package/packages/sprint-planner/package.json +1 -1
  89. package/packages/subscription-engine/package.json +1 -1
  90. package/packages/task-compiler/package.json +1 -1
  91. package/packages/tencentdb-memory/package.json +1 -1
  92. package/packages/tencentdb-sync/package.json +1 -1
  93. package/packages/test-data-generator/package.json +1 -1
  94. package/packages/test-generator/package.json +1 -1
  95. package/packages/todo-bd-sync/package.json +1 -1
  96. package/packages/token-estimation/package.json +1 -1
  97. package/packages/token-optimizer/package.json +1 -1
  98. package/packages/trajectory/package.json +1 -1
  99. package/packages/tui/package.json +1 -1
  100. package/packages/types/package.json +1 -1
  101. package/packages/workflow-events/package.json +1 -1
  102. package/packages/workspace-scanner/package.json +1 -1
  103. package/packages/worktree/package.json +1 -1
  104. package/packages/write-review/package.json +1 -1
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Infisical Configuration Module
3
+ *
4
+ * Manages validated non-secret configuration for Infisical/Varlock integration.
5
+ * Stores metadata at ~/.pi-harness-runtime/infisical/config.json
6
+ *
7
+ * Does NOT store secrets, tokens, or resolved values.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync, rmSync, readdirSync } from "node:fs";
11
+ import { join, dirname } from "node:path";
12
+ import { homedir } from "node:os";
13
+
14
+ /**
15
+ * Infisical site URLs
16
+ */
17
+ export type InfisicalSite = "us" | "eu" | "custom";
18
+
19
+ /**
20
+ * Infisical authentication method
21
+ */
22
+ export type InfisicalAuthMethod = "universal" | "oidc";
23
+
24
+ /**
25
+ * Service that can use Infisical secrets
26
+ */
27
+ export type ServiceName =
28
+ | "infisical" // Infisical provider itself (for logging)
29
+ | "jev" // TypeSafe Jev
30
+ | "honcho" // Honcho memory
31
+ | "qdrant" // Qdrant vector search
32
+ | "telegram" // Telegram bot
33
+ | "embeddings" // OpenAI/embedding provider
34
+ | "planner" // LangChain planner
35
+ | "minimax" // Minimax provider
36
+ | "glm"; // GLM provider
37
+
38
+ /**
39
+ * Secret name mapping for a service
40
+ */
41
+ export interface ServiceSecretMapping {
42
+ service: ServiceName;
43
+ required: boolean;
44
+ keys: Record<string, string>; // local key -> Infisical secret name
45
+ }
46
+
47
+ /**
48
+ * Bootstrap reference type
49
+ */
50
+ export type BootstrapRef =
51
+ | { type: "env"; name: string } // env:VAR_NAME
52
+ | { type: "file"; path: string }; // file:/absolute/path
53
+
54
+ /**
55
+ * Infisical configuration (non-secret metadata only)
56
+ */
57
+ export interface InfisicalConfig {
58
+ /** Schema version for future migrations */
59
+ schemaVersion: 1;
60
+
61
+ /** Whether Infisical integration is enabled */
62
+ enabled: boolean;
63
+
64
+ /** Active revision ID */
65
+ activeRevision: string | null;
66
+
67
+ /** Infisical site */
68
+ site: InfisicalSite;
69
+ customSiteUrl?: string; // Only if site === "custom"
70
+
71
+ /** Project and environment */
72
+ projectId: string;
73
+ environment: string; // e.g., "production", "development"
74
+ secretPath: string; // e.g., "/harness"
75
+
76
+ /** Authentication method */
77
+ authMethod: InfisicalAuthMethod;
78
+
79
+ /** Bootstrap reference (not actual credentials) */
80
+ bootstrapRef: BootstrapRef;
81
+
82
+ /** Enabled services and their secret mappings */
83
+ services: ServiceSecretMapping[];
84
+
85
+ /** Source policy */
86
+ sourcePolicy: {
87
+ /** Use remote as authoritative for selected services */
88
+ remoteAuthoritative: boolean;
89
+ /** Allow environment variable overrides */
90
+ envOverride: boolean;
91
+ };
92
+
93
+ /** Last resolution timestamp */
94
+ lastResolvedAt: string | null;
95
+
96
+ /** Last error if any */
97
+ lastError: string | null;
98
+ }
99
+
100
+ /**
101
+ * Schema revision directory metadata
102
+ */
103
+ export interface SchemaRevision {
104
+ revisionId: string;
105
+ createdAt: string;
106
+ environment: string;
107
+ secretPath: string;
108
+ services: ServiceName[];
109
+ validated: boolean;
110
+ }
111
+
112
+ /**
113
+ * Get Infisical config directory
114
+ */
115
+ export function getInfisicalDir(): string {
116
+ return join(homedir(), ".pi-harness-runtime", "infisical");
117
+ }
118
+
119
+ /**
120
+ * Get config file path
121
+ */
122
+ export function getConfigPath(): string {
123
+ return join(getInfisicalDir(), "config.json");
124
+ }
125
+
126
+ /**
127
+ * Get revisions directory
128
+ */
129
+ export function getRevisionsDir(): string {
130
+ return join(getInfisicalDir(), "revisions");
131
+ }
132
+
133
+ /**
134
+ * Ensure directories exist with proper permissions
135
+ */
136
+ export function ensureDirectories(): void {
137
+ const baseDir = getInfisicalDir();
138
+ const revDir = getRevisionsDir();
139
+
140
+ if (!existsSync(baseDir)) {
141
+ mkdirSync(baseDir, { recursive: true, mode: 0o700 });
142
+ }
143
+
144
+ if (!existsSync(revDir)) {
145
+ mkdirSync(revDir, { recursive: true, mode: 0o700 });
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Load existing configuration
151
+ */
152
+ export function loadConfig(): InfisicalConfig | null {
153
+ const configPath = getConfigPath();
154
+
155
+ if (!existsSync(configPath)) {
156
+ return null;
157
+ }
158
+
159
+ try {
160
+ const content = readFileSync(configPath, "utf8");
161
+ const config = JSON.parse(content) as InfisicalConfig;
162
+
163
+ // Validate schema version
164
+ if (config.schemaVersion !== 1) {
165
+ console.warn("[infisical-config] Unknown schema version:", config.schemaVersion);
166
+ return null;
167
+ }
168
+
169
+ return config;
170
+ } catch (err) {
171
+ console.error("[infisical-config] Failed to load config:", err instanceof Error ? err.message : String(err));
172
+ return null;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Save configuration atomically
178
+ */
179
+ export function saveConfig(config: InfisicalConfig): void {
180
+ ensureDirectories();
181
+
182
+ const configPath = getConfigPath();
183
+ const tempPath = `${configPath}.tmp`;
184
+
185
+ try {
186
+ // Write to temp file with restricted permissions
187
+ writeFileSync(tempPath, JSON.stringify(config, null, 2), { mode: 0o600 });
188
+ chmodSync(tempPath, 0o600);
189
+
190
+ // Atomic rename
191
+ writeFileSync(configPath, readFileSync(tempPath));
192
+ unlinkSync(tempPath);
193
+ } catch (err) {
194
+ // Clean up temp file if it exists
195
+ if (existsSync(tempPath)) {
196
+ try { unlinkSync(tempPath); } catch { /* ignore */ }
197
+ }
198
+ throw err;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Create a new revision directory
204
+ */
205
+ export function createRevision(config: InfisicalConfig): string {
206
+ ensureDirectories();
207
+
208
+ const revId = `rev_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
209
+ const revDir = join(getRevisionsDir(), revId);
210
+
211
+ mkdirSync(revDir, { mode: 0o700 });
212
+
213
+ // Save revision metadata
214
+ const revision: SchemaRevision = {
215
+ revisionId: revId,
216
+ createdAt: new Date().toISOString(),
217
+ environment: config.environment,
218
+ secretPath: config.secretPath,
219
+ services: config.services.map(s => s.service),
220
+ validated: false,
221
+ };
222
+
223
+ writeFileSync(
224
+ join(revDir, "revision.json"),
225
+ JSON.stringify(revision, null, 2),
226
+ { mode: 0o600 }
227
+ );
228
+
229
+ return revId;
230
+ }
231
+
232
+ /**
233
+ * Get revision metadata
234
+ */
235
+ export function getRevision(revisionId: string): SchemaRevision | null {
236
+ const revPath = join(getRevisionsDir(), revisionId, "revision.json");
237
+
238
+ if (!existsSync(revPath)) {
239
+ return null;
240
+ }
241
+
242
+ try {
243
+ const content = readFileSync(revPath, "utf8");
244
+ return JSON.parse(content) as SchemaRevision;
245
+ } catch {
246
+ return null;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Get schema file path for a revision
252
+ */
253
+ export function getSchemaPath(revisionId: string): string {
254
+ return join(getRevisionsDir(), revisionId, ".env.schema");
255
+ }
256
+
257
+ /**
258
+ * Mark revision as validated
259
+ */
260
+ export function markRevisionValidated(revisionId: string): void {
261
+ const revDir = join(getRevisionsDir(), revisionId);
262
+ const revPath = join(revDir, "revision.json");
263
+
264
+ if (!existsSync(revPath)) {
265
+ throw new Error(`Revision not found: ${revisionId}`);
266
+ }
267
+
268
+ let revision: SchemaRevision;
269
+ try {
270
+ revision = JSON.parse(readFileSync(revPath, "utf8")) as SchemaRevision;
271
+ } catch {
272
+ throw new Error(`Failed to parse revision: ${revisionId}`);
273
+ }
274
+
275
+ revision.validated = true;
276
+ writeFileSync(revPath, JSON.stringify(revision, null, 2), { mode: 0o600 });
277
+ }
278
+
279
+ /**
280
+ * Activate a revision
281
+ */
282
+ export function activateRevision(revisionId: string): void {
283
+ const config = loadConfig();
284
+ if (!config) {
285
+ throw new Error("No config loaded");
286
+ }
287
+
288
+ const revision = getRevision(revisionId);
289
+ if (!revision) {
290
+ throw new Error(`Revision not found: ${revisionId}`);
291
+ }
292
+
293
+ if (!revision.validated) {
294
+ throw new Error(`Revision not validated: ${revisionId}`);
295
+ }
296
+
297
+ // Update config with new active revision
298
+ const updatedConfig: InfisicalConfig = {
299
+ ...config,
300
+ activeRevision: revisionId,
301
+ lastResolvedAt: new Date().toISOString(),
302
+ lastError: null,
303
+ };
304
+
305
+ saveConfig(updatedConfig);
306
+ }
307
+
308
+ /**
309
+ * Get the site URL from config
310
+ */
311
+ export function getSiteUrl(config: InfisicalConfig): string {
312
+ switch (config.site) {
313
+ case "us":
314
+ return "https://app.infisical.com";
315
+ case "eu":
316
+ return "https://eu.infisical.com";
317
+ case "custom":
318
+ if (!config.customSiteUrl) {
319
+ throw new Error("Custom site URL not set");
320
+ }
321
+ return config.customSiteUrl;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Validate site URL
327
+ */
328
+ export function isValidSiteUrl(url: string): boolean {
329
+ try {
330
+ const parsed = new URL(url);
331
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password;
332
+ } catch {
333
+ return false;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Clean up old revisions (keep last N)
339
+ */
340
+ export function cleanupOldRevisions(keepCount: number = 5): void {
341
+ const revDir = getRevisionsDir();
342
+ if (!existsSync(revDir)) return;
343
+
344
+ const config = loadConfig();
345
+ const activeRev = config?.activeRevision;
346
+
347
+ // List all revision directories
348
+ const revisions = existsSync(revDir)
349
+ ? readdirSync(revDir).filter(f => f.startsWith("rev_"))
350
+ : [];
351
+
352
+ // Sort by creation time (oldest first)
353
+ revisions.sort((a, b) => {
354
+ const metaA = getRevision(a);
355
+ const metaB = getRevision(b);
356
+ return (metaA?.createdAt ?? "").localeCompare(metaB?.createdAt ?? "");
357
+ });
358
+
359
+ // Delete old revisions (but never delete active)
360
+ const toDelete = revisions.slice(0, Math.max(0, revisions.length - keepCount));
361
+
362
+ for (const rev of toDelete) {
363
+ if (rev === activeRev) continue;
364
+
365
+ const revPath = join(revDir, rev);
366
+ try {
367
+ rmSync(revPath, { recursive: true, force: true });
368
+ } catch {
369
+ // Ignore cleanup errors
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Disable Infisical integration
376
+ */
377
+ export function disableIntegration(): void {
378
+ const config = loadConfig();
379
+ if (!config) return;
380
+
381
+ const updatedConfig: InfisicalConfig = {
382
+ ...config,
383
+ enabled: false,
384
+ };
385
+
386
+ saveConfig(updatedConfig);
387
+ }
388
+
389
+ /**
390
+ * Check if Infisical is configured and enabled
391
+ */
392
+ export function isConfigured(): boolean {
393
+ const config = loadConfig();
394
+ return config !== null && config.enabled;
395
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Infisical Setup Command Module
3
+ *
4
+ * /setup-infisical - Interactive configuration wizard
5
+ * /setup-infisical status - Show current status
6
+ * /setup-infisical test - Test resolution
7
+ * /setup-infisical refresh - Refresh secrets
8
+ * /setup-infisical disable - Disable integration
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
+ import {
13
+ InfisicalConfig,
14
+ InfisicalSite,
15
+ ServiceName,
16
+ loadConfig,
17
+ saveConfig,
18
+ isConfigured,
19
+ disableIntegration,
20
+ getSiteUrl,
21
+ isValidSiteUrl,
22
+ cleanupOldRevisions,
23
+ } from "./infisical-config.js";
24
+ import {
25
+ SecretProvider,
26
+ getSecretProvider,
27
+ awaitProviderReady,
28
+ } from "./secret-provider.js";
29
+ import {
30
+ getVarlockVersion,
31
+ resolveSecrets,
32
+ } from "./varlock-worker.js";
33
+ import { getDefaultMapping } from "./secret-schema.js";
34
+
35
+ /**
36
+ * Parse command arguments
37
+ */
38
+ function parseArgs(args: string): {
39
+ subcommand: string;
40
+ flags: Record<string, boolean | string>;
41
+ } {
42
+ const parts = args.trim().split(/\s+/);
43
+ const subcommand = parts[0] || "status";
44
+ const flags: Record<string, boolean | string> = {};
45
+
46
+ for (let i = 1; i < parts.length; i++) {
47
+ const part = parts[i]!;
48
+ if (part.startsWith("--")) {
49
+ const flagName = part.slice(2);
50
+ if (i + 1 < parts.length && !parts[i + 1]!.startsWith("--")) {
51
+ flags[flagName] = parts[i + 1]!;
52
+ i++;
53
+ } else {
54
+ flags[flagName] = true;
55
+ }
56
+ }
57
+ }
58
+
59
+ return { subcommand, flags };
60
+ }
61
+
62
+ /**
63
+ * Handle /setup-infisical status
64
+ */
65
+ async function handleStatus(ctx: ExtensionContext): Promise<void> {
66
+ const config = loadConfig();
67
+ const provider = getSecretProvider();
68
+ const state = provider.getStatus();
69
+ const varlockVersion = await getVarlockVersion();
70
+
71
+ const lines: string[] = [
72
+ "# Infisical Status",
73
+ "",
74
+ `**Varlock version:** ${varlockVersion ?? "not found"}`,
75
+ `**Provider status:** ${state.status}`,
76
+ "",
77
+ ];
78
+
79
+ if (!config || !config.enabled) {
80
+ lines.push("Infisical is **not configured**.");
81
+ lines.push("Run `/setup-infisical` to configure.");
82
+ ctx.ui.notify(lines.join("\n"), "info");
83
+ return;
84
+ }
85
+
86
+ lines.push(`**Site:** ${getSiteUrl(config)}`);
87
+ lines.push(`**Project:** ${config.projectId}`);
88
+ lines.push(`**Environment:** ${config.environment}`);
89
+ lines.push(`**Secret path:** ${config.secretPath}`);
90
+ lines.push("");
91
+ lines.push("**Enabled services:**");
92
+
93
+ for (const svc of config.services) {
94
+ const required = svc.required ? "(required)" : "(optional)";
95
+ lines.push(`- ${svc.service} ${required}`);
96
+ }
97
+
98
+ lines.push("");
99
+ lines.push(`**Active revision:** ${config.activeRevision ?? "none"}`);
100
+ lines.push(`**Last resolved:** ${config.lastResolvedAt ?? "never"}`);
101
+
102
+ if (state.lastError) {
103
+ lines.push("");
104
+ lines.push(`**Last error:** ${state.lastError}`);
105
+ }
106
+
107
+ ctx.ui.notify(lines.join("\n"), "info");
108
+ }
109
+
110
+ /**
111
+ * Handle /setup-infisical test
112
+ */
113
+ async function handleTest(ctx: ExtensionContext): Promise<void> {
114
+ ctx.ui.notify("[setup-infisical] Testing Infisical resolution...", "info");
115
+
116
+ const config = loadConfig();
117
+ if (!config || !config.enabled) {
118
+ ctx.ui.notify("[setup-infisical] Infisical not configured", "error");
119
+ return;
120
+ }
121
+
122
+ // Quick test - just check Varlock version and config validity
123
+ const varlockVersion = await getVarlockVersion();
124
+
125
+ if (!varlockVersion) {
126
+ ctx.ui.notify("[setup-infisical] Varlock not found - install with: npm install -g varlock", "error");
127
+ return;
128
+ }
129
+
130
+ ctx.ui.notify(`[setup-infisical] Varlock ${varlockVersion} found`, "info");
131
+ ctx.ui.notify(`[setup-infisical] Project: ${config.projectId}`, "info");
132
+ ctx.ui.notify(`[setup-infisical] Environment: ${config.environment}`, "info");
133
+ ctx.ui.notify("[setup-infisical] Configuration appears valid", "info");
134
+ ctx.ui.notify("Run `/setup-infisical refresh` to resolve secrets", "info");
135
+ }
136
+
137
+ /**
138
+ * Handle /setup-infisical refresh
139
+ */
140
+ async function handleRefresh(ctx: ExtensionContext): Promise<void> {
141
+ ctx.ui.notify("[setup-infisical] Refreshing secrets...", "info");
142
+
143
+ const provider = getSecretProvider();
144
+ const result = await provider.refresh();
145
+
146
+ if (result.success) {
147
+ ctx.ui.notify("[setup-infisical] Secrets refreshed successfully", "info");
148
+ } else {
149
+ ctx.ui.notify(`[setup-infisical] Refresh failed: ${result.error}`, "error");
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handle /setup-infisical disable
155
+ */
156
+ async function handleDisable(ctx: ExtensionContext): Promise<void> {
157
+ ctx.ui.notify("[setup-infisical] Disabling Infisical integration...", "info");
158
+
159
+ disableIntegration();
160
+
161
+ ctx.ui.notify("[setup-infisical] Infisical disabled", "info");
162
+ ctx.ui.notify("Legacy key files will be used instead", "info");
163
+ }
164
+
165
+ /**
166
+ * Handle interactive /setup-infisical wizard
167
+ */
168
+ async function handleSetup(ctx: ExtensionContext): Promise<void> {
169
+ // Check if already configured
170
+ const existing = loadConfig();
171
+ if (existing?.enabled) {
172
+ ctx.ui.notify("Infisical is already configured.", "info");
173
+ ctx.ui.notify("Run `/setup-infisical status` for details", "info");
174
+ ctx.ui.notify("Run `/setup-infisical disable` to remove configuration", "info");
175
+ return;
176
+ }
177
+
178
+ // Interactive setup would use ctx.ui.select/input/confirm
179
+ // For now, show instructions
180
+ const instructions = [
181
+ "# Infisical Setup",
182
+ "",
183
+ "This wizard will help you configure Infisical integration.",
184
+ "",
185
+ "## Prerequisites",
186
+ "",
187
+ "1. Create an Infisical account at https://infisical.com",
188
+ "2. Create a project and note the Project ID",
189
+ "3. Create a Machine Identity with Universal Auth",
190
+ "4. Note the Client ID and Client Secret",
191
+ "",
192
+ "## Configuration steps:",
193
+ "",
194
+ "1. Site: Choose US Cloud (https://app.infisical.com) or EU Cloud",
195
+ "2. Enter Project ID from your Infisical project",
196
+ "3. Enter Environment (e.g., production, development)",
197
+ "4. Enter Secret Path (e.g., /harness)",
198
+ "5. Choose bootstrap method:",
199
+ " - env:INFISICAL_CLIENT_SECRET for environment variable",
200
+ " - file:/path/to/client.secret for file-based bootstrap",
201
+ "6. Select services to configure",
202
+ "",
203
+ "## Quick setup:",
204
+ "",
205
+ "Create ~/.pi-harness-runtime/infisical/config.json manually:",
206
+ "",
207
+ "```json",
208
+ '{',
209
+ ' "schemaVersion": 1,',
210
+ ' "enabled": true,',
211
+ ' "site": "us",',
212
+ ' "projectId": "your-project-id",',
213
+ ' "environment": "production",',
214
+ ' "secretPath": "/harness",',
215
+ ' "authMethod": "universal",',
216
+ ' "bootstrapRef": { "type": "env", "name": "INFISICAL_CLIENT_SECRET" },',
217
+ ' "services": [',
218
+ ' { "service": "jev", "required": true, "keys": { "TYPESAFE_API_KEY": "TYPESAFE_API_KEY" } },',
219
+ ' { "service": "telegram", "required": false, "keys": { "TELEGRAM_BOT_TOKEN": "TELEGRAM_BOT_TOKEN" } }',
220
+ " ],",
221
+ ' "sourcePolicy": { "remoteAuthoritative": true, "envOverride": false },',
222
+ ' "activeRevision": null,',
223
+ ' "lastResolvedAt": null,',
224
+ ' "lastError": null',
225
+ "}",
226
+ "```",
227
+ "",
228
+ "Then run `/setup-infisical refresh` to resolve secrets.",
229
+ ];
230
+
231
+ ctx.ui.notify(instructions.join("\n"), "info");
232
+ }
233
+
234
+ /**
235
+ * Register /setup-infisical command
236
+ */
237
+ export function registerInfisicalSetup(pi: ExtensionAPI): void {
238
+ pi.registerCommand("setup-infisical", {
239
+ description: "Configure Infisical secrets. Usage: /setup-infisical [status|test|refresh|disable]",
240
+ handler: async (args: string, ctx: ExtensionContext): Promise<void> => {
241
+ const { subcommand, flags } = parseArgs(args);
242
+
243
+ switch (subcommand) {
244
+ case "status":
245
+ return handleStatus(ctx);
246
+
247
+ case "test":
248
+ return handleTest(ctx);
249
+
250
+ case "refresh":
251
+ return handleRefresh(ctx);
252
+
253
+ case "disable":
254
+ return handleDisable(ctx);
255
+
256
+ case "setup":
257
+ return handleSetup(ctx);
258
+
259
+ default:
260
+ if (subcommand === "") {
261
+ return handleSetup(ctx);
262
+ }
263
+ ctx.ui.notify(
264
+ `Unknown subcommand: ${subcommand}. Use: status, test, refresh, disable`,
265
+ "warning"
266
+ );
267
+ }
268
+ },
269
+ });
270
+ }