@ai-outfitter/outfitter 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +12 -2
  2. package/code/pi-extension/src/outfitter-extension.js +720 -0
  3. package/dist/agents/AdapterStatePaths.js +3 -1
  4. package/dist/agents/AdapterStatePaths.js.map +1 -1
  5. package/dist/agents/AgentAdapter.d.ts +3 -0
  6. package/dist/agents/AgentLaunch.js +5 -0
  7. package/dist/agents/AgentLaunch.js.map +1 -1
  8. package/dist/agents/OutfitterDocs.d.ts +2 -0
  9. package/dist/agents/OutfitterDocs.js +38 -0
  10. package/dist/agents/OutfitterDocs.js.map +1 -0
  11. package/dist/agents/claude/ClaudeAdapter.js +3 -2
  12. package/dist/agents/claude/ClaudeAdapter.js.map +1 -1
  13. package/dist/agents/pi/PiAdapter.js +24 -19
  14. package/dist/agents/pi/PiAdapter.js.map +1 -1
  15. package/dist/agents/pi/PiExtensionCache.d.ts +8 -0
  16. package/dist/agents/pi/PiExtensionCache.js +95 -5
  17. package/dist/agents/pi/PiExtensionCache.js.map +1 -1
  18. package/dist/agents/pi/PiSkillSources.d.ts +8 -0
  19. package/dist/agents/pi/PiSkillSources.js +69 -0
  20. package/dist/agents/pi/PiSkillSources.js.map +1 -0
  21. package/dist/cli/OutfitterCli.js +7 -2
  22. package/dist/cli/OutfitterCli.js.map +1 -1
  23. package/dist/cli/commands/PiLoginLaunch.js +28 -737
  24. package/dist/cli/commands/PiLoginLaunch.js.map +1 -1
  25. package/dist/cli/commands/RunCommand.d.ts +6 -17
  26. package/dist/cli/commands/RunCommand.js +115 -186
  27. package/dist/cli/commands/RunCommand.js.map +1 -1
  28. package/dist/cli/commands/SetupCommand.d.ts +4 -63
  29. package/dist/cli/commands/SetupCommand.js +13 -673
  30. package/dist/cli/commands/SetupCommand.js.map +1 -1
  31. package/dist/cli/commands/SyncCommand.d.ts +1 -1
  32. package/dist/cli/commands/SyncCommand.js +37 -12
  33. package/dist/cli/commands/SyncCommand.js.map +1 -1
  34. package/dist/cli/commands/run/RunFirstRunOnboarding.d.ts +7 -0
  35. package/dist/cli/commands/run/RunFirstRunOnboarding.js +52 -0
  36. package/dist/cli/commands/run/RunFirstRunOnboarding.js.map +1 -0
  37. package/dist/cli/commands/run/RunLaunchSummary.d.ts +2 -0
  38. package/dist/cli/commands/run/RunLaunchSummary.js +35 -0
  39. package/dist/cli/commands/run/RunLaunchSummary.js.map +1 -0
  40. package/dist/cli/commands/run/RunProfileResolution.d.ts +37 -0
  41. package/dist/cli/commands/run/RunProfileResolution.js +124 -0
  42. package/dist/cli/commands/run/RunProfileResolution.js.map +1 -0
  43. package/dist/cli/commands/run/RunStateWritePrompt.d.ts +2 -0
  44. package/dist/cli/commands/run/RunStateWritePrompt.js +29 -0
  45. package/dist/cli/commands/run/RunStateWritePrompt.js.map +1 -0
  46. package/dist/cli/commands/setup/SetupPrompts.d.ts +14 -0
  47. package/dist/cli/commands/setup/SetupPrompts.js +296 -0
  48. package/dist/cli/commands/setup/SetupPrompts.js.map +1 -0
  49. package/dist/cli/commands/setup/SetupSourceImport.d.ts +5 -0
  50. package/dist/cli/commands/setup/SetupSourceImport.js +177 -0
  51. package/dist/cli/commands/setup/SetupSourceImport.js.map +1 -0
  52. package/dist/cli/commands/setup/SetupSourceLaunch.d.ts +4 -0
  53. package/dist/cli/commands/setup/SetupSourceLaunch.js +65 -0
  54. package/dist/cli/commands/setup/SetupSourceLaunch.js.map +1 -0
  55. package/dist/cli/commands/setup/SetupStarterSource.d.ts +21 -0
  56. package/dist/cli/commands/setup/SetupStarterSource.js +133 -0
  57. package/dist/cli/commands/setup/SetupStarterSource.js.map +1 -0
  58. package/dist/cli/commands/setup/SetupTypes.d.ts +91 -0
  59. package/dist/cli/commands/setup/SetupTypes.js +26 -0
  60. package/dist/cli/commands/setup/SetupTypes.js.map +1 -0
  61. package/dist/compositeProfile/CompositeProfileCleanup.d.ts +9 -0
  62. package/dist/compositeProfile/CompositeProfileCleanup.js +87 -0
  63. package/dist/compositeProfile/CompositeProfileCleanup.js.map +1 -0
  64. package/dist/compositeProfile/StatePersistence.d.ts +16 -1
  65. package/dist/compositeProfile/StatePersistence.js +27 -5
  66. package/dist/compositeProfile/StatePersistence.js.map +1 -1
  67. package/dist/fs/SafeSymlink.d.ts +13 -0
  68. package/dist/fs/SafeSymlink.js +50 -0
  69. package/dist/fs/SafeSymlink.js.map +1 -0
  70. package/dist/settings/SettingsLoader.d.ts +1 -0
  71. package/dist/settings/SettingsLoader.js +10 -1
  72. package/dist/settings/SettingsLoader.js.map +1 -1
  73. package/doc/architecture/state_writeback_strategy.md +350 -0
  74. package/doc/documentation/README.md +21 -0
  75. package/doc/documentation/cli.md +74 -0
  76. package/doc/documentation/concepts.md +54 -0
  77. package/doc/documentation/first-time-cli-agent-users.md +137 -0
  78. package/doc/documentation/getting-started.md +49 -0
  79. package/doc/documentation/iterating-on-profiles.md +109 -0
  80. package/doc/documentation/profile-repository.md +111 -0
  81. package/doc/documentation/profiles.md +183 -0
  82. package/doc/documentation/state.md +224 -0
  83. package/doc/documentation/support-matrix.md +46 -0
  84. package/doc/documentation/switching-to-outfitter.md +130 -0
  85. package/doc/documentation/usecases/engineering.md +114 -0
  86. package/doc/documentation/usecases/organization-profile-catalog.md +140 -0
  87. package/doc/documentation/usecases/persona-reviews.md +173 -0
  88. package/doc/philosophy.md +25 -0
  89. package/package.json +3 -2
