@kynetic-ai/spec 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.
- package/README.md +263 -0
- package/dist/acp/client.d.ts +159 -0
- package/dist/acp/client.d.ts.map +1 -0
- package/dist/acp/client.js +255 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/framing.d.ts +119 -0
- package/dist/acp/framing.d.ts.map +1 -0
- package/dist/acp/framing.js +302 -0
- package/dist/acp/framing.js.map +1 -0
- package/dist/acp/index.d.ts +14 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +13 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/types.d.ts +89 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +99 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/agents/adapters.d.ts +55 -0
- package/dist/agents/adapters.d.ts.map +1 -0
- package/dist/agents/adapters.js +84 -0
- package/dist/agents/adapters.js.map +1 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/spawner.d.ts +53 -0
- package/dist/agents/spawner.d.ts.map +1 -0
- package/dist/agents/spawner.js +83 -0
- package/dist/agents/spawner.js.map +1 -0
- package/dist/cli/batch.d.ts +82 -0
- package/dist/cli/batch.d.ts.map +1 -0
- package/dist/cli/batch.js +162 -0
- package/dist/cli/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +6 -0
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
- package/dist/cli/commands/clone-for-testing.js +176 -0
- package/dist/cli/commands/clone-for-testing.js.map +1 -0
- package/dist/cli/commands/derive.d.ts +6 -0
- package/dist/cli/commands/derive.d.ts.map +1 -0
- package/dist/cli/commands/derive.js +450 -0
- package/dist/cli/commands/derive.js.map +1 -0
- package/dist/cli/commands/help.d.ts +6 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +196 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +6 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +235 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/index.d.ts +20 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +21 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +245 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/item.d.ts +6 -0
- package/dist/cli/commands/item.d.ts.map +1 -0
- package/dist/cli/commands/item.js +1311 -0
- package/dist/cli/commands/item.js.map +1 -0
- package/dist/cli/commands/link.d.ts +6 -0
- package/dist/cli/commands/link.d.ts.map +1 -0
- package/dist/cli/commands/link.js +288 -0
- package/dist/cli/commands/link.js.map +1 -0
- package/dist/cli/commands/log.d.ts +16 -0
- package/dist/cli/commands/log.d.ts.map +1 -0
- package/dist/cli/commands/log.js +291 -0
- package/dist/cli/commands/log.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +15 -0
- package/dist/cli/commands/meta.d.ts.map +1 -0
- package/dist/cli/commands/meta.js +1378 -0
- package/dist/cli/commands/meta.js.map +1 -0
- package/dist/cli/commands/module.d.ts +6 -0
- package/dist/cli/commands/module.d.ts.map +1 -0
- package/dist/cli/commands/module.js +102 -0
- package/dist/cli/commands/module.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +9 -0
- package/dist/cli/commands/ralph.d.ts.map +1 -0
- package/dist/cli/commands/ralph.js +465 -0
- package/dist/cli/commands/ralph.js.map +1 -0
- package/dist/cli/commands/search.d.ts +6 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +134 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/session.d.ts +164 -0
- package/dist/cli/commands/session.d.ts.map +1 -0
- package/dist/cli/commands/session.js +745 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +26 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +586 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/shadow.d.ts +6 -0
- package/dist/cli/commands/shadow.d.ts.map +1 -0
- package/dist/cli/commands/shadow.js +299 -0
- package/dist/cli/commands/shadow.js.map +1 -0
- package/dist/cli/commands/task.d.ts +6 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1514 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/tasks.d.ts +6 -0
- package/dist/cli/commands/tasks.d.ts.map +1 -0
- package/dist/cli/commands/tasks.js +347 -0
- package/dist/cli/commands/tasks.js.map +1 -0
- package/dist/cli/commands/trait.d.ts +10 -0
- package/dist/cli/commands/trait.d.ts.map +1 -0
- package/dist/cli/commands/trait.js +295 -0
- package/dist/cli/commands/trait.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +6 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +626 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +62 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +65 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help/content.d.ts +35 -0
- package/dist/cli/help/content.d.ts.map +1 -0
- package/dist/cli/help/content.js +312 -0
- package/dist/cli/help/content.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +85 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/introspection.d.ts +87 -0
- package/dist/cli/introspection.d.ts.map +1 -0
- package/dist/cli/introspection.js +127 -0
- package/dist/cli/introspection.js.map +1 -0
- package/dist/cli/output.d.ts +56 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +467 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/suggest.d.ts +16 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +72 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/alignment.d.ts +113 -0
- package/dist/parser/alignment.d.ts.map +1 -0
- package/dist/parser/alignment.js +261 -0
- package/dist/parser/alignment.js.map +1 -0
- package/dist/parser/assess.d.ts +81 -0
- package/dist/parser/assess.d.ts.map +1 -0
- package/dist/parser/assess.js +197 -0
- package/dist/parser/assess.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +48 -0
- package/dist/parser/convention-validation.d.ts.map +1 -0
- package/dist/parser/convention-validation.js +167 -0
- package/dist/parser/convention-validation.js.map +1 -0
- package/dist/parser/fix.d.ts +38 -0
- package/dist/parser/fix.d.ts.map +1 -0
- package/dist/parser/fix.js +185 -0
- package/dist/parser/fix.js.map +1 -0
- package/dist/parser/index.d.ts +12 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +13 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/items.d.ts +138 -0
- package/dist/parser/items.d.ts.map +1 -0
- package/dist/parser/items.js +321 -0
- package/dist/parser/items.js.map +1 -0
- package/dist/parser/meta.d.ts +120 -0
- package/dist/parser/meta.d.ts.map +1 -0
- package/dist/parser/meta.js +441 -0
- package/dist/parser/meta.js.map +1 -0
- package/dist/parser/refs.d.ts +185 -0
- package/dist/parser/refs.d.ts.map +1 -0
- package/dist/parser/refs.js +404 -0
- package/dist/parser/refs.js.map +1 -0
- package/dist/parser/shadow.d.ts +253 -0
- package/dist/parser/shadow.d.ts.map +1 -0
- package/dist/parser/shadow.js +1053 -0
- package/dist/parser/shadow.js.map +1 -0
- package/dist/parser/traits.d.ts +72 -0
- package/dist/parser/traits.d.ts.map +1 -0
- package/dist/parser/traits.js +120 -0
- package/dist/parser/traits.js.map +1 -0
- package/dist/parser/validate.d.ts +89 -0
- package/dist/parser/validate.d.ts.map +1 -0
- package/dist/parser/validate.js +817 -0
- package/dist/parser/validate.js.map +1 -0
- package/dist/parser/yaml.d.ts +326 -0
- package/dist/parser/yaml.d.ts.map +1 -0
- package/dist/parser/yaml.js +1383 -0
- package/dist/parser/yaml.js.map +1 -0
- package/dist/ralph/cli-renderer.d.ts +20 -0
- package/dist/ralph/cli-renderer.d.ts.map +1 -0
- package/dist/ralph/cli-renderer.js +179 -0
- package/dist/ralph/cli-renderer.js.map +1 -0
- package/dist/ralph/events.d.ts +65 -0
- package/dist/ralph/events.d.ts.map +1 -0
- package/dist/ralph/events.js +397 -0
- package/dist/ralph/events.js.map +1 -0
- package/dist/ralph/index.d.ts +8 -0
- package/dist/ralph/index.d.ts.map +1 -0
- package/dist/ralph/index.js +10 -0
- package/dist/ralph/index.js.map +1 -0
- package/dist/schema/common.d.ts +46 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +71 -0
- package/dist/schema/common.js.map +1 -0
- package/dist/schema/inbox.d.ts +90 -0
- package/dist/schema/inbox.d.ts.map +1 -0
- package/dist/schema/inbox.js +30 -0
- package/dist/schema/inbox.js.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/meta.d.ts +762 -0
- package/dist/schema/meta.d.ts.map +1 -0
- package/dist/schema/meta.js +144 -0
- package/dist/schema/meta.js.map +1 -0
- package/dist/schema/spec.d.ts +912 -0
- package/dist/schema/spec.d.ts.map +1 -0
- package/dist/schema/spec.js +104 -0
- package/dist/schema/spec.js.map +1 -0
- package/dist/schema/task.d.ts +664 -0
- package/dist/schema/task.d.ts.map +1 -0
- package/dist/schema/task.js +130 -0
- package/dist/schema/task.js.map +1 -0
- package/dist/sessions/index.d.ts +11 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +13 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +144 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +325 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/types.d.ts +157 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +90 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/strings/errors.d.ts +420 -0
- package/dist/strings/errors.d.ts.map +1 -0
- package/dist/strings/errors.js +282 -0
- package/dist/strings/errors.js.map +1 -0
- package/dist/strings/guidance.d.ts +65 -0
- package/dist/strings/guidance.d.ts.map +1 -0
- package/dist/strings/guidance.js +66 -0
- package/dist/strings/guidance.js.map +1 -0
- package/dist/strings/index.d.ts +12 -0
- package/dist/strings/index.d.ts.map +1 -0
- package/dist/strings/index.js +12 -0
- package/dist/strings/index.js.map +1 -0
- package/dist/strings/labels.d.ts +74 -0
- package/dist/strings/labels.d.ts.map +1 -0
- package/dist/strings/labels.js +75 -0
- package/dist/strings/labels.js.map +1 -0
- package/dist/strings/validation.d.ts +126 -0
- package/dist/strings/validation.d.ts.map +1 -0
- package/dist/strings/validation.js +135 -0
- package/dist/strings/validation.js.map +1 -0
- package/dist/utils/commit.d.ts +23 -0
- package/dist/utils/commit.d.ts.map +1 -0
- package/dist/utils/commit.js +67 -0
- package/dist/utils/commit.js.map +1 -0
- package/dist/utils/git.d.ts +57 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +192 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/grep.d.ts +28 -0
- package/dist/utils/grep.d.ts.map +1 -0
- package/dist/utils/grep.js +86 -0
- package/dist/utils/grep.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/time.d.ts +18 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +61 -0
- package/dist/utils/time.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { ulid } from 'ulid';
|
|
6
|
+
import { output, error as outputError } from '../output.js';
|
|
7
|
+
import { EXIT_CODES } from '../exit-codes.js';
|
|
8
|
+
// AC: @cmd-clone-for-testing ac-1
|
|
9
|
+
/**
|
|
10
|
+
* Creates isolated repo copy for testing
|
|
11
|
+
* - Clones source repo to destination
|
|
12
|
+
* - Removes remote origin for isolation
|
|
13
|
+
* - Preserves all branches including kspec-meta
|
|
14
|
+
*/
|
|
15
|
+
function cloneRepo(source, dest) {
|
|
16
|
+
// Clone with --mirror to get all branches
|
|
17
|
+
const cloneResult = spawnSync('git', [
|
|
18
|
+
'clone',
|
|
19
|
+
'--mirror',
|
|
20
|
+
source,
|
|
21
|
+
path.join(dest, '.git')
|
|
22
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
23
|
+
if (cloneResult.status !== 0) {
|
|
24
|
+
return { success: false, error: cloneResult.stderr };
|
|
25
|
+
}
|
|
26
|
+
// Convert bare mirror repo to normal repo
|
|
27
|
+
const configResult = spawnSync('git', [
|
|
28
|
+
'config',
|
|
29
|
+
'--bool',
|
|
30
|
+
'core.bare',
|
|
31
|
+
'false'
|
|
32
|
+
], { cwd: dest, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
33
|
+
if (configResult.status !== 0) {
|
|
34
|
+
return { success: false, error: configResult.stderr };
|
|
35
|
+
}
|
|
36
|
+
// Reset working tree to populate files
|
|
37
|
+
const resetResult = spawnSync('git', ['reset', '--hard'], {
|
|
38
|
+
cwd: dest,
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
41
|
+
});
|
|
42
|
+
if (resetResult.status !== 0) {
|
|
43
|
+
return { success: false, error: resetResult.stderr };
|
|
44
|
+
}
|
|
45
|
+
// Remove remote references for true isolation
|
|
46
|
+
const remoteResult = spawnSync('git', ['remote', 'remove', 'origin'], {
|
|
47
|
+
cwd: dest,
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
50
|
+
});
|
|
51
|
+
// Don't fail if no remote exists
|
|
52
|
+
// remoteResult.status will be non-zero if origin doesn't exist, which is fine
|
|
53
|
+
// Clean up worktree metadata from source repo to prevent conflicts
|
|
54
|
+
// The mirror clone copies .git/worktrees/ which causes "already in use" errors
|
|
55
|
+
const worktreesPath = path.join(dest, '.git', 'worktrees');
|
|
56
|
+
if (fs.existsSync(worktreesPath)) {
|
|
57
|
+
fs.rmSync(worktreesPath, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
return { success: true };
|
|
60
|
+
}
|
|
61
|
+
// AC: @cmd-clone-for-testing ac-2
|
|
62
|
+
/**
|
|
63
|
+
* Sets up .kspec worktree if kspec-meta branch exists
|
|
64
|
+
*/
|
|
65
|
+
function setupWorktree(repoPath) {
|
|
66
|
+
// Check for kspec-meta branch
|
|
67
|
+
const branchCheck = spawnSync('git', [
|
|
68
|
+
'branch', '--list', 'kspec-meta'
|
|
69
|
+
], { cwd: repoPath, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
70
|
+
if (!branchCheck.stdout.trim()) {
|
|
71
|
+
// No kspec-meta branch, nothing to do
|
|
72
|
+
return { success: true };
|
|
73
|
+
}
|
|
74
|
+
// Create .kspec worktree
|
|
75
|
+
const worktreePath = path.join(repoPath, '.kspec');
|
|
76
|
+
const worktreeResult = spawnSync('git', [
|
|
77
|
+
'worktree', 'add', '.kspec', 'kspec-meta'
|
|
78
|
+
], { cwd: repoPath, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
79
|
+
if (worktreeResult.status !== 0) {
|
|
80
|
+
return { success: false, error: worktreeResult.stderr };
|
|
81
|
+
}
|
|
82
|
+
return { success: true };
|
|
83
|
+
}
|
|
84
|
+
// AC: @cmd-clone-for-testing ac-3
|
|
85
|
+
/**
|
|
86
|
+
* Checkout specified branch
|
|
87
|
+
*/
|
|
88
|
+
function checkoutBranch(repoPath, branch) {
|
|
89
|
+
const checkoutResult = spawnSync('git', ['checkout', branch], {
|
|
90
|
+
cwd: repoPath,
|
|
91
|
+
encoding: 'utf-8',
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
93
|
+
});
|
|
94
|
+
if (checkoutResult.status !== 0) {
|
|
95
|
+
return { success: false, error: checkoutResult.stderr, currentBranch: '' };
|
|
96
|
+
}
|
|
97
|
+
// Get current branch name
|
|
98
|
+
const branchResult = spawnSync('git', ['branch', '--show-current'], {
|
|
99
|
+
cwd: repoPath,
|
|
100
|
+
encoding: 'utf-8',
|
|
101
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
102
|
+
});
|
|
103
|
+
return { success: true, currentBranch: branchResult.stdout.trim() };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current branch name
|
|
107
|
+
*/
|
|
108
|
+
function getCurrentBranch(repoPath) {
|
|
109
|
+
const branchResult = spawnSync('git', ['branch', '--show-current'], {
|
|
110
|
+
cwd: repoPath,
|
|
111
|
+
encoding: 'utf-8',
|
|
112
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
113
|
+
});
|
|
114
|
+
return branchResult.stdout.trim() || 'main';
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Register the 'clone-for-testing' command
|
|
118
|
+
*/
|
|
119
|
+
export function registerCloneForTestingCommand(program) {
|
|
120
|
+
program
|
|
121
|
+
.command('clone-for-testing [dest] [source]')
|
|
122
|
+
.description('Create isolated repo copy for testing')
|
|
123
|
+
.option('--branch <name>', 'Branch to checkout after cloning')
|
|
124
|
+
.action(async (dest, source, options) => {
|
|
125
|
+
// Default source to current directory
|
|
126
|
+
if (!source) {
|
|
127
|
+
source = process.cwd();
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
// AC: @cmd-clone-for-testing ac-4
|
|
131
|
+
// Generate temp dest if not provided
|
|
132
|
+
if (!dest) {
|
|
133
|
+
dest = path.join(os.tmpdir(), `kspec-test-${ulid().slice(0, 8).toLowerCase()}`);
|
|
134
|
+
}
|
|
135
|
+
// Create destination directory
|
|
136
|
+
if (!fs.existsSync(dest)) {
|
|
137
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
// Clone the repo
|
|
140
|
+
const cloneResult = cloneRepo(source, dest);
|
|
141
|
+
if (!cloneResult.success) {
|
|
142
|
+
outputError(`Failed to clone repository: ${cloneResult.error}`);
|
|
143
|
+
process.exit(EXIT_CODES.ERROR);
|
|
144
|
+
}
|
|
145
|
+
// Setup worktree if kspec-meta exists
|
|
146
|
+
const worktreeResult = setupWorktree(dest);
|
|
147
|
+
if (!worktreeResult.success) {
|
|
148
|
+
outputError(`Failed to setup worktree: ${worktreeResult.error}`);
|
|
149
|
+
process.exit(EXIT_CODES.ERROR);
|
|
150
|
+
}
|
|
151
|
+
// Checkout specified branch if provided
|
|
152
|
+
let currentBranch = getCurrentBranch(dest);
|
|
153
|
+
if (options.branch) {
|
|
154
|
+
const checkoutResult = checkoutBranch(dest, options.branch);
|
|
155
|
+
if (!checkoutResult.success) {
|
|
156
|
+
outputError(`Failed to checkout branch '${options.branch}': ${checkoutResult.error}`);
|
|
157
|
+
process.exit(EXIT_CODES.ERROR);
|
|
158
|
+
}
|
|
159
|
+
currentBranch = checkoutResult.currentBranch;
|
|
160
|
+
}
|
|
161
|
+
// AC: @cmd-clone-for-testing ac-5
|
|
162
|
+
// Output result (respects global --json flag)
|
|
163
|
+
output({ path: dest, branch: currentBranch }, () => {
|
|
164
|
+
console.log(`Created test repo at: ${dest}`);
|
|
165
|
+
if (options.branch) {
|
|
166
|
+
console.log(`Checked out branch: ${currentBranch}`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
outputError(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
172
|
+
process.exit(EXIT_CODES.ERROR);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=clone-for-testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone-for-testing.js","sourceRoot":"","sources":["../../../src/cli/commands/clone-for-testing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derive.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/derive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2WpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyM5D"}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { initContext, loadAllTasks, loadAllItems, saveTask, createTask, createNote, ReferenceIndex, AlignmentIndex, } from '../../parser/index.js';
|
|
2
|
+
import { commitIfShadow } from '../../parser/shadow.js';
|
|
3
|
+
import { output, error, info, isJsonMode } from '../output.js';
|
|
4
|
+
import { errors } from '../../strings/index.js';
|
|
5
|
+
import { EXIT_CODES } from '../exit-codes.js';
|
|
6
|
+
/**
|
|
7
|
+
* Fields that contain nested spec items (mirrors yaml.ts)
|
|
8
|
+
*/
|
|
9
|
+
const NESTED_ITEM_FIELDS = ['modules', 'features', 'requirements', 'constraints', 'decisions'];
|
|
10
|
+
/**
|
|
11
|
+
* Get the parent path from a child's _path.
|
|
12
|
+
* e.g., "features[0].requirements[1]" -> "features[0]"
|
|
13
|
+
* Returns empty string for top-level items.
|
|
14
|
+
*/
|
|
15
|
+
function getParentPath(childPath) {
|
|
16
|
+
if (!childPath)
|
|
17
|
+
return '';
|
|
18
|
+
const lastDotIndex = childPath.lastIndexOf('.');
|
|
19
|
+
if (lastDotIndex === -1)
|
|
20
|
+
return '';
|
|
21
|
+
return childPath.slice(0, lastDotIndex);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if an item is a direct child of another item based on _path.
|
|
25
|
+
* Direct children have a path that extends the parent's path by exactly one field[index].
|
|
26
|
+
*/
|
|
27
|
+
function isDirectChildOf(child, parent) {
|
|
28
|
+
const childPath = child._path || '';
|
|
29
|
+
const parentPath = parent._path || '';
|
|
30
|
+
// If paths are equal, not a child
|
|
31
|
+
if (childPath === parentPath)
|
|
32
|
+
return false;
|
|
33
|
+
// Child path must start with parent path
|
|
34
|
+
if (parentPath && !childPath.startsWith(parentPath + '.'))
|
|
35
|
+
return false;
|
|
36
|
+
// For root parent (empty path), child must be a top-level path like "features[0]"
|
|
37
|
+
if (!parentPath) {
|
|
38
|
+
// Direct child of root has no '.' in its path
|
|
39
|
+
return !childPath.includes('.');
|
|
40
|
+
}
|
|
41
|
+
// Get the remaining path after parent
|
|
42
|
+
const remaining = childPath.slice(parentPath.length + 1);
|
|
43
|
+
// Direct child has no additional '.' (e.g., "requirements[0]" not "requirements[0].something")
|
|
44
|
+
return !remaining.includes('.');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Find the parent spec item of a given item.
|
|
48
|
+
* Returns undefined for root-level items.
|
|
49
|
+
*/
|
|
50
|
+
function findParentItem(item, allItems) {
|
|
51
|
+
const parentPath = getParentPath(item._path);
|
|
52
|
+
// Root-level item or no path
|
|
53
|
+
if (!parentPath && !item._path)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (!parentPath)
|
|
56
|
+
return undefined;
|
|
57
|
+
// Find item with matching path in the same source file
|
|
58
|
+
return allItems.find(i => i._path === parentPath && i._sourceFile === item._sourceFile);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get direct children of a spec item.
|
|
62
|
+
* Only returns immediate children, not grandchildren.
|
|
63
|
+
*/
|
|
64
|
+
function getDirectChildren(parent, allItems) {
|
|
65
|
+
return allItems.filter(item => item._sourceFile === parent._sourceFile && isDirectChildOf(item, parent));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Collect an item and all its descendants in topological order (parent first).
|
|
69
|
+
* This ensures parent tasks are created before child tasks.
|
|
70
|
+
*/
|
|
71
|
+
function collectItemsRecursively(root, allItems) {
|
|
72
|
+
const result = [root];
|
|
73
|
+
const children = getDirectChildren(root, allItems);
|
|
74
|
+
for (const child of children) {
|
|
75
|
+
const descendants = collectItemsRecursively(child, allItems);
|
|
76
|
+
result.push(...descendants);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a spec item reference.
|
|
82
|
+
* Returns the spec item or exits with error.
|
|
83
|
+
*/
|
|
84
|
+
function resolveSpecRef(ref, items, tasks, index) {
|
|
85
|
+
const result = index.resolve(ref);
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
switch (result.error) {
|
|
88
|
+
case 'not_found':
|
|
89
|
+
error(errors.reference.specNotFound(ref));
|
|
90
|
+
break;
|
|
91
|
+
case 'ambiguous':
|
|
92
|
+
error(errors.reference.ambiguous(ref));
|
|
93
|
+
for (const candidate of result.candidates) {
|
|
94
|
+
const item = items.find(i => i._ulid === candidate);
|
|
95
|
+
const slug = item?.slugs[0] || '';
|
|
96
|
+
console.error(` - ${index.shortUlid(candidate)} ${slug ? `(${slug})` : ''}`);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
case 'duplicate_slug':
|
|
100
|
+
error(errors.reference.slugMapsToMultiple(ref));
|
|
101
|
+
for (const candidate of result.candidates) {
|
|
102
|
+
console.error(` - ${index.shortUlid(candidate)}`);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
107
|
+
}
|
|
108
|
+
// Check if it's actually a spec item (not a task)
|
|
109
|
+
const item = items.find(i => i._ulid === result.ulid);
|
|
110
|
+
if (!item) {
|
|
111
|
+
// Check if it's a task
|
|
112
|
+
const task = tasks.find(t => t._ulid === result.ulid);
|
|
113
|
+
if (task) {
|
|
114
|
+
error(errors.reference.notSpecItem(ref));
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
error(errors.reference.specNotFound(ref));
|
|
118
|
+
}
|
|
119
|
+
process.exit(EXIT_CODES.NOT_FOUND);
|
|
120
|
+
}
|
|
121
|
+
return item;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Generate a slug from a spec item title.
|
|
125
|
+
* Converts "My Feature Title" -> "task-my-feature-title"
|
|
126
|
+
*/
|
|
127
|
+
function generateSlugFromTitle(title) {
|
|
128
|
+
return ('task-' +
|
|
129
|
+
title
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
132
|
+
.replace(/^-|-$/g, '')
|
|
133
|
+
.slice(0, 50));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Convert spec priority to task priority (number).
|
|
137
|
+
* Spec can use 'high', 'medium', 'low' or numeric 1-5.
|
|
138
|
+
*/
|
|
139
|
+
function normalizePriority(priority) {
|
|
140
|
+
if (priority === undefined)
|
|
141
|
+
return 3;
|
|
142
|
+
if (typeof priority === 'number')
|
|
143
|
+
return priority;
|
|
144
|
+
switch (priority) {
|
|
145
|
+
case 'high':
|
|
146
|
+
return 1;
|
|
147
|
+
case 'medium':
|
|
148
|
+
return 3;
|
|
149
|
+
case 'low':
|
|
150
|
+
return 5;
|
|
151
|
+
default:
|
|
152
|
+
return 3;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate implementation notes from spec item for newly derived task.
|
|
157
|
+
* Includes description and acceptance criteria summary.
|
|
158
|
+
*/
|
|
159
|
+
function generateImplementationNotes(specItem) {
|
|
160
|
+
const parts = [];
|
|
161
|
+
// Add description if present
|
|
162
|
+
if (specItem.description) {
|
|
163
|
+
parts.push(specItem.description.trim());
|
|
164
|
+
}
|
|
165
|
+
// Add acceptance criteria summary if present
|
|
166
|
+
if (specItem.acceptance_criteria && specItem.acceptance_criteria.length > 0) {
|
|
167
|
+
const acSection = ['', 'Acceptance Criteria:'];
|
|
168
|
+
for (const ac of specItem.acceptance_criteria) {
|
|
169
|
+
const summary = `${ac.given ? 'Given ' + ac.given + ', ' : ''}when ${ac.when}, then ${ac.then}`;
|
|
170
|
+
acSection.push(`- ${ac.id}: ${summary}`);
|
|
171
|
+
}
|
|
172
|
+
parts.push(acSection.join('\n'));
|
|
173
|
+
}
|
|
174
|
+
// Return combined content, or undefined if nothing to add
|
|
175
|
+
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Derive a task from a spec item.
|
|
179
|
+
* Returns result describing what happened.
|
|
180
|
+
*
|
|
181
|
+
* @param dependsOn - Task references to add as dependencies (for hierarchy-based deps)
|
|
182
|
+
* @param priority - Priority override (1-5), if not provided uses spec's priority
|
|
183
|
+
*/
|
|
184
|
+
async function deriveTaskFromSpec(ctx, specItem, existingTasks, items, index, alignmentIndex, options) {
|
|
185
|
+
// Check if a task already exists for this spec
|
|
186
|
+
const linkedTasks = alignmentIndex.getTasksForSpec(specItem._ulid);
|
|
187
|
+
if (linkedTasks.length > 0 && !options.force) {
|
|
188
|
+
const taskRef = linkedTasks[0].slugs[0]
|
|
189
|
+
? `@${linkedTasks[0].slugs[0]}`
|
|
190
|
+
: `@${index.shortUlid(linkedTasks[0]._ulid)}`;
|
|
191
|
+
return {
|
|
192
|
+
specItem,
|
|
193
|
+
action: 'skipped',
|
|
194
|
+
task: linkedTasks[0],
|
|
195
|
+
reason: `task exists: ${taskRef}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// Check if slug would collide with existing task
|
|
199
|
+
const baseSlug = generateSlugFromTitle(specItem.title);
|
|
200
|
+
let slug = baseSlug;
|
|
201
|
+
let slugSuffix = 1;
|
|
202
|
+
// Find unique slug if needed
|
|
203
|
+
while (existingTasks.some(t => t.slugs.includes(slug))) {
|
|
204
|
+
slug = `${baseSlug}-${slugSuffix}`;
|
|
205
|
+
slugSuffix++;
|
|
206
|
+
}
|
|
207
|
+
// Generate implementation notes from spec
|
|
208
|
+
const noteContent = generateImplementationNotes(specItem);
|
|
209
|
+
const initialNotes = noteContent
|
|
210
|
+
? [createNote(`Implementation notes (auto-generated from spec):\n\n${noteContent}`, '@kspec-derive')]
|
|
211
|
+
: [];
|
|
212
|
+
// Build task input with depends_on and initial notes
|
|
213
|
+
const taskInput = {
|
|
214
|
+
title: `Implement: ${specItem.title}`,
|
|
215
|
+
type: 'task',
|
|
216
|
+
spec_ref: `@${specItem.slugs[0] || specItem._ulid}`,
|
|
217
|
+
derivation: 'auto',
|
|
218
|
+
priority: options.priority ?? normalizePriority(specItem.priority),
|
|
219
|
+
slugs: [slug],
|
|
220
|
+
tags: [...(specItem.tags || [])],
|
|
221
|
+
depends_on: options.dependsOn || [],
|
|
222
|
+
notes: initialNotes,
|
|
223
|
+
};
|
|
224
|
+
// Dry run - don't actually create
|
|
225
|
+
if (options.dryRun) {
|
|
226
|
+
const previewTask = createTask(taskInput);
|
|
227
|
+
return {
|
|
228
|
+
specItem,
|
|
229
|
+
action: 'would_create',
|
|
230
|
+
task: previewTask,
|
|
231
|
+
dependsOn: options.dependsOn,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Create and save the task
|
|
235
|
+
const newTask = createTask(taskInput);
|
|
236
|
+
await saveTask(ctx, newTask);
|
|
237
|
+
const specSlug = specItem.slugs[0] || specItem._ulid.slice(0, 8);
|
|
238
|
+
await commitIfShadow(ctx.shadow, 'derive', specSlug);
|
|
239
|
+
// Add to existing tasks list for slug collision checks
|
|
240
|
+
existingTasks.push(newTask);
|
|
241
|
+
return {
|
|
242
|
+
specItem,
|
|
243
|
+
action: 'created',
|
|
244
|
+
task: newTask,
|
|
245
|
+
dependsOn: options.dependsOn,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get a task reference string for use in depends_on.
|
|
250
|
+
* Prefers slug over ULID for readability.
|
|
251
|
+
*/
|
|
252
|
+
function getTaskRef(task, index) {
|
|
253
|
+
return task.slugs[0] ? `@${task.slugs[0]}` : `@${index.shortUlid(task._ulid)}`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Find or get the task for a parent spec item.
|
|
257
|
+
* Looks in:
|
|
258
|
+
* 1. Tasks created in this derive session (specToTaskMap)
|
|
259
|
+
* 2. Existing tasks linked to the parent spec (alignmentIndex)
|
|
260
|
+
*/
|
|
261
|
+
function getParentTaskRef(parentSpec, specToTaskMap, alignmentIndex, index) {
|
|
262
|
+
// Check if we created a task for this parent in this session
|
|
263
|
+
const sessionTask = specToTaskMap.get(parentSpec._ulid);
|
|
264
|
+
if (sessionTask) {
|
|
265
|
+
return getTaskRef(sessionTask, index);
|
|
266
|
+
}
|
|
267
|
+
// Check if an existing task is linked to this parent spec
|
|
268
|
+
const linkedTasks = alignmentIndex.getTasksForSpec(parentSpec._ulid);
|
|
269
|
+
if (linkedTasks.length > 0) {
|
|
270
|
+
return getTaskRef(linkedTasks[0], index);
|
|
271
|
+
}
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Register the 'derive' command
|
|
276
|
+
*/
|
|
277
|
+
export function registerDeriveCommand(program) {
|
|
278
|
+
program
|
|
279
|
+
.command('derive [ref]')
|
|
280
|
+
.description('Create task(s) from spec item(s)')
|
|
281
|
+
.option('--all', 'Derive tasks for all spec items without linked tasks')
|
|
282
|
+
.option('--flat', 'Only derive for the specified item, not children (default: recursive)')
|
|
283
|
+
.option('--force', 'Create task even if one already exists for the spec')
|
|
284
|
+
.option('--dry-run', 'Show what would be created without making changes')
|
|
285
|
+
.option('--priority <n>', 'Set priority for created task(s) (1-5)', parseInt)
|
|
286
|
+
.action(async (ref, options) => {
|
|
287
|
+
try {
|
|
288
|
+
// Validate arguments
|
|
289
|
+
if (!ref && !options.all) {
|
|
290
|
+
error(errors.usage.deriveNoRef);
|
|
291
|
+
console.error('Usage:');
|
|
292
|
+
console.error(' kspec derive @spec-ref');
|
|
293
|
+
console.error(' kspec derive @spec-ref --flat');
|
|
294
|
+
console.error(' kspec derive --all');
|
|
295
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
296
|
+
}
|
|
297
|
+
if (ref && options.all) {
|
|
298
|
+
error(errors.usage.deriveRefAndAll);
|
|
299
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
300
|
+
}
|
|
301
|
+
// Validate priority if provided
|
|
302
|
+
if (options.priority !== undefined) {
|
|
303
|
+
if (isNaN(options.priority) || options.priority < 1 || options.priority > 5) {
|
|
304
|
+
error('Priority must be a number between 1 and 5');
|
|
305
|
+
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const ctx = await initContext();
|
|
309
|
+
const tasks = await loadAllTasks(ctx);
|
|
310
|
+
const items = await loadAllItems(ctx);
|
|
311
|
+
const index = new ReferenceIndex(tasks, items);
|
|
312
|
+
// Build alignment index
|
|
313
|
+
const alignmentIndex = new AlignmentIndex(tasks, items);
|
|
314
|
+
alignmentIndex.buildLinks(index);
|
|
315
|
+
// Collect spec items to process
|
|
316
|
+
let specsToDerive;
|
|
317
|
+
if (options.all) {
|
|
318
|
+
// Get all spec items without linked tasks
|
|
319
|
+
specsToDerive = items.filter(item => {
|
|
320
|
+
const linkedTasks = alignmentIndex.getTasksForSpec(item._ulid);
|
|
321
|
+
return linkedTasks.length === 0 || options.force;
|
|
322
|
+
});
|
|
323
|
+
if (specsToDerive.length === 0) {
|
|
324
|
+
if (isJsonMode()) {
|
|
325
|
+
console.log(JSON.stringify([]));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
info('Nothing to derive (all items have tasks)');
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
// Single spec item - recursive by default, flat if --flat
|
|
335
|
+
const specItem = resolveSpecRef(ref, items, tasks, index);
|
|
336
|
+
if (options.flat) {
|
|
337
|
+
specsToDerive = [specItem];
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Recursive: collect item and all descendants
|
|
341
|
+
specsToDerive = collectItemsRecursively(specItem, items);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Track spec ULID -> created task for dependency resolution
|
|
345
|
+
const specToTaskMap = new Map();
|
|
346
|
+
// Process each spec item in order (parents before children due to topological sort)
|
|
347
|
+
const results = [];
|
|
348
|
+
for (const specItem of specsToDerive) {
|
|
349
|
+
// Determine depends_on based on parent spec's task
|
|
350
|
+
let dependsOn;
|
|
351
|
+
if (!options.flat && !options.all) {
|
|
352
|
+
// Find the parent spec item
|
|
353
|
+
const parentSpec = findParentItem(specItem, items);
|
|
354
|
+
if (parentSpec) {
|
|
355
|
+
const parentTaskRef = getParentTaskRef(parentSpec, specToTaskMap, alignmentIndex, index);
|
|
356
|
+
if (parentTaskRef) {
|
|
357
|
+
dependsOn = [parentTaskRef];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const result = await deriveTaskFromSpec(ctx, specItem, tasks, items, index, alignmentIndex, {
|
|
362
|
+
force: options.force || false,
|
|
363
|
+
dryRun: options.dryRun || false,
|
|
364
|
+
dependsOn,
|
|
365
|
+
priority: options.priority,
|
|
366
|
+
});
|
|
367
|
+
// Track created/would_create tasks for dependency resolution
|
|
368
|
+
if (result.task && (result.action === 'created' || result.action === 'would_create')) {
|
|
369
|
+
specToTaskMap.set(specItem._ulid, result.task);
|
|
370
|
+
}
|
|
371
|
+
// Also track skipped tasks (existing) for dependency resolution
|
|
372
|
+
if (result.action === 'skipped' && result.task) {
|
|
373
|
+
specToTaskMap.set(specItem._ulid, result.task);
|
|
374
|
+
}
|
|
375
|
+
results.push(result);
|
|
376
|
+
}
|
|
377
|
+
// Output results
|
|
378
|
+
if (isJsonMode()) {
|
|
379
|
+
// JSON output format - simplified per AC
|
|
380
|
+
const jsonOutput = results.map(r => ({
|
|
381
|
+
ulid: r.task?._ulid || null,
|
|
382
|
+
slug: r.task?.slugs[0] || null,
|
|
383
|
+
spec_ref: `@${r.specItem.slugs[0] || r.specItem._ulid}`,
|
|
384
|
+
depends_on: r.task?.depends_on || [],
|
|
385
|
+
action: r.action,
|
|
386
|
+
}));
|
|
387
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
388
|
+
return; // Don't call output() which would output full results in global JSON mode
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// Human-readable output
|
|
392
|
+
output(results, () => {
|
|
393
|
+
const created = results.filter(r => r.action === 'created');
|
|
394
|
+
const skipped = results.filter(r => r.action === 'skipped');
|
|
395
|
+
const wouldCreate = results.filter(r => r.action === 'would_create');
|
|
396
|
+
if (options.dryRun) {
|
|
397
|
+
console.log('Would create:');
|
|
398
|
+
for (const r of wouldCreate) {
|
|
399
|
+
const taskSlug = r.task?.slugs[0] || '';
|
|
400
|
+
const deps = r.dependsOn?.length ? ` (depends: ${r.dependsOn.join(', ')})` : '';
|
|
401
|
+
console.log(` + ${r.specItem.title}`);
|
|
402
|
+
console.log(` -> ${taskSlug}${deps}`);
|
|
403
|
+
}
|
|
404
|
+
if (skipped.length > 0) {
|
|
405
|
+
console.log('\nSkipped:');
|
|
406
|
+
for (const r of skipped) {
|
|
407
|
+
const specRef = r.specItem.slugs[0] ? `@${r.specItem.slugs[0]}` : `@${index.shortUlid(r.specItem._ulid)}`;
|
|
408
|
+
console.log(` - ${specRef} (${r.reason})`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
console.log(`\nWould create ${wouldCreate.length} task(s)`);
|
|
412
|
+
if (skipped.length > 0) {
|
|
413
|
+
console.log(`Skipped ${skipped.length} (already have tasks)`);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (created.length > 0) {
|
|
418
|
+
for (const r of created) {
|
|
419
|
+
const taskSlug = r.task?.slugs[0] || '';
|
|
420
|
+
const deps = r.dependsOn?.length ? ` (depends: ${r.dependsOn.join(', ')})` : '';
|
|
421
|
+
console.log(`OK Created task: ${taskSlug}${deps}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (skipped.length > 0 && !options.all) {
|
|
425
|
+
// Show skipped for explicit derive (not --all)
|
|
426
|
+
for (const r of skipped) {
|
|
427
|
+
const specRef = r.specItem.slugs[0] ? `@${r.specItem.slugs[0]}` : `@${index.shortUlid(r.specItem._ulid)}`;
|
|
428
|
+
console.log(`Skipped ${specRef} (${r.reason})`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Summary
|
|
432
|
+
if (created.length > 0 || skipped.length > 0) {
|
|
433
|
+
console.log('');
|
|
434
|
+
if (created.length > 0) {
|
|
435
|
+
console.log(`Created ${created.length} task(s)`);
|
|
436
|
+
}
|
|
437
|
+
if (skipped.length > 0) {
|
|
438
|
+
console.log(`Skipped ${skipped.length} (already have tasks)`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
error(errors.failures.deriveTasks, err);
|
|
446
|
+
process.exit(EXIT_CODES.ERROR);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=derive.js.map
|