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.
- package/CHANGELOG.md +27 -0
- package/README.md +187 -518
- package/README_FOR_AI.md +39 -27
- package/ROADMAP.md +11 -0
- package/assets/quiver-wordmark.svg +22 -0
- package/docs/AI_CONTEXT.md.template +2 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
- package/docs/CLI_UX_GUIDE.md +185 -0
- package/docs/COMMANDS.md.template +22 -3
- package/docs/GITFLOW_PR_GUIDE.md +70 -0
- package/docs/getting-started/linux.md +84 -0
- package/docs/getting-started/macos.md +85 -0
- package/docs/getting-started/windows-git-bash-wsl.md +78 -0
- package/docs/getting-started/windows-powershell.md +96 -0
- package/docs/reference/commands.md +98 -0
- package/docs/workflows/existing-project.md +131 -0
- package/docs/workflows/full-ai-spec-to-pr.md +311 -0
- package/docs/workflows/legacy-quiver-project.md +102 -0
- package/docs/workflows/new-project.md +76 -0
- package/package.json +5 -1
- package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
- package/src/create-quiver/commands/ai.js +809 -71
- package/src/create-quiver/commands/spec.js +167 -5
- package/src/create-quiver/index.js +582 -71
- package/src/create-quiver/lib/agent-profiles.js +111 -10
- package/src/create-quiver/lib/ai/context-proposal.js +389 -0
- package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
- package/src/create-quiver/lib/ai/execution-plan.js +106 -8
- package/src/create-quiver/lib/ai/executor.js +284 -28
- package/src/create-quiver/lib/ai/providers.js +71 -1
- package/src/create-quiver/lib/cli/editor.js +118 -0
- package/src/create-quiver/lib/cli/selectors.js +107 -0
- package/src/create-quiver/lib/cli/theme.js +103 -0
- package/src/create-quiver/lib/cli/ux-flags.js +169 -0
- 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
|
+
};
|