@orgloop/cli 0.1.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 (94) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/commands/add.d.ts +8 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +363 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/apply.d.ts +9 -0
  7. package/dist/commands/apply.d.ts.map +1 -0
  8. package/dist/commands/apply.js +289 -0
  9. package/dist/commands/apply.js.map +1 -0
  10. package/dist/commands/doctor.d.ts +45 -0
  11. package/dist/commands/doctor.d.ts.map +1 -0
  12. package/dist/commands/doctor.js +346 -0
  13. package/dist/commands/doctor.js.map +1 -0
  14. package/dist/commands/env.d.ts +23 -0
  15. package/dist/commands/env.d.ts.map +1 -0
  16. package/dist/commands/env.js +230 -0
  17. package/dist/commands/env.js.map +1 -0
  18. package/dist/commands/hook.d.ts +16 -0
  19. package/dist/commands/hook.d.ts.map +1 -0
  20. package/dist/commands/hook.js +80 -0
  21. package/dist/commands/hook.js.map +1 -0
  22. package/dist/commands/init.d.ts +42 -0
  23. package/dist/commands/init.d.ts.map +1 -0
  24. package/dist/commands/init.js +532 -0
  25. package/dist/commands/init.js.map +1 -0
  26. package/dist/commands/inspect.d.ts +8 -0
  27. package/dist/commands/inspect.d.ts.map +1 -0
  28. package/dist/commands/inspect.js +177 -0
  29. package/dist/commands/inspect.js.map +1 -0
  30. package/dist/commands/install-service.d.ts +8 -0
  31. package/dist/commands/install-service.d.ts.map +1 -0
  32. package/dist/commands/install-service.js +182 -0
  33. package/dist/commands/install-service.js.map +1 -0
  34. package/dist/commands/logs.d.ts +8 -0
  35. package/dist/commands/logs.d.ts.map +1 -0
  36. package/dist/commands/logs.js +197 -0
  37. package/dist/commands/logs.js.map +1 -0
  38. package/dist/commands/plan.d.ts +9 -0
  39. package/dist/commands/plan.d.ts.map +1 -0
  40. package/dist/commands/plan.js +200 -0
  41. package/dist/commands/plan.js.map +1 -0
  42. package/dist/commands/routes.d.ts +37 -0
  43. package/dist/commands/routes.d.ts.map +1 -0
  44. package/dist/commands/routes.js +167 -0
  45. package/dist/commands/routes.js.map +1 -0
  46. package/dist/commands/service.d.ts +9 -0
  47. package/dist/commands/service.d.ts.map +1 -0
  48. package/dist/commands/service.js +178 -0
  49. package/dist/commands/service.js.map +1 -0
  50. package/dist/commands/status.d.ts +8 -0
  51. package/dist/commands/status.d.ts.map +1 -0
  52. package/dist/commands/status.js +176 -0
  53. package/dist/commands/status.js.map +1 -0
  54. package/dist/commands/stop.d.ts +8 -0
  55. package/dist/commands/stop.d.ts.map +1 -0
  56. package/dist/commands/stop.js +102 -0
  57. package/dist/commands/stop.js.map +1 -0
  58. package/dist/commands/test.d.ts +8 -0
  59. package/dist/commands/test.d.ts.map +1 -0
  60. package/dist/commands/test.js +236 -0
  61. package/dist/commands/test.js.map +1 -0
  62. package/dist/commands/validate.d.ts +33 -0
  63. package/dist/commands/validate.d.ts.map +1 -0
  64. package/dist/commands/validate.js +501 -0
  65. package/dist/commands/validate.js.map +1 -0
  66. package/dist/commands/version.d.ts +8 -0
  67. package/dist/commands/version.d.ts.map +1 -0
  68. package/dist/commands/version.js +42 -0
  69. package/dist/commands/version.js.map +1 -0
  70. package/dist/config.d.ts +27 -0
  71. package/dist/config.d.ts.map +1 -0
  72. package/dist/config.js +169 -0
  73. package/dist/config.js.map +1 -0
  74. package/dist/env-metadata.d.ts +12 -0
  75. package/dist/env-metadata.d.ts.map +1 -0
  76. package/dist/env-metadata.js +39 -0
  77. package/dist/env-metadata.js.map +1 -0
  78. package/dist/index.d.ts +8 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +86 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/module-resolver.d.ts +42 -0
  83. package/dist/module-resolver.d.ts.map +1 -0
  84. package/dist/module-resolver.js +134 -0
  85. package/dist/module-resolver.js.map +1 -0
  86. package/dist/output.d.ts +36 -0
  87. package/dist/output.d.ts.map +1 -0
  88. package/dist/output.js +142 -0
  89. package/dist/output.js.map +1 -0
  90. package/dist/resolve-connectors.d.ts +27 -0
  91. package/dist/resolve-connectors.d.ts.map +1 -0
  92. package/dist/resolve-connectors.js +94 -0
  93. package/dist/resolve-connectors.js.map +1 -0
  94. package/package.json +51 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * orgloop hook — Forward hook events to a running OrgLoop engine.
