devvami 1.4.2 → 1.5.1
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 +72 -0
- package/oclif.manifest.json +275 -235
- package/package.json +2 -1
- package/src/commands/auth/login.js +20 -16
- package/src/commands/changelog.js +12 -12
- package/src/commands/costs/get.js +14 -24
- package/src/commands/costs/trend.js +13 -24
- package/src/commands/create/repo.js +72 -54
- package/src/commands/docs/list.js +29 -25
- package/src/commands/docs/projects.js +58 -24
- package/src/commands/docs/read.js +56 -39
- package/src/commands/docs/search.js +37 -25
- package/src/commands/doctor.js +37 -35
- package/src/commands/dotfiles/add.js +51 -39
- package/src/commands/dotfiles/setup.js +62 -33
- package/src/commands/dotfiles/status.js +18 -18
- package/src/commands/dotfiles/sync.js +62 -46
- package/src/commands/init.js +143 -132
- package/src/commands/logs/index.js +10 -16
- package/src/commands/open.js +12 -12
- package/src/commands/pipeline/logs.js +8 -11
- package/src/commands/pipeline/rerun.js +21 -16
- package/src/commands/pipeline/status.js +28 -24
- package/src/commands/pr/create.js +40 -27
- package/src/commands/pr/detail.js +9 -7
- package/src/commands/pr/review.js +18 -19
- package/src/commands/pr/status.js +27 -21
- package/src/commands/prompts/browse.js +15 -15
- package/src/commands/prompts/download.js +15 -16
- package/src/commands/prompts/install-speckit.js +11 -12
- package/src/commands/prompts/list.js +12 -12
- package/src/commands/prompts/run.js +16 -19
- package/src/commands/repo/list.js +57 -41
- package/src/commands/search.js +20 -18
- package/src/commands/security/setup.js +38 -34
- package/src/commands/sync-config-ai/index.js +257 -0
- package/src/commands/tasks/assigned.js +43 -33
- package/src/commands/tasks/list.js +43 -33
- package/src/commands/tasks/today.js +32 -30
- package/src/commands/upgrade.js +18 -17
- package/src/commands/vuln/detail.js +8 -8
- package/src/commands/vuln/scan.js +39 -20
- package/src/commands/vuln/search.js +23 -18
- package/src/commands/welcome.js +2 -2
- package/src/commands/whoami.js +19 -23
- package/src/formatters/ai-config.js +215 -0
- package/src/formatters/charts.js +6 -23
- package/src/formatters/cost.js +1 -7
- package/src/formatters/dotfiles.js +48 -19
- package/src/formatters/markdown.js +11 -6
- package/src/formatters/openapi.js +7 -9
- package/src/formatters/prompts.js +69 -78
- package/src/formatters/security.js +2 -2
- package/src/formatters/status.js +1 -1
- package/src/formatters/table.js +1 -3
- package/src/formatters/vuln.js +33 -20
- package/src/help.js +162 -164
- package/src/hooks/init.js +1 -3
- package/src/hooks/postrun.js +5 -7
- package/src/index.js +1 -1
- package/src/services/ai-config-store.js +349 -0
- package/src/services/ai-env-deployer.js +650 -0
- package/src/services/ai-env-scanner.js +983 -0
- package/src/services/audit-detector.js +2 -2
- package/src/services/audit-runner.js +40 -31
- package/src/services/auth.js +9 -9
- package/src/services/awesome-copilot.js +7 -4
- package/src/services/aws-costs.js +22 -22
- package/src/services/clickup.js +26 -26
- package/src/services/cloudwatch-logs.js +5 -9
- package/src/services/config.js +13 -13
- package/src/services/docs.js +19 -20
- package/src/services/dotfiles.js +149 -51
- package/src/services/github.js +22 -24
- package/src/services/nvd.js +21 -31
- package/src/services/platform.js +2 -2
- package/src/services/prompts.js +23 -35
- package/src/services/security.js +135 -61
- package/src/services/shell.js +4 -4
- package/src/services/skills-sh.js +3 -9
- package/src/services/speckit.js +4 -7
- package/src/services/version-check.js +10 -10
- package/src/types.js +117 -0
- package/src/utils/aws-vault.js +18 -41
- package/src/utils/banner.js +5 -7
- package/src/utils/errors.js +42 -46
- package/src/utils/frontmatter.js +4 -4
- package/src/utils/gradient.js +18 -16
- package/src/utils/open-browser.js +3 -3
- package/src/utils/tui/form.js +1184 -0
- package/src/utils/tui/modal.js +15 -14
- package/src/utils/tui/navigable-table.js +16 -16
- package/src/utils/tui/tab-tui.js +1089 -0
- package/src/utils/typewriter.js +3 -3
- package/src/utils/welcome.js +18 -21
- package/src/validators/repo-name.js +2 -2
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import {confirm, select} from '@inquirer/prompts'
|
|
3
3
|
import ora from 'ora'
|
|
4
4
|
import chalk from 'chalk'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import {execa} from 'execa'
|
|
6
|
+
import {detectPlatform} from '../../services/platform.js'
|
|
7
|
+
import {exec} from '../../services/shell.js'
|
|
8
|
+
import {buildSteps, checkToolStatus, listGpgKeys, deriveOverallStatus} from '../../services/security.js'
|
|
9
|
+
import {formatEducationalIntro, formatStepHeader, formatSecuritySummary} from '../../formatters/security.js'
|
|
10
10
|
/** @import { SetupSession, SetupStep, StepResult, PlatformInfo } from '../../types.js' */
|
|
11
11
|
|
|
12
12
|
export default class SecuritySetup extends Command {
|
|
13
|
-
static description =
|
|
13
|
+
static description =
|
|
14
|
+
'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
|
|
14
15
|
|
|
15
|
-
static examples = [
|
|
16
|
-
'<%= config.bin %> security setup',
|
|
17
|
-
'<%= config.bin %> security setup --json',
|
|
18
|
-
]
|
|
16
|
+
static examples = ['<%= config.bin %> security setup', '<%= config.bin %> security setup --json']
|
|
19
17
|
|
|
20
18
|
static enableJsonFlag = true
|
|
21
19
|
|
|
22
20
|
static flags = {
|
|
23
|
-
help: Flags.help({
|
|
21
|
+
help: Flags.help({char: 'h'}),
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
async run() {
|
|
27
|
-
const {
|
|
25
|
+
const {flags} = await this.parse(SecuritySetup)
|
|
28
26
|
const isJson = flags.json
|
|
29
27
|
|
|
30
28
|
// FR-018: Detect non-interactive environments
|
|
@@ -34,22 +32,19 @@ export default class SecuritySetup extends Command {
|
|
|
34
32
|
if ((isCI || isNonInteractive) && !isJson) {
|
|
35
33
|
this.error(
|
|
36
34
|
'This command requires an interactive terminal (TTY). Run with --json for a non-interactive health check.',
|
|
37
|
-
{
|
|
35
|
+
{exit: 1},
|
|
38
36
|
)
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
// Detect platform
|
|
42
40
|
const platformInfo = await detectPlatform()
|
|
43
|
-
const {
|
|
41
|
+
const {platform} = platformInfo
|
|
44
42
|
|
|
45
43
|
// FR-019: Sudo pre-flight on Linux/WSL2
|
|
46
44
|
if (platform !== 'macos' && !isJson) {
|
|
47
45
|
const sudoCheck = await exec('sudo', ['-n', 'true'])
|
|
48
46
|
if (sudoCheck.exitCode !== 0) {
|
|
49
|
-
this.error(
|
|
50
|
-
'sudo access is required to install packages. Run `sudo -v` to authenticate and retry.',
|
|
51
|
-
{ exit: 1 },
|
|
52
|
-
)
|
|
47
|
+
this.error('sudo access is required to install packages. Run `sudo -v` to authenticate and retry.', {exit: 1})
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
|
|
@@ -68,7 +63,11 @@ export default class SecuritySetup extends Command {
|
|
|
68
63
|
// ---------------------------------------------------------------------------
|
|
69
64
|
// Pre-check: show current tool status
|
|
70
65
|
// ---------------------------------------------------------------------------
|
|
71
|
-
const spinner = ora({
|
|
66
|
+
const spinner = ora({
|
|
67
|
+
spinner: 'arc',
|
|
68
|
+
color: false,
|
|
69
|
+
text: chalk.hex('#FF6B2B')('Checking current tool status...'),
|
|
70
|
+
}).start()
|
|
72
71
|
const currentStatus = await checkToolStatus(platform)
|
|
73
72
|
spinner.stop()
|
|
74
73
|
|
|
@@ -100,7 +99,7 @@ export default class SecuritySetup extends Command {
|
|
|
100
99
|
})
|
|
101
100
|
if (!understood) {
|
|
102
101
|
this.log('Setup cancelled.')
|
|
103
|
-
return {
|
|
102
|
+
return {platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus)}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
// ---------------------------------------------------------------------------
|
|
@@ -109,9 +108,9 @@ export default class SecuritySetup extends Command {
|
|
|
109
108
|
const selectionValue = await select({
|
|
110
109
|
message: 'What would you like to set up?',
|
|
111
110
|
choices: [
|
|
112
|
-
{
|
|
113
|
-
{
|
|
114
|
-
{
|
|
111
|
+
{name: 'Both AWS and Git credentials (recommended)', value: 'both'},
|
|
112
|
+
{name: 'AWS credentials only (aws-vault)', value: 'aws'},
|
|
113
|
+
{name: 'Git credentials only (macOS Keychain / GCM)', value: 'git'},
|
|
115
114
|
],
|
|
116
115
|
})
|
|
117
116
|
|
|
@@ -131,7 +130,7 @@ export default class SecuritySetup extends Command {
|
|
|
131
130
|
name: `${k.name} <${k.email}> (${k.id})`,
|
|
132
131
|
value: k.id,
|
|
133
132
|
})),
|
|
134
|
-
{
|
|
133
|
+
{name: 'Create a new GPG key', value: '__new__'},
|
|
135
134
|
]
|
|
136
135
|
const chosen = await select({
|
|
137
136
|
message: 'Select a GPG key for pass and Git Credential Manager:',
|
|
@@ -145,7 +144,7 @@ export default class SecuritySetup extends Command {
|
|
|
145
144
|
// ---------------------------------------------------------------------------
|
|
146
145
|
// Build steps
|
|
147
146
|
// ---------------------------------------------------------------------------
|
|
148
|
-
const steps = buildSteps(platformInfo, selection, {
|
|
147
|
+
const steps = buildSteps(platformInfo, selection, {gpgId})
|
|
149
148
|
|
|
150
149
|
/** @type {SetupSession} */
|
|
151
150
|
const session = {
|
|
@@ -166,9 +165,9 @@ export default class SecuritySetup extends Command {
|
|
|
166
165
|
|
|
167
166
|
// FR-014: confirmation prompt before system-level changes
|
|
168
167
|
if (step.requiresConfirmation) {
|
|
169
|
-
const proceed = await confirm({
|
|
168
|
+
const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
|
|
170
169
|
if (!proceed) {
|
|
171
|
-
session.results.set(step.id, {
|
|
170
|
+
session.results.set(step.id, {status: 'skipped', message: 'Skipped by user'})
|
|
172
171
|
this.log(chalk.dim(' Skipped.'))
|
|
173
172
|
continue
|
|
174
173
|
}
|
|
@@ -179,17 +178,17 @@ export default class SecuritySetup extends Command {
|
|
|
179
178
|
this.log(chalk.cyan('\n GPG will now prompt you for a passphrase in your terminal.'))
|
|
180
179
|
this.log(chalk.dim(' Follow the interactive prompts to complete key generation.\n'))
|
|
181
180
|
try {
|
|
182
|
-
await execa('gpg', ['--full-generate-key'], {
|
|
181
|
+
await execa('gpg', ['--full-generate-key'], {stdio: 'inherit', reject: true})
|
|
183
182
|
// Refresh the gpgId from newly created key
|
|
184
183
|
const newKeys = await listGpgKeys()
|
|
185
184
|
if (newKeys.length > 0) {
|
|
186
185
|
gpgId = newKeys[0].id
|
|
187
186
|
// gpgId is now set — subsequent step closures capture it via the shared context object
|
|
188
187
|
}
|
|
189
|
-
session.results.set(step.id, {
|
|
188
|
+
session.results.set(step.id, {status: 'success', message: `GPG key created (${gpgId || 'new key'})`})
|
|
190
189
|
this.log(chalk.green(' ✔ GPG key created'))
|
|
191
190
|
} catch {
|
|
192
|
-
const result = {
|
|
191
|
+
const result = {status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key'}
|
|
193
192
|
session.results.set(step.id, result)
|
|
194
193
|
this.log(chalk.red(' ✗ GPG key creation failed'))
|
|
195
194
|
this.log(chalk.dim(` → ${result.hint}`))
|
|
@@ -200,7 +199,7 @@ export default class SecuritySetup extends Command {
|
|
|
200
199
|
}
|
|
201
200
|
|
|
202
201
|
// Regular step with spinner
|
|
203
|
-
const stepSpinner = ora({
|
|
202
|
+
const stepSpinner = ora({spinner: 'arc', color: false, text: chalk.dim(step.label)}).start()
|
|
204
203
|
|
|
205
204
|
let result
|
|
206
205
|
try {
|
|
@@ -243,7 +242,12 @@ export default class SecuritySetup extends Command {
|
|
|
243
242
|
platform,
|
|
244
243
|
selection,
|
|
245
244
|
tools: currentStatus,
|
|
246
|
-
overallStatus:
|
|
245
|
+
overallStatus:
|
|
246
|
+
session.overallStatus === 'completed'
|
|
247
|
+
? 'success'
|
|
248
|
+
: session.overallStatus === 'failed'
|
|
249
|
+
? 'partial'
|
|
250
|
+
: 'not-configured',
|
|
247
251
|
}
|
|
248
252
|
}
|
|
249
253
|
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
|
+
import ora from 'ora'
|
|
3
|
+
|
|
4
|
+
import {scanEnvironments, computeCategoryCounts, parseNativeEntries, detectDrift, ENVIRONMENTS} from '../../services/ai-env-scanner.js'
|
|
5
|
+
import {
|
|
6
|
+
loadAIConfig,
|
|
7
|
+
addEntry,
|
|
8
|
+
updateEntry,
|
|
9
|
+
deactivateEntry,
|
|
10
|
+
activateEntry,
|
|
11
|
+
deleteEntry,
|
|
12
|
+
syncAIConfigToChezmoi,
|
|
13
|
+
} from '../../services/ai-config-store.js'
|
|
14
|
+
import {deployEntry, undeployEntry, reconcileOnScan} from '../../services/ai-env-deployer.js'
|
|
15
|
+
import {loadConfig} from '../../services/config.js'
|
|
16
|
+
import {formatEnvironmentsTable, formatCategoriesTable, formatNativeEntriesTable} from '../../formatters/ai-config.js'
|
|
17
|
+
import {startTabTUI} from '../../utils/tui/tab-tui.js'
|
|
18
|
+
import {DvmiError} from '../../utils/errors.js'
|
|
19
|
+
|
|
20
|
+
/** @import { DetectedEnvironment, CategoryEntry, MCPParams } from '../../types.js' */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extract only MCPParams-relevant fields from raw form values.
|
|
24
|
+
* Parses args (editor newline-joined) into string[] and env vars (KEY=VALUE lines) into Record.
|
|
25
|
+
* @param {Record<string, unknown>} values - Raw form output from extractValues
|
|
26
|
+
* @returns {MCPParams}
|
|
27
|
+
*/
|
|
28
|
+
function buildMCPParams(values) {
|
|
29
|
+
/** @type {MCPParams} */
|
|
30
|
+
const params = {transport: /** @type {'stdio'|'sse'|'streamable-http'} */ (values.transport)}
|
|
31
|
+
|
|
32
|
+
if (params.transport === 'stdio') {
|
|
33
|
+
if (values.command) params.command = /** @type {string} */ (values.command)
|
|
34
|
+
// Args: editor field → newline-joined string → split into array
|
|
35
|
+
if (values.args && typeof values.args === 'string') {
|
|
36
|
+
const arr = /** @type {string} */ (values.args).split('\n').map((a) => a.trim()).filter(Boolean)
|
|
37
|
+
if (arr.length > 0) params.args = arr
|
|
38
|
+
} else if (Array.isArray(values.args) && values.args.length > 0) {
|
|
39
|
+
params.args = values.args
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
if (values.url) params.url = /** @type {string} */ (values.url)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Env vars: editor field → newline-joined KEY=VALUE string → parse into Record.
|
|
46
|
+
// Env vars apply to ALL transports (e.g. API keys for remote servers too).
|
|
47
|
+
if (values.env && typeof values.env === 'string') {
|
|
48
|
+
/** @type {Record<string, string>} */
|
|
49
|
+
const envObj = {}
|
|
50
|
+
for (const line of /** @type {string} */ (values.env).split('\n')) {
|
|
51
|
+
const t = line.trim()
|
|
52
|
+
if (!t) continue
|
|
53
|
+
const eq = t.indexOf('=')
|
|
54
|
+
if (eq > 0) envObj[t.slice(0, eq)] = t.slice(eq + 1)
|
|
55
|
+
}
|
|
56
|
+
if (Object.keys(envObj).length > 0) params.env = envObj
|
|
57
|
+
} else if (values.env && typeof values.env === 'object' && !Array.isArray(values.env)) {
|
|
58
|
+
params.env = /** @type {Record<string, string>} */ (values.env)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return params
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default class SyncConfigAi extends Command {
|
|
65
|
+
static description = 'Manage AI coding tool configurations across environments via TUI'
|
|
66
|
+
|
|
67
|
+
static examples = ['<%= config.bin %> sync-config-ai', '<%= config.bin %> sync-config-ai --json']
|
|
68
|
+
|
|
69
|
+
static enableJsonFlag = true
|
|
70
|
+
|
|
71
|
+
static flags = {
|
|
72
|
+
help: Flags.help({char: 'h'}),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async run() {
|
|
76
|
+
const {flags} = await this.parse(SyncConfigAi)
|
|
77
|
+
const isJson = flags.json
|
|
78
|
+
|
|
79
|
+
// ── Scan environments ────────────────────────────────────────────────────
|
|
80
|
+
const spinner = isJson ? null : ora('Scanning AI coding environments…').start()
|
|
81
|
+
let detectedEnvs
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
detectedEnvs = scanEnvironments(process.cwd())
|
|
85
|
+
} catch (err) {
|
|
86
|
+
spinner?.fail('Scan failed')
|
|
87
|
+
throw new DvmiError(
|
|
88
|
+
'Failed to scan AI coding environments',
|
|
89
|
+
err instanceof Error ? err.message : 'Check filesystem permissions',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Load AI config store ─────────────────────────────────────────────────
|
|
94
|
+
let store
|
|
95
|
+
try {
|
|
96
|
+
store = await loadAIConfig()
|
|
97
|
+
} catch {
|
|
98
|
+
spinner?.fail('Failed to load AI config')
|
|
99
|
+
throw new DvmiError(
|
|
100
|
+
'AI config file is corrupted',
|
|
101
|
+
'Delete `~/.config/dvmi/ai-config.json` to reset, or fix the JSON manually',
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Reconcile: re-deploy/undeploy based on current environment detection ─
|
|
106
|
+
if (detectedEnvs.length > 0 && store.entries.length > 0) {
|
|
107
|
+
try {
|
|
108
|
+
await reconcileOnScan(store.entries, detectedEnvs, process.cwd())
|
|
109
|
+
// Reload store after reconciliation in case it mutated entries
|
|
110
|
+
store = await loadAIConfig()
|
|
111
|
+
} catch {
|
|
112
|
+
// Reconciliation errors are non-fatal — continue with current state
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Compute per-environment category counts ──────────────────────────────
|
|
117
|
+
for (const env of detectedEnvs) {
|
|
118
|
+
env.counts = computeCategoryCounts(env.id, store.entries)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Parse native entries and populate nativeCounts ───────────────────────
|
|
122
|
+
const envDefMap = new Map(ENVIRONMENTS.map((e) => [e.id, e]))
|
|
123
|
+
for (const env of detectedEnvs) {
|
|
124
|
+
const envDef = envDefMap.get(env.id)
|
|
125
|
+
if (!envDef) continue
|
|
126
|
+
const natives = parseNativeEntries(envDef, process.cwd(), store.entries)
|
|
127
|
+
env.nativeEntries = natives
|
|
128
|
+
// Aggregate native counts per category
|
|
129
|
+
env.nativeCounts = {mcp: 0, command: 0, rule: 0, skill: 0, agent: 0}
|
|
130
|
+
for (const ne of natives) {
|
|
131
|
+
env.nativeCounts[ne.type] = (env.nativeCounts[ne.type] ?? 0) + 1
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Detect drift for managed entries ────────────────────────────────────
|
|
136
|
+
const driftInfos = detectDrift(detectedEnvs, store.entries, process.cwd())
|
|
137
|
+
for (const env of detectedEnvs) {
|
|
138
|
+
env.driftedEntries = driftInfos.filter((d) => d.environmentId === env.id)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
spinner?.stop()
|
|
142
|
+
|
|
143
|
+
// ── JSON mode ────────────────────────────────────────────────────────────
|
|
144
|
+
if (isJson) {
|
|
145
|
+
if (detectedEnvs.length === 0) {
|
|
146
|
+
this.exit(2)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Collect all native entries grouped by type
|
|
150
|
+
const allNatives = detectedEnvs.flatMap((e) => e.nativeEntries ?? [])
|
|
151
|
+
|
|
152
|
+
// Build drifted set for quick lookup
|
|
153
|
+
const driftedIds = new Set(driftInfos.map((d) => d.entryId))
|
|
154
|
+
|
|
155
|
+
const categories = {
|
|
156
|
+
mcp: store.entries.filter((e) => e.type === 'mcp').map((e) => ({...e, drifted: driftedIds.has(e.id)})),
|
|
157
|
+
command: store.entries.filter((e) => e.type === 'command').map((e) => ({...e, drifted: driftedIds.has(e.id)})),
|
|
158
|
+
rule: store.entries.filter((e) => e.type === 'rule').map((e) => ({...e, drifted: driftedIds.has(e.id)})),
|
|
159
|
+
skill: store.entries.filter((e) => e.type === 'skill').map((e) => ({...e, drifted: driftedIds.has(e.id)})),
|
|
160
|
+
agent: store.entries.filter((e) => e.type === 'agent').map((e) => ({...e, drifted: driftedIds.has(e.id)})),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const nativeEntries = {
|
|
164
|
+
mcp: allNatives.filter((e) => e.type === 'mcp'),
|
|
165
|
+
command: allNatives.filter((e) => e.type === 'command'),
|
|
166
|
+
rule: allNatives.filter((e) => e.type === 'rule'),
|
|
167
|
+
skill: allNatives.filter((e) => e.type === 'skill'),
|
|
168
|
+
agent: allNatives.filter((e) => e.type === 'agent'),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {environments: detectedEnvs, categories, nativeEntries}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Check chezmoi config ─────────────────────────────────────────────────
|
|
175
|
+
let chezmoiEnabled = false
|
|
176
|
+
try {
|
|
177
|
+
const cliConfig = await loadConfig()
|
|
178
|
+
chezmoiEnabled = cliConfig.dotfiles?.enabled === true
|
|
179
|
+
} catch {
|
|
180
|
+
// Non-fatal — chezmoi tip will show
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Launch TUI ───────────────────────────────────────────────────────────
|
|
184
|
+
await startTabTUI({
|
|
185
|
+
envs: detectedEnvs,
|
|
186
|
+
entries: store.entries,
|
|
187
|
+
chezmoiEnabled,
|
|
188
|
+
formatEnvs: formatEnvironmentsTable,
|
|
189
|
+
formatCats: formatCategoriesTable,
|
|
190
|
+
formatNative: formatNativeEntriesTable,
|
|
191
|
+
refreshEntries: async () => {
|
|
192
|
+
const s = await loadAIConfig()
|
|
193
|
+
return s.entries
|
|
194
|
+
},
|
|
195
|
+
onAction: async (action) => {
|
|
196
|
+
// Reload current entries for each action to avoid stale data
|
|
197
|
+
const currentStore = await loadAIConfig()
|
|
198
|
+
|
|
199
|
+
if (action.type === 'create') {
|
|
200
|
+
const isMCP = action.tabKey === 'mcp'
|
|
201
|
+
const created = await addEntry({
|
|
202
|
+
name: action.values.name,
|
|
203
|
+
type: action.tabKey || 'mcp',
|
|
204
|
+
environments: action.values.environments || [],
|
|
205
|
+
params: isMCP ? buildMCPParams(action.values) : action.values,
|
|
206
|
+
})
|
|
207
|
+
await deployEntry(created, detectedEnvs, process.cwd())
|
|
208
|
+
await syncAIConfigToChezmoi()
|
|
209
|
+
} else if (action.type === 'edit') {
|
|
210
|
+
const entry = currentStore.entries.find((e) => e.id === action.id)
|
|
211
|
+
const isMCP = entry?.type === 'mcp'
|
|
212
|
+
const updated = await updateEntry(action.id, {params: isMCP ? buildMCPParams(action.values) : action.values})
|
|
213
|
+
await deployEntry(updated, detectedEnvs, process.cwd())
|
|
214
|
+
await syncAIConfigToChezmoi()
|
|
215
|
+
} else if (action.type === 'delete') {
|
|
216
|
+
await deleteEntry(action.id)
|
|
217
|
+
await undeployEntry(
|
|
218
|
+
currentStore.entries.find((e) => e.id === action.id),
|
|
219
|
+
detectedEnvs,
|
|
220
|
+
process.cwd(),
|
|
221
|
+
)
|
|
222
|
+
await syncAIConfigToChezmoi()
|
|
223
|
+
} else if (action.type === 'deactivate') {
|
|
224
|
+
const entry = await deactivateEntry(action.id)
|
|
225
|
+
await undeployEntry(entry, detectedEnvs, process.cwd())
|
|
226
|
+
await syncAIConfigToChezmoi()
|
|
227
|
+
} else if (action.type === 'activate') {
|
|
228
|
+
const entry = await activateEntry(action.id)
|
|
229
|
+
await deployEntry(entry, detectedEnvs, process.cwd())
|
|
230
|
+
await syncAIConfigToChezmoi()
|
|
231
|
+
} else if (action.type === 'import-native') {
|
|
232
|
+
// T017: Import native entry into dvmi-managed sync
|
|
233
|
+
const ne = action.nativeEntry
|
|
234
|
+
const created = await addEntry({
|
|
235
|
+
name: ne.name,
|
|
236
|
+
type: ne.type,
|
|
237
|
+
environments: [ne.environmentId],
|
|
238
|
+
params: ne.params,
|
|
239
|
+
})
|
|
240
|
+
await deployEntry(created, detectedEnvs, process.cwd())
|
|
241
|
+
await syncAIConfigToChezmoi()
|
|
242
|
+
} else if (action.type === 'redeploy') {
|
|
243
|
+
// T018: Re-deploy managed entry to overwrite drifted file
|
|
244
|
+
const entry = currentStore.entries.find((e) => e.id === action.id)
|
|
245
|
+
if (entry) await deployEntry(entry, detectedEnvs, process.cwd())
|
|
246
|
+
} else if (action.type === 'accept-drift') {
|
|
247
|
+
// T018: Accept drift — update store params from the actual file state
|
|
248
|
+
const drift = driftInfos.find((d) => d.entryId === action.id)
|
|
249
|
+
if (drift) {
|
|
250
|
+
await updateEntry(action.id, {params: drift.actual})
|
|
251
|
+
await syncAIConfigToChezmoi()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Command, Flags} from '@oclif/core'
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import ora from 'ora'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import {getTasks, getTasksByList, isAuthenticated} from '../../services/clickup.js'
|
|
5
|
+
import {loadConfig} from '../../services/config.js'
|
|
6
|
+
import {renderTable} from '../../formatters/table.js'
|
|
7
7
|
|
|
8
8
|
export default class TasksAssigned extends Command {
|
|
9
9
|
static description = 'Task ClickUp assegnati a te (alias di tasks list)'
|
|
@@ -19,7 +19,7 @@ export default class TasksAssigned extends Command {
|
|
|
19
19
|
static enableJsonFlag = true
|
|
20
20
|
|
|
21
21
|
static flags = {
|
|
22
|
-
status: Flags.string({
|
|
22
|
+
status: Flags.string({description: 'Filtra per status (open, in_progress, done)'}),
|
|
23
23
|
search: Flags.string({
|
|
24
24
|
char: 's',
|
|
25
25
|
description: 'Cerca nel titolo del task (case-insensitive)',
|
|
@@ -30,7 +30,7 @@ export default class TasksAssigned extends Command {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async run() {
|
|
33
|
-
const {
|
|
33
|
+
const {flags} = await this.parse(TasksAssigned)
|
|
34
34
|
const isJson = flags.json
|
|
35
35
|
const config = await loadConfig()
|
|
36
36
|
|
|
@@ -45,7 +45,9 @@ export default class TasksAssigned extends Command {
|
|
|
45
45
|
this.error('ClickUp team ID not configured. Run `dvmi init` to configure ClickUp.')
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const spinner = isJson
|
|
48
|
+
const spinner = isJson
|
|
49
|
+
? null
|
|
50
|
+
: ora({spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...')}).start()
|
|
49
51
|
|
|
50
52
|
/** @param {number} count */
|
|
51
53
|
const onProgress = (count) => {
|
|
@@ -54,40 +56,38 @@ export default class TasksAssigned extends Command {
|
|
|
54
56
|
|
|
55
57
|
let tasks
|
|
56
58
|
if (flags['list-id']) {
|
|
57
|
-
tasks = await getTasksByList(flags['list-id'], {
|
|
59
|
+
tasks = await getTasksByList(flags['list-id'], {status: flags.status}, onProgress).catch((err) => {
|
|
58
60
|
spinner?.stop()
|
|
59
61
|
this.error(err.message)
|
|
60
62
|
})
|
|
61
63
|
} else {
|
|
62
|
-
tasks = await getTasks(/** @type {string} */ (teamId), {
|
|
64
|
+
tasks = await getTasks(/** @type {string} */ (teamId), {status: flags.status}, onProgress)
|
|
63
65
|
}
|
|
64
66
|
spinner?.stop()
|
|
65
67
|
|
|
66
68
|
// Apply search filter
|
|
67
69
|
const searchQuery = flags.search?.toLowerCase()
|
|
68
|
-
const filtered = searchQuery
|
|
69
|
-
? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery))
|
|
70
|
-
: tasks
|
|
70
|
+
const filtered = searchQuery ? tasks.filter((t) => t.name.toLowerCase().includes(searchQuery)) : tasks
|
|
71
71
|
|
|
72
|
-
if (isJson) return {
|
|
72
|
+
if (isJson) return {tasks: filtered}
|
|
73
73
|
|
|
74
74
|
if (tasks.length === 0) {
|
|
75
75
|
this.log(chalk.dim('No tasks assigned to you.'))
|
|
76
|
-
return {
|
|
76
|
+
return {tasks: []}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (filtered.length === 0) {
|
|
80
80
|
this.log(chalk.dim('No tasks matching filters.'))
|
|
81
|
-
return {
|
|
81
|
+
return {tasks: []}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Priority label + color
|
|
85
85
|
const priorityLabel = (p) => ['', 'URGENT', 'HIGH', 'NORMAL', 'LOW'][p] ?? String(p)
|
|
86
86
|
const priorityColor = (label) => {
|
|
87
87
|
if (label === 'URGENT') return chalk.red.bold(label)
|
|
88
|
-
if (label === 'HIGH')
|
|
88
|
+
if (label === 'HIGH') return chalk.yellow(label)
|
|
89
89
|
if (label === 'NORMAL') return chalk.white(label)
|
|
90
|
-
if (label === 'LOW')
|
|
90
|
+
if (label === 'LOW') return chalk.dim(label)
|
|
91
91
|
return label
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -96,7 +96,7 @@ export default class TasksAssigned extends Command {
|
|
|
96
96
|
const s = status.toLowerCase()
|
|
97
97
|
if (s.includes('done') || s.includes('complet') || s.includes('closed')) return chalk.green(status)
|
|
98
98
|
if (s.includes('progress') || s.includes('active') || s.includes('open')) return chalk.cyan(status)
|
|
99
|
-
if (s.includes('block') || s.includes('review') || s.includes('wait'))
|
|
99
|
+
if (s.includes('block') || s.includes('review') || s.includes('wait')) return chalk.yellow(status)
|
|
100
100
|
return chalk.dim(status)
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -105,27 +105,37 @@ export default class TasksAssigned extends Command {
|
|
|
105
105
|
flags.status && chalk.dim(`status: ${chalk.white(flags.status)}`),
|
|
106
106
|
flags.search && chalk.dim(`search: ${chalk.white(`"${flags.search}"`)}`),
|
|
107
107
|
flags['list-id'] && chalk.dim(`list-id: ${chalk.white(flags['list-id'])}`),
|
|
108
|
-
]
|
|
108
|
+
]
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join(chalk.dim(' · '))
|
|
109
111
|
|
|
110
112
|
this.log(
|
|
111
113
|
chalk.bold('\nYour assigned tasks') +
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
(filterInfo ? chalk.dim(' — ') + filterInfo : '') +
|
|
115
|
+
chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
|
|
116
|
+
'\n',
|
|
115
117
|
)
|
|
116
118
|
|
|
117
|
-
this.log(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
119
|
+
this.log(
|
|
120
|
+
renderTable(filtered, [
|
|
121
|
+
{header: 'ID', key: 'id', width: 10},
|
|
122
|
+
{header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—'},
|
|
123
|
+
{
|
|
124
|
+
header: 'Priority',
|
|
125
|
+
key: 'priority',
|
|
126
|
+
width: 8,
|
|
127
|
+
format: (v) => priorityLabel(Number(v)),
|
|
128
|
+
colorize: priorityColor,
|
|
129
|
+
},
|
|
130
|
+
{header: 'Status', key: 'status', width: 15, colorize: statusColor},
|
|
131
|
+
{header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—'},
|
|
132
|
+
{header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—'},
|
|
133
|
+
{header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—'},
|
|
134
|
+
{header: 'Description', key: 'name', width: 55},
|
|
135
|
+
]),
|
|
136
|
+
)
|
|
127
137
|
|
|
128
138
|
this.log('')
|
|
129
|
-
return {
|
|
139
|
+
return {tasks: filtered}
|
|
130
140
|
}
|
|
131
141
|
}
|