create-quiver 0.14.0 → 0.15.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 (92) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +187 -518
  3. package/README_FOR_AI.md +39 -27
  4. package/ROADMAP.md +11 -0
  5. package/assets/quiver-wordmark.svg +22 -0
  6. package/docs/AI_CONTEXT.md.template +2 -0
  7. package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
  8. package/docs/CLI_UX_GUIDE.md +185 -0
  9. package/docs/COMMANDS.md.template +22 -3
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/getting-started/linux.md +84 -0
  12. package/docs/getting-started/macos.md +85 -0
  13. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  14. package/docs/getting-started/windows-powershell.md +96 -0
  15. package/docs/reference/commands.md +98 -0
  16. package/docs/workflows/existing-project.md +131 -0
  17. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  18. package/docs/workflows/legacy-quiver-project.md +102 -0
  19. package/docs/workflows/new-project.md +76 -0
  20. package/package.json +5 -1
  21. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  22. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  23. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  24. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  25. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  26. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  27. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  28. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  29. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  30. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  31. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  32. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  33. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  34. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  35. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  37. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  38. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  40. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  41. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  42. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  43. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  44. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  45. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  46. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  47. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
  48. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
  49. package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
  50. package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
  51. package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
  52. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
  53. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
  54. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
  55. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
  56. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
  57. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
  58. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
  59. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
  60. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
  61. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
  62. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
  63. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
  64. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
  65. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
  66. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
  67. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
  68. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
  69. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
  70. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
  71. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
  72. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
  73. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
  74. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
  75. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
  76. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
  77. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
  78. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
  79. package/src/create-quiver/commands/ai.js +809 -71
  80. package/src/create-quiver/commands/spec.js +167 -5
  81. package/src/create-quiver/index.js +582 -71
  82. package/src/create-quiver/lib/agent-profiles.js +111 -10
  83. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  84. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  85. package/src/create-quiver/lib/ai/execution-plan.js +106 -8
  86. package/src/create-quiver/lib/ai/executor.js +284 -28
  87. package/src/create-quiver/lib/ai/providers.js +71 -1
  88. package/src/create-quiver/lib/cli/editor.js +118 -0
  89. package/src/create-quiver/lib/cli/selectors.js +107 -0
  90. package/src/create-quiver/lib/cli/theme.js +103 -0
  91. package/src/create-quiver/lib/cli/ux-flags.js +169 -0
  92. package/src/create-quiver/lib/cli/ux.js +225 -0
@@ -11,6 +11,8 @@ const PROVIDERS = {
11
11
  id: 'codex',
12
12
  command: 'codex',
13
13
  args: ['exec'],
14
+ supportsModelSelection: true,
15
+ modelArgBuilder: (model) => ['--model', model],
14
16
  timeoutMs: 10 * 60 * 1000,
15
17
  installHint: 'Install the Codex CLI and make sure it is available on PATH.',
16
18
  },
@@ -18,6 +20,8 @@ const PROVIDERS = {
18
20
  id: 'claude',
19
21
  command: 'claude',
20
22
  args: ['-p'],
23
+ supportsModelSelection: true,
24
+ modelArgBuilder: (model) => ['--model', model],
21
25
  timeoutMs: 10 * 60 * 1000,
22
26
  installHint: 'Install the Claude CLI and make sure it is available on PATH.',
23
27
  },
@@ -25,6 +29,8 @@ const PROVIDERS = {
25
29
  id: 'gemini',
26
30
  command: 'gemini',
27
31
  args: ['--prompt', ''],
32
+ supportsModelSelection: true,
33
+ modelArgBuilder: (model) => ['--model', model],
28
34
  timeoutMs: 10 * 60 * 1000,
29
35
  installHint: 'Install the Gemini CLI and make sure it is available on PATH.',
30
36
  },
@@ -61,9 +67,68 @@ function getProviderDefinition(providerId) {
61
67
  return PROVIDERS[normalized];
62
68
  }
63
69
 