3
+ *
4
+ * Reads stdin and POSTs the raw JSON body to the engine's webhook endpoint.
5
+ * This is a stdin-to-HTTP bridge — the connector's webhook handler builds
6
+ * the OrgLoopEvent from the raw payload.
7
+ */
8
+ // ─── Constants ──────────────────────────────────────────────────────────────
9
+ const DEFAULT_PORT = 4800;
10
+ // ─── Helpers ────────────────────────────────────────────────────────────────
11
+ function resolvePort(portFlag) {
12
+ if (portFlag) {
13
+ const n = Number.parseInt(portFlag, 10);
14
+ if (!Number.isNaN(n) && n > 0 && n <= 65535)
15
+ return n;
16
+ }
17
+ const envPort = process.env.ORGLOOP_PORT;
18
+ if (envPort) {
19
+ const n = Number.parseInt(envPort, 10);
20
+ if (!Number.isNaN(n) && n > 0 && n <= 65535)
21
+ return n;
22
+ }
23
+ return DEFAULT_PORT;
24
+ }
25
+ export function readStdin() {
26
+ return new Promise((resolve, reject) => {
27
+ let data = '';
28
+ process.stdin.setEncoding('utf-8');
29
+ process.stdin.on('data', (chunk) => {
30
+ data += chunk;
31
+ });
32
+ process.stdin.on('end', () => resolve(data));
33
+ process.stdin.on('error', reject);
34
+ });
35
+ }
36
+ export async function postToWebhook(sourceId, body, port) {
37
+ const url = `http://127.0.0.1:${port}/webhook/${sourceId}`;
38
+ const res = await fetch(url, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body,
42
+ });
43
+ const text = await res.text();
44
+ return { ok: res.ok, status: res.status, body: text };
45
+ }
46
+ // ─── Command registration ────────────────────────────────────────────────────
47
+ export function registerHookCommand(program) {
48
+ const hook = program.command('hook').description('Forward hook events to running OrgLoop engine');
49
+ hook
50
+ .command('claude-code-stop')
51
+ .description('Forward Claude Code stop hook event')
52
+ .option('--port <port>', 'Engine webhook port')
53
+ .action(async (opts) => {
54
+ const port = resolvePort(opts.port);
55
+ let body;
56
+ try {
57
+ body = await readStdin();
58
+ }
59
+ catch {
60
+ process.stderr.write('orgloop hook: failed to read stdin\n');
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ try {
65
+ const result = await postToWebhook('claude-code', body, port);
66
+ if (!result.ok) {
67
+ process.stderr.write(`orgloop hook: webhook returned ${result.status}: ${result.body}\n`);
68
+ process.exitCode = 1;
69
+ }
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error && 'code' in err && err.code === 'ECONNREFUSED'
73
+ ? 'OrgLoop engine is not running. Start it with: orgloop apply'
74
+ : `Failed to deliver hook event: ${err instanceof Error ? err.message : String(err)}`;
75
+ process.stderr.write(`orgloop hook: ${msg}\n`);
76
+ process.exitCode = 1;
77
+ }
78
+ });
79
+ }
80
+ //# sourceMappingURL=hook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook.js","sourceRoot":"","sources":["../../src/commands/hook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * orgloop init — Scaffold a new OrgLoop project.
3
+ *
4
+ * Interactive mode (default): prompts for project name, description, connectors.
5
+ * Non-interactive: --name, --connectors, --no-interactive flags.
6
+ */
7
+ import type { Command } from 'commander';
8
+ /**
9
+ * Scan connector YAML for ${VAR} references and return a map of
10
+ * var name → connector file that requires it.
11
+ */
12
+ export declare function collectEnvVars(connectors: string[]): Map<string, string>;
13
+ /**
14
+ * Build the contents for a .env.example file from collected env vars.
15
+ */
16
+ export declare function buildEnvExampleContent(envVars: Map<string, string>): string;
17
+ /**
18
+ * Build a Claude Code Stop hook entry in the object format expected by
19
+ * Claude Code's settings.json.
20
+ */
21
+ export declare function buildClaudeCodeHookEntry(command: string): {
22
+ matcher: string;
23
+ hooks: {
24
+ type: string;
25
+ command: string;
26
+ }[];
27
+ };
28
+ /**
29
+ * Check whether a Stop hooks array already contains an orgloop hook.
30
+ * Handles the object format: [{ matcher, hooks: [{ type, command }] }].
31
+ */
32
+ export declare function hasExistingOrgloopHook(stopHooks: unknown[]): boolean;
33
+ /**
34
+ * Merge an orgloop hook into a settings object. Returns the updated settings
35
+ * and a boolean indicating whether the hook was already present.
36
+ */
37
+ export declare function mergeClaudeCodeHook(settings: Record<string, unknown>, hookCommand: string): {
38
+ settings: Record<string, unknown>;
39
+ alreadyInstalled: boolean;
40
+ };
41
+ export declare function registerInitCommand(program: Command): void;
42
+ //# sourceMappingURL=init.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyOzC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAgB3E;AAiGD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM;;;;;;EAKvD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,OAAO,CAOpE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,EAAE,MAAM,GACjB;IAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,CAYlE;AA0DD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyH1D"}
@@ -0,0 +1,532 @@
1
+ /**
2
+ * orgloop init — Scaffold a new OrgLoop project.
3
+ *
4
+ * Interactive mode (default): prompts for project name, description, connectors.
5
+ * Non-interactive: --name, --connectors, --no-interactive flags.
6
+ */
7
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
8
+ import { homedir } from 'node:os';
9
+ import { join, resolve } from 'node:path';
10
+ import chalk from 'chalk';
11
+ import { getEnvVarMeta } from '../env-metadata.js';
12
+ import * as output from '../output.js';
13
+ // ─── Connectors ──────────────────────────────────────────────────────────────
14
+ const AVAILABLE_CONNECTORS = [
15
+ 'github',
16
+ 'linear',
17
+ 'openclaw',
18
+ 'claude-code',
19
+ 'webhook',
20
+ 'slack',
21
+ 'pagerduty',
22
+ ];
23
+ function connectorYaml(name, role) {
24
+ const configs = {
25
+ github: `apiVersion: orgloop/v1alpha1
26
+ kind: ConnectorGroup
27
+
28
+ sources:
29
+ - id: github
30
+ description: GitHub repository events
31
+ connector: "@orgloop/connector-github"
32
+ config:
33
+ repo: "\${GITHUB_REPO}"
34
+ token: "\${GITHUB_TOKEN}"
35
+ events:
36
+ - "pull_request.review_submitted"
37
+ - "pull_request_review_comment"
38
+ - "issue_comment"
39
+ - "pull_request.closed"
40
+ - "pull_request.merged"
41
+ - "workflow_run.completed"
42
+ poll:
43
+ interval: "5m"
44
+ emits:
45
+ - resource.changed
46
+ `,
47
+ linear: `apiVersion: orgloop/v1alpha1
48
+ kind: ConnectorGroup
49
+
50
+ sources:
51
+ - id: linear
52
+ description: Linear project tracking events
53
+ connector: "@orgloop/connector-linear"
54
+ config:
55
+ team: "\${LINEAR_TEAM_KEY}"
56
+ api_key: "\${LINEAR_API_KEY}"
57
+ poll:
58
+ interval: "5m"
59
+ emits:
60
+ - resource.changed`,
61
+ openclaw: `apiVersion: orgloop/v1alpha1
62
+ kind: ConnectorGroup
63
+
64
+ actors:
65
+ - id: openclaw-engineering-agent
66
+ description: OpenClaw engineering agent
67
+ connector: "@orgloop/connector-openclaw"
68
+ config:
69
+ base_url: "http://127.0.0.1:18789"
70
+ auth_token_env: "\${OPENCLAW_WEBHOOK_TOKEN}"
71
+ agent_id: "\${OPENCLAW_AGENT_ID}"`,
72
+ 'claude-code': `apiVersion: orgloop/v1alpha1
73
+ kind: ConnectorGroup
74
+
75
+ sources:
76
+ - id: claude-code
77
+ description: Claude Code session events
78
+ connector: "@orgloop/connector-claude-code"
79
+ config:
80
+ hook_type: post-exit
81
+ emits:
82
+ - actor.stopped`,
83
+ webhook: `apiVersion: orgloop/v1alpha1
84
+ kind: ConnectorGroup
85
+
86
+ sources:
87
+ - id: webhook
88
+ description: Generic webhook receiver
89
+ connector: "@orgloop/connector-webhook"
90
+ config:
91
+ path: "/webhook"
92
+ emits:
93
+ - resource.changed
94
+ - message.received`,
95
+ slack: `apiVersion: orgloop/v1alpha1
96
+ kind: ConnectorGroup
97
+
98
+ actors:
99
+ - id: slack-notify
100
+ description: Slack notification delivery
101
+ connector: "@orgloop/connector-webhook"
102
+ config:
103
+ url: "\${SLACK_WEBHOOK_URL}"`,
104
+ pagerduty: `apiVersion: orgloop/v1alpha1
105
+ kind: ConnectorGroup
106
+
107
+ actors:
108
+ - id: pagerduty
109
+ description: PagerDuty incident delivery
110
+ connector: "@orgloop/connector-webhook"
111
+ config:
112
+ url: "\${PAGERDUTY_WEBHOOK_URL}"`,
113
+ };
114
+ return configs[name] ?? configs.webhook;
115
+ }
116
+ function generateOrgloopYaml(name, description, connectors) {
117
+ const connectorRefs = connectors.map((c) => ` - connectors/${c}.yaml`).join('\n');
118
+ return `apiVersion: orgloop/v1alpha1
119
+ kind: Project
120
+
121
+ metadata:
122
+ name: ${name}
123
+ description: "${description}"
124
+
125
+ defaults:
126
+ poll_interval: "5m"
127
+ event_retention: "30d"
128
+ log_level: info
129
+
130
+ connectors:
131
+ ${connectorRefs}
132
+
133
+ transforms:
134
+ - transforms/transforms.yaml
135
+
136
+ loggers:
137
+ - loggers/default.yaml
138
+ `;
139
+ }
140
+ function generateDefaultRouteYaml() {
141
+ return `apiVersion: orgloop/v1alpha1
142
+ kind: RouteGroup
143
+
144
+ routes:
145
+ - name: example-route
146
+ description: Example route — customize for your setup
147
+ when:
148
+ source: github
149
+ events:
150
+ - resource.changed
151
+ transforms:
152
+ - ref: drop-bot-noise
153
+ then:
154
+ actor: openclaw-engineering-agent
155
+ with:
156
+ prompt_file: ../sops/example.md
157
+ `;
158
+ }
159
+ function generateDefaultTransformsYaml() {
160
+ return `apiVersion: orgloop/v1alpha1
161
+ kind: TransformGroup
162
+
163
+ transforms:
164
+ - name: drop-bot-noise
165
+ type: script
166
+ script: ./drop-bot-noise.sh
167
+ timeout_ms: 5000
168
+ `;
169
+ }
170
+ function generateDropBotScript() {
171
+ return `#!/usr/bin/env bash
172
+ # drop-bot-noise.sh — Drop events from known bot authors.
173
+ #
174
+ # Reads OrgLoop event JSON from stdin.
175
+ # Exit 0 = PASS (forward event), Exit 78 = DROP (discard event).
176
+
177
+ set -euo pipefail
178
+
179
+ EVENT=$(cat)
180
+ AUTHOR_TYPE=$(echo "$EVENT" | jq -r '.provenance.author_type // "unknown"')
181
+
182
+ if [ "$AUTHOR_TYPE" = "bot" ]; then
183
+ exit 78 # DROP
184
+ fi
185
+
186
+ # PASS — forward the event unchanged
187
+ echo "$EVENT"
188
+ exit 0
189
+ `;
190
+ }
191
+ function generateDefaultLoggerYaml() {
192
+ return `apiVersion: orgloop/v1alpha1
193
+ kind: LoggerGroup
194
+
195
+ loggers:
196
+ - name: file-log
197
+ type: "@orgloop/logger-file"
198
+ config:
199
+ path: "~/.orgloop/logs/orgloop.log"
200
+ format: jsonl
201
+ rotate:
202
+ max_size: "50MB"
203
+ max_files: 10
204
+ `;
205
+ }
206
+ function generateExampleSop() {
207
+ return `# Example Launch Prompt
208
+
209
+ You are receiving an event from the organization pipeline.
210
+
211
+ ## Context
212
+ This event was routed through OrgLoop based on the configured rules.
213
+
214
+ ## Instructions
215
+ 1. Review the event payload
216
+ 2. Take appropriate action based on the event type
217
+ 3. Report completion status
218
+
219
+ ## Constraints
220
+ - Do not modify production infrastructure without approval
221
+ - Follow the organization's coding standards
222
+ - Escalate security-related events immediately
223
+ `;
224
+ }
225
+ // ─── Env var collection ──────────────────────────────────────────────────────
226
+ /**
227
+ * Scan connector YAML for ${VAR} references and return a map of
228
+ * var name → connector file that requires it.
229
+ */
230
+ export function collectEnvVars(connectors) {
231
+ const envVars = new Map();
232
+ for (const conn of connectors) {
233
+ const yamlContent = connectorYaml(conn, ['openclaw', 'slack', 'pagerduty'].includes(conn) ? 'actor' : 'source');
234
+ const matches = yamlContent.matchAll(/\$\{([^}]+)\}/g);
235
+ for (const match of matches) {
236
+ envVars.set(match[1], `connectors/${conn}.yaml`);
237
+ }
238
+ }
239
+ return envVars;
240
+ }
241
+ /**
242
+ * Build the contents for a .env.example file from collected env vars.
243
+ */
244
+ export function buildEnvExampleContent(envVars) {
245
+ const lines = [
246
+ '# OrgLoop environment variables',
247
+ '# Copy to .env and fill in values',
248
+ '',
249
+ ];
250
+ for (const [varName] of envVars) {
251
+ const meta = getEnvVarMeta(varName);
252
+ if (meta) {
253
+ lines.push(`# ${meta.description}`);
254
+ if (meta.help_url)
255
+ lines.push(`# ${meta.help_url}`);
256
+ }
257
+ lines.push(`# ${varName}=`);
258
+ lines.push('');
259
+ }
260
+ return lines.join('\n');
261
+ }
262
+ // ─── File creation ───────────────────────────────────────────────────────────
263
+ async function dirExists(path) {
264
+ try {
265
+ await access(path);
266
+ return true;
267
+ }
268
+ catch {
269
+ return false;
270
+ }
271
+ }
272
+ async function scaffoldProject(targetDir, name, description, connectors) {
273
+ const created = [];
274
+ // Create directories
275
+ await mkdir(join(targetDir, 'connectors'), { recursive: true });
276
+ await mkdir(join(targetDir, 'routes'), { recursive: true });
277
+ await mkdir(join(targetDir, 'transforms'), { recursive: true });
278
+ await mkdir(join(targetDir, 'loggers'), { recursive: true });
279
+ await mkdir(join(targetDir, 'sops'), { recursive: true });
280
+ // orgloop.yaml
281
+ const orgloopPath = join(targetDir, 'orgloop.yaml');
282
+ await writeFile(orgloopPath, generateOrgloopYaml(name, description, connectors), 'utf-8');
283
+ created.push('orgloop.yaml');
284
+ // Connector files
285
+ for (const conn of connectors) {
286
+ const connPath = join(targetDir, 'connectors', `${conn}.yaml`);
287
+ const role = ['openclaw', 'slack', 'pagerduty'].includes(conn) ? 'actor' : 'source';
288
+ await writeFile(connPath, connectorYaml(conn, role), 'utf-8');
289
+ created.push(`connectors/${conn}.yaml`);
290
+ }
291
+ // Route files
292
+ const routePath = join(targetDir, 'routes', 'example.yaml');
293
+ await writeFile(routePath, generateDefaultRouteYaml(), 'utf-8');
294
+ created.push('routes/example.yaml');
295
+ // Logger files
296
+ const loggerPath = join(targetDir, 'loggers', 'default.yaml');
297
+ await writeFile(loggerPath, generateDefaultLoggerYaml(), 'utf-8');
298
+ created.push('loggers/default.yaml');
299
+ // Transform files
300
+ const transformsYamlPath = join(targetDir, 'transforms', 'transforms.yaml');
301
+ await writeFile(transformsYamlPath, generateDefaultTransformsYaml(), 'utf-8');
302
+ created.push('transforms/transforms.yaml');
303
+ const scriptPath = join(targetDir, 'transforms', 'drop-bot-noise.sh');
304
+ await writeFile(scriptPath, generateDropBotScript(), 'utf-8');
305
+ const { chmod } = await import('node:fs/promises');
306
+ await chmod(scriptPath, 0o755);
307
+ created.push('transforms/drop-bot-noise.sh');
308
+ // SOP files
309
+ const sopPath = join(targetDir, 'sops', 'example.md');
310
+ await writeFile(sopPath, generateExampleSop(), 'utf-8');
311
+ created.push('sops/example.md');
312
+ // Generate .env.example
313
+ const envVars = collectEnvVars(connectors);
314
+ if (envVars.size > 0) {
315
+ await writeFile(join(targetDir, '.env.example'), buildEnvExampleContent(envVars), 'utf-8');
316
+ created.push('.env.example');
317
+ }
318
+ // Generate .gitignore (only if it doesn't already exist)
319
+ const gitignorePath = join(targetDir, '.gitignore');
320
+ if (!(await dirExists(gitignorePath))) {
321
+ const gitignoreContent = `# Environment variables (contains secrets)
322
+ .env
323
+ .env.local
324
+
325
+ # OrgLoop runtime
326
+ .orgloop/
327
+
328
+ # Node
329
+ node_modules/
330
+ dist/
331
+ `;
332
+ await writeFile(gitignorePath, gitignoreContent, 'utf-8');
333
+ created.push('.gitignore');
334
+ }
335
+ return created;
336
+ }
337
+ // ─── Claude Code hook helpers (exported for testing) ─────────────────────────
338
+ /**
339
+ * Build a Claude Code Stop hook entry in the object format expected by
340
+ * Claude Code's settings.json.
341
+ */
342
+ export function buildClaudeCodeHookEntry(command) {
343
+ return {
344
+ matcher: '',
345
+ hooks: [{ type: 'command', command }],
346
+ };
347
+ }
348
+ /**
349
+ * Check whether a Stop hooks array already contains an orgloop hook.
350
+ * Handles the object format: [{ matcher, hooks: [{ type, command }] }].
351
+ */
352
+ export function hasExistingOrgloopHook(stopHooks) {
353
+ return stopHooks.some((entry) => {
354
+ if (typeof entry !== 'object' || entry === null)
355
+ return false;
356
+ const obj = entry;
357
+ const innerHooks = obj.hooks;
358
+ return innerHooks?.some((h) => typeof h.command === 'string' && h.command.includes('orgloop'));
359
+ });
360
+ }
361
+ /**
362
+ * Merge an orgloop hook into a settings object. Returns the updated settings
363
+ * and a boolean indicating whether the hook was already present.
364
+ */
365
+ export function mergeClaudeCodeHook(settings, hookCommand) {
366
+ const hooks = (settings.hooks ?? {});
367
+ const stopHooks = (hooks.Stop ?? []);
368
+ if (hasExistingOrgloopHook(stopHooks)) {
369
+ return { settings, alreadyInstalled: true };
370
+ }
371
+ stopHooks.push(buildClaudeCodeHookEntry(hookCommand));
372
+ hooks.Stop = stopHooks;
373
+ settings.hooks = hooks;
374
+ return { settings, alreadyInstalled: false };
375
+ }
376
+ // ─── Claude Code hook onboarding ──────────────────────────────────────────────
377
+ async function promptClaudeCodeHook() {
378
+ const { default: inquirer } = await import('inquirer');
379
+ const { scope } = await inquirer.prompt([
380
+ {
381
+ type: 'list',
382
+ name: 'scope',
383
+ message: 'Install OrgLoop hook to Claude Code settings?',
384
+ choices: [
385
+ { name: 'Global (~/.claude/settings.json)', value: 'global' },
386
+ { name: 'Project (.claude/settings.json)', value: 'project' },
387
+ { name: 'Skip', value: 'skip' },
388
+ ],
389
+ },
390
+ ]);
391
+ if (scope === 'skip')
392
+ return;
393
+ const settingsPath = scope === 'global'
394
+ ? join(homedir(), '.claude', 'settings.json')
395
+ : join(process.cwd(), '.claude', 'settings.json');
396
+ const hookCommand = 'orgloop hook claude-code-stop';
397
+ try {
398
+ let settings = {};
399
+ try {
400
+ const content = await readFile(settingsPath, 'utf-8');
401
+ settings = JSON.parse(content);
402
+ }
403
+ catch {
404
+ // File doesn't exist yet
405
+ }
406
+ const result = mergeClaudeCodeHook(settings, hookCommand);
407
+ if (result.alreadyInstalled) {
408
+ output.info(' OrgLoop hook already installed in Claude Code settings.');
409
+ return;
410
+ }
411
+ await mkdir(join(settingsPath, '..'), { recursive: true });
412
+ await writeFile(settingsPath, `${JSON.stringify(result.settings, null, 2)}\n`, 'utf-8');
413
+ output.success(` Installed Claude Code Stop hook → ${chalk.dim(settingsPath)}`);
414
+ }
415
+ catch (err) {
416
+ output.warn(` Could not install hook: ${err instanceof Error ? err.message : String(err)}`);
417
+ output.info(` Manually add to ${settingsPath}:`);
418
+ output.info(` "hooks": { "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "${hookCommand}" }] }] }`);
419
+ }
420
+ }
421
+ // ─── Command registration ────────────────────────────────────────────────────
422
+ export function registerInitCommand(program) {
423
+ program
424
+ .command('init')
425
+ .description('Scaffold a new OrgLoop project')
426
+ .option('--name <name>', 'Project name')
427
+ .option('--description <desc>', 'Project description')
428
+ .option('--connectors <list>', 'Comma-separated connector list')
429
+ .option('--no-interactive', 'Disable interactive prompts')
430
+ .option('--dir <path>', 'Target directory (default: current directory)')
431
+ .action(async (opts) => {
432
+ try {
433
+ const targetDir = opts.dir ? resolve(opts.dir) : process.cwd();
434
+ let name;
435
+ let description;
436
+ let connectors;
437
+ if (opts.interactive === false) {
438
+ // Non-interactive mode
439
+ name = opts.name ?? 'my-org';
440
+ description = opts.description ?? 'OrgLoop project';
441
+ connectors = opts.connectors
442
+ ? opts.connectors.split(',').map((c) => c.trim())
443
+ : ['github'];
444
+ }
445
+ else {
446
+ // Interactive mode
447
+ const { default: inquirer } = await import('inquirer');
448
+ const answers = await inquirer.prompt([
449
+ {
450
+ type: 'input',
451
+ name: 'name',
452
+ message: 'Project name:',
453
+ default: opts.name ?? 'my-org',
454
+ },
455
+ {
456
+ type: 'input',
457
+ name: 'description',
458
+ message: 'Description:',
459
+ default: opts.description ?? 'Organization event routing',
460
+ },
461
+ {
462
+ type: 'checkbox',
463
+ name: 'connectors',
464
+ message: 'Which connectors?',
465
+ choices: AVAILABLE_CONNECTORS.map((c) => ({
466
+ name: c.charAt(0).toUpperCase() + c.slice(1),
467
+ value: c,
468
+ checked: ['github', 'linear', 'openclaw', 'claude-code'].includes(c),
469
+ })),
470
+ },
471
+ ]);
472
+ name = answers.name;
473
+ description = answers.description;
474
+ connectors = answers.connectors;
475
+ }
476
+ // Validate connectors
477
+ for (const c of connectors) {
478
+ if (!AVAILABLE_CONNECTORS.includes(c)) {
479
+ output.error(`Unknown connector: ${c}`);
480
+ output.info(`Available: ${AVAILABLE_CONNECTORS.join(', ')}`);
481
+ process.exitCode = 1;
482
+ return;
483
+ }
484
+ }
485
+ // Check for existing orgloop.yaml
486
+ if (await dirExists(join(targetDir, 'orgloop.yaml'))) {
487
+ output.error('orgloop.yaml already exists in this directory.');
488
+ output.info('Use a different directory or remove the existing file.');
489
+ process.exitCode = 1;
490
+ return;
491
+ }
492
+ const created = await scaffoldProject(targetDir, name, description, connectors);
493
+ output.blank();
494
+ output.heading('Created:');
495
+ for (const file of created) {
496
+ output.info(` ${file}`);
497
+ }
498
+ // Show env var status with ✓/✗ indicators
499
+ const envVars = collectEnvVars(connectors);
500
+ if (envVars.size > 0) {
501
+ output.blank();
502
+ output.heading('Environment variables:');
503
+ for (const [varName, file] of envVars) {
504
+ const isSet = process.env[varName] !== undefined;
505
+ const icon = isSet ? chalk.green('✓') : chalk.red('✗');
506
+ output.info(` ${icon} ${chalk.yellow(varName.padEnd(22))} ${chalk.dim(file)}`);
507
+ if (!isSet) {
508
+ const meta = getEnvVarMeta(varName);
509
+ if (meta) {
510
+ output.info(` ${chalk.dim('\u2192')} ${meta.description}`);
511
+ if (meta.help_url) {
512
+ output.info(` ${chalk.dim('\u2192')} ${chalk.cyan(meta.help_url)}`);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ }
518
+ // Claude Code hook onboarding
519
+ if (connectors.includes('claude-code') && opts.interactive !== false) {
520
+ output.blank();
521
+ await promptClaudeCodeHook();
522
+ }
523
+ output.blank();
524
+ output.info(chalk.dim('Next: run `orgloop add module <name>` to install a workflow module, or `orgloop doctor` to check your environment.'));
525
+ }
526
+ catch (err) {
527
+ output.error(`Init failed: ${err instanceof Error ? err.message : String(err)}`);
528
+ process.exitCode = 1;
529
+ }
530
+ });
531
+ }
532
+ //# sourceMappingURL=init.js.map