skrypt-ai 0.6.0 → 0.7.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 (88) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/auth/index.js +3 -1
  12. package/dist/cli.js +11 -1
  13. package/dist/commands/audit.d.ts +2 -0
  14. package/dist/commands/audit.js +59 -0
  15. package/dist/commands/config.d.ts +2 -0
  16. package/dist/commands/config.js +73 -0
  17. package/dist/commands/cron.js +4 -0
  18. package/dist/commands/generate.d.ts +7 -0
  19. package/dist/commands/generate.js +528 -234
  20. package/dist/commands/refresh.d.ts +2 -0
  21. package/dist/commands/refresh.js +158 -0
  22. package/dist/commands/review-pr.js +5 -0
  23. package/dist/commands/review.d.ts +2 -0
  24. package/dist/commands/review.js +110 -0
  25. package/dist/commands/test.js +177 -236
  26. package/dist/commands/watch.js +29 -20
  27. package/dist/config/loader.d.ts +6 -1
  28. package/dist/config/loader.js +38 -2
  29. package/dist/config/types.d.ts +7 -0
  30. package/dist/generator/generator.js +2 -1
  31. package/dist/generator/types.d.ts +3 -0
  32. package/dist/generator/writer.js +60 -28
  33. package/dist/github/org-discovery.d.ts +17 -0
  34. package/dist/github/org-discovery.js +93 -0
  35. package/dist/llm/index.d.ts +2 -0
  36. package/dist/llm/index.js +8 -2
  37. package/dist/next-actions/actions.d.ts +2 -0
  38. package/dist/next-actions/actions.js +190 -0
  39. package/dist/next-actions/index.d.ts +6 -0
  40. package/dist/next-actions/index.js +39 -0
  41. package/dist/next-actions/setup.d.ts +2 -0
  42. package/dist/next-actions/setup.js +72 -0
  43. package/dist/next-actions/state.d.ts +7 -0
  44. package/dist/next-actions/state.js +68 -0
  45. package/dist/next-actions/suggest.d.ts +3 -0
  46. package/dist/next-actions/suggest.js +47 -0
  47. package/dist/next-actions/types.d.ts +26 -0
  48. package/dist/next-actions/types.js +1 -0
  49. package/dist/refresh/differ.d.ts +9 -0
  50. package/dist/refresh/differ.js +67 -0
  51. package/dist/refresh/index.d.ts +4 -0
  52. package/dist/refresh/index.js +4 -0
  53. package/dist/refresh/manifest.d.ts +18 -0
  54. package/dist/refresh/manifest.js +71 -0
  55. package/dist/refresh/splicer.d.ts +9 -0
  56. package/dist/refresh/splicer.js +50 -0
  57. package/dist/refresh/types.d.ts +37 -0
  58. package/dist/refresh/types.js +1 -0
  59. package/dist/review/index.d.ts +8 -0
  60. package/dist/review/index.js +94 -0
  61. package/dist/review/parser.d.ts +16 -0
  62. package/dist/review/parser.js +95 -0
  63. package/dist/review/types.d.ts +18 -0
  64. package/dist/review/types.js +1 -0
  65. package/dist/scanner/types.d.ts +2 -0
  66. package/dist/structure/index.d.ts +19 -0
  67. package/dist/structure/index.js +92 -0
  68. package/dist/structure/planner.d.ts +8 -0
  69. package/dist/structure/planner.js +180 -0
  70. package/dist/structure/topology.d.ts +16 -0
  71. package/dist/structure/topology.js +49 -0
  72. package/dist/structure/types.d.ts +26 -0
  73. package/dist/structure/types.js +1 -0
  74. package/dist/testing/comparator.d.ts +7 -0
  75. package/dist/testing/comparator.js +77 -0
  76. package/dist/testing/docker.d.ts +21 -0
  77. package/dist/testing/docker.js +234 -0
  78. package/dist/testing/env.d.ts +16 -0
  79. package/dist/testing/env.js +58 -0
  80. package/dist/testing/extractor.d.ts +9 -0
  81. package/dist/testing/extractor.js +195 -0
  82. package/dist/testing/index.d.ts +6 -0
  83. package/dist/testing/index.js +6 -0
  84. package/dist/testing/runner.d.ts +5 -0
  85. package/dist/testing/runner.js +225 -0
  86. package/dist/testing/types.d.ts +58 -0
  87. package/dist/testing/types.js +1 -0
  88. package/package.json +1 -1