70
+ function normalizeProviderModel(model) {
71
+ const value = String(model || '').trim();
72
+ return value || '';
73
+ }
74
+
75
+ function buildProviderModelArgs(providerId, model, options = {}) {
76
+ const provider = getProviderDefinition(providerId);
77
+ const normalizedModel = normalizeProviderModel(model);
78
+ const enforce = options.enforce === true;
79
+
80
+ if (!normalizedModel) {
81
+ return {
82
+ model: '',
83
+ supported: Boolean(provider.supportsModelSelection),
84
+ enforced: enforce,
85
+ args: [],
86
+ reason: 'no model selected',
87
+ };
88
+ }
89
+
90
+ if (provider.supportsModelSelection !== true || typeof provider.modelArgBuilder !== 'function') {
91
+ if (enforce) {
92
+ throw new ProviderRunnerError(
93
+ 'UNSUPPORTED_PROVIDER_MODEL_SELECTION',
94
+ `Provider '${provider.id}' cannot receive model '${normalizedModel}' through its Quiver adapter. Remove the model selection or update the provider adapter before live execution.`,
95
+ {
96
+ provider: provider.id,
97
+ model: normalizedModel,
98
+ nextSteps: [
99
+ `Run without a model override for provider '${provider.id}'.`,
100
+ `Update the '${provider.id}' provider adapter with model argument support.`,
101
+ 'Use --dry-run to inspect the invocation before live execution.',
102
+ ],
103
+ },
104
+ );
105
+ }
106
+
107
+ return {
108
+ model: normalizedModel,
109
+ supported: false,
110
+ enforced: false,
111
+ args: [],
112
+ reason: 'provider adapter does not support model arguments',
113
+ };
114
+ }
115
+
116
+ const args = provider.modelArgBuilder(normalizedModel);
117
+ return {
118
+ model: normalizedModel,
119
+ supported: true,
120
+ enforced: enforce,
121
+ args: Array.isArray(args) ? args.map((arg) => String(arg)) : [],
122
+ reason: 'model argument supported',
123
+ };
124
+ }
125
+
64
126
  function buildProviderInvocation(providerId, options = {}) {
65
127
  const provider = getProviderDefinition(providerId);
66
128
  const extraArgs = Array.isArray(options.args) ? options.args.map((arg) => String(arg)) : [];
129
+ const modelSelection = buildProviderModelArgs(provider.id, options.model, {
130
+ enforce: options.enforceModelSelection === true,
131
+ });
67
132
  const prompt = String(options.prompt ?? '');
68
133
  const timeoutMs = Number.isFinite(options.timeoutMs) ? Number(options.timeoutMs) : provider.timeoutMs;
69
134
  const cwd = options.cwd ? String(options.cwd) : process.cwd();
@@ -72,10 +137,11 @@ function buildProviderInvocation(providerId, options = {}) {
72
137
  return {
73
138
  provider: provider.id,
74
139
  command: provider.command,
75
- args: provider.args.concat(extraArgs),
140
+ args: provider.args.concat(modelSelection.args, extraArgs),
76
141
  cwd,
77
142
  timeoutMs,
78
143
  promptLength: Buffer.byteLength(prompt, 'utf8'),
144
+ modelSelection,
79
145
  promptTransport: {
80
146
  mode: transportMode,
81
147
  promptLength: Buffer.byteLength(prompt, 'utf8'),
@@ -93,6 +159,7 @@ function createDryRunResult(invocation) {
93
159
  args: invocation.args.slice(),
94
160
  cwd: invocation.cwd,
95
161
  timeoutMs: invocation.timeoutMs,
162
+ modelSelection: invocation.modelSelection,
96
163
  promptTransport: invocation.promptTransport,
97
164
  exitCode: 0,
98
165
  stdout: '',
@@ -256,6 +323,7 @@ async function runProvider(providerId, options = {}) {
256
323
  args: invocation.args.slice(),
257
324
  cwd: invocation.cwd,
258
325
  timeoutMs: invocation.timeoutMs,
326
+ modelSelection: invocation.modelSelection,
259
327
  promptTransport: invocation.promptTransport,
260
328
  exitCode: null,
261
329
  stdout: '',
@@ -292,6 +360,7 @@ async function runProvider(providerId, options = {}) {
292
360
  args: invocation.args.slice(),
293
361
  cwd: invocation.cwd,
294
362
  timeoutMs: invocation.timeoutMs,
363
+ modelSelection: invocation.modelSelection,
295
364
  promptTransport: describePromptTransport(transport),
296
365
  exitCode: execution.exitCode,
297
366
  signal: execution.signal,
@@ -309,6 +378,7 @@ module.exports = {
309
378
  SUPPORTED_PROVIDERS,
310
379
  ProviderRunnerError,
311
380
  assertSupportedProvider,
381
+ buildProviderModelArgs,
312
382
  buildProviderInvocation,
313
383
  formatProviderList,
314
384
  getProviderDefinition,
@@ -0,0 +1,118 @@
1
+ const os = require('os');
2
+ const { spawnSync } = require('child_process');
3
+
4
+ function splitEditorCommand(value) {
5
+ const input = String(value || '').trim();
6
+ if (!input) return null;
7
+ const parts = [];
8
+ let current = '';
9
+ let quote = null;
10
+ let escaping = false;
11
+
12
+ for (const char of input) {
13
+ if (escaping) {
14
+ current += char;
15
+ escaping = false;
16
+ continue;
17
+ }
18
+ if (char === '\\') {
19
+ escaping = true;
20
+ continue;
21
+ }
22
+ if (quote) {
23
+ if (char === quote) {
24
+ quote = null;
25
+ } else {
26
+ current += char;
27
+ }
28
+ continue;
29
+ }
30
+ if (char === '"' || char === "'") {
31
+ quote = char;
32
+ continue;
33
+ }
34
+ if (/\s/.test(char)) {
35
+ if (current) {
36
+ parts.push(current);
37
+ current = '';
38
+ }
39
+ continue;
40
+ }
41
+ current += char;
42
+ }
43
+
44
+ if (current) parts.push(current);
45
+ return parts.length > 0 ? { command: parts[0], args: parts.slice(1) } : null;
46
+ }
47
+
48
+ function defaultEditorForPlatform(platform = os.platform()) {
49
+ return platform === 'win32' ? 'notepad' : 'vi';
50
+ }
51
+
52
+ function resolveEditor(env = process.env, options = {}) {
53
+ const candidates = [
54
+ { source: 'VISUAL', value: env.VISUAL },
55
+ { source: 'EDITOR', value: env.EDITOR },
56
+ ...((options.fallbacks || [defaultEditorForPlatform(options.platform)]).map((value) => ({
57
+ source: 'fallback',
58
+ value,
59
+ }))),
60
+ ];
61
+
62
+ for (const candidate of candidates) {
63
+ const parsed = splitEditorCommand(candidate.value);
64
+ if (parsed) {
65
+ return { ...parsed, source: candidate.source };
66
+ }
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ function openEditor(filePath, options = {}) {
73
+ const editor = options.editor || resolveEditor(options.env || process.env, options);
74
+ if (!editor) {
75
+ return {
76
+ ok: false,
77
+ canceled: true,
78
+ filePath,
79
+ reason: 'No editor configured. Set VISUAL or EDITOR, then rerun the command.',
80
+ };
81
+ }
82
+
83
+ const runner = options.spawnSync || spawnSync;
84
+ const result = runner(editor.command, [...editor.args, filePath], {
85
+ cwd: options.cwd || process.cwd(),
86
+ env: options.env || process.env,
87
+ shell: false,
88
+ stdio: options.stdio || 'inherit',
89
+ });
90
+
91
+ if (result.error) {
92
+ return {
93
+ ok: false,
94
+ canceled: true,
95
+ command: editor.command,
96
+ filePath,
97
+ reason: result.error.message,
98
+ source: editor.source,
99
+ };
100
+ }
101
+
102
+ const status = result.status ?? 0;
103
+ return {
104
+ ok: status === 0,
105
+ canceled: status !== 0,
106
+ command: editor.command,
107
+ exitCode: status,
108
+ filePath,
109
+ source: editor.source,
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ defaultEditorForPlatform,
115
+ openEditor,
116
+ resolveEditor,
117
+ splitEditorCommand,
118
+ };
@@ -0,0 +1,107 @@
1
+ const { resolveUxMode } = require('./ux');
2
+
3
+ function formatError(message) {
4
+ return `create-quiver: ${message}`;
5
+ }
6
+
7
+ function normalizeSelectorOptions(options = []) {
8
+ return options.map((option) => {
9
+ if (typeof option === 'string') {
10
+ return {
11
+ label: option,
12
+ value: option,
13
+ hint: '',
14
+ default: false,
15
+ };
16
+ }
17
+ return {
18
+ label: String(option.label || option.name || option.id || option.value || ''),
19
+ value: String(option.value || option.id || option.path || option.label || ''),
20
+ hint: option.hint || option.description || '',
21
+ default: option.default === true || option.recommended === true,
22
+ raw: option.raw || option,
23
+ };
24
+ }).filter((option) => option.label && option.value);
25
+ }
26
+
27
+ function findOption(options, value) {
28
+ const normalized = String(value || '').trim();
29
+ if (!normalized) return null;
30
+ return options.find((option) => option.value === normalized || option.label === normalized) || null;
31
+ }
32
+
33
+ function resolveDefaultOption(options, defaultValue) {
34
+ return findOption(options, defaultValue) || options.find((option) => option.default) || (options.length === 1 ? options[0] : null);
35
+ }
36
+
37
+ async function loadClack(injected) {
38
+ if (injected) return injected;
39
+ return import('@clack/prompts');
40
+ }
41
+
42
+ async function selectOption(message, rawOptions, options = {}) {
43
+ const normalizedOptions = normalizeSelectorOptions(rawOptions);
44
+ if (normalizedOptions.length === 0) {
45
+ throw new Error(formatError(`${options.name || 'selection'} has no available options.`));
46
+ }
47
+
48
+ const explicit = findOption(normalizedOptions, options.value);
49
+ if (explicit) return explicit;
50
+ if (options.value) {
51
+ throw new Error(formatError(`${options.name || 'selection'} option '${options.value}' was not found.`));
52
+ }
53
+
54
+ const mode = resolveUxMode({
55
+ interactive: options.interactive === true,
56
+ json: options.json,
57
+ noColor: options.noColor,
58
+ stdinIsTTY: options.stdinIsTTY,
59
+ stdoutIsTTY: options.stdoutIsTTY,
60
+ stderrIsTTY: options.stderrIsTTY,
61
+ }, options.env || process.env, {
62
+ input: options.input || process.stdin,
63
+ output: options.output || process.stdout,
64
+ error: options.error || process.stderr,
65
+ });
66
+
67
+ if (mode.usePrompts) {
68
+ if (typeof options.promptSelect === 'function') {
69
+ const selected = await options.promptSelect(message, normalizedOptions);
70
+ const match = findOption(normalizedOptions, selected);
71
+ if (!match) {
72
+ throw new Error(formatError(`${options.name || 'selection'} prompt returned unknown option '${selected}'.`));
73
+ }
74
+ return match;
75
+ }
76
+ const clack = await loadClack(options.prompts);
77
+ const selected = await clack.select({
78
+ message,
79
+ options: normalizedOptions.map((option) => ({
80
+ label: option.label,
81
+ value: option.value,
82
+ hint: option.hint || undefined,
83
+ })),
84
+ initialValue: resolveDefaultOption(normalizedOptions, options.defaultValue)?.value,
85
+ });
86
+ if (clack.isCancel && clack.isCancel(selected)) {
87
+ throw new Error(formatError(`${options.name || 'selection'} was canceled.`));
88
+ }
89
+ return findOption(normalizedOptions, selected);
90
+ }
91
+
92
+ const fallback = resolveDefaultOption(normalizedOptions, options.defaultValue);
93
+ if (fallback) return fallback;
94
+
95
+ const flag = options.flag || '--value';
96
+ const available = normalizedOptions.map((option) => option.value).join(', ');
97
+ throw new Error(formatError([
98
+ `${options.name || 'selection'} requires an explicit choice.`,
99
+ `Available options: ${available}.`,
100
+ `Use ${flag} <value> or rerun with --interactive.`,
101
+ ].join('\n')));
102
+ }
103
+
104
+ module.exports = {
105
+ normalizeSelectorOptions,
106
+ selectOption,
107
+ };
@@ -0,0 +1,103 @@
1
+ const QUIVER_COLORS = Object.freeze({
2
+ sky: '#86C8F2',
3
+ blue: '#6BADEB',
4
+ periwinkle: '#7F9EE8',
5
+ violet: '#9B82E6',
6
+ magenta: '#D56AB0',
7
+ });
8
+
9
+ const STATUS_COLORS = Object.freeze({
10
+ info: 'sky',
11
+ command: 'blue',
12
+ success: 'blue',
13
+ planner: 'violet',
14
+ review: 'magenta',
15
+ warning: 'magenta',
16
+ error: 'magenta',
17
+ risk: 'magenta',
18
+ muted: 'periwinkle',
19
+ });
20
+
21
+ function isTruthy(value) {
22
+ return /^(1|true|yes|on)$/i.test(String(value || '').trim());
23
+ }
24
+
25
+ function isAsciiLocale(env = process.env) {
26
+ const locale = `${env.LC_ALL || ''} ${env.LC_CTYPE || ''} ${env.LANG || ''}`;
27
+ return /\bC\b|ASCII/i.test(locale);
28
+ }
29
+
30
+ function isDumbTerminal(env = process.env) {
31
+ return String(env.TERM || '').toLowerCase() === 'dumb';
32
+ }
33
+
34
+ function shouldUseColor(options = {}, env = process.env, tty = {}) {
35
+ if (options.json || options.noColor) return false;
36
+ if (env.NO_COLOR || env.FORCE_COLOR === '0') return false;
37
+ if (isDumbTerminal(env)) return false;
38
+ if (isTruthy(env.CI)) return false;
39
+ if (tty.stdout === false) return false;
40
+ if (options.color === true) return true;
41
+ return Boolean(tty.stdout);
42
+ }
43
+
44
+ function shouldUseUnicode(options = {}, env = process.env, tty = {}) {
45
+ if (options.ascii || options.json) return false;
46
+ if (isDumbTerminal(env) || isAsciiLocale(env)) return false;
47
+ if (tty.stdout === false) return false;
48
+ if (options.unicode === true) return true;
49
+ return Boolean(tty.stdout);
50
+ }
51
+
52
+ function hexToRgb(hex) {
53
+ const normalized = String(hex || '').replace(/^#/, '');
54
+ if (!/^[0-9a-f]{6}$/i.test(normalized)) {
55
+ return null;
56
+ }
57
+ return [
58
+ Number.parseInt(normalized.slice(0, 2), 16),
59
+ Number.parseInt(normalized.slice(2, 4), 16),
60
+ Number.parseInt(normalized.slice(4, 6), 16),
61
+ ];
62
+ }
63
+
64
+ function colorize(value, colorName, theme) {
65
+ const text = String(value ?? '');
66
+ if (!theme?.useColor) return text;
67
+ const rgb = hexToRgb(theme.colors[colorName] || colorName);
68
+ if (!rgb) return text;
69
+ return `\u001b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text}\u001b[0m`;
70
+ }
71
+
72
+ function resolveTheme(options = {}, env = process.env, tty = {}) {
73
+ const useColor = shouldUseColor(options, env, tty);
74
+ const useUnicode = shouldUseUnicode(options, env, tty);
75
+ const symbols = useUnicode
76
+ ? { start: '◇', section: '◆', success: '✓', warning: '!', error: '✖', bullet: '•' }
77
+ : { start: '*', section: '*', success: 'OK', warning: 'WARN', error: 'ERR', bullet: '-' };
78
+ const theme = {
79
+ colors: QUIVER_COLORS,
80
+ statusColors: STATUS_COLORS,
81
+ symbols,
82
+ useColor,
83
+ useUnicode,
84
+ };
85
+
86
+ return {
87
+ ...theme,
88
+ colorize: (value, colorName) => colorize(value, colorName, theme),
89
+ status: (status, value) => colorize(value, STATUS_COLORS[status] || 'sky', theme),
90
+ };
91
+ }
92
+
93
+ module.exports = {
94
+ QUIVER_COLORS,
95
+ STATUS_COLORS,
96
+ colorize,
97
+ isAsciiLocale,
98
+ isDumbTerminal,
99
+ isTruthy,
100
+ resolveTheme,
101
+ shouldUseColor,
102
+ shouldUseUnicode,
103
+ };
@@ -0,0 +1,169 @@
1
+ const UX_FLAG_OPTIONS = Object.freeze({
2
+ withPlanner: '--with-planner',
3
+ interactive: '--interactive',
4
+ review: '--review',
5
+ });
6
+
7
+ const UX_FLAG_MATRIX = Object.freeze({
8
+ init: Object.freeze({
9
+ withPlanner: false,
10
+ interactive: true,
11
+ review: false,
12
+ note: 'guided project onboarding choices',
13
+ }),
14
+ 'ai prepare-context': Object.freeze({
15
+ withPlanner: true,
16
+ interactive: true,
17
+ review: true,
18
+ note: 'planner-assisted docs-only context preparation',
19
+ }),
20
+ 'ai plan': Object.freeze({
21
+ withPlanner: true,
22
+ interactive: true,
23
+ review: true,
24
+ note: 'planner draft generation and human approval workflow',
25
+ }),
26
+ 'spec create': Object.freeze({
27
+ withPlanner: true,
28
+ interactive: true,
29
+ review: true,
30
+ note: 'spec generation from approved planner output',
31
+ }),
32
+ 'ai pr': Object.freeze({
33
+ withPlanner: false,
34
+ interactive: true,
35
+ review: true,
36
+ note: 'PR body review and interactive PR inputs',
37
+ }),
38
+ 'ai execute-slice': Object.freeze({
39
+ withPlanner: false,
40
+ interactive: true,
41
+ review: false,
42
+ note: 'executor profile and ready-slice selection',
43
+ }),
44
+ 'ai execute-plan': Object.freeze({
45
+ withPlanner: false,
46
+ interactive: true,
47
+ review: false,
48
+ note: 'execution strategy confirmation and future selectors',
49
+ }),
50
+ });
51
+
52
+ function formatError(message) {
53
+ return `create-quiver: ${message}`;
54
+ }
55
+
56
+ function normalizeCommandKey(value) {
57
+ return String(value || '').trim().replace(/\s+/g, ' ');
58
+ }
59
+
60
+ function resolveUxCommandKey(args = {}) {
61
+ if (args.mode === 'ai') {
62
+ const command = args.aiCommand || '';
63
+ if (command === 'specs' || command === 'slices' || command === 'trace' || command === 'active-slice') {
64
+ return normalizeCommandKey(`ai ${command} ${args.aiSecondaryCommand || ''}`);
65
+ }
66
+ if (command === 'agent') {
67
+ return normalizeCommandKey(`ai ${command} ${args.aiAgentCommand || ''}`);
68
+ }
69
+ if (command === 'run') {
70
+ return normalizeCommandKey(`ai ${command} ${args.aiRunCommand || ''}`);
71
+ }
72
+ return normalizeCommandKey(`ai ${command}`);
73
+ }
74
+
75
+ if (args.mode === 'spec') {
76
+ return normalizeCommandKey(`spec ${args.specCommand || ''}`);
77
+ }
78
+
79
+ if (args.mode === 'demo') {
80
+ return normalizeCommandKey(`demo ${args.demoCommand || ''} ${args.demoName || ''}`);
81
+ }
82
+
83
+ if (args.mode === 'evidence') {
84
+ return normalizeCommandKey(`evidence ${args.evidenceCommand || ''}`);
85
+ }
86
+
87
+ return normalizeCommandKey(args.mode);
88
+ }
89
+
90
+ function getUxFlagSupport(commandKey) {
91
+ return UX_FLAG_MATRIX[normalizeCommandKey(commandKey)] || Object.freeze({
92
+ withPlanner: false,
93
+ interactive: false,
94
+ review: false,
95
+ note: 'no UX flag support',
96
+ });
97
+ }
98
+
99
+ function getRequestedUxFlags(args = {}) {
100
+ return Object.keys(UX_FLAG_OPTIONS).filter((key) => args[key] === true);
101
+ }
102
+
103
+ function supportedFlagList(support) {
104
+ return Object.entries(UX_FLAG_OPTIONS)
105
+ .filter(([key]) => support[key] === true)
106
+ .map(([, flag]) => flag);
107
+ }
108
+
109
+ function explainAlternative(flag) {
110
+ if (flag === 'withPlanner') {
111
+ return 'Use a planner-capable command such as: npx create-quiver ai prepare-context --with-planner --dry-run';
112
+ }
113
+ if (flag === 'interactive') {
114
+ return 'Use explicit non-interactive flags for automation, or run a command that documents --interactive support.';
115
+ }
116
+ if (flag === 'review') {
117
+ return 'Use a command with persistent AI-generated output, then rerun with --review after inspecting the dry-run.';
118
+ }
119
+ return 'Run: npx create-quiver --help';
120
+ }
121
+
122
+ function buildUnsupportedFlagError(commandKey, flag, support) {
123
+ const flagName = UX_FLAG_OPTIONS[flag];
124
+ const supported = supportedFlagList(support);
125
+ const supportedText = supported.length > 0 ? supported.join(', ') : 'none';
126
+
127
+ return new Error(formatError([
128
+ `UX flag ${flagName} is not supported by '${commandKey}'.`,
129
+ `Supported UX flags for '${commandKey}': ${supportedText}.`,
130
+ explainAlternative(flag),
131
+ ].join('\n')));
132
+ }
133
+
134
+ function validateUxFlagCombinations(args = {}, commandKey = resolveUxCommandKey(args)) {
135
+ if (args.json && args.interactive) {
136
+ throw new Error(formatError(`incompatible UX flags for '${commandKey}': --json cannot be combined with --interactive because JSON output must stay machine-readable.`));
137
+ }
138
+ if (args.json && args.review) {
139
+ throw new Error(formatError(`incompatible UX flags for '${commandKey}': --json cannot be combined with --review because editor review is a human-only flow.`));
140
+ }
141
+ }
142
+
143
+ function validateUxFlags(args = {}) {
144
+ const commandKey = resolveUxCommandKey(args);
145
+ validateUxFlagCombinations(args, commandKey);
146
+
147
+ const support = getUxFlagSupport(commandKey);
148
+ for (const flag of getRequestedUxFlags(args)) {
149
+ if (support[flag] !== true) {
150
+ throw buildUnsupportedFlagError(commandKey, flag, support);
151
+ }
152
+ }
153
+
154
+ return {
155
+ commandKey,
156
+ requested: getRequestedUxFlags(args).map((flag) => UX_FLAG_OPTIONS[flag]),
157
+ supported: supportedFlagList(support),
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ UX_FLAG_MATRIX,
163
+ UX_FLAG_OPTIONS,
164
+ getRequestedUxFlags,
165
+ getUxFlagSupport,
166
+ resolveUxCommandKey,
167
+ validateUxFlagCombinations,
168
+ validateUxFlags,
169
+ };