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.
Files changed (96) hide show
  1. package/README.md +72 -0
  2. package/oclif.manifest.json +275 -235
  3. package/package.json +2 -1
  4. package/src/commands/auth/login.js +20 -16
  5. package/src/commands/changelog.js +12 -12
  6. package/src/commands/costs/get.js +14 -24
  7. package/src/commands/costs/trend.js +13 -24
  8. package/src/commands/create/repo.js +72 -54
  9. package/src/commands/docs/list.js +29 -25
  10. package/src/commands/docs/projects.js +58 -24
  11. package/src/commands/docs/read.js +56 -39
  12. package/src/commands/docs/search.js +37 -25
  13. package/src/commands/doctor.js +37 -35
  14. package/src/commands/dotfiles/add.js +51 -39
  15. package/src/commands/dotfiles/setup.js +62 -33
  16. package/src/commands/dotfiles/status.js +18 -18
  17. package/src/commands/dotfiles/sync.js +62 -46
  18. package/src/commands/init.js +143 -132
  19. package/src/commands/logs/index.js +10 -16
  20. package/src/commands/open.js +12 -12
  21. package/src/commands/pipeline/logs.js +8 -11
  22. package/src/commands/pipeline/rerun.js +21 -16
  23. package/src/commands/pipeline/status.js +28 -24
  24. package/src/commands/pr/create.js +40 -27
  25. package/src/commands/pr/detail.js +9 -7
  26. package/src/commands/pr/review.js +18 -19
  27. package/src/commands/pr/status.js +27 -21
  28. package/src/commands/prompts/browse.js +15 -15
  29. package/src/commands/prompts/download.js +15 -16
  30. package/src/commands/prompts/install-speckit.js +11 -12
  31. package/src/commands/prompts/list.js +12 -12
  32. package/src/commands/prompts/run.js +16 -19
  33. package/src/commands/repo/list.js +57 -41
  34. package/src/commands/search.js +20 -18
  35. package/src/commands/security/setup.js +38 -34
  36. package/src/commands/sync-config-ai/index.js +257 -0
  37. package/src/commands/tasks/assigned.js +43 -33
  38. package/src/commands/tasks/list.js +43 -33
  39. package/src/commands/tasks/today.js +32 -30
  40. package/src/commands/upgrade.js +18 -17
  41. package/src/commands/vuln/detail.js +8 -8
  42. package/src/commands/vuln/scan.js +39 -20
  43. package/src/commands/vuln/search.js +23 -18
  44. package/src/commands/welcome.js +2 -2
  45. package/src/commands/whoami.js +19 -23
  46. package/src/formatters/ai-config.js +215 -0
  47. package/src/formatters/charts.js +6 -23
  48. package/src/formatters/cost.js +1 -7
  49. package/src/formatters/dotfiles.js +48 -19
  50. package/src/formatters/markdown.js +11 -6
  51. package/src/formatters/openapi.js +7 -9
  52. package/src/formatters/prompts.js +69 -78
  53. package/src/formatters/security.js +2 -2
  54. package/src/formatters/status.js +1 -1
  55. package/src/formatters/table.js +1 -3
  56. package/src/formatters/vuln.js +33 -20
  57. package/src/help.js +162 -164
  58. package/src/hooks/init.js +1 -3
  59. package/src/hooks/postrun.js +5 -7
  60. package/src/index.js +1 -1
  61. package/src/services/ai-config-store.js +349 -0
  62. package/src/services/ai-env-deployer.js +650 -0
  63. package/src/services/ai-env-scanner.js +983 -0
  64. package/src/services/audit-detector.js +2 -2
  65. package/src/services/audit-runner.js +40 -31
  66. package/src/services/auth.js +9 -9
  67. package/src/services/awesome-copilot.js +7 -4
  68. package/src/services/aws-costs.js +22 -22
  69. package/src/services/clickup.js +26 -26
  70. package/src/services/cloudwatch-logs.js +5 -9
  71. package/src/services/config.js +13 -13
  72. package/src/services/docs.js +19 -20
  73. package/src/services/dotfiles.js +149 -51
  74. package/src/services/github.js +22 -24
  75. package/src/services/nvd.js +21 -31
  76. package/src/services/platform.js +2 -2
  77. package/src/services/prompts.js +23 -35
  78. package/src/services/security.js +135 -61
  79. package/src/services/shell.js +4 -4
  80. package/src/services/skills-sh.js +3 -9
  81. package/src/services/speckit.js +4 -7
  82. package/src/services/version-check.js +10 -10
  83. package/src/types.js +117 -0
  84. package/src/utils/aws-vault.js +18 -41
  85. package/src/utils/banner.js +5 -7
  86. package/src/utils/errors.js +42 -46
  87. package/src/utils/frontmatter.js +4 -4
  88. package/src/utils/gradient.js +18 -16
  89. package/src/utils/open-browser.js +3 -3
  90. package/src/utils/tui/form.js +1184 -0
  91. package/src/utils/tui/modal.js +15 -14
  92. package/src/utils/tui/navigable-table.js +16 -16
  93. package/src/utils/tui/tab-tui.js +1089 -0
  94. package/src/utils/typewriter.js +3 -3
  95. package/src/utils/welcome.js +18 -21
  96. package/src/validators/repo-name.js +2 -2