@@ -61,6 +61,7 @@ function mergeConfig(defaults, overrides) {
61
61
  ...defaults.source,
62
62
  ...overrides.source
63
63
  },
64
+ sources: overrides.sources,
64
65
  output: {
65
66
  ...defaults.output,
66
67
  ...overrides.output
@@ -74,10 +75,25 @@ function mergeConfig(defaults, overrides) {
74
75
  }
75
76
  export function validateConfig(config) {
76
77
  const errors = [];
77
- if (config.version !== 1) {
78
+ if (config.version !== 1 && config.version !== 2) {
78
79
  errors.push(`Unsupported config version: ${config.version}`);
79
80
  }
80
- if (!config.source.path) {
81
+ // Validate sources array if present (v2)
82
+ if (config.sources && config.sources.length > 0) {
83
+ const labels = new Set();
84
+ for (const entry of config.sources) {
85
+ if (!entry.path) {
86
+ errors.push('Each source entry must have a path');
87
+ }
88
+ if (entry.label) {
89
+ if (labels.has(entry.label)) {
90
+ errors.push(`Duplicate source label: "${entry.label}"`);
91
+ }
92
+ labels.add(entry.label);
93
+ }
94
+ }
95
+ }
96
+ else if (!config.source.path) {
81
97
  errors.push('source.path is required');
82
98
  }
83
99
  if (!config.output.path) {
@@ -88,6 +104,26 @@ export function validateConfig(config) {
88
104
  }
89
105
  return errors;
90
106
  }
107
+ /**
108
+ * Resolve v1 `source` or v2 `sources` into a normalized array of SourceEntry.
109
+ * Falls back to the single source config if no sources array is defined.
110
+ */
111
+ export function resolveSourceEntries(config) {
112
+ if (config.sources && config.sources.length > 0) {
113
+ return config.sources.map(s => ({
114
+ path: s.path,
115
+ label: s.label,
116
+ include: s.include ?? config.source.include,
117
+ exclude: s.exclude ?? config.source.exclude,
118
+ }));
119
+ }
120
+ // Backwards compat: single source
121
+ return [{
122
+ path: config.source.path,
123
+ include: config.source.include,
124
+ exclude: config.source.exclude,
125
+ }];
126
+ }
91
127
  export function checkApiKey(provider) {
92
128
  const envKey = PROVIDER_ENV_KEYS[provider];
93
129
  // Ollama doesn't need an API key
@@ -3,6 +3,12 @@ export interface SourceConfig {
3
3
  include?: string[];
4
4
  exclude?: string[];
5
5
  }
6
+ export interface SourceEntry {
7
+ path: string;
8
+ label?: string;
9
+ include?: string[];
10
+ exclude?: string[];
11
+ }
6
12
  export type LLMProvider = 'deepseek' | 'openai' | 'anthropic' | 'google' | 'ollama' | 'openrouter';
7
13
  export interface LLMConfig {
8
14
  provider: LLMProvider;
@@ -14,6 +20,7 @@ export declare const DEFAULT_MODELS: Record<LLMProvider, string>;
14
20
  export interface Config {
15
21
  version: number;
16
22
  source: SourceConfig;
23
+ sources?: SourceEntry[];
17
24
  output: {
18
25
  path: string;
19
26
  format: 'markdown' | 'mdx';
@@ -68,7 +68,8 @@ export async function generateForElement(element, client, options, onProgress) {
68
68
  report('generating');
69
69
  try {
70
70
  const elementContext = buildElementContext(element, options.externalContext, options.projectContext);
71
- const result = await generateDocumentation(client, elementContext, { multiLanguage: useMultiLang });
71
+ const errorContext = options.previousErrors?.get(element.name);
72
+ const result = await generateDocumentation(client, elementContext, { multiLanguage: useMultiLang, verify: options.verify, previousError: errorContext });
72
73
  report('done');
73
74
  return {
74
75
  element,
@@ -33,6 +33,9 @@ export interface GenerationOptions {
33
33
  externalContext?: Map<string, string>;
34
34
  maxTokens?: number;
35
35
  projectContext?: string;
36
+ verify?: boolean;
37
+ /** Error context from previous verification failures, keyed by element name */
38
+ previousErrors?: Map<string, string>;
36
39
  }
37
40
  /**
38
41
  * Result of generating docs for a file
@@ -13,36 +13,45 @@ export async function writeLlmsTxt(docs, outputDir, options = {}) {
13
13
  const description = options.description || 'API documentation generated by skrypt';
14
14
  let content = `# ${projectName}\n\n`;
15
15
  content += `> ${description}\n\n`;
16
- // Group by file for organization
17
- const byFile = new Map();
16
+ // Check if docs have source labels (multi-source mode)
17
+ const hasSourceLabels = docs.some(d => d.element.sourceLabel);
18
+ // Group by source label then by file
19
+ const bySource = new Map();
18
20
  for (const doc of docs) {
21
+ const sourceKey = doc.element.sourceLabel || '_default';
22
+ if (!bySource.has(sourceKey))
23
+ bySource.set(sourceKey, new Map());
24
+ const sourceMap = bySource.get(sourceKey);
19
25
  const file = basename(doc.element.filePath).replace(/\.[^.]+$/, '');
20
- if (!byFile.has(file)) {
21
- byFile.set(file, []);
22
- }
23
- const fileDocs = byFile.get(file);
24
- if (fileDocs)
25
- fileDocs.push(doc);
26
+ if (!sourceMap.has(file))
27
+ sourceMap.set(file, []);
28
+ sourceMap.get(file).push(doc);
26
29
  }
30
+ const totalModules = Array.from(bySource.values()).reduce((sum, m) => sum + m.size, 0);
27
31
  // Summary section
28
32
  content += `## Overview\n\n`;
29
- content += `This project contains ${docs.length} documented API elements across ${byFile.size} modules.\n\n`;
33
+ content += `This project contains ${docs.length} documented API elements across ${totalModules} modules.\n\n`;
30
34
  // Quick reference
31
35
  content += `## Quick Reference\n\n`;
32
- for (const [file, fileDocs] of byFile) {
33
- content += `### ${file}\n\n`;
34
- for (const doc of fileDocs) {
35
- content += `- \`${doc.element.name}\`: ${doc.element.kind}`;
36
- if (doc.markdown) {
37
- // Extract first sentence as summary
38
- const firstSentence = doc.markdown.split(/\.\s/)[0]?.slice(0, 100);
39
- if (firstSentence) {
40
- content += ` - ${firstSentence}`;
36
+ for (const [sourceLabel, byFile] of bySource) {
37
+ if (hasSourceLabels && sourceLabel !== '_default') {
38
+ content += `### ${sourceLabel}\n\n`;
39
+ }
40
+ for (const [file, fileDocs] of byFile) {
41
+ content += `${hasSourceLabels ? '####' : '###'} ${file}\n\n`;
42
+ for (const doc of fileDocs) {
43
+ content += `- \`${doc.element.name}\`: ${doc.element.kind}`;
44
+ if (doc.markdown) {
45
+ // Extract first sentence as summary
46
+ const firstSentence = doc.markdown.split(/\.\s/)[0]?.slice(0, 100);
47
+ if (firstSentence) {
48
+ content += ` - ${firstSentence}`;
49
+ }
41
50
  }
51
+ content += '\n';
42
52
  }
43
53
  content += '\n';
44
54
  }
45
- content += '\n';
46
55
  }
47
56
  // Detailed API section
48
57
  content += `## API Details\n\n`;
@@ -134,15 +143,38 @@ async function writeIndexFile(results, outputDir, sourceDir) {
134
143
  const totalElements = results.reduce((sum, r) => sum + r.docs.length, 0);
135
144
  content += '## Summary\n\n';
136
145
  content += `- **Total elements:** ${totalElements}\n\n`;
137
- // File index
138
- content += '## Files\n\n';
139
- for (const result of results) {
140
- if (result.docs.length === 0)
141
- continue;
142
- const relPath = relative(sourceDir, result.filePath);
143
- const docFileName = relPath.replace(/\.[^.]+$/, '.md');
144
- content += `- [${relPath}](./${docFileName})`;
145
- content += ` (${result.docs.length} elements)\n`;
146
+ // Check if any docs have source labels
147
+ const hasLabels = results.some(r => r.docs.some(d => d.element.sourceLabel));
148
+ if (hasLabels) {
149
+ // Group by source label
150
+ const byLabel = new Map();
151
+ for (const result of results) {
152
+ if (result.docs.length === 0)
153
+ continue;
154
+ const label = result.docs[0]?.element.sourceLabel || 'Other';
155
+ if (!byLabel.has(label))
156
+ byLabel.set(label, []);
157
+ byLabel.get(label).push(result);
158
+ }
159
+ for (const [label, labelResults] of byLabel) {
160
+ content += `## ${label}\n\n`;
161
+ for (const result of labelResults) {
162
+ const relPath = relative(sourceDir, result.filePath);
163
+ const docFileName = relPath.replace(/\.[^.]+$/, '.md');
164
+ content += `- [${relPath}](./${docFileName}) (${result.docs.length} elements)\n`;
165
+ }
166
+ content += '\n';
167
+ }
168
+ }
169
+ else {
170
+ content += '## Files\n\n';
171
+ for (const result of results) {
172
+ if (result.docs.length === 0)
173
+ continue;
174
+ const relPath = relative(sourceDir, result.filePath);
175
+ const docFileName = relPath.replace(/\.[^.]+$/, '.md');
176
+ content += `- [${relPath}](./${docFileName}) (${result.docs.length} elements)\n`;
177
+ }
146
178
  }
147
179
  await writeFile(join(outputDir, 'README.md'), content, 'utf-8');
148
180
  }
@@ -0,0 +1,17 @@
1
+ export interface DiscoveredRepo {
2
+ name: string;
3
+ full_name: string;
4
+ clone_url: string;
5
+ default_branch: string;
6
+ private: boolean;
7
+ }
8
+ /**
9
+ * Discover repositories in a GitHub organization.
10
+ * Returns up to MAX_REPOS repos sorted by most recently pushed.
11
+ */
12
+ export declare function discoverOrgRepos(org: string, token: string): Promise<DiscoveredRepo[]>;
13
+ /**
14
+ * Shallow clone a repository to a temporary directory.
15
+ * Uses spawnSync with array args to prevent shell injection.
16
+ */
17
+ export declare function cloneRepoToTemp(repo: DiscoveredRepo, token: string): Promise<string>;
@@ -0,0 +1,93 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { mkdtempSync, rmSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { tmpdir } from 'os';
5
+ const MAX_REPOS = 50;
6
+ /**
7
+ * Discover repositories in a GitHub organization.
8
+ * Returns up to MAX_REPOS repos sorted by most recently pushed.
9
+ */
10
+ export async function discoverOrgRepos(org, token) {
11
+ const repos = [];
12
+ let page = 1;
13
+ while (repos.length < MAX_REPOS) {
14
+ const url = `https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=100&sort=pushed&page=${page}`;
15
+ const response = await fetch(url, {
16
+ headers: {
17
+ 'Authorization': `Bearer ${token}`,
18
+ 'Accept': 'application/vnd.github.v3+json',
19
+ 'User-Agent': 'Skrypt-CLI',
20
+ },
21
+ });
22
+ if (!response.ok) {
23
+ if (response.status === 404) {
24
+ throw new Error(`Organization "${org}" not found`);
25
+ }
26
+ if (response.status === 401 || response.status === 403) {
27
+ throw new Error('GitHub token does not have access to this organization');
28
+ }
29
+ throw new Error(`GitHub API error: ${response.status}`);
30
+ }
31
+ const data = await response.json();
32
+ if (data.length === 0)
33
+ break;
34
+ for (const repo of data) {
35
+ if (repos.length >= MAX_REPOS)
36
+ break;
37
+ // Skip archived and forked repos
38
+ if (repo.archived || repo.fork)
39
+ continue;
40
+ repos.push({
41
+ name: repo.name,
42
+ full_name: repo.full_name,
43
+ clone_url: repo.clone_url,
44
+ default_branch: repo.default_branch,
45
+ private: repo.private,
46
+ });
47
+ }
48
+ page++;
49
+ }
50
+ return repos;
51
+ }
52
+ /**
53
+ * Shallow clone a repository to a temporary directory.
54
+ * Uses spawnSync with array args to prevent shell injection.
55
+ */
56
+ export async function cloneRepoToTemp(repo, token) {
57
+ const tempDir = mkdtempSync(join(tmpdir(), `skrypt-${repo.name}-`));
58
+ // Build authenticated clone URL
59
+ const cloneUrl = repo.clone_url.replace('https://', `https://x-access-token:${token}@`);
60
+ let result;
61
+ try {
62
+ result = spawnSync('git', [
63
+ 'clone',
64
+ '--depth', '1',
65
+ '--single-branch',
66
+ '--branch', repo.default_branch,
67
+ cloneUrl,
68
+ tempDir,
69
+ ], {
70
+ stdio: 'pipe',
71
+ timeout: 60_000,
72
+ });
73
+ }
74
+ catch (err) {
75
+ // Clean up temp dir on spawn failure
76
+ try {
77
+ rmSync(tempDir, { recursive: true, force: true });
78
+ }
79
+ catch { /* ignore */ }
80
+ throw new Error(`Failed to clone ${repo.full_name}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
81
+ }
82
+ if (result.status !== 0) {
83
+ // Clean up temp dir on clone failure
84
+ try {
85
+ rmSync(tempDir, { recursive: true, force: true });
86
+ }
87
+ catch { /* ignore */ }
88
+ // Sanitize stderr to avoid leaking the token
89
+ const stderr = (result.stderr?.toString() || '').replace(/x-access-token:[^@]+@/g, 'x-access-token:***@');
90
+ throw new Error(`Failed to clone ${repo.full_name}: ${stderr}`);
91
+ }
92
+ return tempDir;
93
+ }
@@ -52,4 +52,6 @@ export interface GeneratedDocResult {
52
52
  */
53
53
  export declare function generateDocumentation(client: LLMClient, element: ElementContext, options?: {
54
54
  multiLanguage?: boolean;
55
+ verify?: boolean;
56
+ previousError?: string;
55
57
  }): Promise<GeneratedDocResult>;
package/dist/llm/index.js CHANGED
@@ -31,7 +31,10 @@ export function createLLMClient(config) {
31
31
  */
32
32
  export async function generateDocumentation(client, element, options) {
33
33
  const useMultiLang = options?.multiLanguage ?? true;
34
- const prompt = buildDocPrompt(element, useMultiLang);
34
+ let prompt = buildDocPrompt(element, useMultiLang, options?.verify);
35
+ if (options?.previousError) {
36
+ prompt += `\n\n⚠️ IMPORTANT: The previous code example for this element FAILED verification with the following error:\n\`\`\`\n${options.previousError}\n\`\`\`\nGenerate a DIFFERENT, working code example that avoids this error. Ensure the example is self-contained and runs without external dependencies unless specified.`;
37
+ }
35
38
  const response = await client.complete({
36
39
  messages: [
37
40
  {
@@ -127,7 +130,7 @@ Generate ONE self-contained, executable example:
127
130
  ---CODE---
128
131
  [Self-contained example — no markdown fences]
129
132
  ---END---`;
130
- function buildDocPrompt(element, multiLanguage = false) {
133
+ function buildDocPrompt(element, multiLanguage = false, verify = false) {
131
134
  let prompt = '';
132
135
  // Project context first — gives the LLM the "big picture" for better explanations
133
136
  if (element.projectContext) {
@@ -174,6 +177,9 @@ function buildDocPrompt(element, multiLanguage = false) {
174
177
  if (multiLanguage) {
175
178
  prompt += `\nGenerate BOTH TypeScript AND Python self-contained examples.`;
176
179
  }
180
+ if (verify) {
181
+ prompt += `\n\nIMPORTANT: Include \`// Output: <expected>\` comments showing expected console output for every \`console.log\` call (or \`# Output: <expected>\` for Python \`print\` calls). These will be verified by running the code.`;
182
+ }
177
183
  return prompt;
178
184
  }
179
185
  function parseDocResponse(content, elementName) {
@@ -0,0 +1,2 @@
1
+ import { ActionDefinition } from './types.js';
2
+ export declare const ACTION_DEFINITIONS: ActionDefinition[];
@@ -0,0 +1,190 @@
1
+ import { existsSync } from 'fs';
2
+ function noWorkflowFile() {
3
+ return !existsSync('.github/workflows/skrypt.yml') &&
4
+ !existsSync('.github/workflows/skrypt.yaml');
5
+ }
6
+ export const ACTION_DEFINITIONS = [
7
+ // ── After init ──────────────────────────────────────────────
8
+ {
9
+ id: 'gen-init',
10
+ afterCommands: ['init'],
11
+ category: 'workflow',
12
+ message: 'Generate docs from your source code',
13
+ command: 'skrypt generate ./src -o ./content/docs',
14
+ priority: 100,
15
+ },
16
+ // ── After generate ──────────────────────────────────────────
17
+ {
18
+ id: 'test-gen',
19
+ afterCommands: ['generate'],
20
+ category: 'workflow',
21
+ message: 'Verify code snippets execute correctly',
22
+ command: 'skrypt test ./docs',
23
+ priority: 100,
24
+ },
25
+ {
26
+ id: 'qa-gen',
27
+ afterCommands: ['generate'],
28
+ category: 'quality',
29
+ message: 'Run quality checks on generated docs',
30
+ command: 'skrypt qa ./docs',
31
+ priority: 80,
32
+ },
33
+ {
34
+ id: 'audit-gen',
35
+ afterCommands: ['generate'],
36
+ category: 'advanced',
37
+ message: 'Check documentation coverage',
38
+ command: 'skrypt audit ./src --docs ./docs',
39
+ priority: 50,
40
+ },
41
+ // ── After test ──────────────────────────────────────────────
42
+ {
43
+ id: 'audit-test',
44
+ afterCommands: ['test'],
45
+ category: 'advanced',
46
+ message: 'Check documentation coverage',
47
+ command: 'skrypt audit ./src --docs ./docs',
48
+ priority: 60,
49
+ },
50
+ // ── After qa ────────────────────────────────────────────────
51
+ {
52
+ id: 'heal-qa',
53
+ afterCommands: ['qa'],
54
+ category: 'quality',
55
+ message: 'Auto-fix QA issues',
56
+ command: 'skrypt heal',
57
+ priority: 90,
58
+ },
59
+ {
60
+ id: 'lint-qa',
61
+ afterCommands: ['qa'],
62
+ category: 'quality',
63
+ message: 'Lint markdown docs',
64
+ command: 'skrypt lint ./docs',
65
+ priority: 70,
66
+ },
67
+ {
68
+ id: 'security-qa',
69
+ afterCommands: ['qa'],
70
+ category: 'quality',
71
+ message: 'Check for security issues',
72
+ command: 'skrypt security ./docs',
73
+ priority: 60,
74
+ },
75
+ // ── After audit ─────────────────────────────────────────────
76
+ {
77
+ id: 'refresh-audit',
78
+ afterCommands: ['audit'],
79
+ category: 'advanced',
80
+ message: 'Update stale docs based on code changes',
81
+ command: 'skrypt refresh ./src --docs ./docs',
82
+ priority: 80,
83
+ },
84
+ {
85
+ id: 'review-audit',
86
+ afterCommands: ['audit'],
87
+ category: 'advanced',
88
+ message: 'Review docs quality with feedback',
89
+ command: 'skrypt review ./docs',
90
+ priority: 60,
91
+ },
92
+ // ── After refresh / review / heal / autofix ─────────────────
93
+ {
94
+ id: 'test-after-fix',
95
+ afterCommands: ['refresh', 'review', 'heal', 'autofix'],
96
+ category: 'workflow',
97
+ message: 'Verify updated docs pass tests',
98
+ command: 'skrypt test ./docs',
99
+ priority: 100,
100
+ },
101
+ // ── After deploy ────────────────────────────────────────────
102
+ {
103
+ id: 'watch-deploy',
104
+ afterCommands: ['deploy'],
105
+ category: 'workflow',
106
+ message: 'Watch for changes in development',
107
+ command: 'skrypt watch',
108
+ priority: 50,
109
+ },
110
+ // ── After import ────────────────────────────────────────────
111
+ {
112
+ id: 'qa-import',
113
+ afterCommands: ['import'],
114
+ category: 'quality',
115
+ message: 'Check imported docs quality',
116
+ command: 'skrypt qa ./docs',
117
+ priority: 90,
118
+ },
119
+ {
120
+ id: 'gen-import',
121
+ afterCommands: ['import'],
122
+ category: 'workflow',
123
+ message: 'Generate docs to fill gaps',
124
+ command: 'skrypt generate ./src -o ./content/docs',
125
+ priority: 70,
126
+ },
127
+ // ── After login ─────────────────────────────────────────────
128
+ {
129
+ id: 'pro-login',
130
+ afterCommands: ['login'],
131
+ category: 'workflow',
132
+ message: 'Unlock Pro: test, heal, autofix, refresh',
133
+ command: 'skrypt test ./docs',
134
+ priority: 100,
135
+ },
136
+ // ── After gh-action ─────────────────────────────────────────
137
+ {
138
+ id: 'push-ci',
139
+ afterCommands: ['gh-action'],
140
+ category: 'cicd',
141
+ message: 'Push to GitHub to trigger CI',
142
+ command: 'git push',
143
+ priority: 100,
144
+ },
145
+ // ── CI/CD setup (cross-command) ─────────────────────────────
146
+ {
147
+ id: 'ci-setup',
148
+ afterCommands: ['generate', 'test', 'deploy'],
149
+ category: 'cicd',
150
+ message: 'Set up GitHub Actions for CI',
151
+ command: 'skrypt gh-action',
152
+ priority: 70,
153
+ condition: noWorkflowFile,
154
+ },
155
+ // ── Deploy (cross-command) ──────────────────────────────────
156
+ {
157
+ id: 'deploy-docs',
158
+ afterCommands: ['generate', 'test', 'heal', 'review', 'refresh', 'autofix'],
159
+ category: 'workflow',
160
+ message: 'Deploy docs to skrypt.sh',
161
+ command: 'skrypt deploy',
162
+ priority: 40,
163
+ },
164
+ // ── After lint / check-links / security ─────────────────────
165
+ {
166
+ id: 'deploy-quality',
167
+ afterCommands: ['lint', 'check-links', 'security'],
168
+ category: 'workflow',
169
+ message: 'Deploy docs to skrypt.sh',
170
+ command: 'skrypt deploy',
171
+ priority: 40,
172
+ },
173
+ {
174
+ id: 'qa-quality',
175
+ afterCommands: ['lint', 'check-links', 'security'],
176
+ category: 'quality',
177
+ message: 'Run full quality checks',
178
+ command: 'skrypt qa ./docs',
179
+ priority: 60,
180
+ },
181
+ // ── After llms-txt ──────────────────────────────────────────
182
+ {
183
+ id: 'deploy-llms',
184
+ afterCommands: ['llms-txt'],
185
+ category: 'workflow',
186
+ message: 'Deploy docs with llms.txt',
187
+ command: 'skrypt deploy',
188
+ priority: 80,
189
+ },
190
+ ];
@@ -0,0 +1,6 @@
1
+ export { readPreferences, writePreferences, DEFAULT_PREFERENCES } from './state.js';
2
+ export { getSuggestions, printSuggestions } from './suggest.js';
3
+ export { runFirstTimeSetup } from './setup.js';
4
+ export { ACTION_DEFINITIONS } from './actions.js';
5
+ export type { NextActionPreferences, ProjectActionState, ActionDefinition, Suggestion } from './types.js';
6
+ export declare function handlePostAction(commandName: string): Promise<void>;
@@ -0,0 +1,39 @@
1
+ import { readPreferences, markCommandCompleted } from './state.js';
2
+ import { getSuggestions, printSuggestions } from './suggest.js';
3
+ import { runFirstTimeSetup } from './setup.js';
4
+ export { readPreferences, writePreferences, DEFAULT_PREFERENCES } from './state.js';
5
+ export { getSuggestions, printSuggestions } from './suggest.js';
6
+ export { runFirstTimeSetup } from './setup.js';
7
+ export { ACTION_DEFINITIONS } from './actions.js';
8
+ const SKIP_COMMANDS = new Set(['config', 'mcp', 'whoami', 'version', 'logout']);
9
+ export async function handlePostAction(commandName) {
10
+ try {
11
+ // Only in interactive mode
12
+ if (!process.stdin.isTTY)
13
+ return;
14
+ // Skip meta commands
15
+ if (SKIP_COMMANDS.has(commandName))
16
+ return;
17
+ let prefs = readPreferences();
18
+ // First-time setup
19
+ if (!prefs) {
20
+ try {
21
+ prefs = await runFirstTimeSetup();
22
+ }
23
+ catch {
24
+ // Non-interactive or error — skip silently
25
+ return;
26
+ }
27
+ }
28
+ if (!prefs.enabled)
29
+ return;
30
+ // Track this command
31
+ markCommandCompleted(commandName);
32
+ // Show suggestions
33
+ const suggestions = getSuggestions(commandName, prefs);
34
+ printSuggestions(suggestions);
35
+ }
36
+ catch {
37
+ // Never crash the CLI over suggestions
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ import { NextActionPreferences } from './types.js';
2
+ export declare function runFirstTimeSetup(): Promise<NextActionPreferences>;