@solidnumber/cli 2.17.0 → 2.18.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/dist/commands/apply.d.ts +10 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +436 -51
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/how-to.js +1 -1
- package/dist/commands/how-to.js.map +1 -1
- package/dist/lib/apply/engine.d.ts +13 -0
- package/dist/lib/apply/engine.d.ts.map +1 -1
- package/dist/lib/apply/engine.js +122 -13
- package/dist/lib/apply/engine.js.map +1 -1
- package/dist/lib/apply/lock.d.ts +107 -0
- package/dist/lib/apply/lock.d.ts.map +1 -0
- package/dist/lib/apply/lock.js +285 -0
- package/dist/lib/apply/lock.js.map +1 -0
- package/dist/lib/apply/registry.d.ts +62 -5
- package/dist/lib/apply/registry.d.ts.map +1 -1
- package/dist/lib/apply/registry.js +151 -1
- package/dist/lib/apply/registry.js.map +1 -1
- package/package.json +2 -2
- package/platform-docs/ai.txt +2 -2
- package/platform-docs/llms.txt +9 -9
package/dist/commands/apply.d.ts
CHANGED
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
*
|
|
11
11
|
* solid apply infra.yaml
|
|
12
12
|
* solid apply infra.yaml --dry-run # plan only, no writes
|
|
13
|
+
* solid apply infra.yaml --strict # refuse if production moved since the last apply
|
|
13
14
|
* solid apply infra.yaml --prune --yes # also delete unmanaged resources
|
|
14
15
|
* solid apply --kinds # list supported kinds
|
|
16
|
+
*
|
|
17
|
+
* solid apply drift infra.yaml # three-way: manifest vs lock vs production
|
|
18
|
+
* solid apply rollback infra.yaml # revert the last run from its recorded pre-images
|
|
19
|
+
* solid apply history infra.yaml # the runs the lock remembers
|
|
20
|
+
* solid apply export business.yaml # write the live tenant as a manifest
|
|
21
|
+
*
|
|
22
|
+
* Every non-dry-run apply writes `<manifest>.lock.json` beside the manifest:
|
|
23
|
+
* server ids, the managed fields as applied, and a pre-image for every write.
|
|
24
|
+
* Commit it with the manifest — it is what drift and rollback read.
|
|
15
25
|
*/
|
|
16
26
|
import { Command } from 'commander';
|
|
17
27
|
export declare const applyCommand: Command;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/commands/apply.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/commands/apply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0IpC,eAAO,MAAM,YAAY,SAiHrB,CAAC"}
|
package/dist/commands/apply.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -16,108 +49,230 @@ exports.applyCommand = void 0;
|
|
|
16
49
|
*
|
|
17
50
|
* solid apply infra.yaml
|
|
18
51
|
* solid apply infra.yaml --dry-run # plan only, no writes
|
|
52
|
+
* solid apply infra.yaml --strict # refuse if production moved since the last apply
|
|
19
53
|
* solid apply infra.yaml --prune --yes # also delete unmanaged resources
|
|
20
54
|
* solid apply --kinds # list supported kinds
|
|
55
|
+
*
|
|
56
|
+
* solid apply drift infra.yaml # three-way: manifest vs lock vs production
|
|
57
|
+
* solid apply rollback infra.yaml # revert the last run from its recorded pre-images
|
|
58
|
+
* solid apply history infra.yaml # the runs the lock remembers
|
|
59
|
+
* solid apply export business.yaml # write the live tenant as a manifest
|
|
60
|
+
*
|
|
61
|
+
* Every non-dry-run apply writes `<manifest>.lock.json` beside the manifest:
|
|
62
|
+
* server ids, the managed fields as applied, and a pre-image for every write.
|
|
63
|
+
* Commit it with the manifest — it is what drift and rollback read.
|
|
21
64
|
*/
|
|
22
65
|
const commander_1 = require("commander");
|
|
23
66
|
const chalk_1 = __importDefault(require("chalk"));
|
|
24
|
-
const
|
|
67
|
+
const fs = __importStar(require("node:fs"));
|
|
68
|
+
const path = __importStar(require("node:path"));
|
|
69
|
+
const yaml = __importStar(require("js-yaml"));
|
|
25
70
|
const api_client_1 = require("../lib/api-client");
|
|
71
|
+
const config_1 = require("../lib/config");
|
|
26
72
|
const json_output_1 = require("../lib/json-output");
|
|
27
73
|
const command_kit_1 = require("../lib/command-kit");
|
|
28
74
|
const dry_run_1 = require("../lib/dry-run");
|
|
75
|
+
const tenant_guard_1 = require("../lib/tenant-guard");
|
|
29
76
|
const engine_1 = require("../lib/apply/engine");
|
|
30
77
|
const registry_1 = require("../lib/apply/registry");
|
|
78
|
+
const lock_1 = require("../lib/apply/lock");
|
|
31
79
|
const ICON = {
|
|
32
80
|
create: chalk_1.default.green('+ create'),
|
|
33
81
|
update: chalk_1.default.yellow('~ update'),
|
|
34
82
|
noop: chalk_1.default.dim('= unchanged'),
|
|
35
83
|
prune: chalk_1.default.red('- prune'),
|
|
36
|
-
unsupported: chalk_1.default.magenta('!
|
|
84
|
+
unsupported: chalk_1.default.magenta('! unsupported'),
|
|
37
85
|
};
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return void (0, json_output_1.emitJson)({ kinds });
|
|
51
|
-
console.log(chalk_1.default.bold('Supported kinds:'));
|
|
52
|
-
for (const k of kinds)
|
|
53
|
-
console.log(` ${k}`);
|
|
54
|
-
return;
|
|
86
|
+
const DRIFT_ICON = {
|
|
87
|
+
in_sync: chalk_1.default.dim('= in sync'),
|
|
88
|
+
manifest_changed: chalk_1.default.yellow('~ manifest changed'),
|
|
89
|
+
live_changed: chalk_1.default.red('! production changed'),
|
|
90
|
+
conflict: chalk_1.default.red('!! conflict'),
|
|
91
|
+
missing: chalk_1.default.magenta('? missing on server'),
|
|
92
|
+
never_applied: chalk_1.default.dim('· never applied'),
|
|
93
|
+
removed_from_manifest: chalk_1.default.yellow('- removed from manifest'),
|
|
94
|
+
};
|
|
95
|
+
function cliVersion() {
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')).version;
|
|
55
98
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.error(chalk_1.default.red('Missing manifest file. Usage: solid apply <file> (or "-" for stdin).'));
|
|
59
|
-
process.exit(2);
|
|
99
|
+
catch {
|
|
100
|
+
return 'unknown';
|
|
60
101
|
}
|
|
102
|
+
}
|
|
103
|
+
function readManifest(file) {
|
|
61
104
|
let text;
|
|
62
105
|
try {
|
|
63
|
-
text = file === '-' ?
|
|
106
|
+
text = file === '-' ? fs.readFileSync(0, 'utf8') : fs.readFileSync(file, 'utf8');
|
|
64
107
|
}
|
|
65
108
|
catch (e) {
|
|
66
109
|
console.error(chalk_1.default.red(`Cannot read manifest: ${e instanceof Error ? e.message : String(e)}`));
|
|
67
110
|
process.exit(2);
|
|
68
111
|
}
|
|
69
|
-
let resources;
|
|
70
112
|
try {
|
|
71
|
-
|
|
113
|
+
return (0, engine_1.parseManifest)(text);
|
|
72
114
|
}
|
|
73
115
|
catch (e) {
|
|
74
116
|
console.error(chalk_1.default.red(`Invalid manifest: ${e instanceof Error ? e.message : String(e)}`));
|
|
75
117
|
process.exit(2);
|
|
76
118
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
process.exit(0);
|
|
80
|
-
}
|
|
81
|
-
const dryRun = !!options.dryRun || (0, dry_run_1.isDryRun)();
|
|
82
|
-
// Group by kind so we fetch each collection's current state once.
|
|
119
|
+
}
|
|
120
|
+
function groupByKind(resources) {
|
|
83
121
|
const byKind = new Map();
|
|
84
122
|
for (const r of resources) {
|
|
85
123
|
const arr = byKind.get(r.kind) ?? [];
|
|
86
124
|
arr.push(r);
|
|
87
125
|
byKind.set(r.kind, arr);
|
|
88
126
|
}
|
|
89
|
-
|
|
90
|
-
|
|
127
|
+
return byKind;
|
|
128
|
+
}
|
|
129
|
+
async function fetchLive(kinds, json) {
|
|
130
|
+
const live = new Map();
|
|
91
131
|
try {
|
|
92
|
-
for (const
|
|
93
|
-
const recon = (0, registry_1.reconcilerFor)(kind);
|
|
132
|
+
for (const kind of kinds) {
|
|
133
|
+
const recon = (0, registry_1.reconcilerFor)(kind);
|
|
94
134
|
const resp = await api_client_1.apiClient.get(recon.list);
|
|
95
|
-
|
|
96
|
-
const actions = (0, engine_1.planKind)(recon, desired, current, { prune: options.prune });
|
|
97
|
-
planByKind.push({ kind, actions });
|
|
98
|
-
allActions.push(...actions);
|
|
135
|
+
live.set(kind, { recon, items: (0, engine_1.extractList)(resp.data, recon) });
|
|
99
136
|
}
|
|
100
137
|
}
|
|
101
138
|
catch (e) {
|
|
102
139
|
const err = (0, api_client_1.handleApiError)(e);
|
|
103
|
-
if (
|
|
140
|
+
if (json) {
|
|
104
141
|
(0, json_output_1.emitJson)({ error: err.message });
|
|
105
142
|
process.exit(1);
|
|
106
143
|
}
|
|
107
144
|
console.error(chalk_1.default.red(`Failed to read current state: ${err.message}`));
|
|
108
145
|
process.exit(1);
|
|
109
146
|
}
|
|
147
|
+
return live;
|
|
148
|
+
}
|
|
149
|
+
/** Lock handling for a manifest path: null when the manifest is stdin or --no-lock. */
|
|
150
|
+
function lockFor(file, useLock, json) {
|
|
151
|
+
if (!useLock || file === '-')
|
|
152
|
+
return null;
|
|
153
|
+
const lockPath = (0, lock_1.lockPathFor)(path.resolve(file));
|
|
154
|
+
if ((0, tenant_guard_1.isProtectedRoot)(path.dirname(lockPath))) {
|
|
155
|
+
// Never write tenant state into $HOME or the platform monorepo.
|
|
156
|
+
if (!json)
|
|
157
|
+
(0, tenant_guard_1.refuseProtectedRoot)(path.dirname(lockPath));
|
|
158
|
+
(0, json_output_1.emitJson)({ error: 'refusing to write a lockfile in a protected directory; use --no-lock or move the manifest' });
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return { path: lockPath, lock: (0, lock_1.readLock)(lockPath) };
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
166
|
+
if (json) {
|
|
167
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
console.error(chalk_1.default.red(msg));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function printResults(results) {
|
|
175
|
+
for (const r of results) {
|
|
176
|
+
const tag = ICON[r.action] ?? r.action;
|
|
177
|
+
const suffix = r.status === 'failed' ? chalk_1.default.red(` FAILED: ${r.error}`)
|
|
178
|
+
: r.reason ? chalk_1.default.dim(` (${r.reason})`) : '';
|
|
179
|
+
console.log(` ${tag} ${chalk_1.default.cyan(r.kind)}/${r.identity}${suffix}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function printDrift(entries) {
|
|
183
|
+
for (const e of entries) {
|
|
184
|
+
console.log(` ${DRIFT_ICON[e.state]} ${chalk_1.default.cyan(e.kind)}/${e.identity}`);
|
|
185
|
+
for (const d of e.live_diff ?? []) {
|
|
186
|
+
console.log(chalk_1.default.dim(` ${d.field}: applied ${JSON.stringify(d.lock)} → production ${JSON.stringify(d.live)}`));
|
|
187
|
+
}
|
|
188
|
+
for (const d of e.manifest_diff ?? []) {
|
|
189
|
+
console.log(chalk_1.default.dim(` ${d.field}: applied ${JSON.stringify(d.lock)} → manifest ${JSON.stringify(d.manifest)}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── solid apply <file> ───────────────────────────────────────────────────────
|
|
194
|
+
exports.applyCommand = new commander_1.Command('apply')
|
|
195
|
+
.description('Reconcile the company to a declarative manifest (create/update/prune); drift, rollback, history, export')
|
|
196
|
+
// Parent flags (--json, --yes, --dry-run, --kinds) must not swallow the
|
|
197
|
+
// subcommands' own flags: `solid apply drift site.yaml --json` is drift's --json.
|
|
198
|
+
.enablePositionalOptions()
|
|
199
|
+
.argument('[file]', 'Manifest file (YAML or JSON). Use "-" for stdin.')
|
|
200
|
+
.option('--dry-run', 'Show the plan without making any changes')
|
|
201
|
+
.option('--prune', 'Delete managed resources that are absent from the manifest')
|
|
202
|
+
.option('--yes', 'Skip the prune confirmation')
|
|
203
|
+
.option('--strict', 'Refuse to write if production changed since the last apply (what `solid apply drift` reports)')
|
|
204
|
+
.option('--no-lock', 'Do not read or write <manifest>.lock.json')
|
|
205
|
+
.option('--json', 'Machine-readable plan/result')
|
|
206
|
+
.option('--kinds', 'List the resource kinds apply supports, then exit')
|
|
207
|
+
.action(async (file, options) => {
|
|
208
|
+
const json = (0, json_output_1.isJsonOutput)(options);
|
|
209
|
+
if (options.kinds) {
|
|
210
|
+
const kinds = (0, registry_1.knownKinds)();
|
|
211
|
+
if (json)
|
|
212
|
+
return void (0, json_output_1.emitJson)({ kinds });
|
|
213
|
+
console.log(chalk_1.default.bold('Supported kinds:'));
|
|
214
|
+
for (const k of kinds)
|
|
215
|
+
console.log(` ${k}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
(0, command_kit_1.requireCompanyContext)();
|
|
219
|
+
if (!file) {
|
|
220
|
+
console.error(chalk_1.default.red('Missing manifest file. Usage: solid apply <file> (or "-" for stdin).'));
|
|
221
|
+
process.exit(2);
|
|
222
|
+
}
|
|
223
|
+
const resources = readManifest(file);
|
|
224
|
+
if (resources.length === 0) {
|
|
225
|
+
console.error(chalk_1.default.yellow('Manifest declares no resources.'));
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
const dryRun = !!options.dryRun || (0, dry_run_1.isDryRun)();
|
|
229
|
+
const lockState = lockFor(file, options.lock !== false, json);
|
|
230
|
+
const byKind = groupByKind(resources);
|
|
231
|
+
const live = await fetchLive(byKind.keys(), json);
|
|
232
|
+
// --strict: production must be where the last apply left it, or we do nothing.
|
|
233
|
+
let drift;
|
|
234
|
+
if (options.strict) {
|
|
235
|
+
if (!lockState) {
|
|
236
|
+
const msg = '--strict needs a lockfile (manifest from a file, without --no-lock)';
|
|
237
|
+
if (json) {
|
|
238
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
console.error(chalk_1.default.red(msg));
|
|
242
|
+
process.exit(2);
|
|
243
|
+
}
|
|
244
|
+
drift = (0, lock_1.classifyDrift)(resources, lockState.lock, live);
|
|
245
|
+
const moved = drift.filter(lock_1.isProductionDrift);
|
|
246
|
+
if (moved.length > 0 && !dryRun) {
|
|
247
|
+
if (json) {
|
|
248
|
+
(0, json_output_1.emitJson)({ error: 'production changed since the last apply', drift: moved });
|
|
249
|
+
process.exit(3);
|
|
250
|
+
}
|
|
251
|
+
console.error(chalk_1.default.red(`Refusing to apply: ${moved.length} resource(s) changed in production since the last apply.`));
|
|
252
|
+
printDrift(moved);
|
|
253
|
+
console.error(chalk_1.default.dim('Re-apply without --strict to overwrite production, or update the manifest to match it.'));
|
|
254
|
+
process.exit(3);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const allActions = [];
|
|
258
|
+
const planByKind = [];
|
|
259
|
+
for (const [kind, desired] of byKind) {
|
|
260
|
+
const { recon, items } = live.get(kind);
|
|
261
|
+
const actions = (0, engine_1.planKind)(recon, desired, items, { prune: options.prune });
|
|
262
|
+
planByKind.push({ kind, actions });
|
|
263
|
+
allActions.push(...actions);
|
|
264
|
+
}
|
|
110
265
|
const changes = allActions.filter((a) => a.action === 'create' || a.action === 'update' || a.action === 'prune');
|
|
111
266
|
const prunes = allActions.filter((a) => a.action === 'prune');
|
|
112
267
|
// Confirm destructive prunes before any write (unless --yes / --dry-run / --json).
|
|
113
|
-
if (!dryRun && prunes.length > 0 && !options.yes && !
|
|
268
|
+
if (!dryRun && prunes.length > 0 && !options.yes && !json) {
|
|
114
269
|
const ok = await (0, command_kit_1.confirm)(`Prune ${prunes.length} resource(s)? This deletes them.`);
|
|
115
270
|
if (!ok) {
|
|
116
271
|
console.error(chalk_1.default.yellow('Aborted.'));
|
|
117
272
|
process.exit(1);
|
|
118
273
|
}
|
|
119
274
|
}
|
|
120
|
-
if (!dryRun && prunes.length > 0 && !options.yes &&
|
|
275
|
+
if (!dryRun && prunes.length > 0 && !options.yes && json) {
|
|
121
276
|
(0, json_output_1.emitJson)({ error: 'Refusing to prune without --yes in non-interactive/json mode', prunes: prunes.length });
|
|
122
277
|
process.exit(1);
|
|
123
278
|
}
|
|
@@ -127,23 +282,253 @@ exports.applyCommand = new commander_1.Command('apply')
|
|
|
127
282
|
results.push(...(await (0, engine_1.executePlan)(api_client_1.apiClient, recon, actions, dryRun)));
|
|
128
283
|
}
|
|
129
284
|
const counts = (0, engine_1.tallyResults)(results);
|
|
130
|
-
|
|
131
|
-
|
|
285
|
+
let lockWritten;
|
|
286
|
+
let runId;
|
|
287
|
+
if (!dryRun && lockState) {
|
|
288
|
+
const base = lockState.lock ?? (0, lock_1.emptyLock)(config_1.config.companyId, file, cliVersion());
|
|
289
|
+
const merged = (0, lock_1.mergeRun)(base, results, { cliVersion: cliVersion() });
|
|
290
|
+
(0, lock_1.writeLock)(lockState.path, merged.lock);
|
|
291
|
+
lockWritten = lockState.path;
|
|
292
|
+
runId = merged.run.run_id;
|
|
132
293
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const suffix = r.status === 'failed' ? chalk_1.default.red(` FAILED: ${r.error}`)
|
|
136
|
-
: r.reason ? chalk_1.default.dim(` (${r.reason})`) : '';
|
|
137
|
-
console.log(` ${tag} ${chalk_1.default.cyan(r.kind)}/${r.identity}${suffix}`);
|
|
294
|
+
if (json) {
|
|
295
|
+
return void (0, json_output_1.emitJson)({ dryRun, counts, actions: results, lock: lockWritten ?? null, run_id: runId ?? null, drift: drift ?? undefined });
|
|
138
296
|
}
|
|
297
|
+
printResults(results);
|
|
139
298
|
const verb = dryRun ? 'Plan' : 'Applied';
|
|
140
299
|
console.log(`\n${chalk_1.default.bold(verb)}: ${counts.create} to create, ${counts.update} to update, ` +
|
|
141
300
|
`${counts.noop} unchanged, ${counts.prune} to prune` +
|
|
142
301
|
(counts.failed ? chalk_1.default.red(`, ${counts.failed} failed`) : '') +
|
|
143
|
-
(counts.unsupported ? chalk_1.default.magenta(`, ${counts.unsupported}
|
|
302
|
+
(counts.unsupported ? chalk_1.default.magenta(`, ${counts.unsupported} unsupported`) : ''));
|
|
144
303
|
if (dryRun && changes.length > 0)
|
|
145
304
|
console.log(chalk_1.default.dim('Run without --dry-run to apply.'));
|
|
305
|
+
if (lockWritten)
|
|
306
|
+
console.log(chalk_1.default.dim(`Lock: ${path.relative(process.cwd(), lockWritten)} run ${runId}`));
|
|
146
307
|
if (counts.failed > 0)
|
|
147
308
|
process.exit(1);
|
|
148
309
|
});
|
|
310
|
+
// ── solid apply drift <file> ─────────────────────────────────────────────────
|
|
311
|
+
exports.applyCommand
|
|
312
|
+
.command('drift')
|
|
313
|
+
.description('Three-way comparison: manifest (git) vs lock (last apply) vs production. Exit 1 when production moved.')
|
|
314
|
+
.argument('<file>', 'Manifest file')
|
|
315
|
+
.option('--fail-on-any', 'Also exit 1 for manifest changes, missing and never-applied resources')
|
|
316
|
+
.option('--json', 'Machine-readable report')
|
|
317
|
+
.action(async (file, options) => {
|
|
318
|
+
const json = (0, json_output_1.isJsonOutput)(options);
|
|
319
|
+
(0, command_kit_1.requireCompanyContext)();
|
|
320
|
+
const resources = readManifest(file);
|
|
321
|
+
const lockPath = (0, lock_1.lockPathFor)(path.resolve(file));
|
|
322
|
+
let lock = null;
|
|
323
|
+
try {
|
|
324
|
+
lock = (0, lock_1.readLock)(lockPath);
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
328
|
+
if (json) {
|
|
329
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
console.error(chalk_1.default.red(msg));
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
const live = await fetchLive(groupByKind(resources).keys(), json);
|
|
336
|
+
const entries = (0, lock_1.classifyDrift)(resources, lock, live);
|
|
337
|
+
const production = entries.filter(lock_1.isProductionDrift);
|
|
338
|
+
const pending = entries.filter((e) => !(0, lock_1.isProductionDrift)(e) && e.state !== 'in_sync');
|
|
339
|
+
const code = production.length > 0 ? 1 : (options.failOnAny && pending.length > 0 ? 1 : 0);
|
|
340
|
+
if (json) {
|
|
341
|
+
(0, json_output_1.emitJson)({
|
|
342
|
+
lock: lock ? path.relative(process.cwd(), lockPath) : null,
|
|
343
|
+
last_applied: lock?.applied_at ?? null,
|
|
344
|
+
production_drift: production.length, pending: pending.length,
|
|
345
|
+
entries,
|
|
346
|
+
});
|
|
347
|
+
process.exit(code);
|
|
348
|
+
}
|
|
349
|
+
if (!lock)
|
|
350
|
+
console.log(chalk_1.default.yellow(`No lockfile at ${path.relative(process.cwd(), lockPath)} — nothing has been applied from this manifest yet, so there is no baseline.`));
|
|
351
|
+
else
|
|
352
|
+
console.log(chalk_1.default.dim(`Baseline: ${path.relative(process.cwd(), lockPath)} (applied ${lock.applied_at})`));
|
|
353
|
+
printDrift(entries);
|
|
354
|
+
console.log(`\n${chalk_1.default.bold('Drift')}: ${production.length} changed in production, ${pending.length} pending from git, ` +
|
|
355
|
+
`${entries.filter((e) => e.state === 'in_sync').length} in sync`);
|
|
356
|
+
if (production.length)
|
|
357
|
+
console.log(chalk_1.default.dim('Production moved since the last apply. `solid apply <file>` overwrites it; update the manifest to keep it.'));
|
|
358
|
+
process.exit(code);
|
|
359
|
+
});
|
|
360
|
+
// ── solid apply rollback <file> ──────────────────────────────────────────────
|
|
361
|
+
exports.applyCommand
|
|
362
|
+
.command('rollback')
|
|
363
|
+
.description('Revert a recorded run from its pre-images, most recent write first, then verify. Ordered, not atomic.')
|
|
364
|
+
.argument('<file>', 'Manifest file (its lock holds the runs)')
|
|
365
|
+
.option('--run <id>', 'Run to revert (default: the last apply run)')
|
|
366
|
+
.option('--dry-run', 'Show the revert plan without writing')
|
|
367
|
+
.option('--yes', 'Skip confirmation')
|
|
368
|
+
.option('--json', 'Machine-readable result')
|
|
369
|
+
.action(async (file, options) => {
|
|
370
|
+
const json = (0, json_output_1.isJsonOutput)(options);
|
|
371
|
+
(0, command_kit_1.requireCompanyContext)();
|
|
372
|
+
const lockState = lockFor(file, true, json);
|
|
373
|
+
if (!lockState.lock || lockState.lock.runs.length === 0) {
|
|
374
|
+
const msg = `No runs recorded in ${path.relative(process.cwd(), lockState.path)} — nothing to roll back.`;
|
|
375
|
+
if (json) {
|
|
376
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
console.error(chalk_1.default.yellow(msg));
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const lock = lockState.lock;
|
|
383
|
+
const run = options.run
|
|
384
|
+
? lock.runs.find((r) => r.run_id === options.run)
|
|
385
|
+
: [...lock.runs].reverse().find((r) => r.kind === 'apply' && r.actions.length > 0);
|
|
386
|
+
if (!run) {
|
|
387
|
+
const msg = options.run ? `Run ${options.run} not found; see \`solid apply history ${file}\`` : 'No apply run with writes to revert.';
|
|
388
|
+
if (json) {
|
|
389
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
console.error(chalk_1.default.red(msg));
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
const dryRun = !!options.dryRun || (0, dry_run_1.isDryRun)();
|
|
396
|
+
const { actions, skipped } = (0, lock_1.buildRollbackPlan)(run, registry_1.reconcilerFor);
|
|
397
|
+
if (!json) {
|
|
398
|
+
console.log(chalk_1.default.bold(`Reverting run ${run.run_id} (${run.at}) — ${actions.length} action(s)` + (skipped.length ? `, ${skipped.length} not revertible` : '')));
|
|
399
|
+
for (const a of actions)
|
|
400
|
+
console.log(` ${ICON[a.action]} ${chalk_1.default.cyan(a.kind)}/${a.identity}`);
|
|
401
|
+
for (const s of skipped)
|
|
402
|
+
console.log(` ${chalk_1.default.magenta('! skip')} ${chalk_1.default.cyan(s.kind)}/${s.identity} ${chalk_1.default.dim(`(${s.reason})`)}`);
|
|
403
|
+
}
|
|
404
|
+
if (actions.length === 0) {
|
|
405
|
+
if (json)
|
|
406
|
+
return void (0, json_output_1.emitJson)({ run_id: run.run_id, dryRun, actions: [], skipped });
|
|
407
|
+
console.log(chalk_1.default.yellow('\nNothing revertible in this run.'));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (!dryRun && !options.yes) {
|
|
411
|
+
if (json) {
|
|
412
|
+
(0, json_output_1.emitJson)({ error: 'Refusing to roll back without --yes in json mode', run_id: run.run_id, actions, skipped });
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
const ok = await (0, command_kit_1.confirm)(`Revert ${actions.length} write(s) on production?`);
|
|
416
|
+
if (!ok) {
|
|
417
|
+
console.error(chalk_1.default.yellow('Aborted.'));
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Execute per kind, preserving reverse order within the run.
|
|
422
|
+
const results = [];
|
|
423
|
+
for (const a of actions) {
|
|
424
|
+
const recon = (0, registry_1.reconcilerFor)(a.kind);
|
|
425
|
+
results.push(...(await (0, engine_1.executePlan)(api_client_1.apiClient, recon, [a], dryRun)));
|
|
426
|
+
}
|
|
427
|
+
// Verification pass: re-read and check each reverted update now matches its target.
|
|
428
|
+
const verified = [];
|
|
429
|
+
if (!dryRun) {
|
|
430
|
+
const live = await fetchLive(new Set(actions.map((a) => a.kind)), json);
|
|
431
|
+
for (const a of actions) {
|
|
432
|
+
if (a.action !== 'update' || !a.spec)
|
|
433
|
+
continue;
|
|
434
|
+
const { recon, items } = live.get(a.kind);
|
|
435
|
+
const item = items.find((i) => String(i[recon.identity]) === a.identity);
|
|
436
|
+
const ok = !!item && Object.keys(a.spec).every((k) => JSON.stringify(item[k] ?? null) === JSON.stringify(a.spec[k] ?? null));
|
|
437
|
+
verified.push({ kind: a.kind, identity: a.identity, ok });
|
|
438
|
+
}
|
|
439
|
+
const merged = (0, lock_1.mergeRun)(lock, results, { cliVersion: cliVersion(), kind: 'rollback', reverts: run.run_id });
|
|
440
|
+
(0, lock_1.writeLock)(lockState.path, merged.lock);
|
|
441
|
+
}
|
|
442
|
+
const counts = (0, engine_1.tallyResults)(results);
|
|
443
|
+
const failedVerify = verified.filter((v) => !v.ok);
|
|
444
|
+
if (json) {
|
|
445
|
+
(0, json_output_1.emitJson)({ reverted: run.run_id, dryRun, counts, actions: results, skipped, verified });
|
|
446
|
+
process.exit(counts.failed || failedVerify.length ? 1 : 0);
|
|
447
|
+
}
|
|
448
|
+
console.log();
|
|
449
|
+
printResults(results);
|
|
450
|
+
for (const v of verified)
|
|
451
|
+
console.log(` ${v.ok ? chalk_1.default.green('✓ verified') : chalk_1.default.red('✗ mismatch')} ${chalk_1.default.cyan(v.kind)}/${v.identity}`);
|
|
452
|
+
console.log(`\n${chalk_1.default.bold(dryRun ? 'Revert plan' : 'Reverted')}: ${counts.create} recreated, ${counts.update} restored, ${counts.prune} removed` +
|
|
453
|
+
(counts.failed ? chalk_1.default.red(`, ${counts.failed} failed`) : '') + (failedVerify.length ? chalk_1.default.red(`, ${failedVerify.length} did not verify`) : ''));
|
|
454
|
+
if (!dryRun)
|
|
455
|
+
console.log(chalk_1.default.dim('Rollback is ordered and verified, not atomic: a failure above leaves earlier reverts in place.'));
|
|
456
|
+
if (counts.failed || failedVerify.length)
|
|
457
|
+
process.exit(1);
|
|
458
|
+
});
|
|
459
|
+
// ── solid apply history <file> ───────────────────────────────────────────────
|
|
460
|
+
exports.applyCommand
|
|
461
|
+
.command('history')
|
|
462
|
+
.description('Runs recorded in the lockfile (what each apply or rollback wrote)')
|
|
463
|
+
.argument('<file>', 'Manifest file')
|
|
464
|
+
.option('--json', 'Machine-readable list')
|
|
465
|
+
.action((file, options) => {
|
|
466
|
+
const json = (0, json_output_1.isJsonOutput)(options);
|
|
467
|
+
const lockPath = (0, lock_1.lockPathFor)(path.resolve(file));
|
|
468
|
+
let lock = null;
|
|
469
|
+
try {
|
|
470
|
+
lock = (0, lock_1.readLock)(lockPath);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
474
|
+
if (json) {
|
|
475
|
+
(0, json_output_1.emitJson)({ error: msg });
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
console.error(chalk_1.default.red(msg));
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
if (json)
|
|
482
|
+
return void (0, json_output_1.emitJson)({ lock: lock ? path.relative(process.cwd(), lockPath) : null, runs: lock?.runs ?? [], resources: Object.keys(lock?.resources ?? {}).length });
|
|
483
|
+
if (!lock) {
|
|
484
|
+
console.log(chalk_1.default.yellow(`No lockfile at ${path.relative(process.cwd(), lockPath)}.`));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.log(chalk_1.default.dim(`${path.relative(process.cwd(), lockPath)} — ${Object.keys(lock.resources).length} resource(s) tracked, company ${lock.company_id}`));
|
|
488
|
+
for (const r of lock.runs) {
|
|
489
|
+
const c = { create: 0, update: 0, prune: 0 };
|
|
490
|
+
for (const a of r.actions)
|
|
491
|
+
c[a.action]++;
|
|
492
|
+
const tag = r.kind === 'rollback' ? chalk_1.default.yellow(`rollback of ${r.reverts}`) : chalk_1.default.green('apply');
|
|
493
|
+
console.log(` ${r.run_id} ${r.at} ${tag} +${c.create} ~${c.update} -${c.prune} ${chalk_1.default.dim(`cli ${r.cli_version}`)}`);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
// ── solid apply export [file] ────────────────────────────────────────────────
|
|
497
|
+
exports.applyCommand
|
|
498
|
+
.command('export')
|
|
499
|
+
.description('Write the live tenant as an apply manifest (all kinds, or --kinds a,b)')
|
|
500
|
+
.argument('[file]', 'Output file (default: stdout)')
|
|
501
|
+
.option('--kinds <list>', 'Comma-separated kinds to export (default: all)')
|
|
502
|
+
.option('--json', 'Emit JSON instead of YAML')
|
|
503
|
+
.action(async (file, options) => {
|
|
504
|
+
const json = (0, json_output_1.isJsonOutput)(options);
|
|
505
|
+
(0, command_kit_1.requireCompanyContext)();
|
|
506
|
+
const kinds = options.kinds ? options.kinds.split(',').map((k) => k.trim()).filter(Boolean) : (0, registry_1.knownKinds)();
|
|
507
|
+
const unknown = kinds.filter((k) => !(0, registry_1.reconcilerFor)(k));
|
|
508
|
+
if (unknown.length) {
|
|
509
|
+
console.error(chalk_1.default.red(`Unknown kind(s): ${unknown.join(', ')} — see \`solid apply --kinds\``));
|
|
510
|
+
process.exit(2);
|
|
511
|
+
}
|
|
512
|
+
if (file)
|
|
513
|
+
(0, tenant_guard_1.refuseProtectedRoot)(path.dirname(path.resolve(file)));
|
|
514
|
+
const live = await fetchLive(kinds, json);
|
|
515
|
+
const docs = [];
|
|
516
|
+
for (const kind of kinds) {
|
|
517
|
+
const { recon, items } = live.get(kind);
|
|
518
|
+
for (const item of items)
|
|
519
|
+
docs.push({ kind, spec: (0, lock_1.shapeForExport)(recon, item) });
|
|
520
|
+
}
|
|
521
|
+
const text = json
|
|
522
|
+
? JSON.stringify({ resources: docs.map((d) => ({ kind: d.kind, ...d.spec })) }, null, 2) + '\n'
|
|
523
|
+
: (0, lock_1.toManifestYaml)(docs, (o) => yaml.dump(o, { lineWidth: 120, noRefs: true }));
|
|
524
|
+
if (file) {
|
|
525
|
+
fs.writeFileSync(file, text);
|
|
526
|
+
if (json)
|
|
527
|
+
return void (0, json_output_1.emitJson)({ file, resources: docs.length, kinds });
|
|
528
|
+
console.log(chalk_1.default.green(`Wrote ${docs.length} resource(s) across ${kinds.length} kind(s) to ${file}`));
|
|
529
|
+
console.log(chalk_1.default.dim('Review before applying elsewhere: per-company ids (agent_id, site_id) do not travel between tenants.'));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
process.stdout.write(text);
|
|
533
|
+
});
|
|
149
534
|
//# sourceMappingURL=apply.js.map
|