instabox 0.6.0 → 0.6.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/bin/instabox.mjs +2 -1
- package/package.json +1 -1
- package/skills/instabox/SKILL.md +31 -20
- package/skills/instabox-basic-setup/SKILL.md +57 -57
- package/skills/instabox-basic-setup/scripts/content.mjs +10 -5
- package/skills/instabox-basic-setup/scripts/install-files.py +82 -0
- package/skills/instabox-basic-setup/scripts/lib.mjs +61 -39
- package/skills/instabox-basic-setup/scripts/plan.mjs +248 -230
- package/skills/instabox-basic-setup/scripts/push.mjs +318 -151
- package/skills/instabox-basic-setup/scripts/scan.mjs +73 -46
- package/skills/instabox-move-repo/SKILL.md +22 -12
- package/skills/instabox-snapshots/SKILL.md +8 -2
|
@@ -1,295 +1,313 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// node plan.mjs write --scan scan.json --vault vault.json [--selector k=v,...] > plan.md
|
|
5
|
-
// node plan.mjs parse --scan scan.json --vault vault.json plan.md > selection.json
|
|
6
|
-
//
|
|
7
|
-
// vault.json is `instabox vault ls --json`: items already in the Vault. One
|
|
8
|
-
// set there by an earlier run of this skill (listed in the manifest, see
|
|
9
|
-
// push.mjs) is replaced; one set any other way is unchecked, and checking it
|
|
10
|
-
// replaces it.
|
|
11
|
-
//
|
|
12
|
-
// `parse` reads the plan back strictly: every id exactly once, nothing
|
|
13
|
-
// unknown, no malformed checkbox. Errors go to stderr, all at once, exit 1.
|
|
14
|
-
// Over the Vault's limits: exit 3, with the sizes.
|
|
2
|
+
// Names-only approval plan for org Secrets and explicit sandbox config copies.
|
|
15
3
|
import { createHash } from 'node:crypto';
|
|
16
4
|
import { readFileSync } from 'node:fs';
|
|
17
5
|
import { homedir } from 'node:os';
|
|
18
6
|
import { join } from 'node:path';
|
|
19
7
|
import { fileContent } from './content.mjs';
|
|
20
8
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
9
|
+
SECRET_MAX_ITEMS,
|
|
10
|
+
SECRET_MAX_TOTAL,
|
|
11
|
+
FILE_MAX_VALUE,
|
|
12
|
+
FILE_MAX_TOTAL,
|
|
13
|
+
configPathOK,
|
|
14
|
+
deliveryPolicy,
|
|
24
15
|
human,
|
|
25
16
|
isMain,
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
sameSecret,
|
|
18
|
+
secretIdentity,
|
|
19
|
+
validID,
|
|
28
20
|
} from './lib.mjs';
|
|
29
21
|
|
|
30
22
|
export const MANIFEST = join(homedir(), '.config', 'instabox-basic-setup', 'pushed.json');
|
|
31
|
-
|
|
32
23
|
export function readManifest(path = MANIFEST) {
|
|
33
24
|
try {
|
|
34
25
|
const m = JSON.parse(readFileSync(path, 'utf8'));
|
|
35
|
-
|
|
36
|
-
} catch {
|
|
37
|
-
|
|
38
|
-
}
|
|
26
|
+
if (m.version === 3 && Array.isArray(m.secrets) && Array.isArray(m.files)) return m;
|
|
27
|
+
} catch {}
|
|
28
|
+
// Legacy names and file paths confer no ownership of an org Secret.
|
|
29
|
+
return { version: 3, secrets: [], files: [] };
|
|
39
30
|
}
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
32
|
+
export function existingFor(it, secrets, manifest) {
|
|
33
|
+
if (it.kind !== 'env') return [];
|
|
34
|
+
return secrets
|
|
35
|
+
.filter((s) => s.name === it.name)
|
|
36
|
+
.map((item) => ({
|
|
37
|
+
item,
|
|
38
|
+
ours: (manifest.secrets || []).some((m) => sameSecret(m, item)),
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
export function planDigest(scan, secrets, manifest, orgId) {
|
|
43
|
+
const rows = secrets.map(secretIdentity).sort((a, b) => a.name.localeCompare(b.name));
|
|
44
|
+
const own = (manifest.secrets || [])
|
|
45
|
+
.filter((s) => s.orgId === orgId)
|
|
46
|
+
.map(secretIdentity)
|
|
47
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
return createHash('sha256')
|
|
49
|
+
.update(JSON.stringify([scan, rows, own, orgId]))
|
|
50
|
+
.digest('hex')
|
|
51
|
+
.slice(0, 16);
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
export
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
export class PlanError extends Error {
|
|
55
|
+
constructor(errors, code = 1) {
|
|
56
|
+
super(errors.join('\n'));
|
|
57
|
+
this.errors = errors;
|
|
58
|
+
this.code = code;
|
|
59
|
+
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
62
|
+
function checkContext(scan, secrets, orgId) {
|
|
63
|
+
if (scan.version !== 3 || !Array.isArray(scan.items))
|
|
64
|
+
throw new PlanError(['scan format changed; run scan.mjs again']);
|
|
65
|
+
if (!validID(orgId)) throw new PlanError(['an explicit org ID is required']);
|
|
66
|
+
if (!Array.isArray(secrets) || secrets.some((s) => s.orgId !== orgId || !s.id || !Number.isSafeInteger(s.version)))
|
|
67
|
+
throw new PlanError(['Secret metadata must belong to the selected org and include IDs and versions']);
|
|
75
68
|
}
|
|
76
69
|
|
|
77
|
-
export function writePlan(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
export function writePlan(
|
|
71
|
+
scan,
|
|
72
|
+
{ secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [], delivery = 'env', hosts = [] } = {},
|
|
73
|
+
) {
|
|
74
|
+
checkContext(scan, secrets, orgId);
|
|
75
|
+
if (sandboxes.some((id) => !validID(id) || !id.startsWith('m_')))
|
|
76
|
+
throw new PlanError(['sandboxes must use stable m_ IDs']);
|
|
77
|
+
const policy = deliveryPolicy(delivery, hosts);
|
|
78
|
+
const out = [
|
|
79
|
+
'# instabox basic setup: plan',
|
|
84
80
|
'',
|
|
85
|
-
'
|
|
86
|
-
'
|
|
81
|
+
'Check `[x]` the items to save or copy. Do not change IDs or delete lines.',
|
|
82
|
+
'Tokens become encrypted org Secrets. Each Delivery line explicitly chooses env (real values) or proxy (HTTPS-header placeholders with allowed hosts).',
|
|
83
|
+
'This run adds env grants only to the sandbox IDs below; an empty list adds no grants. Existing grants remain in force.',
|
|
84
|
+
'Proxy Secrets also become available to other org sandboxes whose proxy selection is all, including future sandboxes.',
|
|
85
|
+
'Replacing an existing Secret affects its other authorized sandboxes. Config files are copied once to the named running sandboxes and may overwrite those paths; they are not Secrets.',
|
|
86
|
+
'Env values can be copied into files, processes or snapshots; removing a selection cannot recover those copies.',
|
|
87
87
|
'',
|
|
88
|
-
`
|
|
88
|
+
`Org: \`${orgId}\``,
|
|
89
|
+
`Sandboxes: \`${[...new Set(sandboxes)].join(',') || '-'}\``,
|
|
89
90
|
'',
|
|
90
|
-
`<!-- plan ${planDigest(scan,
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
`<!-- plan ${planDigest(scan, secrets, manifest, orgId)}: scan and org metadata; regenerate if either changes -->`,
|
|
92
|
+
];
|
|
93
|
+
for (const [section, title] of [
|
|
94
|
+
['tokens', 'Tokens'],
|
|
95
|
+
['git', 'Git config copies'],
|
|
96
|
+
['editor', 'Editor and terminal config copies'],
|
|
97
|
+
]) {
|
|
98
|
+
out.push('', `## ${title}`, '');
|
|
99
|
+
const rows = scan.items.filter((it) => it.section === section);
|
|
100
|
+
if (!rows.length) out.push('_Nothing found._');
|
|
101
|
+
for (const it of rows) {
|
|
102
|
+
const ex = existingFor(it, secrets, manifest);
|
|
103
|
+
const other = ex.some((e) => !e.ours);
|
|
104
|
+
const checked = it.checked && !other && (it.kind === 'env' || sandboxes.length > 0);
|
|
105
|
+
let description =
|
|
106
|
+
it.kind === 'env'
|
|
107
|
+
? `${it.name} from ${it.from}`
|
|
108
|
+
: `~/${it.path}: ${it.files.length} file(s), ${human(it.bytes)}`;
|
|
109
|
+
if (it.reason) description += `; off by default: ${it.reason}`;
|
|
110
|
+
if (it.note) description += `; ${it.note}`;
|
|
111
|
+
if (other) description += '; already in this org, set or changed elsewhere: checking replaces it';
|
|
112
|
+
else if (ex.length) description += '; replaces the unchanged Secret from your previous run';
|
|
113
|
+
if (it.kind === 'file' && !sandboxes.length) description += '; select running sandbox IDs before checking this';
|
|
114
|
+
out.push(`- [${checked ? 'x' : ' '}] \`${it.id}\` ${description}`);
|
|
115
|
+
if (it.kind === 'env') {
|
|
116
|
+
// Preserve an existing mode as a starting point; never silently change
|
|
117
|
+
// a proxy credential into a real-value env credential on a repeat run.
|
|
118
|
+
const p = ex.length ? deliveryPolicy(ex[0].item.delivery, ex[0].item.hosts || []) : policy;
|
|
119
|
+
out.push(`Delivery \`${it.id}\`: \`${p.delivery}\` hosts \`${p.hosts.join(',') || '-'}\``);
|
|
120
|
+
}
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
|
-
out.push('', '##
|
|
111
|
-
out.push('No checkbox: the agent repeats these at the end, with the command for each.', '');
|
|
112
|
-
if (!scan.manual.length) out.push('_Nothing._');
|
|
123
|
+
out.push('', '## Manual setup', '', 'These entries have no checkbox; no credentials are read for them.', '');
|
|
113
124
|
for (const m of scan.manual) out.push(`- ${m.what}: ${m.why}. ${m.todo}`);
|
|
114
|
-
out.
|
|
115
|
-
return out.join('\n');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export class PlanError extends Error {
|
|
119
|
-
constructor(errors, code = 1) {
|
|
120
|
-
super(errors.join('\n'));
|
|
121
|
-
this.errors = errors;
|
|
122
|
-
this.code = code;
|
|
123
|
-
}
|
|
125
|
+
return out.join('\n') + '\n';
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const maxItems = limits.maxItems || VAULT_MAX_ITEMS;
|
|
129
|
-
const maxBytes = limits.maxBytes || VAULT_MAX_TOTAL;
|
|
128
|
+
export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }, orgId, limits = {} } = {}) {
|
|
129
|
+
checkContext(scan, secrets, orgId);
|
|
130
130
|
const errors = [];
|
|
131
|
-
const byId = new Map(scan.items.map((
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
131
|
+
const byId = new Map(scan.items.map((it) => [it.id, it]));
|
|
132
|
+
const states = new Map(),
|
|
133
|
+
policies = new Map();
|
|
134
|
+
let planOrg, sandboxes, digest;
|
|
135
|
+
for (const [index, raw] of text.split('\n').entries()) {
|
|
136
|
+
const line = raw.replace(/\r$/, ''),
|
|
137
|
+
n = index + 1;
|
|
138
|
+
if (/^(Org|Sandboxes):/.test(line)) {
|
|
139
|
+
const m = line.match(/^(Org|Sandboxes): `([^`]*)`$/);
|
|
140
|
+
if (!m) {
|
|
141
|
+
errors.push(`line ${n}: invalid Org or Sandboxes line`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (m[1] === 'Org') {
|
|
145
|
+
if (planOrg !== undefined) errors.push(`line ${n}: duplicate Org line`);
|
|
146
|
+
planOrg = m[2];
|
|
147
|
+
} else {
|
|
148
|
+
if (sandboxes !== undefined) errors.push(`line ${n}: duplicate Sandboxes line`);
|
|
149
|
+
sandboxes = m[2] === '-' ? [] : m[2].split(',').map((s) => s.trim());
|
|
150
|
+
if (
|
|
151
|
+
sandboxes.some((id) => !validID(id) || !id.startsWith('m_')) ||
|
|
152
|
+
new Set(sandboxes).size !== sandboxes.length
|
|
153
|
+
)
|
|
154
|
+
errors.push(`line ${n}: use distinct stable sandbox IDs, comma-separated, or -`);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (/^Delivery\b/.test(line)) {
|
|
159
|
+
const m = line.match(/^Delivery `([^`]+)`: `([^`]+)` hosts `([^`]*)`$/);
|
|
160
|
+
if (!m || byId.get(m[1])?.kind !== 'env') {
|
|
161
|
+
errors.push(`line ${n}: invalid Delivery line`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (policies.has(m[1])) errors.push(`line ${n}: duplicate delivery for ${m[1]}`);
|
|
165
|
+
try {
|
|
166
|
+
policies.set(m[1], deliveryPolicy(m[2], m[3] === '-' ? [] : m[3].split(',').map((h) => h.trim())));
|
|
167
|
+
} catch (e) {
|
|
168
|
+
errors.push(`line ${n}: ${e.message}`);
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (/^Selector:/.test(line)) errors.push(`line ${n}: legacy selectors are retired`);
|
|
173
|
+
const d = line.match(/^<!-- plan ([0-9a-f]+):/);
|
|
174
|
+
if (d) {
|
|
175
|
+
if (digest) errors.push(`line ${n}: duplicate plan fingerprint`);
|
|
176
|
+
digest = d[1];
|
|
152
177
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// Anything that looks like a list item with a checkbox must be exactly one of ours.
|
|
156
|
-
if (!/^\s*(?:[-*+]|\d+[.)])\s*\[/.test(l) && !/^\s*[-*+]\s*`(?:env|file|files):/.test(l)) return;
|
|
157
|
-
const m = l.match(/^- \[([ xX])\] `([^`]+)`(?: .*)?$/);
|
|
178
|
+
if (!/^\s*(?:[-*+]|\d+[.)])\s*\[/.test(line) && !/^\s*[-*+]\s*`(?:env|file|files):/.test(line)) continue;
|
|
179
|
+
const m = line.match(/^- \[([ xX])\] `([^`]+)`(?: .*)?$/);
|
|
158
180
|
if (!m) {
|
|
159
|
-
errors.push(`line ${n}: not a checkbox line
|
|
160
|
-
|
|
181
|
+
errors.push(`line ${n}: not a checkbox line; use - [x] or - [ ] with the original ID`);
|
|
182
|
+
continue;
|
|
161
183
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
if (!byId.has(m[2])) {
|
|
185
|
+
errors.push(`line ${n}: unknown id \`${m[2]}\``);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (states.has(m[2])) errors.push(`line ${n}: \`${m[2]}\` appears twice`);
|
|
189
|
+
states.set(m[2], m[1] !== ' ');
|
|
190
|
+
}
|
|
191
|
+
if (planOrg !== orgId) errors.push('Org must match the metadata used to write this plan; regenerate for another org');
|
|
192
|
+
if (sandboxes === undefined)
|
|
193
|
+
errors.push('Sandboxes line is required; use - to save Secrets without adding sandbox grants');
|
|
194
|
+
if (digest !== planDigest(scan, secrets, manifest, orgId))
|
|
195
|
+
errors.push('different scan or org Secret state; scan, list Secrets and write a new plan');
|
|
196
|
+
for (const it of scan.items) {
|
|
197
|
+
if (!states.has(it.id)) errors.push(`\`${it.id}\` is missing from the plan`);
|
|
198
|
+
if (it.kind === 'env' && !policies.has(it.id))
|
|
199
|
+
errors.push(`\`${it.id}\` needs an explicit delivery mode and hosts line`);
|
|
200
|
+
}
|
|
172
201
|
if (errors.length) throw new PlanError(errors);
|
|
173
202
|
|
|
174
|
-
const picked = scan.items.filter((
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const items =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
203
|
+
const picked = scan.items.filter((it) => states.get(it.id));
|
|
204
|
+
const ghToken = picked.some(
|
|
205
|
+
(it) => it.kind === 'env' && it.name === 'GH_TOKEN' && policies.get(it.id).delivery === 'env',
|
|
206
|
+
);
|
|
207
|
+
const items = picked.map((it) => {
|
|
208
|
+
const item = { ...it };
|
|
209
|
+
delete item.checked;
|
|
210
|
+
if (it.kind === 'env') {
|
|
211
|
+
Object.assign(item, policies.get(it.id));
|
|
212
|
+
item.expected = secrets.find((s) => s.name === it.name);
|
|
213
|
+
item.expected = item.expected ? secretIdentity(item.expected) : null;
|
|
214
|
+
} else {
|
|
215
|
+
if (!sandboxes.length) errors.push(`\`${it.id}\`: config copying requires explicit running sandbox IDs`);
|
|
216
|
+
item.files = it.files.map((f) => {
|
|
217
|
+
if (!configPathOK(f.path)) errors.push(`\`${it.id}\`: invalid config path`);
|
|
186
218
|
const c = fileContent(f, { home: scan.home, ghToken });
|
|
187
219
|
if (c.error) errors.push(`\`${it.id}\`: ${c.error}`);
|
|
188
220
|
const bytes = c.error ? 0 : Buffer.byteLength(c.text);
|
|
189
|
-
if (bytes >
|
|
190
|
-
if (!c.error && !bytes) errors.push(`\`${it.id}\`: ~/${f.path} is empty, and the Vault takes no empty file: uncheck it`);
|
|
221
|
+
if (bytes > FILE_MAX_VALUE) errors.push(`\`${it.id}\`: file exceeds the config-copy limit of 64 KiB`);
|
|
191
222
|
return { ...f, bytes };
|
|
192
223
|
});
|
|
193
|
-
|
|
224
|
+
item.bytes = item.files.reduce((sum, f) => sum + f.bytes, 0);
|
|
194
225
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
226
|
+
return item;
|
|
227
|
+
});
|
|
198
228
|
if (errors.length) throw new PlanError(errors);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
const tokens = items.filter((it) => it.kind === 'env');
|
|
230
|
+
const names = new Set(tokens.map((it) => it.name));
|
|
231
|
+
const staying = secrets.filter((it) => !names.has(it.name));
|
|
232
|
+
const maxItems = limits.maxItems || SECRET_MAX_ITEMS,
|
|
233
|
+
maxBytes = limits.maxBytes || SECRET_MAX_TOTAL;
|
|
234
|
+
const totals = {
|
|
235
|
+
items: staying.length + tokens.length,
|
|
236
|
+
maxItems,
|
|
237
|
+
stayingBytes: staying.reduce((sum, s) => sum + (s.size || 0), 0),
|
|
238
|
+
maxBytes,
|
|
239
|
+
};
|
|
240
|
+
if (totals.items > maxItems || totals.stayingBytes > maxBytes)
|
|
209
241
|
throw new PlanError(
|
|
210
242
|
[
|
|
211
|
-
`over the
|
|
212
|
-
|
|
213
|
-
'uncheck some, or remove Vault items you no longer need (`instabox vault ls`, `instabox vault rm NAME`)',
|
|
243
|
+
`over the org Secret limits: ${totals.items} items of ${maxItems}; ${human(totals.stayingBytes)} of ${human(maxBytes)} already staying (selected values measured before push)`,
|
|
244
|
+
'Uncheck tokens or remove unused org Secrets; config files do not consume Secret quota.',
|
|
214
245
|
],
|
|
215
246
|
3,
|
|
216
247
|
);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const pickedNames = new Set(items.flatMap(vaultNames));
|
|
248
|
+
const fileBytes = items.filter((it) => it.kind === 'file').reduce((sum, it) => sum + it.bytes, 0);
|
|
249
|
+
if (fileBytes > FILE_MAX_TOTAL)
|
|
250
|
+
throw new PlanError(['config copies exceed 512 KiB; uncheck files or transfer them separately'], 3);
|
|
221
251
|
return {
|
|
222
|
-
version:
|
|
252
|
+
version: 3,
|
|
223
253
|
home: scan.home,
|
|
224
|
-
|
|
254
|
+
orgId,
|
|
255
|
+
sandboxes,
|
|
225
256
|
ghToken,
|
|
226
257
|
items,
|
|
227
258
|
manual: scan.manual,
|
|
228
|
-
skipped: scan.items.filter((
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
skipped: scan.items.filter((it) => !states.get(it.id)).map((it) => it.id),
|
|
260
|
+
leftover: (manifest.secrets || [])
|
|
261
|
+
.filter((m) => m.orgId === orgId && !names.has(m.name) && secrets.some((s) => sameSecret(m, s)))
|
|
262
|
+
.map((m) => m.name),
|
|
263
|
+
secrets: totals,
|
|
264
|
+
fileBytes,
|
|
233
265
|
};
|
|
234
266
|
}
|
|
235
267
|
|
|
236
|
-
function readVault(file) {
|
|
237
|
-
const v = JSON.parse(readFileSync(file, 'utf8'));
|
|
238
|
-
if (!Array.isArray(v.items)) throw new Error(`${file}: not \`instabox vault ls --json\` output`);
|
|
239
|
-
return v.items;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
268
|
function main() {
|
|
243
|
-
const [
|
|
269
|
+
const [command, ...args] = process.argv.slice(2);
|
|
244
270
|
const opt = (name) => {
|
|
245
|
-
const i =
|
|
271
|
+
const i = args.indexOf(name);
|
|
246
272
|
if (i < 0) return undefined;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
return
|
|
250
|
-
};
|
|
251
|
-
const usage = () => {
|
|
252
|
-
process.stderr.write(
|
|
253
|
-
'usage: plan.mjs write --scan scan.json --vault vault.json [--selector k=v,...] [--manifest FILE]\n' +
|
|
254
|
-
' plan.mjs parse --scan scan.json --vault vault.json [--limits limits.json] [--manifest FILE] plan.md\n',
|
|
255
|
-
);
|
|
256
|
-
process.exit(2);
|
|
273
|
+
const value = args[i + 1];
|
|
274
|
+
args.splice(i, 2);
|
|
275
|
+
return value;
|
|
257
276
|
};
|
|
258
|
-
const scanFile = opt('--scan')
|
|
259
|
-
|
|
277
|
+
const scanFile = opt('--scan'),
|
|
278
|
+
secretsFile = opt('--secrets'),
|
|
279
|
+
orgId = opt('--org');
|
|
260
280
|
const manifest = readManifest(opt('--manifest') || MANIFEST);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
process.stdout.write(JSON.stringify(
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
} else usage();
|
|
281
|
+
try {
|
|
282
|
+
if (!scanFile || !secretsFile || !orgId)
|
|
283
|
+
throw new Error('use --scan scan.json --secrets secrets.json --org ORG_ID');
|
|
284
|
+
const scan = JSON.parse(readFileSync(scanFile, 'utf8'));
|
|
285
|
+
const secrets = JSON.parse(readFileSync(secretsFile, 'utf8')).items;
|
|
286
|
+
if (!Array.isArray(secrets)) throw new Error('metadata must be instabox secrets ls --json output');
|
|
287
|
+
if (command === 'write') {
|
|
288
|
+
const sandboxes = [],
|
|
289
|
+
hosts = [];
|
|
290
|
+
let value;
|
|
291
|
+
while ((value = opt('--sandbox')) !== undefined) sandboxes.push(value);
|
|
292
|
+
while ((value = opt('--host')) !== undefined) hosts.push(value);
|
|
293
|
+
const delivery = opt('--delivery') || 'env';
|
|
294
|
+
if (args.length) throw new Error('unknown write arguments');
|
|
295
|
+
process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes, delivery, hosts }));
|
|
296
|
+
} else if (command === 'parse') {
|
|
297
|
+
const limitFile = opt('--limits');
|
|
298
|
+
const limits = limitFile ? JSON.parse(readFileSync(limitFile, 'utf8')) : {};
|
|
299
|
+
if (args.length !== 1) throw new Error('parse requires one plan.md path');
|
|
300
|
+
const selection = parsePlan(scan, readFileSync(args[0], 'utf8'), {
|
|
301
|
+
secrets,
|
|
302
|
+
orgId,
|
|
303
|
+
manifest,
|
|
304
|
+
limits: limits || {},
|
|
305
|
+
});
|
|
306
|
+
process.stdout.write(JSON.stringify(selection, null, 2) + '\n');
|
|
307
|
+
} else throw new Error('command must be write or parse');
|
|
308
|
+
} catch (e) {
|
|
309
|
+
process.stderr.write(e.message + '\n');
|
|
310
|
+
process.exit(e instanceof PlanError ? e.code : 1);
|
|
311
|
+
}
|
|
293
312
|
}
|
|
294
|
-
|
|
295
313
|
if (isMain(import.meta)) main();
|