@@ -1,30 +1,28 @@
1
- import { Command, Flags } from '@oclif/core'
2
- import { confirm, select } from '@inquirer/prompts'
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 { 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'
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 = 'Interactive wizard to install and configure credential protection tools (aws-vault, pass, GPG, Git Credential Manager, macOS Keychain)'
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({ char: 'h' }),
21
+ help: Flags.help({char: 'h'}),
24
22
  }
25
23
 
26
24
  async run() {
27
- const { flags } = await this.parse(SecuritySetup)
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
- { exit: 1 },
35
+ {exit: 1},
38
36
  )
39
37
  }
40
38
 
41
39
  // Detect platform
42
40
  const platformInfo = await detectPlatform()
43
- const { platform } = platformInfo
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({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Checking current tool status...') }).start()
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 { platform, selection: null, tools: currentStatus, overallStatus: deriveOverallStatus(currentStatus) }
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
- { name: 'Both AWS and Git credentials (recommended)', value: 'both' },
113
- { name: 'AWS credentials only (aws-vault)', value: 'aws' },
114
- { name: 'Git credentials only (macOS Keychain / GCM)', value: 'git' },
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
- { name: 'Create a new GPG key', value: '__new__' },
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, { gpgId })
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({ message: `Proceed with: ${step.label}?`, default: true })
168
+ const proceed = await confirm({message: `Proceed with: ${step.label}?`, default: true})
170
169
  if (!proceed) {
171
- session.results.set(step.id, { status: 'skipped', message: 'Skipped by user' })
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'], { stdio: 'inherit', reject: true })
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, { status: 'success', message: `GPG key created (${gpgId || 'new key'})` })
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 = { status: /** @type {'failed'} */ ('failed'), hint: 'Run manually: gpg --full-generate-key' }
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({ spinner: 'arc', color: false, text: chalk.dim(step.label) }).start()
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: session.overallStatus === 'completed' ? 'success' : session.overallStatus === 'failed' ? 'partial' : 'not-configured',
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 { Command, Flags } from '@oclif/core'
1
+ import {Command, Flags} from '@oclif/core'
2
2
  import chalk from 'chalk'
3
3
  import ora from 'ora'
4
- import { getTasks, getTasksByList, isAuthenticated } from '../../services/clickup.js'
5
- import { loadConfig } from '../../services/config.js'
6
- import { renderTable } from '../../formatters/table.js'
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({ description: 'Filtra per status (open, in_progress, done)' }),
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 { flags } = await this.parse(TasksAssigned)
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 ? null : ora({ spinner: 'arc', color: false, text: chalk.hex('#FF6B2B')('Fetching tasks...') }).start()
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'], { status: flags.status }, onProgress).catch((err) => {
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), { status: flags.status }, onProgress)
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 { tasks: filtered }
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 { tasks: [] }
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 { tasks: [] }
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') return chalk.yellow(label)
88
+ if (label === 'HIGH') return chalk.yellow(label)
89
89
  if (label === 'NORMAL') return chalk.white(label)
90
- if (label === 'LOW') return chalk.dim(label)
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')) return chalk.yellow(status)
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
- ].filter(Boolean).join(chalk.dim(' · '))
108
+ ]
109
+ .filter(Boolean)
110
+ .join(chalk.dim(' · '))
109
111
 
110
112
  this.log(
111
113
  chalk.bold('\nYour assigned tasks') +
112
- (filterInfo ? chalk.dim(' — ') + filterInfo : '') +
113
- chalk.dim(` (${filtered.length}${filtered.length < tasks.length ? `/${tasks.length}` : ''})`) +
114
- '\n',
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(renderTable(filtered, [
118
- { header: 'ID', key: 'id', width: 10 },
119
- { header: 'Link', key: 'url', width: 42, format: (v) => v ?? '—' },
120
- { header: 'Priority', key: 'priority', width: 8, format: (v) => priorityLabel(Number(v)), colorize: priorityColor },
121
- { header: 'Status', key: 'status', width: 15, colorize: statusColor },
122
- { header: 'Due', key: 'dueDate', width: 12, format: (v) => v ?? '—' },
123
- { header: 'Lista', key: 'listName', width: 20, format: (v) => v ?? '—' },
124
- { header: 'Cartella', key: 'folderName', width: 20, format: (v) => v ?? '—' },
125
- { header: 'Description', key: 'name', width: 55 },
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 { tasks: filtered }
139
+ return {tasks: filtered}
130
140
  }
131
141
  }