@substrio/cli 0.0.0-stage → 0.1.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/LICENSE +177 -0
- package/README.md +66 -2
- package/bin/substrio.mjs +91 -0
- package/package.json +44 -4
- package/src/api.mjs +107 -0
- package/src/apply.mjs +204 -0
- package/src/blobs.mjs +102 -0
- package/src/cli.mjs +405 -0
- package/src/collections.mjs +701 -0
- package/src/command-table.mjs +436 -0
- package/src/commands/bundle.mjs +369 -0
- package/src/commands/count.mjs +39 -0
- package/src/commands/deploy.mjs +488 -0
- package/src/commands/dev.mjs +246 -0
- package/src/commands/env.mjs +115 -0
- package/src/commands/files.mjs +172 -0
- package/src/commands/git.mjs +195 -0
- package/src/commands/groups-runtime.mjs +227 -0
- package/src/commands/groups.mjs +417 -0
- package/src/commands/history.mjs +374 -0
- package/src/commands/import.mjs +506 -0
- package/src/commands/init.mjs +175 -0
- package/src/commands/key.mjs +172 -0
- package/src/commands/login.mjs +167 -0
- package/src/commands/logs.mjs +285 -0
- package/src/commands/mcp-config.mjs +102 -0
- package/src/commands/op.mjs +265 -0
- package/src/commands/open.mjs +35 -0
- package/src/commands/push.mjs +199 -0
- package/src/commands/signup.mjs +164 -0
- package/src/commands/site.mjs +129 -0
- package/src/commands/skills.mjs +177 -0
- package/src/commands/status.mjs +78 -0
- package/src/commands/tenants.mjs +172 -0
- package/src/commands/test.mjs +167 -0
- package/src/commands/tools.mjs +63 -0
- package/src/commands/versions.mjs +293 -0
- package/src/config.mjs +569 -0
- package/src/documents.mjs +139 -0
- package/src/function-build.mjs +273 -0
- package/src/identity.mjs +248 -0
- package/src/jsonfile.mjs +22 -0
- package/src/live-move.mjs +267 -0
- package/src/local/args.mjs +58 -0
- package/src/local/reload.mjs +408 -0
- package/src/local/server.mjs +291 -0
- package/src/local/watch.mjs +175 -0
- package/src/manifest.mjs +591 -0
- package/src/ops.mjs +265 -0
- package/src/readers.mjs +416 -0
- package/src/sdk.mjs +26 -0
- package/src/settings.mjs +117 -0
- package/src/testing/app.mjs +507 -0
- package/src/testing/env.mjs +35 -0
- package/src/testing/index.mjs +11 -0
- package/src/testing/types.mjs +55 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrio CLI: the app's history in the words a developer who knows git
|
|
3
|
+
* already types. Every verb here is one call to one registry operation, with
|
|
4
|
+
* its answer printed as a table (or as the server's own JSON under --json):
|
|
5
|
+
* nothing is worked out twice on this side.
|
|
6
|
+
*
|
|
7
|
+
* substrio branch branch.list
|
|
8
|
+
* substrio branch create NAME [--from REF] branch.create
|
|
9
|
+
* substrio branch delete NAME [--yes] branch.delete
|
|
10
|
+
* substrio branch protect NAME [--off] [--checks A,B] branch.protect
|
|
11
|
+
* substrio log [PATH] [--branch NAME] [--limit N] version.log
|
|
12
|
+
* substrio tag [NAME [VERSION]] [--branch NAME] tag.list, tag.create
|
|
13
|
+
* substrio merge FROM [--into NAME] merge.plan
|
|
14
|
+
* substrio merge status [--into NAME] merge.get
|
|
15
|
+
* substrio merge resolve PATH (--file F | --text T | -) [--into NAME] merge.resolve
|
|
16
|
+
* substrio merge commit [-m MSG] [--into NAME] [--checks-green] merge.commit
|
|
17
|
+
* substrio merge abort [--into NAME] [--yes] merge.abort
|
|
18
|
+
* substrio gc [--status] [--dry-run] version.gc, version.gc.status
|
|
19
|
+
*
|
|
20
|
+
* A word none of these owns runs the registry operation it names (substrio
|
|
21
|
+
* branch WORD ..., the same way substrio versions does), so an operation
|
|
22
|
+
* added to branch.*, tag.* or merge.* is reachable the day it ships.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
|
|
27
|
+
import { CliError, ErrorCode, confirmDestructive, emit, fail, globals } from '../cli.mjs';
|
|
28
|
+
import { FlagKind, parseFlags, wholeNumber } from '../local/args.mjs';
|
|
29
|
+
import { cmdRegistry } from './groups-runtime.mjs';
|
|
30
|
+
import { invokeOperation } from './op.mjs';
|
|
31
|
+
|
|
32
|
+
const C = {
|
|
33
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
34
|
+
red: '\x1b[91m', green: '\x1b[92m', yellow: '\x1b[93m', cyan: '\x1b[96m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The branch every verb works on when none is named, as the server's own default is. */
|
|
38
|
+
export const MAIN_BRANCH = 'main';
|
|
39
|
+
/** How much of a version id a table shows (a git short hash is seven). */
|
|
40
|
+
const ID_CHARS = 12;
|
|
41
|
+
|
|
42
|
+
const when = (seconds) => (seconds ? new Date(seconds * 1000).toISOString().replace('T', ' ').slice(0, 16) : '');
|
|
43
|
+
const shortId = (id) => (typeof id === 'string' ? id.slice(0, ID_CHARS) : '');
|
|
44
|
+
const authorOf = (row) => (row.author ? `${row.author_kind ?? 'person'} ${row.author}` : '');
|
|
45
|
+
|
|
46
|
+
// The one-letter flags git's own verbs take, as the long flag the table holds
|
|
47
|
+
// (parseFlags reads `--name` alone, so a short flag is spelled out before it).
|
|
48
|
+
const SHORT_FLAGS = { '-m': '--message' };
|
|
49
|
+
|
|
50
|
+
// The words of a verb, with its flags taken out: a flag it does not take is a
|
|
51
|
+
// usage error here, not a word sent to the server (parseFlags is the one flag
|
|
52
|
+
// parser).
|
|
53
|
+
function split(args, table, command) {
|
|
54
|
+
try {
|
|
55
|
+
return parseFlags(args.map((arg) => SHORT_FLAGS[arg] ?? arg), table);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return fail(err, `substrio ${command} --help`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A comma list of check names, or undefined when the flag is absent.
|
|
62
|
+
function checkList(flags) {
|
|
63
|
+
const text = flags.get('--checks');
|
|
64
|
+
if (text === undefined) return undefined;
|
|
65
|
+
return text.split(',').map((name) => name.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// The bytes a merge resolution settles a unit with: --text, --file, or stdin
|
|
69
|
+
// for "-". A unit that is not text goes as base64, which is the operation's
|
|
70
|
+
// own second way of carrying it.
|
|
71
|
+
function resolutionBody(flags, positional) {
|
|
72
|
+
if (flags.has('--text')) return { text: flags.get('--text') };
|
|
73
|
+
const file = flags.get('--file') ?? (positional.includes('-') ? '-' : undefined);
|
|
74
|
+
if (file === undefined) {
|
|
75
|
+
throw new CliError('merge resolve needs the bytes the unit is settled with', {
|
|
76
|
+
code: ErrorCode.INVALID_ARGUMENT,
|
|
77
|
+
hint: 'substrio merge resolve PATH --file FILE, --text TEXT, or - to read stdin',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const bytes = readFileSync(file === '-' ? 0 : file);
|
|
81
|
+
// Text as text, so the stored unit is the file: a unit with a NUL in it is
|
|
82
|
+
// not text and crosses as base64.
|
|
83
|
+
return bytes.includes(0) ? { base64: bytes.toString('base64') } : { text: bytes.toString('utf8') };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── branch ──────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const BRANCH_FLAGS = { '--from': FlagKind.VALUE, '--off': FlagKind.SWITCH, '--checks': FlagKind.VALUE };
|
|
89
|
+
|
|
90
|
+
export async function cmdBranch(api, args = []) {
|
|
91
|
+
const word = args[0];
|
|
92
|
+
if (word === undefined || word.startsWith('-')) return branchList(api, args);
|
|
93
|
+
switch (word) {
|
|
94
|
+
case 'list': case 'ls': return branchList(api, args.slice(1));
|
|
95
|
+
case 'create': case 'add': case 'new': return branchCreate(api, args.slice(1));
|
|
96
|
+
case 'delete': case 'rm': case 'remove': return branchDelete(api, args.slice(1));
|
|
97
|
+
case 'protect': return branchProtect(api, args.slice(1));
|
|
98
|
+
default: return cmdRegistry(api, 'branch', args);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function branchList(api, args) {
|
|
103
|
+
split(args, {}, 'branch');
|
|
104
|
+
const listed = await invokeOperation(api, 'branch.list', undefined);
|
|
105
|
+
if (globals.json) return emit(listed);
|
|
106
|
+
const branches = listed.branches || [];
|
|
107
|
+
if (branches.length === 0) {
|
|
108
|
+
console.log(`${C.dim}No branches yet. The first push makes ${MAIN_BRANCH}.${C.reset}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const width = Math.max('BRANCH'.length, ...branches.map((b) => b.name.length)) + 2;
|
|
112
|
+
console.log(`\n${C.bold}Branches (${branches.length})${C.reset}\n`);
|
|
113
|
+
console.log(` ${'BRANCH'.padEnd(width)}${'VERSION'.padEnd(14)}${'MOVED (UTC)'.padEnd(18)}PROTECTED`);
|
|
114
|
+
for (const b of branches) {
|
|
115
|
+
const guard = b.protected ? `yes${b.required_checks?.length ? ` (${b.required_checks.join(', ')})` : ''}` : '';
|
|
116
|
+
console.log(` ${b.name.padEnd(width)}${shortId(b.version).padEnd(14)}${when(b.updated_at).padEnd(18)}${guard}`);
|
|
117
|
+
}
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function branchCreate(api, args) {
|
|
122
|
+
const { flags, positional } = split(args, BRANCH_FLAGS, 'branch');
|
|
123
|
+
const [name] = positional;
|
|
124
|
+
if (!name) return fail('substrio branch create NAME [--from REF] names the branch to make');
|
|
125
|
+
const from = flags.get('--from');
|
|
126
|
+
const made = await invokeOperation(api, 'branch.create', from ? { name, from } : { name });
|
|
127
|
+
emit(made, () => console.log(`${C.green}✓${C.reset} Branch ${C.bold}${made.name}${C.reset} at ${shortId(made.version) || 'no version yet'}` +
|
|
128
|
+
`${from ? ` ${C.dim}(from ${from})${C.reset}` : ''}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function branchDelete(api, args) {
|
|
132
|
+
const { positional } = split(args, BRANCH_FLAGS, 'branch');
|
|
133
|
+
const [name] = positional;
|
|
134
|
+
if (!name) return fail('substrio branch delete NAME names the branch whose name is freed');
|
|
135
|
+
await confirmDestructive(`substrio branch delete ${name} frees the name (its versions and its log are kept)`,
|
|
136
|
+
`substrio branch delete ${name} --yes`);
|
|
137
|
+
const gone = await invokeOperation(api, 'branch.delete', { name });
|
|
138
|
+
emit(gone, () => console.log(`${C.green}✓${C.reset} Branch ${C.bold}${name}${C.reset} is free` +
|
|
139
|
+
`${gone.version ? ` ${C.dim}(it pointed at ${shortId(gone.version)}; substrio ref restore branch ${name} puts it back)${C.reset}` : ''}`));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function branchProtect(api, args) {
|
|
143
|
+
const { flags, positional } = split(args, BRANCH_FLAGS, 'branch');
|
|
144
|
+
const [name] = positional;
|
|
145
|
+
if (!name) return fail('substrio branch protect NAME [--off] [--checks A,B] names the branch');
|
|
146
|
+
const checks = checkList(flags);
|
|
147
|
+
const input = { name, protected: !flags.has('--off') };
|
|
148
|
+
if (checks !== undefined) input.required_checks = checks;
|
|
149
|
+
const set = await invokeOperation(api, 'branch.protect', input);
|
|
150
|
+
emit(set, () => console.log(set.protected
|
|
151
|
+
? `${C.green}✓${C.reset} Branch ${C.bold}${name}${C.reset} is protected: a commit or merge onto it needs ` +
|
|
152
|
+
`${set.required_checks?.length ? `${set.required_checks.join(', ')} green` : 'a green verdict'}`
|
|
153
|
+
: `${C.green}✓${C.reset} Branch ${C.bold}${name}${C.reset} is no longer protected`));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── log ─────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const LOG_FLAGS = { '--branch': FlagKind.VALUE, '--limit': FlagKind.VALUE };
|
|
159
|
+
|
|
160
|
+
export async function cmdLog(api, args = []) {
|
|
161
|
+
const { flags, positional } = split(args, LOG_FLAGS, 'log');
|
|
162
|
+
const branch = flags.get('--branch') ?? MAIN_BRANCH;
|
|
163
|
+
let limit;
|
|
164
|
+
try {
|
|
165
|
+
limit = wholeNumber(flags, '--limit', 1, 1_000_000, undefined);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return fail(err);
|
|
168
|
+
}
|
|
169
|
+
const [path] = positional;
|
|
170
|
+
const input = { branch };
|
|
171
|
+
if (path) input.path = path;
|
|
172
|
+
if (limit !== undefined) input.limit = limit;
|
|
173
|
+
const read = await invokeOperation(api, 'version.log', input);
|
|
174
|
+
if (globals.json) return emit(read);
|
|
175
|
+
const commits = read.versions || [];
|
|
176
|
+
if (commits.length === 0) {
|
|
177
|
+
console.log(`${C.dim}No commits on ${branch}${path ? ` touching ${path}` : ''}.${C.reset}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(`\n${C.bold}${branch}${C.reset}${path ? ` ${C.dim}· ${path}${C.reset}` : ''} ${C.dim}(${commits.length})${C.reset}\n`);
|
|
181
|
+
for (const c of commits) {
|
|
182
|
+
const merge = (c.parents || []).length > 1 ? ` ${C.yellow}(merge)${C.reset}` : '';
|
|
183
|
+
console.log(` ${C.cyan}${shortId(c.version)}${C.reset} ${`#${c.number}`.padEnd(7)}${when(c.created_at).padEnd(18)}${c.message || ''}${merge}`);
|
|
184
|
+
if (c.author) console.log(` ${C.dim}${' '.repeat(ID_CHARS)} ${authorOf(c)}${C.reset}`);
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── tag ─────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const TAG_FLAGS = { '--branch': FlagKind.VALUE };
|
|
192
|
+
|
|
193
|
+
export async function cmdTag(api, args = []) {
|
|
194
|
+
const word = args[0];
|
|
195
|
+
if (word === undefined || word.startsWith('-')) return tagList(api, args);
|
|
196
|
+
if (word === 'list' || word === 'ls') return tagList(api, args.slice(1));
|
|
197
|
+
if (word === 'create' || word === 'add') return tagCreate(api, args.slice(1));
|
|
198
|
+
// A bare name is a tag to make (substrio tag v1.2.0 7), as git's own is.
|
|
199
|
+
if (word === 'delete' || word === 'rm') return cmdRegistry(api, 'tag', args);
|
|
200
|
+
return tagCreate(api, args);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function tagList(api, args) {
|
|
204
|
+
split(args, {}, 'tag');
|
|
205
|
+
const listed = await invokeOperation(api, 'tag.list', undefined);
|
|
206
|
+
if (globals.json) return emit(listed);
|
|
207
|
+
const tags = listed.tags || [];
|
|
208
|
+
if (tags.length === 0) {
|
|
209
|
+
console.log(`${C.dim}No tags yet. Make one with: substrio tag v1.0.0${C.reset}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const width = Math.max('TAG'.length, ...tags.map((t) => t.name.length)) + 2;
|
|
213
|
+
console.log(`\n${C.bold}Tags (${tags.length})${C.reset}\n`);
|
|
214
|
+
console.log(` ${'TAG'.padEnd(width)}${'VERSION'.padEnd(14)}MADE (UTC)`);
|
|
215
|
+
for (const t of tags) console.log(` ${t.name.padEnd(width)}${shortId(t.version).padEnd(14)}${when(t.created_at)}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function tagCreate(api, args) {
|
|
220
|
+
const { flags, positional } = split(args, TAG_FLAGS, 'tag');
|
|
221
|
+
const [name, version] = positional;
|
|
222
|
+
if (!name) return fail('substrio tag NAME [VERSION] names the tag to make');
|
|
223
|
+
const input = { name };
|
|
224
|
+
if (version !== undefined) input.version = version;
|
|
225
|
+
else if (flags.has('--branch')) input.branch = flags.get('--branch');
|
|
226
|
+
const made = await invokeOperation(api, 'tag.create', input);
|
|
227
|
+
emit(made, () => console.log(`${C.green}✓${C.reset} Tag ${C.bold}${made.name}${C.reset} marks ${shortId(made.version)} ${C.dim}(a tag never moves)${C.reset}`));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── merge ───────────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
const MERGE_FLAGS = {
|
|
233
|
+
'--into': FlagKind.VALUE, '--message': FlagKind.VALUE,
|
|
234
|
+
'--file': FlagKind.VALUE, '--text': FlagKind.VALUE, '--checks-green': FlagKind.SWITCH,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export async function cmdMerge(api, args = []) {
|
|
238
|
+
const word = args[0];
|
|
239
|
+
switch (word) {
|
|
240
|
+
case 'status': case 'get': case 'show': return mergeDraft(api, args.slice(1));
|
|
241
|
+
case 'resolve': return mergeResolve(api, args.slice(1));
|
|
242
|
+
case 'commit': return mergeCommit(api, args.slice(1));
|
|
243
|
+
case 'abort': return mergeAbort(api, args.slice(1));
|
|
244
|
+
case undefined: return fail('substrio merge FROM [--into NAME] names the branch whose work comes in',
|
|
245
|
+
'substrio merge --help shows resolve, commit and abort');
|
|
246
|
+
default:
|
|
247
|
+
if (word.startsWith('-')) return fail('substrio merge FROM [--into NAME] names the branch whose work comes in');
|
|
248
|
+
return mergePlan(api, args);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// One line per unit the merge touched, conflicts first: what a developer has
|
|
253
|
+
// to settle before the merge can be committed.
|
|
254
|
+
function printDraft(draft) {
|
|
255
|
+
const units = draft.units || [];
|
|
256
|
+
const conflicted = units.filter((u) => u.kind?.startsWith('conflict'));
|
|
257
|
+
console.log(`\n${C.bold}${draft.from ? `${draft.from} → ` : ''}${draft.branch}${C.reset} ` +
|
|
258
|
+
`${C.dim}(base ${shortId(draft.base) || 'none'}, ours ${shortId(draft.ours) || 'none'}, theirs ${shortId(draft.theirs) || 'none'})${C.reset}\n`);
|
|
259
|
+
if (draft.up_to_date) console.log(` ${C.green}Already up to date.${C.reset}`);
|
|
260
|
+
else if (draft.fast_forward) console.log(` ${C.green}Fast forward: substrio merge commit moves ${draft.branch}.${C.reset}`);
|
|
261
|
+
if (units.length) {
|
|
262
|
+
const width = Math.max('UNIT'.length, ...units.map((u) => u.path.length)) + 2;
|
|
263
|
+
console.log(` ${'UNIT'.padEnd(width)}WHAT THE MERGE DID`);
|
|
264
|
+
for (const u of units) {
|
|
265
|
+
const colour = u.kind?.startsWith('conflict') ? C.red : u.kind === 'resolved' ? C.green : C.reset;
|
|
266
|
+
console.log(` ${u.path.padEnd(width)}${colour}${u.kind}${u.conflicts ? ` (${u.conflicts} region${u.conflicts === 1 ? '' : 's'})` : ''}${C.reset}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (conflicted.length) {
|
|
270
|
+
console.log(`\n ${C.yellow}${conflicted.length} unit${conflicted.length === 1 ? '' : 's'} to settle${C.reset}: ` +
|
|
271
|
+
`substrio merge resolve ${conflicted[0].path} --file FILE${draft.branch === MAIN_BRANCH ? '' : ` --into ${draft.branch}`}`);
|
|
272
|
+
console.log(` ${C.dim}Each conflicted unit's merged bytes, markers and all: substrio versions unit ${shortId(draft.ours)} path=${conflicted[0].path}${C.reset}`);
|
|
273
|
+
} else if (!draft.up_to_date) {
|
|
274
|
+
console.log(`\n ${C.dim}Nothing is conflicted: substrio merge commit${draft.branch === MAIN_BRANCH ? '' : ` --into ${draft.branch}`} publishes it${C.reset}`);
|
|
275
|
+
}
|
|
276
|
+
console.log('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function mergePlan(api, args) {
|
|
280
|
+
const { flags, positional } = split(args, MERGE_FLAGS, 'merge');
|
|
281
|
+
const [from] = positional;
|
|
282
|
+
const into = flags.get('--into') ?? MAIN_BRANCH;
|
|
283
|
+
const draft = await invokeOperation(api, 'merge.plan', { from, into });
|
|
284
|
+
emit(draft, () => printDraft(draft));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function mergeDraft(api, args) {
|
|
288
|
+
const { flags } = split(args, MERGE_FLAGS, 'merge');
|
|
289
|
+
const branch = flags.get('--into') ?? MAIN_BRANCH;
|
|
290
|
+
const draft = await invokeOperation(api, 'merge.get', { branch }, {
|
|
291
|
+
hint: (result) => (result.status === 404 ? `${branch} has no merge in hand: substrio merge FROM --into ${branch} starts one` : null),
|
|
292
|
+
});
|
|
293
|
+
emit(draft, () => printDraft(draft));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function mergeResolve(api, args) {
|
|
297
|
+
const { flags, positional } = split(args, MERGE_FLAGS, 'merge');
|
|
298
|
+
const branch = flags.get('--into') ?? MAIN_BRANCH;
|
|
299
|
+
const [path] = positional.filter((a) => a !== '-');
|
|
300
|
+
if (!path) return fail('substrio merge resolve PATH (--file FILE | --text TEXT | -) names the unit to settle');
|
|
301
|
+
let bytes;
|
|
302
|
+
try {
|
|
303
|
+
bytes = resolutionBody(flags, positional);
|
|
304
|
+
} catch (err) {
|
|
305
|
+
return fail(err);
|
|
306
|
+
}
|
|
307
|
+
const draft = await invokeOperation(api, 'merge.resolve', { branch, path, ...bytes });
|
|
308
|
+
emit(draft, () => {
|
|
309
|
+
console.log(`${C.green}✓${C.reset} ${path} is settled ${C.dim}(${draft.conflicts} left)${C.reset}`);
|
|
310
|
+
printDraft(draft);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function mergeCommit(api, args) {
|
|
315
|
+
const { flags } = split(args, MERGE_FLAGS, 'merge');
|
|
316
|
+
const branch = flags.get('--into') ?? MAIN_BRANCH;
|
|
317
|
+
const message = flags.get('--message');
|
|
318
|
+
const input = { branch };
|
|
319
|
+
if (message !== undefined) input.message = message;
|
|
320
|
+
if (flags.has('--checks-green')) input.checks_green = true;
|
|
321
|
+
const done = await invokeOperation(api, 'merge.commit', input);
|
|
322
|
+
emit(done, () => {
|
|
323
|
+
if (done.up_to_date) return console.log(`${C.dim}${branch} already holds that work; nothing was committed.${C.reset}`);
|
|
324
|
+
const how = done.fast_forward ? 'fast forwarded to' : 'committed';
|
|
325
|
+
console.log(`${C.green}✓${C.reset} ${C.bold}${branch}${C.reset} ${how} ${shortId(done.version)}` +
|
|
326
|
+
`${done.number ? ` ${C.dim}(version ${done.number})${C.reset}` : ''}`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function mergeAbort(api, args) {
|
|
331
|
+
const { flags } = split(args, MERGE_FLAGS, 'merge');
|
|
332
|
+
const branch = flags.get('--into') ?? MAIN_BRANCH;
|
|
333
|
+
await confirmDestructive(`substrio merge abort --into ${branch} throws away the merge in hand (the branch itself never moved)`,
|
|
334
|
+
`substrio merge abort${branch === MAIN_BRANCH ? '' : ` --into ${branch}`} --yes`);
|
|
335
|
+
const gone = await invokeOperation(api, 'merge.abort', { branch });
|
|
336
|
+
emit(gone, () => console.log(gone.deleted
|
|
337
|
+
? `${C.green}✓${C.reset} The merge onto ${C.bold}${branch}${C.reset} is thrown away`
|
|
338
|
+
: `${C.dim}${branch} had no merge in hand.${C.reset}`));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── gc ──────────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
const GC_FLAGS = { '--status': FlagKind.SWITCH, '--dry-run': FlagKind.SWITCH };
|
|
344
|
+
|
|
345
|
+
export async function cmdGc(api, args = []) {
|
|
346
|
+
const { flags } = split(args, GC_FLAGS, 'gc');
|
|
347
|
+
const status = flags.has('--status');
|
|
348
|
+
const dryRun = flags.has('--dry-run');
|
|
349
|
+
if (status && dryRun) return fail('substrio gc --status reads the last collection; --dry-run works one out. Give one');
|
|
350
|
+
if (!status && !dryRun) {
|
|
351
|
+
await confirmDestructive('substrio gc erases every object of the app\'s history no name reaches (--dry-run works it out and erases nothing)',
|
|
352
|
+
'substrio gc --yes');
|
|
353
|
+
}
|
|
354
|
+
const report = status
|
|
355
|
+
? await invokeOperation(api, 'version.gc.status', undefined)
|
|
356
|
+
: await invokeOperation(api, 'version.gc', dryRun ? { dry_run: true } : {});
|
|
357
|
+
if (globals.json) return emit(report);
|
|
358
|
+
if (status && !report.started_at) {
|
|
359
|
+
console.log(`${C.dim}This app was never collected. substrio gc --dry-run says what one would take.${C.reset}`);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const megabytes = (report.bytes / 1_048_576).toFixed(2);
|
|
363
|
+
console.log(`\n${C.bold}${report.dry_run ? 'A collection would take' : 'Collected'}${C.reset} ${C.dim}(${when(report.started_at)} UTC)${C.reset}\n`);
|
|
364
|
+
console.log(` ${'roots'.padEnd(12)}${report.roots}`);
|
|
365
|
+
console.log(` ${'kept'.padEnd(12)}${report.marked}${report.kept_new ? ` ${C.dim}(+${report.kept_new} written while it ran)${C.reset}` : ''}`);
|
|
366
|
+
console.log(` ${'read'.padEnd(12)}${report.read}`);
|
|
367
|
+
console.log(` ${'collected'.padEnd(12)}${report.collected} ${C.dim}(${megabytes} MB)${C.reset}`);
|
|
368
|
+
if (report.missing) console.log(` ${'missing'.padEnd(12)}${C.yellow}${report.missing}${C.reset}`);
|
|
369
|
+
if (report.foreign) console.log(` ${'foreign'.padEnd(12)}${report.foreign} ${C.dim}(objects of another app, left alone)${C.reset}`);
|
|
370
|
+
if (!report.complete) {
|
|
371
|
+
console.log(`\n ${C.yellow}Stopped at its sweep bound${C.reset}: substrio gc carries it on from ${report.resume_after}`);
|
|
372
|
+
}
|
|
373
|
+
console.log('');
|
|
374
|
+
}
|