@@ -1,42 +1,20 @@
1
- /* eslint-disable max-lines */
2
1
  // Provides the command object for first-run Outfitter setup.
3
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
- import { createInterface } from 'node:readline/promises';
2
+ import { existsSync, rmSync } from 'node:fs';
5
3
  import { homedir } from 'node:os';
6
- import { dirname, isAbsolute, join, resolve } from 'node:path';
7
- import spawn from 'cross-spawn';
8
- import { parse, stringify } from 'yaml';
9
- import { createProfileSourceCachePath, createRemoteRepositoryCachePath, normalizeGitUri, redactProfileSourceUriCredentials, resolveRemoteRepositorySubpath, } from '../../profiles/ProfileCache.js';
10
- import { isValidProfileId, loadLocalProfileSource } from '../../profiles/ProfileLoader.js';
11
- import { resolveProfile } from '../../profiles/ProfileMerger.js';
12
- import { createSettingsLoadPlan, discoverSettingsLoadPlan, loadSettings, loadSettingsFiles, loadSettingsWithCachedRemoteSettings, } from '../../settings/SettingsLoader.js';
4
+ import { join } from 'node:path';
5
+ import { redactProfileSourceUriCredentials } from '../../profiles/ProfileCache.js';
6
+ import { loadLocalProfileSource } from '../../profiles/ProfileLoader.js';
7
+ import { discoverSettingsLoadPlan, loadSettings, loadSettingsWithCachedRemoteSettings, } from '../../settings/SettingsLoader.js';
13
8
  import { persistFirstRunWelcomeProfile, updateSettingsDefaultProfile, } from './FirstRunWelcomeProfile.js';
14
9
  import { executeSyncCommand } from './SyncCommand.js';
15
- import { executeWelcomeCommand, writeWelcomeIntro } from './WelcomeCommand.js';
16
- const setupSourceImportModeChoices = [
17
- {
18
- mode: 'copy',
19
- label: 'Copy snapshot',
20
- description: 'copy profiles into the selected .outfitter folder; safest for normal use',
21
- },
22
- {
23
- mode: 'symlink',
24
- label: 'Symlink for development',
25
- description: 'link the selected .outfitter folder to the local source so shared profile edits apply immediately',
26
- },
27
- ];
28
- const setupSourceImportTargetChoices = [
29
- {
30
- target: 'home',
31
- label: 'User home',
32
- description: 'install profiles into ~/.outfitter for all repositories on this machine',
33
- },
34
- {
35
- target: 'project',
36
- label: 'Current project',
37
- description: 'install profiles into this project .outfitter folder only',
38
- },
39
- ];
10
+ import { executeWelcomeCommand } from './WelcomeCommand.js';
11
+ import { materializeSetupProfileSource, runSetupSourceOnboarding, selectDefaultProfileIfInteractive, } from './setup/SetupPrompts.js';
12
+ import { applySetupSourceImport, copyStarterProfileFilesIfPresent, createDefaultProfileIfMissing, findSetupProfilePath, } from './setup/SetupSourceImport.js';
13
+ import { formatRunProfileExample, formatSetupSourceExitMessages, runSetupSourcePostImportAction, } from './setup/SetupSourceLaunch.js';
14
+ import { assertValidDefaultProfileId, createInitialSettingsIfMissing, ensureExistingUserSettingsDefaultProfile, prepareStarterLayout, readStarterDefaultProfileId, readUserDefaultProfileId, } from './setup/SetupStarterSource.js';
15
+ import { formatSettingsIssue } from './setup/SetupTypes.js';
16
+ export { createDefaultSettingsContent } from './setup/SetupStarterSource.js';
17
+ export { updateSettingsDefaultProfile };
40
18
  /* eslint-disable complexity -- setup orchestration coordinates settings, sync, prompts, and welcome persistence. */
41
19
  export const executeSetupCommand = async (input, dependencies = {}) => {
42
20
  requireInteractiveTerminalIfNeeded(dependencies);
@@ -188,65 +166,6 @@ const shouldReportDefaultProfileStatus = (input) => {
188
166
  }
189
167
  return input.input.setupSourceUri === undefined || input.starterLayout?.profilesPath === undefined;
190
168
  };
