@the-open-engine/zeroshot 6.3.0 → 6.5.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/cli/commands/providers.js +11 -5
- package/cli/index.js +83 -17
- package/cli/lib/first-run.js +14 -3
- package/docker/zeroshot-cluster/Dockerfile +11 -1
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts +4 -0
- package/lib/agent-cli-provider/acp-stdio-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js +259 -0
- package/lib/agent-cli-provider/acp-stdio-runner.js.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts +23 -0
- package/lib/agent-cli-provider/adapters/acp.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/acp.js +373 -0
- package/lib/agent-cli-provider/adapters/acp.js.map +1 -0
- package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/claude.js +2 -29
- package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js +154 -0
- package/lib/agent-cli-provider/adapters/copilot-parser.js.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/copilot.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/copilot.js +176 -0
- package/lib/agent-cli-provider/adapters/copilot.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts +6 -0
- package/lib/agent-cli-provider/adapters/gateway.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/gateway.js +161 -0
- package/lib/agent-cli-provider/adapters/gateway.js.map +1 -0
- package/lib/agent-cli-provider/adapters/gemini.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/gemini.js +9 -2
- package/lib/agent-cli-provider/adapters/gemini.js.map +1 -1
- package/lib/agent-cli-provider/adapters/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/index.js +6 -34
- package/lib/agent-cli-provider/adapters/index.js.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
- package/lib/agent-cli-provider/adapters/opencode.js +5 -1
- package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
- package/lib/agent-cli-provider/adapters/pi.d.ts +3 -0
- package/lib/agent-cli-provider/adapters/pi.d.ts.map +1 -0
- package/lib/agent-cli-provider/adapters/pi.js +331 -0
- package/lib/agent-cli-provider/adapters/pi.js.map +1 -0
- package/lib/agent-cli-provider/claude-command.d.ts +6 -0
- package/lib/agent-cli-provider/claude-command.d.ts.map +1 -0
- package/lib/agent-cli-provider/claude-command.js +43 -0
- package/lib/agent-cli-provider/claude-command.js.map +1 -0
- package/lib/agent-cli-provider/contract-actions.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-actions.js +13 -17
- package/lib/agent-cli-provider/contract-actions.js.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-invoke.js +10 -3
- package/lib/agent-cli-provider/contract-invoke.js.map +1 -1
- package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-options.js +57 -2
- package/lib/agent-cli-provider/contract-options.js.map +1 -1
- package/lib/agent-cli-provider/contract-support.d.ts +1 -0
- package/lib/agent-cli-provider/contract-support.d.ts.map +1 -1
- package/lib/agent-cli-provider/contract-support.js +25 -3
- package/lib/agent-cli-provider/contract-support.js.map +1 -1
- package/lib/agent-cli-provider/errors.d.ts +1 -0
- package/lib/agent-cli-provider/errors.d.ts.map +1 -1
- package/lib/agent-cli-provider/errors.js +14 -5
- package/lib/agent-cli-provider/errors.js.map +1 -1
- package/lib/agent-cli-provider/gateway-client.d.ts +32 -0
- package/lib/agent-cli-provider/gateway-client.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-client.js +135 -0
- package/lib/agent-cli-provider/gateway-client.js.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts +9 -0
- package/lib/agent-cli-provider/gateway-runner.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-runner.js +294 -0
- package/lib/agent-cli-provider/gateway-runner.js.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts +14 -0
- package/lib/agent-cli-provider/gateway-tools.d.ts.map +1 -0
- package/lib/agent-cli-provider/gateway-tools.js +495 -0
- package/lib/agent-cli-provider/gateway-tools.js.map +1 -0
- package/lib/agent-cli-provider/index.d.ts +3 -2
- package/lib/agent-cli-provider/index.d.ts.map +1 -1
- package/lib/agent-cli-provider/index.js +14 -1
- package/lib/agent-cli-provider/index.js.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.d.ts +2 -2
- package/lib/agent-cli-provider/invoke-evidence.d.ts.map +1 -1
- package/lib/agent-cli-provider/invoke-evidence.js +17 -3
- package/lib/agent-cli-provider/invoke-evidence.js.map +1 -1
- package/lib/agent-cli-provider/provider-registry.d.ts +437 -0
- package/lib/agent-cli-provider/provider-registry.d.ts.map +1 -0
- package/lib/agent-cli-provider/provider-registry.js +411 -0
- package/lib/agent-cli-provider/provider-registry.js.map +1 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts +7 -0
- package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
- package/lib/agent-cli-provider/single-agent-runtime.js +139 -19
- package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
- package/lib/agent-cli-provider/types.d.ts +106 -4
- package/lib/agent-cli-provider/types.d.ts.map +1 -1
- package/lib/agent-cli-provider/types.js.map +1 -1
- package/lib/detached-startup.js +4 -2
- package/lib/docker-config.js +24 -13
- package/lib/provider-defaults.js +15 -4
- package/lib/provider-detection.js +2 -0
- package/lib/provider-names.js +32 -12
- package/lib/repo-settings.js +15 -1
- package/lib/run-mode.js +18 -6
- package/lib/run-plan.js +32 -0
- package/lib/settings/claude-auth.js +3 -6
- package/lib/settings.js +17 -2
- package/lib/setup-apply.js +300 -0
- package/lib/setup-journal.js +109 -0
- package/lib/setup-plan.js +406 -0
- package/lib/setup-undo.js +88 -0
- package/lib/start-cluster.js +34 -9
- package/lib/stream-json-parser.js +7 -5
- package/package.json +33 -3
- package/scripts/live-provider-smoke.js +221 -0
- package/src/agent/agent-lifecycle.js +7 -4
- package/src/agent/agent-quality-gate-schema.js +2 -2
- package/src/agent/agent-task-executor.js +41 -2
- package/src/agent-cli-provider/acp-stdio-runner.ts +314 -0
- package/src/agent-cli-provider/adapters/acp.ts +493 -0
- package/src/agent-cli-provider/adapters/claude.ts +2 -32
- package/src/agent-cli-provider/adapters/copilot-parser.ts +166 -0
- package/src/agent-cli-provider/adapters/copilot.ts +231 -0
- package/src/agent-cli-provider/adapters/gateway.ts +186 -0
- package/src/agent-cli-provider/adapters/gemini.ts +9 -2
- package/src/agent-cli-provider/adapters/index.ts +5 -36
- package/src/agent-cli-provider/adapters/opencode.ts +4 -1
- package/src/agent-cli-provider/adapters/pi.ts +410 -0
- package/src/agent-cli-provider/claude-command.ts +47 -0
- package/src/agent-cli-provider/contract-actions.ts +14 -17
- package/src/agent-cli-provider/contract-invoke.ts +12 -3
- package/src/agent-cli-provider/contract-options.ts +72 -3
- package/src/agent-cli-provider/contract-support.ts +30 -3
- package/src/agent-cli-provider/errors.ts +14 -4
- package/src/agent-cli-provider/gateway-client.ts +170 -0
- package/src/agent-cli-provider/gateway-runner.ts +353 -0
- package/src/agent-cli-provider/gateway-tools.ts +616 -0
- package/src/agent-cli-provider/index.ts +28 -0
- package/src/agent-cli-provider/invoke-evidence.ts +24 -3
- package/src/agent-cli-provider/provider-registry.ts +515 -0
- package/src/agent-cli-provider/single-agent-runtime.ts +192 -18
- package/src/agent-cli-provider/types.ts +145 -4
- package/src/config-validator.js +33 -15
- package/src/isolation-manager.js +124 -19
- package/src/ledger.js +3 -0
- package/src/message-bus.js +12 -2
- package/src/orchestrator.js +37 -17
- package/src/preflight.js +83 -30
- package/src/providers/capabilities.js +15 -42
- package/src/providers/index.js +52 -72
- package/src/worktree-claude-config.js +17 -0
- package/task-lib/commands/run.js +1 -0
- package/task-lib/provider-helper-runtime.js +11 -0
- package/task-lib/runner.js +7 -0
package/lib/settings.js
CHANGED
|
@@ -33,6 +33,14 @@ function getSettingsFile() {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Whether the global settings file exists on disk (vs. running on defaults).
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function settingsFileExists() {
|
|
41
|
+
return fs.existsSync(getSettingsFile());
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
// Import provider defaults from separate module to avoid circular dependency
|
|
37
45
|
const { getProviderDefaults, clearProviderDefaultsCache } = require('./provider-defaults');
|
|
38
46
|
|
|
@@ -103,6 +111,7 @@ const DEFAULT_SETTINGS_BASE = {
|
|
|
103
111
|
},
|
|
104
112
|
defaultConfig: 'conductor-bootstrap',
|
|
105
113
|
defaultDocker: false,
|
|
114
|
+
defaultDelivery: 'none', // 'none' | 'pr' | 'ship' - folded into resolveEffectiveRunPlan same as defaultDocker
|
|
106
115
|
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
107
116
|
logLevel: 'normal',
|
|
108
117
|
// Auto-update settings
|
|
@@ -113,7 +122,7 @@ const DEFAULT_SETTINGS_BASE = {
|
|
|
113
122
|
// Example: 'ccr code' for claude-code-router integration
|
|
114
123
|
claudeCommand: 'claude',
|
|
115
124
|
// Docker isolation mounts - preset names or {host, container, readonly?} objects
|
|
116
|
-
// Valid presets:
|
|
125
|
+
// Valid presets: infrastructure presets plus provider ids from the provider registry
|
|
117
126
|
dockerMounts: ['gh', 'git', 'ssh'],
|
|
118
127
|
// Extra env vars to pass to Docker container (in addition to preset-implied ones)
|
|
119
128
|
// Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
|
|
@@ -323,7 +332,7 @@ function validateProviderSettings(value) {
|
|
|
323
332
|
|
|
324
333
|
for (const [providerName, settings] of Object.entries(normalizedSettings || {})) {
|
|
325
334
|
if (!VALID_PROVIDERS.includes(providerName)) {
|
|
326
|
-
return `Unknown provider in providerSettings: ${providerName}`;
|
|
335
|
+
return `Unknown provider in providerSettings: ${providerName}. Valid providers: ${VALID_PROVIDERS.join(', ')}`;
|
|
327
336
|
}
|
|
328
337
|
|
|
329
338
|
// Delegate validation to the provider
|
|
@@ -391,6 +400,10 @@ function validateSetting(key, value) {
|
|
|
391
400
|
return `Invalid log level: ${value}. Valid levels: quiet, normal, verbose`;
|
|
392
401
|
}
|
|
393
402
|
|
|
403
|
+
if (key === 'defaultDelivery' && !['none', 'pr', 'ship'].includes(value)) {
|
|
404
|
+
return `Invalid defaultDelivery: ${value}. Valid: none, pr, ship`;
|
|
405
|
+
}
|
|
406
|
+
|
|
394
407
|
if (key === 'claudeCommand') {
|
|
395
408
|
return validateClaudeCommand(value);
|
|
396
409
|
}
|
|
@@ -528,6 +541,7 @@ module.exports = {
|
|
|
528
541
|
coerceValue,
|
|
529
542
|
DEFAULT_SETTINGS,
|
|
530
543
|
getSettingsFile,
|
|
544
|
+
settingsFileExists,
|
|
531
545
|
getClaudeCommand,
|
|
532
546
|
// Model validation exports
|
|
533
547
|
MODEL_HIERARCHY,
|
|
@@ -535,6 +549,7 @@ module.exports = {
|
|
|
535
549
|
validateModelAgainstMax,
|
|
536
550
|
// Provider defaults exports
|
|
537
551
|
clearProviderDefaultsCache,
|
|
552
|
+
mapLegacyModelToLevel,
|
|
538
553
|
// Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
|
|
539
554
|
get SETTINGS_FILE() {
|
|
540
555
|
return getSettingsFile();
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zeroshot setup apply` — writes decisions from #A's plan/decision contract
|
|
3
|
+
* (lib/setup-plan.js) to global settings, repo-local settings, and the undo
|
|
4
|
+
* journal (lib/setup-journal.js).
|
|
5
|
+
*
|
|
6
|
+
* Fail-closed: every input decisionId + value is resolved and validated
|
|
7
|
+
* against its domain before ANY write happens. Writes are then applied only
|
|
8
|
+
* to settings keys a run-mode resolver actually reads (setup-plan.js's
|
|
9
|
+
* CONSUMED_PATHS/isConsumedPath — the same set that filters proposedWrites)
|
|
10
|
+
* — a settings key nobody reads is dead config, the exact drift the
|
|
11
|
+
* canonical-path rule (issue #605) forbids.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { resolveDecisionPath, domainFor, isConsumedPath, CONSUMED_PATHS } = require('./setup-plan');
|
|
15
|
+
const { validateMountConfig, validateEnvPassthrough } = require('./docker-config');
|
|
16
|
+
const { VALID_PROVIDERS } = require('./provider-names');
|
|
17
|
+
const { VALID_MODELS, mapLegacyModelToLevel } = require('./settings');
|
|
18
|
+
const {
|
|
19
|
+
loadJournal,
|
|
20
|
+
saveJournal,
|
|
21
|
+
upsertJournalEntry,
|
|
22
|
+
getNestedValue,
|
|
23
|
+
setNestedValue,
|
|
24
|
+
deepEqual,
|
|
25
|
+
} = require('./setup-journal');
|
|
26
|
+
|
|
27
|
+
const SECRET_PATTERN = /token|secret|password|api[_-]?key|credential/i;
|
|
28
|
+
|
|
29
|
+
function assertSecretSafePath(targetPath) {
|
|
30
|
+
if (SECRET_PATTERN.test(targetPath)) {
|
|
31
|
+
throw new Error(`Refusing to write secret-shaped settings path: ${targetPath}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function domainError(decisionId, value) {
|
|
36
|
+
return new Error(
|
|
37
|
+
`Invalid value for decision "${decisionId}": expected ${domainFor(decisionId)}, got ${JSON.stringify(value)}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolves a submitted decision value into the raw form actually stored at
|
|
42
|
+
// its target settings path, validating it against #A's domain first.
|
|
43
|
+
// (defaultIsolation's domain is worktree|docker|none, but its target path is
|
|
44
|
+
// the boolean settings.defaultDocker — this is where that translation lives.)
|
|
45
|
+
function convertDecisionValue({ decisionId, value, globalSettings, deps }) {
|
|
46
|
+
switch (decisionId) {
|
|
47
|
+
case 'defaultProvider':
|
|
48
|
+
if (typeof value !== 'string' || !deps.VALID_PROVIDERS.includes(value)) {
|
|
49
|
+
throw domainError(decisionId, value);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
|
|
53
|
+
case 'defaultIsolation':
|
|
54
|
+
if (!['worktree', 'docker', 'none'].includes(value)) throw domainError(decisionId, value);
|
|
55
|
+
return value === 'docker';
|
|
56
|
+
|
|
57
|
+
case 'allowLocalNoIsolation':
|
|
58
|
+
if (typeof value !== 'boolean') throw domainError(decisionId, value);
|
|
59
|
+
return value;
|
|
60
|
+
|
|
61
|
+
case 'defaultDelivery':
|
|
62
|
+
if (!['none', 'pr', 'ship'].includes(value)) throw domainError(decisionId, value);
|
|
63
|
+
return value;
|
|
64
|
+
|
|
65
|
+
case 'defaultIssueSource':
|
|
66
|
+
if (typeof value !== 'string' || !deps.listIssueProviders().includes(value)) {
|
|
67
|
+
throw domainError(decisionId, value);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
|
|
71
|
+
case 'prBase':
|
|
72
|
+
if (typeof value !== 'string' || value.trim() === '') throw domainError(decisionId, value);
|
|
73
|
+
return value;
|
|
74
|
+
|
|
75
|
+
case 'dockerMounts': {
|
|
76
|
+
const err = validateMountConfig(value);
|
|
77
|
+
if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'dockerEnvPassthrough': {
|
|
82
|
+
const err = validateEnvPassthrough(value);
|
|
83
|
+
if (err) throw new Error(`Invalid value for decision "${decisionId}": ${err}`);
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case 'updatePolicy':
|
|
88
|
+
if (!['off', 'notify', 'auto'].includes(value)) throw domainError(decisionId, value);
|
|
89
|
+
return value;
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
if (decisionId.startsWith('providerLevel.')) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
94
|
+
throw domainError(decisionId, value);
|
|
95
|
+
}
|
|
96
|
+
for (const key of ['min', 'default', 'max']) {
|
|
97
|
+
if (!VALID_MODELS.includes(value[key])) throw domainError(decisionId, value);
|
|
98
|
+
}
|
|
99
|
+
const providerName = decisionId.slice('providerLevel.'.length);
|
|
100
|
+
const existing = getNestedValue(globalSettings, `providerSettings.${providerName}`) || {};
|
|
101
|
+
return {
|
|
102
|
+
...existing,
|
|
103
|
+
minLevel: mapLegacyModelToLevel(value.min),
|
|
104
|
+
defaultLevel: mapLegacyModelToLevel(value.default),
|
|
105
|
+
maxLevel: mapLegacyModelToLevel(value.max),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
throw new Error(`Unknown decision ID: ${decisionId}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function defaultApplyDeps() {
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
const { loadSettings, saveSettings } = require('./settings');
|
|
115
|
+
const { readRepoSettings, writeRepoSettings } = require('./repo-settings');
|
|
116
|
+
const { checkGhAuth } = require('../src/preflight');
|
|
117
|
+
const { listProviders: listIssueProviders } = require('../src/issue-providers');
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
readFile: (filePath) => fs.readFileSync(filePath, 'utf8'),
|
|
121
|
+
loadSettings,
|
|
122
|
+
saveSettings,
|
|
123
|
+
readRepoSettings,
|
|
124
|
+
writeRepoSettings,
|
|
125
|
+
checkGhAuth,
|
|
126
|
+
listIssueProviders,
|
|
127
|
+
VALID_PROVIDERS,
|
|
128
|
+
now: () => new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Phase 1: resolve + validate EVERY input decision before any write happens
|
|
133
|
+
// (fail-closed — a single bad decisionId or out-of-domain value rejects the
|
|
134
|
+
// whole request, never a partial apply).
|
|
135
|
+
function resolveAndValidateDecisions(input, globalSettings, deps) {
|
|
136
|
+
const resolved = [];
|
|
137
|
+
for (const [decisionId, value] of Object.entries(input)) {
|
|
138
|
+
const target = resolveDecisionPath(decisionId);
|
|
139
|
+
if (!target) {
|
|
140
|
+
throw new Error(`Unknown decision ID: ${decisionId}`);
|
|
141
|
+
}
|
|
142
|
+
assertSecretSafePath(target.path);
|
|
143
|
+
const writeValue = convertDecisionValue({ decisionId, value, globalSettings, deps });
|
|
144
|
+
resolved.push({ decisionId, target, inputValue: value, writeValue });
|
|
145
|
+
}
|
|
146
|
+
return resolved;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Decides the outcome for a single resolved decision: a skip (with reason)
|
|
150
|
+
// or the write to perform. Returns the write descriptor rather than mutating
|
|
151
|
+
// anything, so the caller controls when settings objects/journal are touched.
|
|
152
|
+
function resolveDecisionOutcome(
|
|
153
|
+
{ decisionId, target, inputValue, writeValue },
|
|
154
|
+
{ globalSettings, repoSettings, allowRiskyDefaults }
|
|
155
|
+
) {
|
|
156
|
+
const settingsObj = target.scope === 'repo' ? repoSettings : globalSettings;
|
|
157
|
+
const currentValue = getNestedValue(settingsObj, target.path) ?? null;
|
|
158
|
+
const base = { decisionId, from: currentValue, to: writeValue };
|
|
159
|
+
|
|
160
|
+
if (decisionId === 'defaultDelivery' && inputValue === 'ship' && !allowRiskyDefaults) {
|
|
161
|
+
return { result: { ...base, applied: false, skippedReason: 'requires-explicit-opt-in' } };
|
|
162
|
+
}
|
|
163
|
+
if (!isConsumedPath(target.scope, target.path)) {
|
|
164
|
+
return { result: { ...base, applied: false, skippedReason: 'no-consumer' } };
|
|
165
|
+
}
|
|
166
|
+
if (deepEqual(currentValue, writeValue)) {
|
|
167
|
+
return { result: { ...base, applied: false, skippedReason: 'unchanged' } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
result: { ...base, applied: true },
|
|
172
|
+
write: {
|
|
173
|
+
settingsObj,
|
|
174
|
+
scope: target.scope,
|
|
175
|
+
path: target.path,
|
|
176
|
+
writeValue,
|
|
177
|
+
priorValue: currentValue,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Applies one decision's write to its in-memory settings object and journals
|
|
183
|
+
// it (deferred persistence — the caller flushes to disk once at the end).
|
|
184
|
+
function applyWrite(write, { repoRoot, journal, deps }) {
|
|
185
|
+
setNestedValue(write.settingsObj, write.path, write.writeValue);
|
|
186
|
+
upsertJournalEntry(journal, {
|
|
187
|
+
scope: write.scope,
|
|
188
|
+
path: write.path,
|
|
189
|
+
repoRoot: write.scope === 'repo' ? repoRoot : null,
|
|
190
|
+
priorValue: write.priorValue,
|
|
191
|
+
appliedValue: write.writeValue,
|
|
192
|
+
appliedAt: deps.now(),
|
|
193
|
+
});
|
|
194
|
+
return write.scope;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function persistIfDirty({
|
|
198
|
+
globalDirty,
|
|
199
|
+
repoDirty,
|
|
200
|
+
globalSettings,
|
|
201
|
+
repoSettings,
|
|
202
|
+
repoRoot,
|
|
203
|
+
journal,
|
|
204
|
+
deps,
|
|
205
|
+
}) {
|
|
206
|
+
if (globalDirty) deps.saveSettings(globalSettings);
|
|
207
|
+
if (repoDirty) deps.writeRepoSettings(repoRoot, repoSettings);
|
|
208
|
+
if (globalDirty || repoDirty) saveJournal(journal);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Phase 2: write every resolved decision, journaling each mutation and
|
|
212
|
+
// skipping (never throwing) decisions that are unchanged, risky-without-optin,
|
|
213
|
+
// or targeted at a settings key no resolver consumes.
|
|
214
|
+
function writeResolvedDecisions(
|
|
215
|
+
resolved,
|
|
216
|
+
{ globalSettings, repoSettings, repoRoot, journal, allowRiskyDefaults, deps }
|
|
217
|
+
) {
|
|
218
|
+
const results = [];
|
|
219
|
+
let globalDirty = false;
|
|
220
|
+
let repoDirty = false;
|
|
221
|
+
|
|
222
|
+
for (const decision of resolved) {
|
|
223
|
+
const { result, write } = resolveDecisionOutcome(decision, {
|
|
224
|
+
globalSettings,
|
|
225
|
+
repoSettings,
|
|
226
|
+
allowRiskyDefaults,
|
|
227
|
+
});
|
|
228
|
+
results.push(result);
|
|
229
|
+
if (!write) continue;
|
|
230
|
+
|
|
231
|
+
const scope = applyWrite(write, { repoRoot, journal, deps });
|
|
232
|
+
if (scope === 'repo') repoDirty = true;
|
|
233
|
+
else globalDirty = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
persistIfDirty({ globalDirty, repoDirty, globalSettings, repoSettings, repoRoot, journal, deps });
|
|
237
|
+
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Apply a decisions file `{ "<decisionId>": <value>, ... }` to settings.
|
|
243
|
+
*
|
|
244
|
+
* @param {Object} params
|
|
245
|
+
* @param {string} params.decisionsPath - Path to the decisions JSON file.
|
|
246
|
+
* @param {string} params.cwd - Working directory (for repo-scope settings lookup).
|
|
247
|
+
* @param {boolean} [params.allowRiskyDefaults] - Required to store defaultDelivery='ship'.
|
|
248
|
+
* @param {Object} [params.deps] - Injected dependencies (for testing).
|
|
249
|
+
* @returns {Array<{decisionId: string, applied: boolean, from: *, to: *, skippedReason?: string}>}
|
|
250
|
+
*/
|
|
251
|
+
function applyDecisions({ decisionsPath, cwd, allowRiskyDefaults = false, deps = {} }) {
|
|
252
|
+
const resolvedDeps = { ...defaultApplyDeps(), ...deps };
|
|
253
|
+
|
|
254
|
+
let input;
|
|
255
|
+
try {
|
|
256
|
+
input = JSON.parse(resolvedDeps.readFile(decisionsPath));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
throw new Error(`Failed to read decisions file "${decisionsPath}": ${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
261
|
+
throw new Error('Decisions file must be a JSON object of { decisionId: value }');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const globalSettings = resolvedDeps.loadSettings();
|
|
265
|
+
const { repoRoot, settings: repoSettingsRaw } = resolvedDeps.readRepoSettings(cwd);
|
|
266
|
+
const repoSettings = repoSettingsRaw || {};
|
|
267
|
+
|
|
268
|
+
const resolved = resolveAndValidateDecisions(input, globalSettings, resolvedDeps);
|
|
269
|
+
|
|
270
|
+
const results = writeResolvedDecisions(resolved, {
|
|
271
|
+
globalSettings,
|
|
272
|
+
repoSettings,
|
|
273
|
+
repoRoot,
|
|
274
|
+
journal: loadJournal(),
|
|
275
|
+
allowRiskyDefaults,
|
|
276
|
+
deps: resolvedDeps,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Never store provider-auth secrets: print the login command instead.
|
|
280
|
+
const issueSourceApplied = results.find(
|
|
281
|
+
(r) => r.decisionId === 'defaultIssueSource' && r.applied
|
|
282
|
+
);
|
|
283
|
+
if (issueSourceApplied && issueSourceApplied.to === 'github') {
|
|
284
|
+
const auth = resolvedDeps.checkGhAuth();
|
|
285
|
+
if (!auth || !auth.authenticated) {
|
|
286
|
+
console.log('Run: gh auth login');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return results;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
applyDecisions,
|
|
295
|
+
resolveAndValidateDecisions,
|
|
296
|
+
writeResolvedDecisions,
|
|
297
|
+
assertSecretSafePath,
|
|
298
|
+
isConsumedPath,
|
|
299
|
+
CONSUMED_PATHS,
|
|
300
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo journal for `zeroshot setup apply` / `zeroshot setup undo`.
|
|
3
|
+
*
|
|
4
|
+
* One journal entry per setting this setup owns: { scope, path, repoRoot,
|
|
5
|
+
* priorValue, appliedValue, appliedAt }. `priorValue: null` means the key
|
|
6
|
+
* did not exist before apply, so undo deletes it rather than restoring it.
|
|
7
|
+
* Shared by lib/setup-apply.js (writer) and lib/setup-undo.js (reader) so the
|
|
8
|
+
* nested-path mutation and equality semantics can't drift between the two.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { getSettingsFile } = require('./settings');
|
|
14
|
+
const { getNestedValue } = require('./setup-plan');
|
|
15
|
+
|
|
16
|
+
function getJournalPath() {
|
|
17
|
+
return path.join(path.dirname(getSettingsFile()), 'setup-undo-journal.json');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadJournal() {
|
|
21
|
+
const journalPath = getJournalPath();
|
|
22
|
+
if (!fs.existsSync(journalPath)) {
|
|
23
|
+
return { version: 1, entries: [] };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
|
|
27
|
+
if (!parsed || !Array.isArray(parsed.entries)) {
|
|
28
|
+
return { version: 1, entries: [] };
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return { version: 1, entries: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function saveJournal(journal) {
|
|
37
|
+
const journalPath = getJournalPath();
|
|
38
|
+
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
|
|
39
|
+
fs.writeFileSync(journalPath, JSON.stringify(journal, null, 2), 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function entryKey(entry) {
|
|
43
|
+
return `${entry.scope}:${entry.repoRoot || ''}:${entry.path}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Re-applying an already-journaled write updates appliedValue/appliedAt but
|
|
47
|
+
// keeps the original priorValue — that's the true pre-apply state undo must
|
|
48
|
+
// restore to, and it must not drift across repeated `apply` runs.
|
|
49
|
+
function upsertJournalEntry(journal, entry) {
|
|
50
|
+
const key = entryKey(entry);
|
|
51
|
+
const existingIndex = journal.entries.findIndex((e) => entryKey(e) === key);
|
|
52
|
+
if (existingIndex === -1) {
|
|
53
|
+
journal.entries.push(entry);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
journal.entries[existingIndex] = {
|
|
57
|
+
...entry,
|
|
58
|
+
priorValue: journal.entries[existingIndex].priorValue,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setNestedValue(target, pathStr, value) {
|
|
63
|
+
const keys = pathStr.split('.');
|
|
64
|
+
let node = target;
|
|
65
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
66
|
+
const key = keys[i];
|
|
67
|
+
if (typeof node[key] !== 'object' || node[key] === null) {
|
|
68
|
+
node[key] = {};
|
|
69
|
+
}
|
|
70
|
+
node = node[key];
|
|
71
|
+
}
|
|
72
|
+
node[keys[keys.length - 1]] = value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function deleteNestedKey(target, pathStr) {
|
|
76
|
+
const keys = pathStr.split('.');
|
|
77
|
+
let node = target;
|
|
78
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
79
|
+
const key = keys[i];
|
|
80
|
+
if (typeof node[key] !== 'object' || node[key] === null) return;
|
|
81
|
+
node = node[key];
|
|
82
|
+
}
|
|
83
|
+
delete node[keys[keys.length - 1]];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function deepEqual(a, b) {
|
|
87
|
+
if (a === b) return true;
|
|
88
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
89
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
90
|
+
if (Array.isArray(a)) {
|
|
91
|
+
if (a.length !== b.length) return false;
|
|
92
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
93
|
+
}
|
|
94
|
+
const aKeys = Object.keys(a);
|
|
95
|
+
const bKeys = Object.keys(b);
|
|
96
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
97
|
+
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
getJournalPath,
|
|
102
|
+
loadJournal,
|
|
103
|
+
saveJournal,
|
|
104
|
+
upsertJournalEntry,
|
|
105
|
+
getNestedValue,
|
|
106
|
+
setNestedValue,
|
|
107
|
+
deleteNestedKey,
|
|
108
|
+
deepEqual,
|
|
109
|
+
};
|