@the-open-engine/zeroshot 6.31.3 → 6.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -98
- package/cli/index.js +251 -252
- package/cli/lib/setup-provider-readiness.js +86 -0
- package/cli/lib/setup-scanner-worker.js +120 -0
- package/cli/lib/setup-scanner.js +185 -0
- package/cli/lib/setup-wizard-input.js +146 -0
- package/cli/lib/setup-wizard-model.js +205 -0
- package/cli/lib/setup-wizard-plan-view.js +157 -0
- package/cli/lib/setup-wizard-scan-view.js +144 -0
- package/cli/lib/setup-wizard-terminal.js +237 -0
- package/cli/lib/setup-wizard-view.js +180 -0
- package/cli/lib/setup-wizard.js +281 -0
- package/cli/message-formatters-normal.js +14 -18
- package/cli/message-formatters-watch.js +53 -141
- package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/codex.js +8 -2
- package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +4 -2
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -1
- package/lib/agent-cli-provider/provider-registry.js +13 -3
- package/lib/agent-cli-provider/provider-registry.js.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +7 -4
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +2 -0
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/completion.js +102 -153
- package/lib/settings.js +10 -2
- package/lib/setup-apply.js +62 -55
- package/lib/setup-plan.js +32 -52
- package/lib/start-cluster.js +65 -25
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -3
- package/scripts/postinstall.js +54 -0
- package/src/agent/agent-lifecycle.js +11 -1
- package/src/agent/agent-task-executor.js +25 -7
- package/src/agent/structured-output-error.js +42 -0
- package/src/agent-cli-provider/adapters/codex.ts +8 -12
- package/src/agent-cli-provider/provider-registry.ts +15 -3
- package/src/agent-cli-provider/single-agent-runtime.ts +11 -11
- package/src/agent-cli-provider/types.ts +2 -0
- package/src/preflight.js +27 -1
- package/src/status-footer.js +19 -12
- package/task-lib/commands/list.js +90 -78
- package/task-lib/commands/status.js +97 -40
- package/task-lib/effective-status.js +52 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const { isDeepStrictEqual } = require('util');
|
|
2
|
+
|
|
3
|
+
const { getNestedValue, resolveDecisionPath } = require('../../lib/setup-plan');
|
|
4
|
+
|
|
5
|
+
function enabled(enabledGroups, id) {
|
|
6
|
+
return enabledGroups?.[id] !== false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function buildWizardDecisions(plan, provider, isolation, enabledGroups = {}) {
|
|
10
|
+
const decisions = {
|
|
11
|
+
defaultProvider: provider,
|
|
12
|
+
[`providerLevel.${provider}`]: plan.recommended[`providerLevel.${provider}`],
|
|
13
|
+
defaultIsolation: isolation,
|
|
14
|
+
defaultDelivery: 'none',
|
|
15
|
+
};
|
|
16
|
+
if (enabled(enabledGroups, 'issue-source')) {
|
|
17
|
+
decisions.defaultIssueSource = plan.recommended.defaultIssueSource;
|
|
18
|
+
}
|
|
19
|
+
if (enabled(enabledGroups, 'updates')) decisions.updatePolicy = plan.recommended.updatePolicy;
|
|
20
|
+
if (isolation === 'docker' && enabled(enabledGroups, 'docker')) {
|
|
21
|
+
decisions.dockerMounts = plan.recommended.dockerMounts;
|
|
22
|
+
decisions.dockerEnvPassthrough = plan.recommended.dockerEnvPassthrough;
|
|
23
|
+
}
|
|
24
|
+
return decisions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function previewValue(decisionId, value, settings) {
|
|
28
|
+
if (!decisionId.startsWith('providerLevel.')) return value;
|
|
29
|
+
const provider = decisionId.slice('providerLevel.'.length);
|
|
30
|
+
return { ...(settings.providerSettings?.[provider] || {}), ...value };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decisionPreview(decisionId, value, settings, settingsFile) {
|
|
34
|
+
const target = resolveDecisionPath(decisionId);
|
|
35
|
+
if (!target) throw new Error(`Unknown setup decision: ${decisionId}`);
|
|
36
|
+
const source = target.scope === 'global' ? settings : {};
|
|
37
|
+
const from = getNestedValue(source, target.path) ?? null;
|
|
38
|
+
const to = previewValue(decisionId, value, settings);
|
|
39
|
+
if (isDeepStrictEqual(from, to)) return null;
|
|
40
|
+
return {
|
|
41
|
+
decisionId,
|
|
42
|
+
scope: target.scope,
|
|
43
|
+
path: target.path,
|
|
44
|
+
from,
|
|
45
|
+
to,
|
|
46
|
+
targetFile: target.scope === 'global' ? settingsFile : '.zeroshot/settings.json',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function groupDefinition(id, title, required, decisionIds) {
|
|
51
|
+
return { id, title, required, decisionIds };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wizardDecisionGroups(provider, isolation) {
|
|
55
|
+
const groups = [
|
|
56
|
+
groupDefinition('execution', 'Execution defaults', true, [
|
|
57
|
+
'defaultProvider',
|
|
58
|
+
`providerLevel.${provider}`,
|
|
59
|
+
'defaultIsolation',
|
|
60
|
+
'defaultDelivery',
|
|
61
|
+
]),
|
|
62
|
+
groupDefinition('issue-source', 'Repository integration', false, ['defaultIssueSource']),
|
|
63
|
+
groupDefinition('updates', 'Update notifications', false, ['updatePolicy']),
|
|
64
|
+
];
|
|
65
|
+
if (isolation === 'docker') {
|
|
66
|
+
groups.splice(
|
|
67
|
+
1,
|
|
68
|
+
0,
|
|
69
|
+
groupDefinition('docker', 'Docker defaults', true, ['dockerMounts', 'dockerEnvPassthrough'])
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return groups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildWizardPlanModel({
|
|
76
|
+
plan,
|
|
77
|
+
settings,
|
|
78
|
+
settingsFile,
|
|
79
|
+
provider,
|
|
80
|
+
isolation,
|
|
81
|
+
enabledGroups,
|
|
82
|
+
}) {
|
|
83
|
+
const decisions = buildWizardDecisions(plan, provider, isolation, enabledGroups);
|
|
84
|
+
const previewById = new Map();
|
|
85
|
+
for (const [decisionId, value] of Object.entries(decisions)) {
|
|
86
|
+
const preview = decisionPreview(decisionId, value, settings, settingsFile);
|
|
87
|
+
if (preview) previewById.set(decisionId, preview);
|
|
88
|
+
}
|
|
89
|
+
const groups = wizardDecisionGroups(provider, isolation).map((group) => ({
|
|
90
|
+
...group,
|
|
91
|
+
enabled: group.required || enabled(enabledGroups, group.id),
|
|
92
|
+
writes: group.decisionIds.map((id) => previewById.get(id)).filter(Boolean),
|
|
93
|
+
}));
|
|
94
|
+
const writes = groups.flatMap((group) => (group.enabled ? group.writes : []));
|
|
95
|
+
return {
|
|
96
|
+
decisions,
|
|
97
|
+
groups,
|
|
98
|
+
writes,
|
|
99
|
+
files: [...new Set(writes.map((write) => write.targetFile))],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isolationChoices(plan) {
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
value: 'worktree',
|
|
107
|
+
label: 'Worktree',
|
|
108
|
+
detail: plan.facts.git.isRepo
|
|
109
|
+
? 'isolated checkout · current checkout untouched'
|
|
110
|
+
: 'unavailable · not a git repository',
|
|
111
|
+
disabled: !plan.facts.git.isRepo,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
value: 'docker',
|
|
115
|
+
label: 'Docker',
|
|
116
|
+
detail: plan.facts.docker.available
|
|
117
|
+
? 'strongest isolation · slower startup'
|
|
118
|
+
: 'unavailable · Docker is not running',
|
|
119
|
+
disabled: !plan.facts.docker.available,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
value: 'none',
|
|
123
|
+
label: 'Current checkout',
|
|
124
|
+
detail: 'no isolation · edits the active checkout directly',
|
|
125
|
+
disabled: false,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function preferredIndex(choices, preferredValue) {
|
|
131
|
+
const preferred = choices.findIndex(
|
|
132
|
+
(choice) => choice.value === preferredValue && !choice.disabled
|
|
133
|
+
);
|
|
134
|
+
if (preferred >= 0) return preferred;
|
|
135
|
+
return choices.findIndex((choice) => !choice.disabled);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function syntheticScan(plan, onProgress) {
|
|
139
|
+
const providerSpecs = Object.keys(plan.facts.providers).map((id) => ({ kind: 'provider', id }));
|
|
140
|
+
const specs = [{ kind: 'git' }, { kind: 'docker' }, { kind: 'issue' }, ...providerSpecs];
|
|
141
|
+
const probes = {
|
|
142
|
+
git: { ...plan.facts.git, clean: true, defaultBranch: plan.facts.git.branch },
|
|
143
|
+
docker: { ...plan.facts.docker },
|
|
144
|
+
issue: {
|
|
145
|
+
installed: plan.facts.git.ghAvailable === true,
|
|
146
|
+
authenticated: plan.facts.git.ghAuthed === true,
|
|
147
|
+
error: null,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
for (const [id, facts] of Object.entries(plan.facts.providers)) {
|
|
151
|
+
probes[`provider:${id}`] = {
|
|
152
|
+
id,
|
|
153
|
+
available: facts.available,
|
|
154
|
+
commandAvailable: facts.available,
|
|
155
|
+
displayName: facts.displayName,
|
|
156
|
+
path: facts.path,
|
|
157
|
+
authStatus: facts.available ? 'ready' : 'unknown',
|
|
158
|
+
authReason: null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
onProgress?.({ type: 'start', probes: specs, elapsedMs: 0 });
|
|
162
|
+
for (const spec of specs) {
|
|
163
|
+
const id = spec.id ? `provider:${spec.id}` : spec.kind;
|
|
164
|
+
onProgress?.({
|
|
165
|
+
type: 'complete',
|
|
166
|
+
id,
|
|
167
|
+
kind: spec.kind,
|
|
168
|
+
providerId: spec.id || null,
|
|
169
|
+
ok: true,
|
|
170
|
+
elapsedMs: 0,
|
|
171
|
+
result: probes[id],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
onProgress?.({ type: 'finish', elapsedMs: 0, plan, probes });
|
|
175
|
+
return { plan, probes, elapsedMs: 0 };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function collectSetupScan({ cwd, settings, repoSettings, env, resolved, deps, onProgress }) {
|
|
179
|
+
if (deps.buildSetupPlan && !deps.scanSetupEnvironment) {
|
|
180
|
+
const plan = resolved.buildSetupPlan({
|
|
181
|
+
cwd,
|
|
182
|
+
settings,
|
|
183
|
+
repoSettings,
|
|
184
|
+
env: { ...env, __isTTY: true },
|
|
185
|
+
deps: resolved.setupPlanDeps,
|
|
186
|
+
});
|
|
187
|
+
return syntheticScan(plan, onProgress);
|
|
188
|
+
}
|
|
189
|
+
return resolved.scanSetupEnvironment({
|
|
190
|
+
cwd,
|
|
191
|
+
settings,
|
|
192
|
+
repoSettings,
|
|
193
|
+
env,
|
|
194
|
+
onProgress,
|
|
195
|
+
deps: resolved.setupScanDeps,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
buildWizardDecisions,
|
|
201
|
+
buildWizardPlanModel,
|
|
202
|
+
collectSetupScan,
|
|
203
|
+
isolationChoices,
|
|
204
|
+
preferredIndex,
|
|
205
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const { CANCEL_KEYS, fit, gutter, stepHead, terminalWidth } = require('./setup-wizard-terminal');
|
|
2
|
+
|
|
3
|
+
function formatValue(value) {
|
|
4
|
+
if (typeof value === 'string') return value;
|
|
5
|
+
const serialized = JSON.stringify(value);
|
|
6
|
+
return serialized.length > 44 ? `${serialized.slice(0, 41)}…` : serialized;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function writeMarker(write) {
|
|
10
|
+
return write.from === null || write.from === undefined ? '+' : '~';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createPlanState(groups) {
|
|
14
|
+
return {
|
|
15
|
+
status: 'active',
|
|
16
|
+
focus: 0,
|
|
17
|
+
action: 0,
|
|
18
|
+
enabled: Object.fromEntries(groups.map((group) => [group.id, group.enabled !== false])),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function movePlanFocus(state, direction, groupCount) {
|
|
23
|
+
const count = groupCount + 1;
|
|
24
|
+
return { ...state, focus: (state.focus + direction + count) % count };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function togglePlanGroup(state, group) {
|
|
28
|
+
if (!group || group.required) return state;
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
enabled: { ...state.enabled, [group.id]: !state.enabled[group.id] },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function choosePlanAction(state, groups) {
|
|
36
|
+
if (state.focus < groups.length) return { ...state, focus: groups.length };
|
|
37
|
+
return { ...state, status: state.action === 0 ? 'apply' : 'cancelled' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function navigatePlanActions(state, key, groupCount) {
|
|
41
|
+
if (state.focus !== groupCount) return null;
|
|
42
|
+
if (key !== 'left' && key !== 'right') return null;
|
|
43
|
+
return { ...state, action: state.action === 0 ? 1 : 0 };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reducePlanState(state, key, groups) {
|
|
47
|
+
if (state.status !== 'active') return state;
|
|
48
|
+
if (CANCEL_KEYS.has(key)) return { ...state, status: 'cancelled' };
|
|
49
|
+
if (key === 'up') return movePlanFocus(state, -1, groups.length);
|
|
50
|
+
if (key === 'down') return movePlanFocus(state, 1, groups.length);
|
|
51
|
+
const actionNavigation = navigatePlanActions(state, key, groups.length);
|
|
52
|
+
if (actionNavigation) return actionNavigation;
|
|
53
|
+
if (key === 'space') return togglePlanGroup(state, groups[state.focus]);
|
|
54
|
+
return key === 'enter' ? choosePlanAction(state, groups) : state;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function planFrame({ stdout, theme, model, state, active = true }) {
|
|
58
|
+
const width = terminalWidth(stdout);
|
|
59
|
+
const targetCount = new Set(model.writes.map((write) => write.targetFile)).size;
|
|
60
|
+
const meta = `${targetCount} ${targetCount === 1 ? 'file' : 'files'} · ${model.writes.length} settings`;
|
|
61
|
+
const rows = [
|
|
62
|
+
stepHead(theme, active ? 'active' : 'done', 'Plan', { meta, width }),
|
|
63
|
+
gutter(theme),
|
|
64
|
+
];
|
|
65
|
+
model.groups.forEach((group, index) => {
|
|
66
|
+
const selected = active && state.focus === index;
|
|
67
|
+
const enabled = group.required || state.enabled[group.id];
|
|
68
|
+
const cursor = selected ? theme.accent('▸') : ' ';
|
|
69
|
+
const box = enabled ? '[x]' : '[ ]';
|
|
70
|
+
const title = selected ? theme.bold(group.title) : group.title;
|
|
71
|
+
rows.push(
|
|
72
|
+
gutter(
|
|
73
|
+
theme,
|
|
74
|
+
fit(`${cursor} ${box} ${title}${group.required ? ' · required' : ''}`, width - 3)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
if (enabled) {
|
|
78
|
+
for (const write of group.writes) {
|
|
79
|
+
rows.push(
|
|
80
|
+
gutter(
|
|
81
|
+
theme,
|
|
82
|
+
fit(` ${writeMarker(write)} ${write.path} = ${formatValue(write.to)}`, width - 3)
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (model.files.length > 0) {
|
|
89
|
+
rows.push(gutter(theme));
|
|
90
|
+
rows.push(gutter(theme, fit(theme.dim(`files: ${model.files.join(', ')}`), width - 3)));
|
|
91
|
+
}
|
|
92
|
+
if (active) {
|
|
93
|
+
const actionFocus = state.focus === model.groups.length;
|
|
94
|
+
const apply = state.action === 0 && actionFocus ? theme.bold('▸ Apply') : theme.dim(' Apply');
|
|
95
|
+
const cancel =
|
|
96
|
+
state.action === 1 && actionFocus ? theme.bold('▸ Cancel') : theme.dim(' Cancel');
|
|
97
|
+
rows.push(gutter(theme));
|
|
98
|
+
rows.push(gutter(theme, fit(`${apply} ${cancel}`, width - 3)));
|
|
99
|
+
rows.push(
|
|
100
|
+
gutter(theme, fit(theme.dim('↑↓ move · space toggle · ←→ choose · ↵ continue'), width - 3))
|
|
101
|
+
);
|
|
102
|
+
} else {
|
|
103
|
+
rows.push(gutter(theme, theme.dim('approved · writes begin below')));
|
|
104
|
+
rows.push(gutter(theme));
|
|
105
|
+
}
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function selectPlan({ stdout, reader, live, theme, buildModel }) {
|
|
110
|
+
let model = buildModel();
|
|
111
|
+
let state = createPlanState(model.groups);
|
|
112
|
+
live.paint(planFrame({ stdout, theme, model, state }));
|
|
113
|
+
while (state.status === 'active') {
|
|
114
|
+
const key = await reader.read();
|
|
115
|
+
state = reducePlanState(state, key, model.groups);
|
|
116
|
+
model = buildModel(state.enabled);
|
|
117
|
+
if (state.status === 'active') live.paint(planFrame({ stdout, theme, model, state }));
|
|
118
|
+
}
|
|
119
|
+
if (state.status === 'apply') {
|
|
120
|
+
live.commit(planFrame({ stdout, theme, model, state, active: false }));
|
|
121
|
+
return { action: 'apply', model, enabled: state.enabled };
|
|
122
|
+
}
|
|
123
|
+
live.clear();
|
|
124
|
+
return { action: 'cancel', model, enabled: state.enabled };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function receiptLine(theme, result) {
|
|
128
|
+
if (result.applied) return `${theme.success('v')} ${result.decisionId}`;
|
|
129
|
+
return `${theme.dim('–')} ${result.decisionId} · ${result.skippedReason}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function applyState(verified, failed) {
|
|
133
|
+
if (failed) return { state: 'failed', meta: 'failed' };
|
|
134
|
+
if (verified) return { state: 'done', meta: 'verified' };
|
|
135
|
+
return { state: 'active', meta: 'writing' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderApplyFrame({ stdout, theme, results, verified, failed }) {
|
|
139
|
+
const width = terminalWidth(stdout);
|
|
140
|
+
const { state, meta } = applyState(verified, failed);
|
|
141
|
+
const rows = [stepHead(theme, state, 'Apply', { meta, width }), gutter(theme)];
|
|
142
|
+
for (const result of results) {
|
|
143
|
+
rows.push(gutter(theme, fit(receiptLine(theme, result), width - 3)));
|
|
144
|
+
}
|
|
145
|
+
if (!verified && !failed) rows.push(gutter(theme, theme.dim('writing settings atomically')));
|
|
146
|
+
if (verified) rows.push(gutter(theme, `${theme.success('v')} persisted settings verified`));
|
|
147
|
+
rows.push(gutter(theme));
|
|
148
|
+
return rows;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
createPlanState,
|
|
153
|
+
planFrame,
|
|
154
|
+
reducePlanState,
|
|
155
|
+
renderApplyFrame,
|
|
156
|
+
selectPlan,
|
|
157
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const { getProviderMetadata } = require('../../lib/provider-names');
|
|
2
|
+
const { WIZARD_SPINNER, fit, gutter, stepHead, terminalWidth } = require('./setup-wizard-terminal');
|
|
3
|
+
|
|
4
|
+
function formatSeconds(ms) {
|
|
5
|
+
return `${(Math.max(0, ms) / 1000).toFixed(1)}s`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function probeLabel(spec) {
|
|
9
|
+
if (spec.kind === 'git') return 'Repository';
|
|
10
|
+
if (spec.kind === 'docker') return 'Docker';
|
|
11
|
+
if (spec.kind === 'issue') return 'Issue host';
|
|
12
|
+
return getProviderMetadata(spec.id).displayName;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function gitProbeDetail(result) {
|
|
16
|
+
if (!result.isRepo) return 'not a git repository';
|
|
17
|
+
return `${result.branch || 'detached'}${result.clean === true ? ' · clean' : ''}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function issueProbeDetail(result) {
|
|
21
|
+
if (!result.installed) return 'gh unavailable';
|
|
22
|
+
return result.authenticated ? 'GitHub authenticated' : 'GitHub login required';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function providerProbeDetail(result) {
|
|
26
|
+
if (!result.available) return result.commandAvailable ? 'probe failed' : 'not installed';
|
|
27
|
+
if (result.authStatus === 'login-required') return 'login required';
|
|
28
|
+
return result.path || 'available';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function probeDetail(event) {
|
|
32
|
+
const result = event.result;
|
|
33
|
+
if (event.kind === 'git') return gitProbeDetail(result);
|
|
34
|
+
if (event.kind === 'docker')
|
|
35
|
+
return result.available ? 'available' : result.error || 'unavailable';
|
|
36
|
+
if (event.kind === 'issue') return issueProbeDetail(result);
|
|
37
|
+
return providerProbeDetail(result);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class ScanPresenter {
|
|
41
|
+
constructor({ stdout, theme, live, motion, clock }) {
|
|
42
|
+
this.stdout = stdout;
|
|
43
|
+
this.theme = theme;
|
|
44
|
+
this.live = live;
|
|
45
|
+
this.motion = motion;
|
|
46
|
+
this.clock = clock;
|
|
47
|
+
this.specs = [];
|
|
48
|
+
this.completed = new Map();
|
|
49
|
+
this.frameIndex = 0;
|
|
50
|
+
this.elapsedMs = 0;
|
|
51
|
+
this.timer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
handle(event) {
|
|
55
|
+
this.elapsedMs = event.elapsedMs || 0;
|
|
56
|
+
if (event.type === 'start') {
|
|
57
|
+
this.specs = event.probes;
|
|
58
|
+
this.startTimer();
|
|
59
|
+
} else if (event.type === 'complete') {
|
|
60
|
+
this.completed.set(event.id, event);
|
|
61
|
+
}
|
|
62
|
+
this.paint();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
startTimer() {
|
|
66
|
+
if (!this.motion || this.timer) return;
|
|
67
|
+
this.timer = this.clock.setInterval(() => {
|
|
68
|
+
this.frameIndex = (this.frameIndex + 1) % WIZARD_SPINNER.length;
|
|
69
|
+
this.elapsedMs += 80;
|
|
70
|
+
this.paint();
|
|
71
|
+
}, 80);
|
|
72
|
+
this.timer.unref?.();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stopTimer() {
|
|
76
|
+
if (!this.timer) return;
|
|
77
|
+
this.clock.clearInterval(this.timer);
|
|
78
|
+
this.timer = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
paint() {
|
|
82
|
+
if (this.specs.length === 0) return;
|
|
83
|
+
const width = terminalWidth(this.stdout);
|
|
84
|
+
const done = this.completed.size;
|
|
85
|
+
const spinner = this.motion ? WIZARD_SPINNER[this.frameIndex] : '*';
|
|
86
|
+
const rows = [
|
|
87
|
+
stepHead(this.theme, 'active', 'Scan', {
|
|
88
|
+
meta: `${formatSeconds(this.elapsedMs)} · ${done}/${this.specs.length}`,
|
|
89
|
+
width,
|
|
90
|
+
}),
|
|
91
|
+
gutter(this.theme),
|
|
92
|
+
];
|
|
93
|
+
for (const spec of this.specs) {
|
|
94
|
+
const id = spec.id ? `provider:${spec.id}` : spec.kind;
|
|
95
|
+
const event = this.completed.get(id);
|
|
96
|
+
const glyph = event ? this.theme.success('v') : this.theme.accent(spinner);
|
|
97
|
+
const detail = event ? this.theme.dim(probeDetail(event)) : this.theme.dim('checking');
|
|
98
|
+
rows.push(gutter(this.theme, fit(`${glyph} ${probeLabel(spec)} · ${detail}`, width - 3)));
|
|
99
|
+
}
|
|
100
|
+
this.live.paint(rows);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
summaryRows({ probes, elapsedMs, width }) {
|
|
104
|
+
const providerResults = Object.entries(probes)
|
|
105
|
+
.filter(([id]) => id.startsWith('provider:'))
|
|
106
|
+
.map(([, result]) => result);
|
|
107
|
+
const ready = providerResults.filter((result) => result.available).length;
|
|
108
|
+
const unavailable = providerResults.length - ready;
|
|
109
|
+
const cleanliness = probes.git.clean ? 'clean repository' : 'local changes';
|
|
110
|
+
const gitSummary = probes.git.isRepo
|
|
111
|
+
? `git ${probes.git.branch || 'detached'} · ${cleanliness}`
|
|
112
|
+
: 'not a git repository';
|
|
113
|
+
const dockerSummary = probes.docker.available ? 'available' : 'unavailable';
|
|
114
|
+
const issueSummary = probes.issue.authenticated ? 'authenticated' : 'not authenticated';
|
|
115
|
+
return [
|
|
116
|
+
stepHead(this.theme, 'done', 'Scan', {
|
|
117
|
+
meta: `${formatSeconds(elapsedMs)} · ${ready} providers found`,
|
|
118
|
+
width,
|
|
119
|
+
}),
|
|
120
|
+
gutter(this.theme, fit(gitSummary, width - 3)),
|
|
121
|
+
gutter(this.theme, fit(`docker ${dockerSummary}`, width - 3)),
|
|
122
|
+
gutter(
|
|
123
|
+
this.theme,
|
|
124
|
+
fit(
|
|
125
|
+
`providers ${ready} found · ${unavailable} unavailable · GitHub ${issueSummary}`,
|
|
126
|
+
width - 3
|
|
127
|
+
)
|
|
128
|
+
),
|
|
129
|
+
gutter(this.theme),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
commit({ probes, elapsedMs }) {
|
|
134
|
+
this.stopTimer();
|
|
135
|
+
this.live.commit(this.summaryRows({ probes, elapsedMs, width: terminalWidth(this.stdout) }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
clear() {
|
|
139
|
+
this.stopTimer();
|
|
140
|
+
this.live.clear();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { ScanPresenter, formatSeconds, probeDetail };
|