devflow-kit 1.8.0 → 1.8.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/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/commands/init.d.ts +1 -12
- package/dist/commands/init.js +267 -195
- package/dist/utils/post-install.d.ts +8 -1
- package/dist/utils/post-install.js +41 -0
- package/package.json +1 -1
- package/plugins/devflow-accessibility/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-ambient/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-audit-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-code-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-core-skills/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-debug/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-frontend-design/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-go/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-implement/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-java/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-python/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-react/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-resolve/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-rust/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-self-review/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-specify/.claude-plugin/plugin.json +1 -1
- package/plugins/devflow-typescript/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to DevFlow will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.8.1] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Init wizard**: individual feature prompts with explanatory notes replace extras multiselect
|
|
12
|
+
- **Init wizard**: scope-aware `.claudeignore` batch install across all discovered projects (user scope)
|
|
13
|
+
- **Init wizard**: project discovery via `~/.claude/history.jsonl` to find all Claude-used git repos
|
|
14
|
+
- **Init wizard**: managed settings sudo confirmation moved to prompt phase (before spinner)
|
|
15
|
+
- **Init wizard**: safe-delete prompt moved to prompt phase for uninterrupted install
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `--hud` flag for `devflow init` to explicitly enable HUD
|
|
19
|
+
- `discoverProjectGitRoots()` utility for finding projects from Claude history
|
|
20
|
+
|
|
21
|
+
### Removed
|
|
22
|
+
- Extras multiselect (`buildExtrasOptions`) — replaced by individual feature prompts
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
8
26
|
## [1.8.0] - 2026-03-22
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -958,6 +976,7 @@ devflow init
|
|
|
958
976
|
---
|
|
959
977
|
|
|
960
978
|
[Unreleased]: https://github.com/dean0x/devflow/compare/v1.4.0...HEAD
|
|
979
|
+
[1.8.1]: https://github.com/dean0x/devflow/compare/v1.8.0...v1.8.1
|
|
961
980
|
[1.8.0]: https://github.com/dean0x/devflow/compare/v1.7.0...v1.8.0
|
|
962
981
|
[1.7.0]: https://github.com/dean0x/devflow/compare/v1.6.1...v1.7.0
|
|
963
982
|
[1.6.1]: https://github.com/dean0x/devflow/compare/v1.6.0...v1.6.1
|
package/README.md
CHANGED
|
@@ -248,8 +248,8 @@ Session context is saved and restored automatically via Working Memory hooks —
|
|
|
248
248
|
| `--teams` / `--no-teams` | Enable/disable Agent Teams (experimental, default: off) |
|
|
249
249
|
| `--ambient` / `--no-ambient` | Enable/disable ambient mode (default: on) |
|
|
250
250
|
| `--memory` / `--no-memory` | Enable/disable working memory (default: on) |
|
|
251
|
+
| `--hud` / `--no-hud` | Enable/disable HUD status line (default: on) |
|
|
251
252
|
| `--hud-only` | Install only the HUD (no plugins, hooks, or extras) |
|
|
252
|
-
| `--no-hud` | Disable HUD status line |
|
|
253
253
|
| `--verbose` | Show detailed output |
|
|
254
254
|
|
|
255
255
|
### HUD Options
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type PluginDefinition } from '../plugins.js';
|
|
3
|
-
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
|
|
3
|
+
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList, discoverProjectGitRoots } from '../utils/post-install.js';
|
|
4
4
|
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
5
5
|
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
6
6
|
export { addHudStatusLine, removeHudStatusLine, hasHudStatusLine } from './hud.js';
|
|
@@ -12,16 +12,5 @@ export declare function parsePluginSelection(input: string, validPlugins: Plugin
|
|
|
12
12
|
selected: string[];
|
|
13
13
|
invalid: string[];
|
|
14
14
|
};
|
|
15
|
-
export type ExtraId = 'settings' | 'claudeignore' | 'gitignore' | 'docs' | 'safe-delete';
|
|
16
|
-
interface ExtraOption {
|
|
17
|
-
value: ExtraId;
|
|
18
|
-
label: string;
|
|
19
|
-
hint: string;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Build the list of configuration extras available for the given scope/git context.
|
|
23
|
-
* Pure function — no I/O, no side effects.
|
|
24
|
-
*/
|
|
25
|
-
export declare function buildExtrasOptions(scope: 'user' | 'local', gitRoot: string | null): ExtraOption[];
|
|
26
15
|
export declare const initCommand: Command;
|
|
27
16
|
//# sourceMappingURL=init.d.ts.map
|
package/dist/commands/init.js
CHANGED
|
@@ -9,7 +9,7 @@ import { getInstallationPaths } from '../utils/paths.js';
|
|
|
9
9
|
import { getGitRoot } from '../utils/git.js';
|
|
10
10
|
import { isClaudeCliAvailable } from '../utils/cli.js';
|
|
11
11
|
import { installViaCli, installViaFileCopy, copyDirectory } from '../utils/installer.js';
|
|
12
|
-
import { installSettings, installManagedSettings, installClaudeignore, updateGitignore, createDocsStructure, createMemoryDir, migrateMemoryFiles, } from '../utils/post-install.js';
|
|
12
|
+
import { installSettings, installManagedSettings, installClaudeignore, discoverProjectGitRoots, updateGitignore, createDocsStructure, createMemoryDir, migrateMemoryFiles, } from '../utils/post-install.js';
|
|
13
13
|
import { DEVFLOW_PLUGINS, LEGACY_SKILL_NAMES, LEGACY_COMMAND_NAMES, buildAssetMaps } from '../plugins.js';
|
|
14
14
|
import { detectPlatform, detectShell, getProfilePath, getSafeDeleteInfo, hasSafeDelete } from '../utils/safe-delete.js';
|
|
15
15
|
import { generateSafeDeleteBlock, installToProfile, removeFromProfile, getInstalledVersion, SAFE_DELETE_BLOCK_VERSION } from '../utils/safe-delete-install.js';
|
|
@@ -19,7 +19,7 @@ import { addHudStatusLine, removeHudStatusLine } from './hud.js';
|
|
|
19
19
|
import { loadConfig as loadHudConfig, saveConfig as saveHudConfig } from '../hud/config.js';
|
|
20
20
|
import { readManifest, writeManifest, resolvePluginList, detectUpgrade } from '../utils/manifest.js';
|
|
21
21
|
// Re-export pure functions for tests (canonical source is post-install.ts)
|
|
22
|
-
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList } from '../utils/post-install.js';
|
|
22
|
+
export { substituteSettingsTemplate, computeGitignoreAppend, applyTeamsConfig, stripTeamsConfig, mergeDenyList, discoverProjectGitRoots } from '../utils/post-install.js';
|
|
23
23
|
export { addAmbientHook, removeAmbientHook, hasAmbientHook } from './ambient.js';
|
|
24
24
|
export { addMemoryHooks, removeMemoryHooks, hasMemoryHooks } from './memory.js';
|
|
25
25
|
export { addHudStatusLine, removeHudStatusLine, hasHudStatusLine } from './hud.js';
|
|
@@ -38,26 +38,6 @@ export function parsePluginSelection(input, validPlugins) {
|
|
|
38
38
|
const invalid = selected.filter(p => !validNames.includes(p));
|
|
39
39
|
return { selected, invalid };
|
|
40
40
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Build the list of configuration extras available for the given scope/git context.
|
|
43
|
-
* Pure function — no I/O, no side effects.
|
|
44
|
-
*/
|
|
45
|
-
export function buildExtrasOptions(scope, gitRoot) {
|
|
46
|
-
const options = [
|
|
47
|
-
{ value: 'settings', label: 'Settings & Working Memory', hint: 'Model defaults, session memory hooks, status line' },
|
|
48
|
-
];
|
|
49
|
-
if (gitRoot) {
|
|
50
|
-
options.push({ value: 'claudeignore', label: '.claudeignore', hint: 'Exclude secrets, deps, build artifacts from Claude context' });
|
|
51
|
-
}
|
|
52
|
-
if (scope === 'local' && gitRoot) {
|
|
53
|
-
options.push({ value: 'gitignore', label: '.gitignore entries', hint: 'Add .claude/ and .devflow/ to .gitignore' });
|
|
54
|
-
}
|
|
55
|
-
if (scope === 'local') {
|
|
56
|
-
options.push({ value: 'docs', label: '.docs/ directory', hint: 'Review reports, dev logs, status tracking for this project' });
|
|
57
|
-
}
|
|
58
|
-
options.push({ value: 'safe-delete', label: 'Safe-delete (rm → trash)', hint: 'Override rm to use trash CLI — prevents accidental deletion' });
|
|
59
|
-
return options;
|
|
60
|
-
}
|
|
61
41
|
export const initCommand = new Command('init')
|
|
62
42
|
.description('Initialize DevFlow for Claude Code')
|
|
63
43
|
.option('--scope <type>', 'Installation scope: user or local (project-only)', /^(user|local)$/i)
|
|
@@ -69,6 +49,7 @@ export const initCommand = new Command('init')
|
|
|
69
49
|
.option('--no-ambient', 'Disable ambient mode')
|
|
70
50
|
.option('--memory', 'Enable working memory (session context preservation)')
|
|
71
51
|
.option('--no-memory', 'Disable working memory hooks')
|
|
52
|
+
.option('--hud', 'Enable HUD (git info, context usage, session stats)')
|
|
72
53
|
.option('--no-hud', 'Disable HUD status line')
|
|
73
54
|
.option('--hud-only', 'Install only the HUD (no plugins, hooks, or extras)')
|
|
74
55
|
.action(async (options) => {
|
|
@@ -193,12 +174,29 @@ export const initCommand = new Command('init')
|
|
|
193
174
|
}
|
|
194
175
|
}
|
|
195
176
|
else if (process.stdin.isTTY) {
|
|
177
|
+
// Short hints to prevent overflow in multiselect — full descriptions live in plugins.ts
|
|
178
|
+
const pluginHints = {
|
|
179
|
+
'devflow-specify': 'feature specs → GitHub issues',
|
|
180
|
+
'devflow-implement': 'explore, plan, code, review',
|
|
181
|
+
'devflow-code-review': 'parallel specialized reviewers',
|
|
182
|
+
'devflow-resolve': 'fix review issues by risk',
|
|
183
|
+
'devflow-debug': 'competing hypotheses',
|
|
184
|
+
'devflow-self-review': 'Simplifier + Scrutinizer',
|
|
185
|
+
'devflow-typescript': 'TypeScript patterns',
|
|
186
|
+
'devflow-react': 'React patterns',
|
|
187
|
+
'devflow-accessibility': 'WCAG compliance',
|
|
188
|
+
'devflow-frontend-design': 'typography, color, spacing',
|
|
189
|
+
'devflow-go': 'Go patterns',
|
|
190
|
+
'devflow-java': 'Java patterns',
|
|
191
|
+
'devflow-python': 'Python patterns',
|
|
192
|
+
'devflow-rust': 'Rust patterns',
|
|
193
|
+
};
|
|
196
194
|
const choices = DEVFLOW_PLUGINS
|
|
197
195
|
.filter(pl => pl.name !== 'devflow-core-skills' && pl.name !== 'devflow-ambient' && pl.name !== 'devflow-audit-claude')
|
|
198
196
|
.map(pl => ({
|
|
199
197
|
value: pl.name,
|
|
200
198
|
label: pl.name.replace('devflow-', ''),
|
|
201
|
-
hint: pl.description,
|
|
199
|
+
hint: pluginHints[pl.name] ?? pl.description,
|
|
202
200
|
}));
|
|
203
201
|
const preSelected = DEVFLOW_PLUGINS
|
|
204
202
|
.filter(pl => !pl.optional && pl.name !== 'devflow-core-skills' && pl.name !== 'devflow-ambient')
|
|
@@ -224,11 +222,14 @@ export const initCommand = new Command('init')
|
|
|
224
222
|
teamsEnabled = false;
|
|
225
223
|
}
|
|
226
224
|
else {
|
|
225
|
+
p.note('Agent Teams enable peer debate between agents — adversarial\n' +
|
|
226
|
+
'review, competing hypotheses in debugging, and consensus-driven\n' +
|
|
227
|
+
'exploration. Experimental: may be unstable.', 'Agent Teams');
|
|
227
228
|
const teamsChoice = await p.select({
|
|
228
229
|
message: 'Enable Agent Teams?',
|
|
229
230
|
options: [
|
|
230
|
-
{ value: false, label: '
|
|
231
|
-
{ value: true, label: 'Yes', hint: '
|
|
231
|
+
{ value: false, label: 'Not yet', hint: 'Recommended' },
|
|
232
|
+
{ value: true, label: 'Yes', hint: 'Experimental' },
|
|
232
233
|
],
|
|
233
234
|
});
|
|
234
235
|
if (p.isCancel(teamsChoice)) {
|
|
@@ -246,11 +247,15 @@ export const initCommand = new Command('init')
|
|
|
246
247
|
ambientEnabled = true;
|
|
247
248
|
}
|
|
248
249
|
else {
|
|
250
|
+
p.note('Auto-classifies every prompt by intent and depth. Loads relevant\n' +
|
|
251
|
+
'skills automatically and escalates to full agent pipelines\n' +
|
|
252
|
+
'(review, debug, implement) when the task warrants it.\n\n' +
|
|
253
|
+
'Adds a small amount of context to each prompt for classification.', 'Ambient Mode');
|
|
249
254
|
const ambientChoice = await p.select({
|
|
250
255
|
message: 'Enable ambient mode?',
|
|
251
256
|
options: [
|
|
252
|
-
{ value: true, label: 'Yes
|
|
253
|
-
{ value: false, label: 'No', hint: '
|
|
257
|
+
{ value: true, label: 'Yes', hint: 'Recommended' },
|
|
258
|
+
{ value: false, label: 'No', hint: 'Manual skill loading via slash commands' },
|
|
254
259
|
],
|
|
255
260
|
});
|
|
256
261
|
if (p.isCancel(ambientChoice)) {
|
|
@@ -268,8 +273,13 @@ export const initCommand = new Command('init')
|
|
|
268
273
|
memoryEnabled = true;
|
|
269
274
|
}
|
|
270
275
|
else {
|
|
276
|
+
p.note('Preserves session context across /clear, restarts, and context\n' +
|
|
277
|
+
'compaction. Clear your session at any point and resume right\n' +
|
|
278
|
+
'where you left off.\n\n' +
|
|
279
|
+
'Runs a background agent on session stop that consumes additional\n' +
|
|
280
|
+
'tokens. Consider skipping if token usage is a concern.', 'Working Memory');
|
|
271
281
|
const memoryChoice = await p.confirm({
|
|
272
|
-
message: 'Enable working memory? (
|
|
282
|
+
message: 'Enable working memory? (Recommended)',
|
|
273
283
|
initialValue: true,
|
|
274
284
|
});
|
|
275
285
|
if (p.isCancel(memoryChoice)) {
|
|
@@ -287,8 +297,10 @@ export const initCommand = new Command('init')
|
|
|
287
297
|
hudEnabled = true;
|
|
288
298
|
}
|
|
289
299
|
else {
|
|
300
|
+
p.note('The HUD displays git branch, context usage, and session stats\n' +
|
|
301
|
+
'in the Claude Code status bar. Configurable via devflow hud.', 'HUD');
|
|
290
302
|
const hudChoice = await p.confirm({
|
|
291
|
-
message: 'Enable HUD
|
|
303
|
+
message: 'Enable HUD? (Recommended)',
|
|
292
304
|
initialValue: true,
|
|
293
305
|
});
|
|
294
306
|
if (p.isCancel(hudChoice)) {
|
|
@@ -297,14 +309,110 @@ export const initCommand = new Command('init')
|
|
|
297
309
|
}
|
|
298
310
|
hudEnabled = hudChoice;
|
|
299
311
|
}
|
|
300
|
-
//
|
|
312
|
+
// .claudeignore prompt (needs early git detection)
|
|
313
|
+
const earlyGitRoot = await getGitRoot();
|
|
314
|
+
let claudeignoreEnabled = true;
|
|
315
|
+
let discoveredProjects = [];
|
|
316
|
+
if (earlyGitRoot && process.stdin.isTTY) {
|
|
317
|
+
if (scope === 'user') {
|
|
318
|
+
// User scope: discover all projects and offer batch install
|
|
319
|
+
discoveredProjects = await discoverProjectGitRoots();
|
|
320
|
+
p.note('Scans all projects Claude has worked on and creates a\n' +
|
|
321
|
+
'.claudeignore in each git repository. Excludes secrets,\n' +
|
|
322
|
+
'API keys, dependencies, and build artifacts from context.', '.claudeignore');
|
|
323
|
+
if (discoveredProjects.length > 0) {
|
|
324
|
+
const maxShow = 5;
|
|
325
|
+
const projectLines = discoveredProjects.slice(0, maxShow).join('\n');
|
|
326
|
+
const overflow = discoveredProjects.length > maxShow
|
|
327
|
+
? `\n... (${discoveredProjects.length - maxShow} more)`
|
|
328
|
+
: '';
|
|
329
|
+
p.note(projectLines + overflow, `Discovered ${discoveredProjects.length} projects`);
|
|
330
|
+
const claudeignoreChoice = await p.confirm({
|
|
331
|
+
message: `Install .claudeignore to ${discoveredProjects.length} projects? (Recommended)`,
|
|
332
|
+
initialValue: true,
|
|
333
|
+
});
|
|
334
|
+
if (p.isCancel(claudeignoreChoice)) {
|
|
335
|
+
p.cancel('Installation cancelled.');
|
|
336
|
+
process.exit(0);
|
|
337
|
+
}
|
|
338
|
+
claudeignoreEnabled = claudeignoreChoice;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// No projects discovered — fall back to current project only
|
|
342
|
+
const claudeignoreChoice = await p.confirm({
|
|
343
|
+
message: 'Create .claudeignore? (Recommended)',
|
|
344
|
+
initialValue: true,
|
|
345
|
+
});
|
|
346
|
+
if (p.isCancel(claudeignoreChoice)) {
|
|
347
|
+
p.cancel('Installation cancelled.');
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
claudeignoreEnabled = claudeignoreChoice;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Local scope: single-project note
|
|
355
|
+
p.note('Creates a .claudeignore in this project that excludes\n' +
|
|
356
|
+
'secrets, API keys, dependencies, and build artifacts from\n' +
|
|
357
|
+
'Claude\'s context window.', '.claudeignore');
|
|
358
|
+
const claudeignoreChoice = await p.confirm({
|
|
359
|
+
message: 'Create .claudeignore? (Recommended)',
|
|
360
|
+
initialValue: true,
|
|
361
|
+
});
|
|
362
|
+
if (p.isCancel(claudeignoreChoice)) {
|
|
363
|
+
p.cancel('Installation cancelled.');
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
claudeignoreEnabled = claudeignoreChoice;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (!earlyGitRoot) {
|
|
370
|
+
claudeignoreEnabled = false;
|
|
371
|
+
}
|
|
372
|
+
// Safe-delete detection + prompt (all detection early, action deferred)
|
|
373
|
+
const platform = detectPlatform();
|
|
374
|
+
const shell = detectShell();
|
|
375
|
+
const safeDeleteInfo = getSafeDeleteInfo(platform);
|
|
376
|
+
const safeDeleteAvailable = hasSafeDelete(platform);
|
|
377
|
+
const profilePath = getProfilePath(shell);
|
|
378
|
+
let safeDeleteAction = 'skip';
|
|
379
|
+
let safeDeleteBlock = null;
|
|
380
|
+
if (process.stdin.isTTY && profilePath && safeDeleteAvailable) {
|
|
381
|
+
const trashCmd = safeDeleteInfo.command;
|
|
382
|
+
safeDeleteBlock = generateSafeDeleteBlock(shell, process.platform, trashCmd);
|
|
383
|
+
if (safeDeleteBlock) {
|
|
384
|
+
const installedVersion = await getInstalledVersion(profilePath);
|
|
385
|
+
if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
|
|
386
|
+
safeDeleteAction = 'skip'; // already current
|
|
387
|
+
}
|
|
388
|
+
else if (installedVersion > 0) {
|
|
389
|
+
safeDeleteAction = 'upgrade'; // auto-upgrade, no prompt needed
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Fresh install — prompt
|
|
393
|
+
p.note('Overrides rm to use your system trash CLI instead of permanent\n' +
|
|
394
|
+
'deletion. Prevents accidental data loss from rm -rf.', 'Safe Delete');
|
|
395
|
+
const safeDeleteConfirm = await p.confirm({
|
|
396
|
+
message: `Install safe-delete to ${profilePath}? (uses ${trashCmd ?? 'recycle bin'})`,
|
|
397
|
+
initialValue: true,
|
|
398
|
+
});
|
|
399
|
+
if (!p.isCancel(safeDeleteConfirm) && safeDeleteConfirm) {
|
|
400
|
+
safeDeleteAction = 'install';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Security deny list placement (user scope + TTY only — last prompt before install)
|
|
301
406
|
let securityMode = 'user';
|
|
302
407
|
if (scope === 'user' && process.stdin.isTTY) {
|
|
408
|
+
p.note('DevFlow includes a security deny list that blocks dangerous\n' +
|
|
409
|
+
'commands (rm -rf, sudo, eval, etc). It can be installed as a\n' +
|
|
410
|
+
'read-only system file or in your editable settings.json.', 'Security Deny List');
|
|
303
411
|
const securityChoice = await p.select({
|
|
304
|
-
message: 'How should DevFlow install the
|
|
412
|
+
message: 'How should DevFlow install the deny list?',
|
|
305
413
|
options: [
|
|
306
|
-
{ value: 'managed', label: 'Managed settings
|
|
307
|
-
{ value: 'user', label: 'User settings', hint: '
|
|
414
|
+
{ value: 'managed', label: 'Managed settings', hint: 'Recommended — read-only, cannot be overridden' },
|
|
415
|
+
{ value: 'user', label: 'User settings', hint: 'Editable in settings.json' },
|
|
308
416
|
],
|
|
309
417
|
});
|
|
310
418
|
if (p.isCancel(securityChoice)) {
|
|
@@ -313,7 +421,32 @@ export const initCommand = new Command('init')
|
|
|
313
421
|
}
|
|
314
422
|
securityMode = securityChoice;
|
|
315
423
|
}
|
|
316
|
-
//
|
|
424
|
+
// Managed settings sudo confirmation (last interactive step — may prompt for password)
|
|
425
|
+
let managedSettingsConfirmed = false;
|
|
426
|
+
if (securityMode === 'managed') {
|
|
427
|
+
p.note('This writes a read-only security deny list to a system directory\n' +
|
|
428
|
+
'and may prompt for your password (sudo).\n\n' +
|
|
429
|
+
'Not sure about this? Paste this into another Claude Code session:\n\n' +
|
|
430
|
+
' "I\'m installing DevFlow and it wants to write a\n' +
|
|
431
|
+
' managed-settings.json file using sudo. Review the source\n' +
|
|
432
|
+
' at https://github.com/dean0x/devflow and tell me if\n' +
|
|
433
|
+
' it\'s safe."', 'Managed Settings');
|
|
434
|
+
const sudoChoice = await p.select({
|
|
435
|
+
message: 'Continue with managed settings?',
|
|
436
|
+
options: [
|
|
437
|
+
{ value: 'yes', label: 'Yes, continue', hint: 'May prompt for your password' },
|
|
438
|
+
{ value: 'no', label: 'No, fall back to settings.json', hint: 'Editable user settings instead' },
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
if (p.isCancel(sudoChoice)) {
|
|
442
|
+
p.cancel('Installation cancelled.');
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
managedSettingsConfirmed = sudoChoice === 'yes';
|
|
446
|
+
}
|
|
447
|
+
// ╭──────────────────────────────────────────────────────────╮
|
|
448
|
+
// │ All prompts collected — installation begins │
|
|
449
|
+
// ╰──────────────────────────────────────────────────────────╯
|
|
317
450
|
const s = p.spinner();
|
|
318
451
|
s.start('Resolving paths');
|
|
319
452
|
// Get installation paths
|
|
@@ -324,7 +457,7 @@ export const initCommand = new Command('init')
|
|
|
324
457
|
const paths = await getInstallationPaths(scope);
|
|
325
458
|
claudeDir = paths.claudeDir;
|
|
326
459
|
devflowDir = paths.devflowDir;
|
|
327
|
-
gitRoot = paths.gitRoot ??
|
|
460
|
+
gitRoot = paths.gitRoot ?? earlyGitRoot;
|
|
328
461
|
}
|
|
329
462
|
catch (error) {
|
|
330
463
|
s.stop('Path resolution failed');
|
|
@@ -408,8 +541,8 @@ export const initCommand = new Command('init')
|
|
|
408
541
|
process.exit(1);
|
|
409
542
|
}
|
|
410
543
|
}
|
|
411
|
-
s.stop('Plugins installed');
|
|
412
544
|
// Clean up stale skills from previous installations
|
|
545
|
+
s.message('Cleaning up');
|
|
413
546
|
const skillsDir = path.join(claudeDir, 'skills');
|
|
414
547
|
let staleRemoved = 0;
|
|
415
548
|
for (const legacy of LEGACY_SKILL_NAMES) {
|
|
@@ -443,131 +576,90 @@ export const initCommand = new Command('init')
|
|
|
443
576
|
if (staleCommandsRemoved > 0 && verbose) {
|
|
444
577
|
p.log.info(`Cleaned up ${staleCommandsRemoved} legacy command(s)`);
|
|
445
578
|
}
|
|
446
|
-
// ===
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
options: extrasOptions,
|
|
453
|
-
initialValues: extrasOptions.map(o => o.value),
|
|
454
|
-
required: false,
|
|
455
|
-
});
|
|
456
|
-
if (p.isCancel(extrasSelection)) {
|
|
457
|
-
p.cancel('Installation cancelled.');
|
|
458
|
-
process.exit(0);
|
|
459
|
-
}
|
|
460
|
-
selectedExtras = extrasSelection;
|
|
579
|
+
// === Settings & hooks (all automatic based on collected choices) ===
|
|
580
|
+
s.message('Configuring settings');
|
|
581
|
+
// Determine effective security mode (managed settings executed later, after safe-delete)
|
|
582
|
+
let effectiveSecurityMode = securityMode;
|
|
583
|
+
if (securityMode === 'managed' && !managedSettingsConfirmed) {
|
|
584
|
+
effectiveSecurityMode = 'user';
|
|
461
585
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if (securityMode === 'managed') {
|
|
470
|
-
p.note('This writes a read-only security deny list to a system directory\n' +
|
|
471
|
-
'and may prompt for your password (sudo).\n\n' +
|
|
472
|
-
'Not sure about this? Paste this into another Claude Code session:\n\n' +
|
|
473
|
-
' "I\'m installing DevFlow and it wants to write a\n' +
|
|
474
|
-
' managed-settings.json file using sudo. Review the source\n' +
|
|
475
|
-
' at https://github.com/dean0x/devflow and tell me if\n' +
|
|
476
|
-
' it\'s safe."', 'Managed Settings');
|
|
477
|
-
const sudoChoice = await p.select({
|
|
478
|
-
message: 'Continue with managed settings?',
|
|
479
|
-
options: [
|
|
480
|
-
{ value: 'yes', label: 'Yes, continue', hint: 'May prompt for your password' },
|
|
481
|
-
{ value: 'no', label: 'No, fall back to settings.json', hint: 'Deny list stored in editable user settings instead' },
|
|
482
|
-
],
|
|
483
|
-
});
|
|
484
|
-
if (p.isCancel(sudoChoice)) {
|
|
485
|
-
p.cancel('Installation cancelled.');
|
|
486
|
-
process.exit(0);
|
|
487
|
-
}
|
|
488
|
-
if (sudoChoice === 'yes') {
|
|
489
|
-
const managed = await installManagedSettings(rootDir, verbose);
|
|
490
|
-
if (!managed) {
|
|
491
|
-
p.log.warn('Managed settings write failed — falling back to user settings');
|
|
492
|
-
effectiveSecurityMode = 'user';
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
effectiveSecurityMode = 'user';
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
await installSettings(claudeDir, rootDir, devflowDir, verbose, teamsEnabled, effectiveSecurityMode);
|
|
500
|
-
// Install ambient hook if enabled
|
|
586
|
+
await installSettings(claudeDir, rootDir, devflowDir, verbose, teamsEnabled, effectiveSecurityMode);
|
|
587
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
588
|
+
// Configure ambient hook, memory hooks, and HUD statusLine in a single read-modify-write pass
|
|
589
|
+
try {
|
|
590
|
+
let content = await fs.readFile(settingsPath, 'utf-8');
|
|
591
|
+
const original = content;
|
|
592
|
+
// Ambient hook
|
|
501
593
|
if (ambientEnabled) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
594
|
+
content = addAmbientHook(content, devflowDir);
|
|
595
|
+
}
|
|
596
|
+
// Memory hooks — always remove-then-add to upgrade hook format (e.g., .sh → run-hook)
|
|
597
|
+
const cleaned = removeMemoryHooks(content);
|
|
598
|
+
content = memoryEnabled ? addMemoryHooks(cleaned, devflowDir) : cleaned;
|
|
599
|
+
// HUD statusLine
|
|
600
|
+
content = hudEnabled
|
|
601
|
+
? addHudStatusLine(content, devflowDir)
|
|
602
|
+
: removeHudStatusLine(content);
|
|
603
|
+
if (content !== original) {
|
|
604
|
+
await fs.writeFile(settingsPath, content, 'utf-8');
|
|
605
|
+
if (verbose) {
|
|
606
|
+
if (ambientEnabled)
|
|
607
|
+
p.log.success('Ambient mode hook installed');
|
|
608
|
+
p.log.info(`Working memory ${memoryEnabled ? 'enabled' : 'disabled'}`);
|
|
609
|
+
p.log.info(`HUD ${hudEnabled ? 'enabled' : 'disabled'}`);
|
|
512
610
|
}
|
|
513
|
-
catch { /* settings.json may not exist yet */ }
|
|
514
611
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
612
|
+
}
|
|
613
|
+
catch { /* settings.json may not exist yet */ }
|
|
614
|
+
// Ensure .memory/ exists when memory is enabled (hooks are no-ops without it)
|
|
615
|
+
if (memoryEnabled) {
|
|
616
|
+
await createMemoryDir(verbose);
|
|
617
|
+
await migrateMemoryFiles(verbose);
|
|
618
|
+
}
|
|
619
|
+
// Configure HUD
|
|
620
|
+
const existingHud = loadHudConfig();
|
|
621
|
+
saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
|
|
622
|
+
// File extras
|
|
623
|
+
if (claudeignoreEnabled) {
|
|
624
|
+
if (scope === 'user' && discoveredProjects.length > 0) {
|
|
625
|
+
const results = await Promise.all(discoveredProjects.map(root => installClaudeignore(root, rootDir, verbose)));
|
|
626
|
+
const created = results.filter(Boolean).length;
|
|
627
|
+
if (created > 0) {
|
|
628
|
+
p.log.success(`.claudeignore created in ${created} project(s)`);
|
|
529
629
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// Ensure .memory/ exists when memory is enabled (hooks are no-ops without it)
|
|
533
|
-
if (memoryEnabled) {
|
|
534
|
-
await createMemoryDir(verbose);
|
|
535
|
-
await migrateMemoryFiles(verbose);
|
|
536
|
-
}
|
|
537
|
-
// Configure HUD
|
|
538
|
-
const existingHud = loadHudConfig();
|
|
539
|
-
saveHudConfig({ enabled: hudEnabled, detail: existingHud.detail });
|
|
540
|
-
// Update statusLine in settings.json (add or remove based on choice)
|
|
541
|
-
try {
|
|
542
|
-
const hudContent = await fs.readFile(settingsPath, 'utf-8');
|
|
543
|
-
const hudUpdated = hudEnabled
|
|
544
|
-
? addHudStatusLine(hudContent, devflowDir)
|
|
545
|
-
: removeHudStatusLine(hudContent);
|
|
546
|
-
if (hudUpdated !== hudContent) {
|
|
547
|
-
await fs.writeFile(settingsPath, hudUpdated, 'utf-8');
|
|
548
|
-
if (verbose) {
|
|
549
|
-
p.log.info(`HUD ${hudEnabled ? 'enabled' : 'disabled'}`);
|
|
550
|
-
}
|
|
630
|
+
else {
|
|
631
|
+
p.log.info(`.claudeignore already exists in all ${discoveredProjects.length} project(s)`);
|
|
551
632
|
}
|
|
552
633
|
}
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
const fileExtras = selectedExtras.filter(e => e !== 'settings' && e !== 'safe-delete');
|
|
556
|
-
if (fileExtras.length > 0) {
|
|
557
|
-
const sExtras = p.spinner();
|
|
558
|
-
sExtras.start('Configuring extras');
|
|
559
|
-
if (selectedExtras.includes('claudeignore') && gitRoot) {
|
|
634
|
+
else if (gitRoot) {
|
|
560
635
|
await installClaudeignore(gitRoot, rootDir, verbose);
|
|
561
636
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
637
|
+
}
|
|
638
|
+
if (scope === 'local' && gitRoot) {
|
|
639
|
+
await updateGitignore(gitRoot, verbose);
|
|
640
|
+
}
|
|
641
|
+
if (scope === 'local') {
|
|
642
|
+
await createDocsStructure(verbose);
|
|
643
|
+
}
|
|
644
|
+
// Safe-delete execution (decision was captured during prompt phase)
|
|
645
|
+
if (safeDeleteAction === 'install' && safeDeleteBlock && profilePath) {
|
|
646
|
+
await installToProfile(profilePath, safeDeleteBlock);
|
|
647
|
+
}
|
|
648
|
+
else if (safeDeleteAction === 'upgrade' && safeDeleteBlock && profilePath) {
|
|
649
|
+
await removeFromProfile(profilePath);
|
|
650
|
+
await installToProfile(profilePath, safeDeleteBlock);
|
|
651
|
+
}
|
|
652
|
+
// Managed settings (last install step — sudo may prompt for password)
|
|
653
|
+
if (securityMode === 'managed' && managedSettingsConfirmed) {
|
|
654
|
+
s.stop('Installing managed settings (may prompt for password)');
|
|
655
|
+
const managed = await installManagedSettings(rootDir, verbose);
|
|
656
|
+
if (!managed) {
|
|
657
|
+
p.log.warn('Managed settings write failed — falling back to user settings');
|
|
567
658
|
}
|
|
568
|
-
|
|
659
|
+
s.start('Finalizing');
|
|
569
660
|
}
|
|
570
|
-
|
|
661
|
+
s.stop('Installation complete');
|
|
662
|
+
// === Summary ===
|
|
571
663
|
if (usedNativeCli) {
|
|
572
664
|
p.log.success('Installed via Claude plugin system');
|
|
573
665
|
}
|
|
@@ -581,54 +673,34 @@ export const initCommand = new Command('init')
|
|
|
581
673
|
.join(' ');
|
|
582
674
|
p.note(commandsNote, 'Available commands');
|
|
583
675
|
}
|
|
584
|
-
// Safe-delete
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const safeDeleteAvailable = hasSafeDelete(platform);
|
|
590
|
-
const profilePath = getProfilePath(shell);
|
|
591
|
-
if (process.stdin.isTTY && profilePath) {
|
|
592
|
-
if (!safeDeleteAvailable && safeDeleteInfo.installHint) {
|
|
593
|
-
p.log.info(`Install ${color.cyan(safeDeleteInfo.command ?? 'trash')} first: ${color.dim(safeDeleteInfo.installHint)}`);
|
|
594
|
-
p.log.info(`Then re-run ${color.cyan('devflow init')} to auto-configure safe-delete.`);
|
|
595
|
-
}
|
|
596
|
-
else if (safeDeleteAvailable) {
|
|
597
|
-
const trashCmd = safeDeleteInfo.command;
|
|
598
|
-
const block = generateSafeDeleteBlock(shell, process.platform, trashCmd);
|
|
599
|
-
if (block) {
|
|
600
|
-
const installedVersion = await getInstalledVersion(profilePath);
|
|
601
|
-
if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
|
|
602
|
-
p.log.info(`Safe-delete already configured in ${color.dim(profilePath)}`);
|
|
603
|
-
}
|
|
604
|
-
else if (installedVersion > 0) {
|
|
605
|
-
await removeFromProfile(profilePath);
|
|
606
|
-
await installToProfile(profilePath, block);
|
|
607
|
-
p.log.success(`Safe-delete upgraded in ${color.dim(profilePath)}`);
|
|
608
|
-
p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
const confirm = await p.confirm({
|
|
612
|
-
message: `Install safe-delete to ${profilePath}? (overrides rm to use ${trashCmd ?? 'recycle bin'})`,
|
|
613
|
-
initialValue: true,
|
|
614
|
-
});
|
|
615
|
-
if (!p.isCancel(confirm) && confirm) {
|
|
616
|
-
await installToProfile(profilePath, block);
|
|
617
|
-
p.log.success(`Safe-delete installed to ${color.dim(profilePath)}`);
|
|
618
|
-
p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
676
|
+
// Safe-delete status messages (after spinner)
|
|
677
|
+
if (process.stdin.isTTY && profilePath) {
|
|
678
|
+
if (safeDeleteAction === 'install') {
|
|
679
|
+
p.log.success(`Safe-delete installed to ${color.dim(profilePath)}`);
|
|
680
|
+
p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
|
|
623
681
|
}
|
|
624
|
-
else if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
682
|
+
else if (safeDeleteAction === 'upgrade') {
|
|
683
|
+
p.log.success(`Safe-delete upgraded in ${color.dim(profilePath)}`);
|
|
684
|
+
p.log.info('Restart your shell or run: ' + color.cyan(`source ${profilePath}`));
|
|
685
|
+
}
|
|
686
|
+
else if (safeDeleteAvailable && safeDeleteBlock) {
|
|
687
|
+
const installedVersion = await getInstalledVersion(profilePath);
|
|
688
|
+
if (installedVersion === SAFE_DELETE_BLOCK_VERSION) {
|
|
689
|
+
p.log.info(`Safe-delete already configured in ${color.dim(profilePath)}`);
|
|
630
690
|
}
|
|
631
691
|
}
|
|
692
|
+
else if (!safeDeleteAvailable && safeDeleteInfo.installHint) {
|
|
693
|
+
p.log.info(`Install ${color.cyan(safeDeleteInfo.command ?? 'trash')} first: ${color.dim(safeDeleteInfo.installHint)}`);
|
|
694
|
+
p.log.info(`Then re-run ${color.cyan('devflow init')} to auto-configure safe-delete.`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
else if (!process.stdin.isTTY) {
|
|
698
|
+
if (safeDeleteAvailable && safeDeleteInfo.command) {
|
|
699
|
+
p.log.info(`Safe-delete available (${safeDeleteInfo.command}). Run interactively to auto-install.`);
|
|
700
|
+
}
|
|
701
|
+
else if (safeDeleteInfo.installHint) {
|
|
702
|
+
p.log.info(`Protect against accidental ${color.red('rm -rf')}: ${color.cyan(safeDeleteInfo.installHint)}`);
|
|
703
|
+
}
|
|
632
704
|
}
|
|
633
705
|
// Verbose mode: show details
|
|
634
706
|
if (verbose) {
|
|
@@ -55,8 +55,15 @@ export type SecurityMode = 'managed' | 'user';
|
|
|
55
55
|
export declare function installSettings(claudeDir: string, rootDir: string, devflowDir: string, verbose: boolean, teamsEnabled?: boolean, securityMode?: SecurityMode): Promise<void>;
|
|
56
56
|
/**
|
|
57
57
|
* Create .claudeignore in git repository root (skip if already exists).
|
|
58
|
+
* Returns true if a new file was created, false if it already existed or on error.
|
|
58
59
|
*/
|
|
59
|
-
export declare function installClaudeignore(gitRoot: string, rootDir: string, verbose: boolean): Promise<
|
|
60
|
+
export declare function installClaudeignore(gitRoot: string, rootDir: string, verbose: boolean): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Discover git repository roots from Claude's project history.
|
|
63
|
+
* Parses ~/.claude/history.jsonl for unique project paths that are valid git repos.
|
|
64
|
+
* @param homeDir - Override home directory (dependency injection for tests)
|
|
65
|
+
*/
|
|
66
|
+
export declare function discoverProjectGitRoots(homeDir?: string): Promise<string[]>;
|
|
60
67
|
/**
|
|
61
68
|
* Update .gitignore with DevFlow entries (for local scope installs).
|
|
62
69
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs, writeFileSync, unlinkSync } from 'fs';
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
4
5
|
import * as p from '@clack/prompts';
|
|
5
6
|
import { getManagedSettingsPath } from './paths.js';
|
|
6
7
|
function isNodeSystemError(error) {
|
|
@@ -353,6 +354,7 @@ export async function installSettings(claudeDir, rootDir, devflowDir, verbose, t
|
|
|
353
354
|
}
|
|
354
355
|
/**
|
|
355
356
|
* Create .claudeignore in git repository root (skip if already exists).
|
|
357
|
+
* Returns true if a new file was created, false if it already existed or on error.
|
|
356
358
|
*/
|
|
357
359
|
export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
358
360
|
const claudeignorePath = path.join(gitRoot, '.claudeignore');
|
|
@@ -363,6 +365,7 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
363
365
|
if (verbose) {
|
|
364
366
|
p.log.success('.claudeignore created');
|
|
365
367
|
}
|
|
368
|
+
return true;
|
|
366
369
|
}
|
|
367
370
|
catch (error) {
|
|
368
371
|
if (isNodeSystemError(error) && error.code === 'EEXIST') {
|
|
@@ -371,7 +374,45 @@ export async function installClaudeignore(gitRoot, rootDir, verbose) {
|
|
|
371
374
|
else if (verbose) {
|
|
372
375
|
p.log.warn(`Could not create .claudeignore: ${error}`);
|
|
373
376
|
}
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Discover git repository roots from Claude's project history.
|
|
382
|
+
* Parses ~/.claude/history.jsonl for unique project paths that are valid git repos.
|
|
383
|
+
* @param homeDir - Override home directory (dependency injection for tests)
|
|
384
|
+
*/
|
|
385
|
+
export async function discoverProjectGitRoots(homeDir) {
|
|
386
|
+
const historyPath = path.join(homeDir ?? os.homedir(), '.claude', 'history.jsonl');
|
|
387
|
+
let content;
|
|
388
|
+
try {
|
|
389
|
+
content = await fs.readFile(historyPath, 'utf-8');
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
const projects = new Set();
|
|
395
|
+
for (const line of content.split('\n')) {
|
|
396
|
+
if (!line.trim())
|
|
397
|
+
continue;
|
|
398
|
+
try {
|
|
399
|
+
const entry = JSON.parse(line);
|
|
400
|
+
if (typeof entry?.project === 'string') {
|
|
401
|
+
projects.add(path.resolve(entry.project));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Malformed line — skip
|
|
406
|
+
}
|
|
374
407
|
}
|
|
408
|
+
const results = await Promise.allSettled([...projects].map(async (project) => {
|
|
409
|
+
await fs.access(path.join(project, '.git'));
|
|
410
|
+
return project;
|
|
411
|
+
}));
|
|
412
|
+
const gitRoots = results
|
|
413
|
+
.filter((r) => r.status === 'fulfilled')
|
|
414
|
+
.map((r) => r.value);
|
|
415
|
+
return gitRoots.sort();
|
|
375
416
|
}
|
|
376
417
|
/**
|
|
377
418
|
* Update .gitignore with DevFlow entries (for local scope installs).
|
package/package.json
CHANGED