191
- const formatRunProfileExample = (profileId) => `Start the selected default profile either way:\n outfitter\n outfitter --profile ${profileId}`;
192
- const formatRunDefaultProfileExample = () => `Start the current default profile:\n outfitter`;
193
- /* v8 ignore start -- setup-source launch visibility fallbacks are exercised through integration-style CLI flows; unit tests cover the primary imported-profile outcomes. */
194
- const formatSetupSourceExitMessages = (input, importTarget, profileId, launchTarget) => {
195
- if (launchTarget === 'default') {
196
- return [formatRunDefaultProfileExample()];
197
- }
198
- if (importTarget !== 'home' || canResolveProfileForLaunch(input, profileId)) {
199
- return [formatRunProfileExample(profileId)];
200
- }
201
- return [formatHiddenHomeImportMessage(profileId)];
202
- };
203
- const formatHiddenHomeImportMessage = (profileId) => `Imported profile '${profileId}' into user home, but this project overrides profile_sources and does not expose ~/.outfitter/profiles. ` +
204
- 'Import into the current project, add ~/.outfitter/profiles to project profile_sources, or run from a directory without project Outfitter settings.';
205
- const runSetupSourcePostImportAction = async (input, dependencies, importTarget, profileId, launchTarget) => {
206
- if (dependencies.interactive !== true ||
207
- (dependencies.launchSetupSourceProfile === undefined && dependencies.selectSetupSourceLaunchAction === undefined)) {
208
- return 'exit';
209
- }
210
- const action = await selectSetupSourceLaunchAction(profileId, launchTarget, dependencies);
211
- if (action === 'start') {
212
- if (launchTarget === 'selected') {
213
- assertSetupSourceProfileCanLaunch(input, importTarget, profileId);
214
- }
215
- await dependencies.launchSetupSourceProfile?.({
216
- homeDirectory: input.homeDirectory,
217
- projectDirectory: input.projectDirectory,
218
- profileId: launchTarget === 'selected' ? profileId : undefined,
219
- });
220
- }
221
- return action;
222
- };
223
- const assertSetupSourceProfileCanLaunch = (input, importTarget, profileId) => {
224
- if (importTarget !== 'home' || canResolveProfileForLaunch(input, profileId)) {
225
- return;
226
- }
227
- throw new Error(formatHiddenHomeImportMessage(profileId));
228
- };
229
- const canResolveProfileForLaunch = (input, profileId) => {
230
- const loadedSettings = loadSettingsWithCachedRemoteSettings(input);
231
- if (loadedSettings.issues.length > 0) {
232
- return true;
233
- }
234
- const profiles = loadedSettings.settings.profileSources.flatMap((source) => loadLocalProfileSource({
235
- path: materializeSetupProfileSource(input.homeDirectory, source),
236
- only: source.only,
237
- except: source.except,
238
- }).profiles);
239
- const resolution = resolveProfile({ profiles, profileId });
240
- const selectedProfile = resolution.profileStack.find((profile) => profile.id === profileId);
241
- return resolution.profile !== undefined && resolution.issues.length === 0 && selectedProfile?.template !== true;
242
- };
243
- const selectSetupSourceLaunchAction = async (profileId, launchTarget, dependencies) => {
244
- if (dependencies.selectSetupSourceLaunchAction !== undefined) {
245
- return dependencies.selectSetupSourceLaunchAction(profileId, launchTarget);
246
- }
247
- return promptForSetupSourceLaunchAction(profileId, launchTarget, dependencies);
248
- };
249
- /* v8 ignore stop */
250
169
  const capitalize = (value) => `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
251
170
  export const createSetupCommand = (dependencies = {}) => {
252
171
  const command = {
@@ -283,297 +202,6 @@ const launchPiOnboardingWithRunCommand = async (input, dependencies) => {
283
202
  forceRuntimeOnboarding: true,
284
203
  }, { ...dependencies, interactive: true });
285
204
  };
286
- const prepareStarterLayout = (homeDirectory, projectDirectory, setupSourceUri, synchronizer = createGitSetupSourceSynchronizer()) => {
287
- const localOutfitterPath = resolveLocalSetupSourceOutfitterPathFromUri(setupSourceUri, projectDirectory);
288
- if (localOutfitterPath !== undefined) {
289
- const settingsPath = join(localOutfitterPath, 'settings.yml');
290
- validateStarterSettingsIfPresent(existsSync(settingsPath) ? settingsPath : undefined);
291
- return {
292
- cachePath: localOutfitterPath,
293
- settingsPath: existsSync(settingsPath) ? settingsPath : undefined,
294
- profilesPath: firstExistingPath(join(localOutfitterPath, 'profiles')),
295
- sourceKind: 'local-live',
296
- sourceOutfitterPath: localOutfitterPath,
297
- };
298
- }
299
- const cachePath = createSetupSourceCachePath(homeDirectory, setupSourceUri);
300
- synchronizer.sync(setupSourceUri, cachePath);
301
- const settingsPath = firstExistingPath(join(cachePath, 'settings.yml'), join(cachePath, '.outfitter', 'settings.yml'));
302
- validateStarterSettingsIfPresent(settingsPath);
303
- const preferredProfilesPath = settingsPath?.endsWith(join('.outfitter', 'settings.yml'))
304
- ? join(cachePath, '.outfitter', 'profiles')
305
- : join(cachePath, 'profiles');
306
- const profilesPath = firstExistingPath(preferredProfilesPath, join(cachePath, 'profiles'), join(cachePath, '.outfitter', 'profiles'));
307
- return { cachePath, settingsPath, profilesPath, sourceKind: 'remote-cache' };
308
- };
309
- const createSetupSourceCachePath = (homeDirectory, setupSourceUri) => createRemoteRepositoryCachePath(homeDirectory, { uri: setupSourceUri });
310
- const createGitSetupSourceSynchronizer = () => ({
311
- sync(uri, cachePath) {
312
- mkdirSync(dirname(cachePath), { recursive: true });
313
- if (existsSync(cachePath)) {
314
- runGit(['-C', cachePath, 'pull', '--ff-only'], uri);
315
- return;
316
- }
317
- runGit(['clone', '--', normalizeGitUri(uri), cachePath], uri);
318
- },
319
- });
320
- const runGit = (args, sensitiveUri) => {
321
- const result = spawn.sync('git', args, { stdio: 'pipe', encoding: 'utf8' });
322
- if (result.status !== 0) {
323
- /* v8 ignore next -- the final fallback only applies if git emits no stdout or stderr. */
324
- throw new Error(redactSensitiveText((result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim(), sensitiveUri));
325
- }
326
- };
327
- const redactSensitiveText = (message, uri) => message
328
- .split(uri)
329
- .join(redactProfileSourceUriCredentials(uri))
330
- .split(normalizeGitUri(uri))
331
- .join(redactProfileSourceUriCredentials(normalizeGitUri(uri)));
332
- const firstExistingPath = (...paths) => paths.find((path) => existsSync(path));
333
- const validateStarterSettingsIfPresent = (settingsPath) => {
334
- if (settingsPath === undefined) {
335
- return;
336
- }
337
- const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
338
- if (loaded.issues.length > 0) {
339
- throw new Error(`Cannot setup from invalid starter settings: ${loaded.issues.map(formatSettingsIssue).join('; ')}`);
340
- }
341
- };
342
- const readStarterDefaultProfileId = (settingsPath) => {
343
- if (settingsPath === undefined) {
344
- return 'engineer';
345
- }
346
- const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
347
- return loaded.files[0]?.settings.defaultProfile ?? 'engineer';
348
- };
349
- const readStarterExplicitDefaultProfileId = (settingsPath) => {
350
- if (settingsPath === undefined) {
351
- return undefined;
352
- }
353
- const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
354
- return loaded.files[0]?.settings.defaultProfile;
355
- };
356
- const readUserDefaultProfileId = (files) => files.find((file) => file.location.scope === 'user')?.settings.defaultProfile ?? 'engineer';
357
- const ensureExistingUserSettingsDefaultProfile = (settingsPath, files, defaultProfileId) => {
358
- const userSettings = files.find((file) => file.location.scope === 'user');
359
- if (userSettings === undefined || userSettings.settings.defaultProfile !== undefined) {
360
- return;
361
- }
362
- const content = readFileSync(settingsPath, 'utf8');
363
- writeFileSync(settingsPath, `${content}\ndefault_profile: ${defaultProfileId}\n`);
364
- };
365
- const assertValidDefaultProfileId = (profileId) => {
366
- if (!isValidProfileId(profileId)) {
367
- throw new Error(`Default profile '${profileId}' is not a filesystem-safe Outfitter profile id.`);
368
- }
369
- };
370
- export const createDefaultSettingsContent = (defaultProfileId = 'engineer') => [
371
- `default_profile: ${defaultProfileId}`,
372
- 'profile_sources:',
373
- ' - github: ai-outfitter/default-profiles',
374
- ' path: profiles',
375
- ' - path: ./profiles',
376
- '',
377
- ].join('\n');
378
- const createInitialSettingsIfMissing = (settingsPath, starterSettingsPath) => {
379
- if (existsSync(settingsPath)) {
380
- return false;
381
- }
382
- mkdirSync(dirname(settingsPath), { recursive: true });
383
- writeFileSync(settingsPath, starterSettingsPath === undefined
384
- ? createDefaultSettingsContent()
385
- : readStarterSettingsContent(starterSettingsPath));
386
- return true;
387
- };
388
- const readStarterSettingsContent = (starterSettingsPath) => {
389
- const content = readFileSync(starterSettingsPath, 'utf8');
390
- const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: starterSettingsPath }]));
391
- if (loaded.files[0]?.settings.defaultProfile !== undefined) {
392
- return content;
393
- }
394
- return `default_profile: engineer\n${content}`;
395
- };
396
- /* v8 ignore start -- setup-source filesystem import variants are covered by integration-style fixtures; core settings outcomes are unit covered. */
397
- const applySetupSourceImport = (input, starterLayout, onboarding) => {
398
- const target = createSetupSourceImportTargetLayout(input, onboarding.importTarget);
399
- if (onboarding.importMode === 'symlink') {
400
- return applySetupSourceSymlinkImport(input, target, onboarding);
401
- }
402
- return applySetupSourceCopyImport(starterLayout, target, onboarding);
403
- };
404
- const applySetupSourceSymlinkImport = (input, target, onboarding) => {
405
- const sourceOutfitterPath = resolveLocalSetupSourceOutfitterPath(input);
406
- if (sourceOutfitterPath === undefined) {
407
- throw new Error('Local setup-source symlink mode requires a source .outfitter directory.');
408
- }
409
- const sourceSettingsPath = join(sourceOutfitterPath, 'settings.yml');
410
- if (!existsSync(sourceSettingsPath)) {
411
- throw new Error('Local setup-source symlink mode requires source .outfitter/settings.yml.');
412
- }
413
- validateStarterSettingsIfPresent(sourceSettingsPath);
414
- const sourceProfilesPath = join(sourceOutfitterPath, 'profiles');
415
- const sourceSelectedProfilePath = findSetupProfilePath(sourceProfilesPath, onboarding.selectedProfileId);
416
- if (!existsSync(sourceSelectedProfilePath)) {
417
- throw new Error(`Local setup-source symlink mode requires selected profile '${onboarding.selectedProfileId}'.`);
418
- }
419
- symlinkLocalOutfitterSource(sourceOutfitterPath, dirname(target.settingsPath));
420
- return {
421
- ...target,
422
- createdSettings: false,
423
- copiedStarterProfileFiles: 0,
424
- copiedStarterResourceFiles: 0,
425
- selectedProfileAlreadyExists: false,
426
- symlinkedOutfitter: true,
427
- };
428
- };
429
- const applySetupSourceCopyImport = (starterLayout, target, onboarding) => {
430
- const createdSettings = createImportSettingsIfMissing(target.settingsPath, starterLayout.settingsPath, onboarding.selectedProfileId);
431
- const selectedProfilePath = findSetupProfilePath(target.profilesPath, onboarding.selectedProfileId);
432
- const selectedProfileAlreadyExists = existsSync(selectedProfilePath);
433
- ensureLocalProfileSource(target.settingsPath, target.profilesPath);
434
- updateSettingsDefaultProfile(target.settingsPath, onboarding.selectedProfileId);
435
- return {
436
- ...target,
437
- createdSettings,
438
- copiedStarterProfileFiles: copyStarterProfileFilesIfPresent(starterLayout.profilesPath, target.profilesPath),
439
- copiedStarterResourceFiles: copyStarterResourceFilesIfPresent(starterLayout.profilesPath, dirname(target.settingsPath)),
440
- selectedProfileAlreadyExists,
441
- selectedProfileConflictMessage: selectedProfileAlreadyExists
442
- ? `Existing selected setup-source profile '${onboarding.selectedProfileId}' at ${selectedProfilePath} was not overwritten.`
443
- : undefined,
444
- symlinkedOutfitter: false,
445
- };
446
- };
447
- /* v8 ignore stop */
448
- /* v8 ignore start -- local setup-source path probing and symlink safety are covered by filesystem integration tests. */
449
- const resolveLocalSetupSourceOutfitterPath = (input) => input.setupSourceUri === undefined
450
- ? undefined
451
- : resolveLocalSetupSourceOutfitterPathFromUri(input.setupSourceUri, input.projectDirectory);
452
- const resolveLocalSetupSourceOutfitterPathFromUri = (setupSourceUri, projectDirectory) => {
453
- if (isRemoteSetupSourceUri(setupSourceUri)) {
454
- return undefined;
455
- }
456
- const sourcePath = isAbsolute(setupSourceUri) ? setupSourceUri : resolve(projectDirectory, setupSourceUri);
457
- const outfitterPath = sourcePath.endsWith('.outfitter') ? sourcePath : join(sourcePath, '.outfitter');
458
- return existsSync(outfitterPath) ? outfitterPath : undefined;
459
- };
460
- const isRemoteSetupSourceUri = (source) => /^[a-z][a-z0-9+.-]*:/iu.test(source) && !isAbsolute(source);
461
- const symlinkLocalOutfitterSource = (sourceOutfitterPath, targetOutfitterPath) => {
462
- if (existsSync(targetOutfitterPath)) {
463
- const entries = readdirSync(targetOutfitterPath);
464
- if (entries.length > 0) {
465
- throw new Error(`Cannot symlink local setup source into non-empty .outfitter directory '${targetOutfitterPath}'. ` +
466
- 'Move it aside or use copy snapshot setup.');
467
- }
468
- rmSync(targetOutfitterPath, { recursive: true, force: true });
469
- }
470
- mkdirSync(dirname(targetOutfitterPath), { recursive: true });
471
- symlinkSync(sourceOutfitterPath, targetOutfitterPath, 'dir');
472
- };
473
- /* v8 ignore stop */
474
- const createSetupSourceImportTargetLayout = (input, target) => {
475
- if (target === 'project') {
476
- return {
477
- settingsPath: join(input.projectDirectory, '.outfitter', 'settings.yml'),
478
- settingsDescription: 'project',
479
- profilesPath: join(input.projectDirectory, '.outfitter', 'profiles'),
480
- };
481
- }
482
- return {
483
- settingsPath: join(input.homeDirectory, '.outfitter', 'settings.yml'),
484
- settingsDescription: 'user',
485
- profilesPath: join(input.homeDirectory, '.outfitter', 'profiles'),
486
- };
487
- };
488
- const createImportSettingsIfMissing = (settingsPath, starterSettingsPath, selectedProfileId) => {
489
- if (existsSync(settingsPath)) {
490
- return false;
491
- }
492
- mkdirSync(dirname(settingsPath), { recursive: true });
493
- writeFileSync(settingsPath,
494
- /* v8 ignore next -- setup-source tests exercise starter settings; missing starter settings is defensive fallback. */
495
- starterSettingsPath === undefined
496
- ? createLocalProfileSettingsContent(selectedProfileId)
497
- : readStarterSettingsContent(starterSettingsPath));
498
- return true;
499
- };
500
- const createLocalProfileSettingsContent = (defaultProfileId) => ['default_profile: ' + defaultProfileId, 'profile_sources:', ' - path: ./profiles', ''].join('\n');
501
- const ensureLocalProfileSource = (settingsPath, profilesPath) => {
502
- const loaded = loadSettingsFiles(createSettingsLoadPlan([{ scope: 'user', path: settingsPath }]));
503
- const sources = loaded.files[0]?.settings.profileSources ?? [];
504
- if (sources.some((source) => source.path === profilesPath)) {
505
- return;
506
- }
507
- const document = readYamlRecord(settingsPath);
508
- /* v8 ignore next -- appending to existing non-local source lists is equivalent to the covered empty-source case. */
509
- const existingSources = Array.isArray(document.profile_sources) ? document.profile_sources : [];
510
- writeFileSync(settingsPath, stringify({ ...document, profile_sources: [...existingSources, { path: './profiles' }] }));
511
- };
512
- const readYamlRecord = (path) => {
513
- const parsed = parse(readFileSync(path, 'utf8'));
514
- /* v8 ignore next -- settings schema validation guarantees object documents before this helper mutates them. */
515
- return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
516
- ? { ...parsed }
517
- : {};
518
- };
519
- const copyStarterProfileFilesIfPresent = (sourceProfilesPath, targetProfilesPath) => {
520
- if (sourceProfilesPath === undefined) {
521
- return 0;
522
- }
523
- return copyDirectoryContentsWithoutOverwriting(sourceProfilesPath, targetProfilesPath);
524
- };
525
- const copyStarterResourceFilesIfPresent = (sourceProfilesPath, targetOutfitterPath) => {
526
- if (sourceProfilesPath === undefined) {
527
- return 0;
528
- }
529
- const sourceOutfitterPath = dirname(sourceProfilesPath);
530
- return ['prompts', 'deepwork', 'skills'].reduce((copiedFiles, resourceName) => copiedFiles + copyNamedStarterResourceDirectoryIfPresent(sourceOutfitterPath, targetOutfitterPath, resourceName), 0);
531
- };
532
- const copyNamedStarterResourceDirectoryIfPresent = (sourceOutfitterPath, targetOutfitterPath, resourceName) => {
533
- const sourceResourcePath = join(sourceOutfitterPath, resourceName);
534
- if (!existsSync(sourceResourcePath)) {
535
- return 0;
536
- }
537
- return copyDirectoryContentsWithoutOverwriting(sourceResourcePath, join(targetOutfitterPath, resourceName));
538
- };
539
- const copyDirectoryContentsWithoutOverwriting = (sourceDirectory, targetDirectory) => {
540
- mkdirSync(targetDirectory, { recursive: true });
541
- let copiedFiles = 0;
542
- for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
543
- const sourcePath = join(sourceDirectory, entry.name);
544
- const targetPath = join(targetDirectory, entry.name);
545
- if (entry.isDirectory()) {
546
- copiedFiles += copyDirectoryContentsWithoutOverwriting(sourcePath, targetPath);
547
- continue;
548
- }
549
- if (entry.isFile() && !existsSync(targetPath)) {
550
- mkdirSync(dirname(targetPath), { recursive: true });
551
- cpSync(sourcePath, targetPath, { force: false });
552
- copiedFiles += 1;
553
- }
554
- }
555
- return copiedFiles;
556
- };
557
- const findSetupProfilePath = (profilesPath, profileId) => {
558
- for (const profilePath of [
559
- join(profilesPath, `${profileId}.yml`),
560
- join(profilesPath, `${profileId}.yaml`),
561
- join(profilesPath, profileId, 'profile.yml'),
562
- ]) {
563
- if (existsSync(profilePath)) {
564
- return profilePath;
565
- }
566
- }
567
- return join(profilesPath, profileId, 'profile.yml');
568
- };
569
- const createDefaultProfileIfMissing = (profilePath, profileId) => {
570
- if (existsSync(profilePath)) {
571
- return false;
572
- }
573
- mkdirSync(dirname(profilePath), { recursive: true });
574
- writeFileSync(profilePath, `id: ${profileId}\nlabel: Default\ncontrols: {}\n`);
575
- return true;
576
- };
577
205
  const persistWelcomeProfileForSetup = (input, settingsPath, welcomeResult) => persistFirstRunWelcomeProfile(input.homeDirectory, settingsPath, welcomeResult, {
578
206
  sourceProfileDirectory: findWelcomeSourceProfileDirectory(input, welcomeResult?.selectedRole?.id),
579
207
  });
@@ -618,292 +246,4 @@ const requireInteractiveTerminalIfNeeded = (dependencies) => {
618
246
  }
619
247
  };
620
248
  const shouldSkipInitialDefaultProfilePrompt = (initialSettingsMissing, dependencies) => initialSettingsMissing && dependencies.interactive === true;
621
- /* v8 ignore next -- default process stdout is direct terminal behavior; tests inject writable streams. */
622
- const resolveReadlineOutput = (dependencies) => typeof dependencies.output?.write === 'function' ? dependencies.output : process.stdout;
623
- const resolvePromptOutput = (dependencies) => {
624
- if (typeof dependencies.output?.write === 'function') {
625
- return dependencies.output;
626
- }
627
- if (dependencies.writeLine !== undefined) {
628
- return {
629
- write(message) {
630
- dependencies.writeLine?.(message.replace(/\n$/u, ''));
631
- return true;
632
- },
633
- };
634
- }
635
- /* v8 ignore next 7 -- defensive non-writable injected output fallback; normal tests inject writeLine or writable output. */
636
- if (dependencies.output !== undefined) {
637
- return {
638
- write() {
639
- return true;
640
- },
641
- };
642
- }
643
- return process.stdout;
644
- };
645
- const runSetupSourceOnboarding = async (input, dependencies, starterLayout, currentDefault) => {
646
- const discoveredProfiles = discoverSetupProfileChoices(input, starterLayout);
647
- const sourceDefault = discoverSetupSourcePromptDefault(input, starterLayout, discoveredProfiles);
648
- const promptDefault = chooseSetupPromptDefault(discoveredProfiles, sourceDefault, currentDefault);
649
- const profiles = selectSetupPromptProfiles(discoveredProfiles, currentDefault, promptDefault);
650
- const localSymlinkAvailable = resolveLocalSetupSourceOutfitterPath(input) !== undefined;
651
- if (dependencies.selectSetupSourceImportTarget === undefined &&
652
- dependencies.selectDefaultProfile === undefined &&
653
- dependencies.selectSetupSourceImportMode === undefined) {
654
- return promptForSetupSourceOnboarding(input, profiles, promptDefault, localSymlinkAvailable, dependencies);
655
- }
656
- writeSetupSourceWelcome(input, profiles, resolvePromptOutput(dependencies));
657
- const importTarget = await selectSetupSourceImportTarget(dependencies);
658
- const importMode = await selectSetupSourceImportMode(dependencies, localSymlinkAvailable);
659
- const selectedProfileId = await selectSetupProfile(profiles, promptDefault, dependencies);
660
- assertValidSelectedDefaultProfile(selectedProfileId, profiles);
661
- return { importTarget, selectedProfileId, importMode };
662
- };
663
- /* v8 ignore start -- readline fallback is smoke-tested through terminal streams; injected selector paths carry deterministic setup-source coverage. */
664
- const promptForSetupSourceOnboarding = async (input, profiles, currentDefault, localSymlinkAvailable, dependencies) => {
665
- const output = resolvePromptOutput(dependencies);
666
- /* v8 ignore next -- default process streams are direct terminal behavior; tests inject streams. */
667
- const readline = createInterface({
668
- input: dependencies.input ?? process.stdin,
669
- output: resolveReadlineOutput(dependencies),
670
- });
671
- try {
672
- writeSetupSourceWelcome(input, profiles, output);
673
- const importTarget = await promptForSetupSourceImportTargetWithReadline(readline, output, setupSourceImportTargetChoices, 'home');
674
- const importMode = await promptForSetupSourceImportModeWithReadline(readline, output, localSymlinkAvailable);
675
- const selectedProfileId = await promptForSetupProfileWithReadline(readline, output, profiles, currentDefault, 'Choose the default profile from this setup source:');
676
- assertValidSelectedDefaultProfile(selectedProfileId, profiles);
677
- return { importTarget, selectedProfileId, importMode };
678
- }
679
- finally {
680
- readline.close();
681
- }
682
- };
683
- const writeSetupSourceWelcome = (input, profiles, output) => {
684
- writeWelcomeIntro(output);
685
- output.write(`\nYou're importing Outfitter profiles from ${redactProfileSourceUriCredentials(input.setupSourceUri)}.\n`);
686
- output.write(`Found ${profiles.length} profile(s)${formatSetupSourceProfileList(profiles)}.\n`);
687
- if (resolveLocalSetupSourceOutfitterPath(input) !== undefined) {
688
- output.write('Local setup source detected. Copy snapshot setup is safest; symlink setup links your target .outfitter to the local source .outfitter so shared-profile edits apply immediately during development.\n');
689
- }
690
- };
691
- const formatSetupSourceProfileList = (profiles) => {
692
- /* v8 ignore next -- setup-source profile prompts normally require discovered source profiles. */
693
- if (profiles.length === 0) {
694
- return '';
695
- }
696
- return `: ${profiles.map((profile) => profile.id).join(', ')}`;
697
- };
698
- const selectSetupSourceImportTarget = async (dependencies) => {
699
- /* v8 ignore else -- mixed dependency injection path; the full readline path prompts with one shared readline. */
700
- if (dependencies.selectSetupSourceImportTarget !== undefined) {
701
- const selectedTarget = await dependencies.selectSetupSourceImportTarget(setupSourceImportTargetChoices, 'home');
702
- assertValidSetupSourceImportTarget(selectedTarget);
703
- return selectedTarget;
704
- }
705
- return 'home';
706
- };
707
- const selectSetupSourceImportMode = async (dependencies, localSymlinkAvailable) => {
708
- if (!localSymlinkAvailable) {
709
- return 'copy';
710
- }
711
- if (dependencies.selectSetupSourceImportMode !== undefined) {
712
- const selectedMode = await dependencies.selectSetupSourceImportMode(setupSourceImportModeChoices, 'copy');
713
- assertValidSetupSourceImportMode(selectedMode);
714
- return selectedMode;
715
- }
716
- return 'copy';
717
- };
718
- const assertValidSetupSourceImportMode = (mode) => {
719
- if (setupSourceImportModeChoices.every((choice) => choice.mode !== mode)) {
720
- throw new Error(`Selected setup-source import mode '${mode}' is not available.`);
721
- }
722
- };
723
- const assertValidSetupSourceImportTarget = (target) => {
724
- /* v8 ignore next -- defensive validation for custom dependency injection. */
725
- if (setupSourceImportTargetChoices.every((choice) => choice.target !== target)) {
726
- throw new Error(`Selected setup-source import target '${target}' is not available.`);
727
- }
728
- };
729
- const promptForSetupSourceImportTargetWithReadline = async (readline, output, choices, defaultTarget) => {
730
- output.write('\nChoose where to install these profiles:\n');
731
- choices.forEach((choice, index) => {
732
- output.write(`${index + 1}. ${choice.label}\n`);
733
- output.write(` ${choice.description}.\n`);
734
- });
735
- const defaultIndex = Math.max(choices.findIndex((choice) => choice.target === defaultTarget), 0);
736
- const answer = await readline.question(`Import target [${defaultIndex + 1}]: `);
737
- const selectedIndex = Number.parseInt(answer.trim() || String(defaultIndex + 1), 10) - 1;
738
- const selectedChoice = choices[selectedIndex];
739
- if (selectedChoice === undefined) {
740
- throw new Error('Selected setup-source import target number is out of range.');
741
- }
742
- return selectedChoice.target;
743
- };
744
- const promptForSetupSourceImportModeWithReadline = async (readline, output, localSymlinkAvailable) => {
745
- if (!localSymlinkAvailable) {
746
- return 'copy';
747
- }
748
- output.write('\nChoose how to install this local setup source:\n');
749
- setupSourceImportModeChoices.forEach((choice, index) => {
750
- output.write(`${index + 1}. ${choice.label}\n`);
751
- output.write(` ${choice.description}.\n`);
752
- });
753
- const answer = await readline.question('Import mode [1]: ');
754
- const selectedIndex = Number.parseInt(answer.trim() || '1', 10) - 1;
755
- const selectedChoice = setupSourceImportModeChoices[selectedIndex];
756
- if (selectedChoice === undefined) {
757
- throw new Error('Selected setup-source import mode number is out of range.');
758
- }
759
- return selectedChoice.mode;
760
- };
761
- /* v8 ignore next -- covered by interactive CLI smoke tests; unit tests inject the launch choice. */
762
- const promptForSetupSourceLaunchAction = async (profileId, launchTarget, dependencies) => {
763
- const readline = createInterface({
764
- input: dependencies.input ?? process.stdin,
765
- output: resolveReadlineOutput(dependencies),
766
- });
767
- try {
768
- const prompt = launchTarget === 'selected'
769
- ? `Start Outfitter with profile '${profileId}' now? [Y/n]: `
770
- : 'Start Outfitter with the current default profile now? [Y/n]: ';
771
- const answer = await readline.question(prompt);
772
- return answer.trim().toLowerCase().startsWith('n') ? 'exit' : 'start';
773
- }
774
- finally {
775
- readline.close();
776
- }
777
- };
778
- /* v8 ignore stop */
779
- const selectDefaultProfileIfInteractive = async (input, settingsPath, currentDefault, dependencies, starterLayout) => {
780
- if (dependencies.interactive !== true) {
781
- return currentDefault;
782
- }
783
- const discoveredProfiles = discoverSetupProfileChoices(input, starterLayout);
784
- const sourceDefault = discoverSetupSourcePromptDefault(input, starterLayout, discoveredProfiles);
785
- const promptDefault = chooseSetupPromptDefault(discoveredProfiles, sourceDefault, currentDefault);
786
- const profiles = selectSetupPromptProfiles(discoveredProfiles, currentDefault, promptDefault);
787
- writeWelcomeIntro(resolvePromptOutput(dependencies));
788
- const selectedProfile = await selectSetupProfile(profiles, promptDefault, dependencies);
789
- assertValidSelectedDefaultProfile(selectedProfile, profiles);
790
- updateSettingsDefaultProfile(settingsPath, selectedProfile);
791
- return selectedProfile;
792
- };
793
- const assertValidSelectedDefaultProfile = (selectedProfile, profiles) => {
794
- assertValidDefaultProfileId(selectedProfile);
795
- if (profiles.length > 0 && profiles.every((profile) => profile.id !== selectedProfile)) {
796
- throw new Error(`Selected default profile '${selectedProfile}' was not one of the available setup profiles.`);
797
- }
798
- };
799
- const discoverSetupSourcePromptDefault = (input, starterLayout, profiles) => {
800
- if (input.setupSourceUri === undefined) {
801
- return undefined;
802
- }
803
- const sourceDefault = readStarterExplicitDefaultProfileId(starterLayout?.settingsPath);
804
- return profiles.some((profile) => profile.id === sourceDefault) ? sourceDefault : undefined;
805
- };
806
- const selectSetupPromptProfiles = (discoveredProfiles, currentDefault, promptDefault) => {
807
- const profiles = discoveredProfiles.length > 0 ? discoveredProfiles : [{ id: currentDefault }];
808
- return prioritizeSetupProfileChoice(profiles, promptDefault);
809
- };
810
- const chooseSetupPromptDefault = (profiles, sourceDefault, fallbackDefault) => {
811
- if (sourceDefault !== undefined && profiles.some((profile) => profile.id === sourceDefault)) {
812
- return sourceDefault;
813
- }
814
- if (profiles.some((profile) => profile.id === fallbackDefault)) {
815
- return fallbackDefault;
816
- }
817
- return profiles[0]?.id ?? fallbackDefault;
818
- };
819
- const prioritizeSetupProfileChoice = (profiles, profileId) => [
820
- ...profiles.filter((profile) => profile.id === profileId),
821
- ...profiles.filter((profile) => profile.id !== profileId),
822
- ];
823
- const discoverSetupProfileChoices = (input, starterLayout) => {
824
- if (input.setupSourceUri !== undefined && starterLayout?.profilesPath !== undefined) {
825
- return discoverSetupProfileChoicesFromLocalSource(starterLayout.profilesPath);
826
- }
827
- return discoverSetupProfileChoicesFromEffectiveSettings(input);
828
- };
829
- const discoverSetupProfileChoicesFromLocalSource = (path) => {
830
- const loadedProfiles = loadLocalProfileSource({ path });
831
- return loadedProfiles.profiles
832
- .map((profile) => ({
833
- id: profile.profile.id,
834
- label: profile.profile.label,
835
- description: profile.profile.description,
836
- }))
837
- .sort((left, right) => left.id.localeCompare(right.id));
838
- };
839
- const discoverSetupProfileChoicesFromEffectiveSettings = (input) => {
840
- const loadedSettings = loadSettingsWithCachedRemoteSettings(input);
841
- const choices = new Map();
842
- for (const source of loadedSettings.settings.profileSources) {
843
- const materializedPath = materializeSetupProfileSource(input.homeDirectory, source);
844
- const loadedProfiles = loadLocalProfileSource({ path: materializedPath, only: source.only, except: source.except });
845
- for (const profile of loadedProfiles.profiles) {
846
- const existingChoice = choices.get(profile.profile.id);
847
- choices.set(profile.profile.id, {
848
- id: profile.profile.id,
849
- label: profile.profile.label ?? existingChoice?.label,
850
- description: profile.profile.description ?? existingChoice?.description,
851
- });
852
- }
853
- }
854
- return [...choices.values()].sort((left, right) => left.id.localeCompare(right.id));
855
- };
856
- const materializeSetupProfileSource = (homeDirectory, source) => {
857
- if (source.path !== undefined && source.uri === undefined && source.github === undefined) {
858
- return source.path;
859
- }
860
- if (source.uri !== undefined && source.ref === undefined && source.path === undefined) {
861
- return createProfileSourceCachePath(homeDirectory, source.uri);
862
- }
863
- if (source.uri !== undefined) {
864
- return resolveRemoteRepositorySubpath(createRemoteRepositoryCachePath(homeDirectory, { uri: source.uri, ref: source.ref }), source.path);
865
- }
866
- return resolveRemoteRepositorySubpath(createRemoteRepositoryCachePath(homeDirectory, { github: source.github, ref: source.ref }), source.path);
867
- };
868
- const selectSetupProfile = async (profiles, currentDefault, dependencies) => {
869
- if (dependencies.selectDefaultProfile !== undefined) {
870
- return dependencies.selectDefaultProfile(profiles, currentDefault);
871
- }
872
- return promptForSetupProfile(profiles, currentDefault, dependencies);
873
- };
874
- const promptForSetupProfile = async (profiles, currentDefault, dependencies) => {
875
- const output = resolvePromptOutput(dependencies);
876
- /* v8 ignore next -- default process streams are direct terminal behavior; tests inject streams. */
877
- const readline = createInterface({
878
- input: dependencies.input ?? process.stdin,
879
- output: resolveReadlineOutput(dependencies),
880
- });
881
- try {
882
- return await promptForSetupProfileWithReadline(readline, output, profiles, currentDefault, 'Choose the default profile for your sessions:');
883
- }
884
- finally {
885
- readline.close();
886
- }
887
- };
888
- const promptForSetupProfileWithReadline = async (readline, output, profiles, currentDefault, heading) => {
889
- const candidates = profiles.length > 0 ? profiles : [{ id: currentDefault }];
890
- output.write(`\n${heading}\n`);
891
- candidates.forEach((profile, index) => {
892
- const label = profile.label === undefined ? '' : ` - ${profile.label}`;
893
- output.write(`${index + 1}. ${profile.id}${label}\n`);
894
- if (profile.description !== undefined) {
895
- output.write(` ${profile.description}\n`);
896
- }
897
- });
898
- const currentIndex = Math.max(candidates.findIndex((profile) => profile.id === currentDefault), 0);
899
- const answer = await readline.question(`Default profile [${currentIndex + 1}]: `);
900
- const selectedIndex = Number.parseInt(answer.trim() || String(currentIndex + 1), 10) - 1;
901
- const selectedProfile = candidates[selectedIndex];
902
- if (selectedProfile === undefined) {
903
- throw new Error('Selected default profile number is out of range.');
904
- }
905
- return selectedProfile.id;
906
- };
907
- export { updateSettingsDefaultProfile };
908
- const formatSettingsIssue = (issue) => `${issue.filePath}#${issue.path} ${issue.message}`;
909
249
  //# sourceMappingURL=SetupCommand.js.map