@outerlayer/cli 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +202 -0
- package/README.md +320 -0
- package/dist/build-info.json +1 -0
- package/dist/chunk-4H56Y7O6.js +589 -0
- package/dist/chunk-A3WLZX2F.js +120 -0
- package/dist/chunk-A6NNKRJU.js +650 -0
- package/dist/chunk-D77LS3UI.js +42 -0
- package/dist/chunk-DEDV5V4U.js +87324 -0
- package/dist/chunk-ECSYRPCC.js +334 -0
- package/dist/chunk-IWIUYLDR.js +472 -0
- package/dist/chunk-JJP7YLMN.js +25 -0
- package/dist/chunk-KFYJV2ZG.js +7978 -0
- package/dist/chunk-OZ7C3XUE.js +34 -0
- package/dist/chunk-RCQXYLMO.js +96 -0
- package/dist/chunk-TFUIDMOB.js +34 -0
- package/dist/chunk-VNVZDWO3.js +10 -0
- package/dist/chunk-VU34RWDU.js +339 -0
- package/dist/chunk-WQ6VGRGZ.js +150 -0
- package/dist/chunk-XN2XZFTK.js +165 -0
- package/dist/chunk-YMTXR7PJ.js +200 -0
- package/dist/chunk-YRYMNBGQ.js +54 -0
- package/dist/chunk-Z5GEFMRW.js +45 -0
- package/dist/chunk-ZJSNLVCE.js +58 -0
- package/dist/cli-3JYGL2OB.js +5311 -0
- package/dist/config-POF7DEQW.js +7 -0
- package/dist/context-materialize-J6CRQ7O7.js +589 -0
- package/dist/emit-artifact-cmd-23F4YKQ3.js +339 -0
- package/dist/emit-cmd-TWSYYEZI.js +208 -0
- package/dist/emit-commit-credit-cmd-2GX5M2BR.js +121 -0
- package/dist/emit-finding-cmd-6K7ERPKG.js +235 -0
- package/dist/emit-result-cmd-NDFHMJX7.js +225 -0
- package/dist/hook-fast-CBW4ZETX.js +8 -0
- package/dist/hook-wrap-fast-36WGE6EX.js +8 -0
- package/dist/import-capture-cmd-EUMBGIV3.js +176 -0
- package/dist/import-ruler-cmd-7LH2QOMG.js +173 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/init-PTBITAUO.js +103 -0
- package/dist/login-cmd-IRX6LZT7.js +62 -0
- package/dist/logs-TRNPQM42.js +63 -0
- package/dist/loop-NFSOBUUJ.js +646 -0
- package/dist/mcp-install-cmd-FDQH6SEN.js +75 -0
- package/dist/mcp-serve-cmd-57EZZOTL.js +123 -0
- package/dist/paths-D2VGWWFI.js +6 -0
- package/dist/pidfile-PTW76F56.js +8 -0
- package/dist/status-WGOJTXZF.js +94 -0
- package/dist/statusline-fast-Z5U5CEC6.js +8 -0
- package/dist/sync-cmd-C5UGI4SF.js +15 -0
- package/dist/watch-V3K4PESQ.js +70 -0
- package/dist/work-claim-cmd-FQDWQVT2.js +98 -0
- package/dist/work-cmd-CW26EZZO.js +16 -0
- package/dist/work-launch-YNNCKIH3.js +6 -0
- package/dist/work-pr-cmd-W2QNMWXP.js +115 -0
- package/package.json +58 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { normalizePath } from './chunk-KFYJV2ZG.js';
|
|
4
|
+
import './chunk-Z5GEFMRW.js';
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
10
|
+
var GREEN = "\x1B[32m";
|
|
11
|
+
var YELLOW = "\x1B[33m";
|
|
12
|
+
var DIM = "\x1B[2m";
|
|
13
|
+
var RESET = "\x1B[0m";
|
|
14
|
+
var ImportRulerError = class extends Error {
|
|
15
|
+
};
|
|
16
|
+
function toRepoRelative(base, name) {
|
|
17
|
+
return normalizePath(`${base}/${name}`);
|
|
18
|
+
}
|
|
19
|
+
function discoverRulerDirs(repoRoot) {
|
|
20
|
+
const out = [];
|
|
21
|
+
walk(repoRoot, repoRoot, "", out);
|
|
22
|
+
return out.sort();
|
|
23
|
+
}
|
|
24
|
+
function walk(dirAbs, repoRoot, dirRel, out) {
|
|
25
|
+
let entries;
|
|
26
|
+
try {
|
|
27
|
+
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (entry.isSymbolicLink() || !entry.isDirectory()) continue;
|
|
33
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
34
|
+
const childRel = dirRel === "" ? entry.name : toRepoRelative(dirRel, entry.name);
|
|
35
|
+
if (entry.name === ".ruler") {
|
|
36
|
+
out.push(childRel);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
walk(join(dirAbs, entry.name), repoRoot, childRel, out);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function collectFilesRecursively(dirAbs, dirRel) {
|
|
43
|
+
const out = [];
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry.isSymbolicLink()) continue;
|
|
52
|
+
const childRel = toRepoRelative(dirRel, entry.name);
|
|
53
|
+
const childAbs = join(dirAbs, entry.name);
|
|
54
|
+
if (entry.isDirectory()) out.push(...collectFilesRecursively(childAbs, childRel));
|
|
55
|
+
else if (entry.isFile()) out.push(childRel);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function catalogRulerDir(repoRoot, rulerDirRel) {
|
|
60
|
+
const abs = join(repoRoot, rulerDirRel);
|
|
61
|
+
const catalog = { otherMd: [], unmapped: [] };
|
|
62
|
+
const mdFilenames = [];
|
|
63
|
+
let entries;
|
|
64
|
+
try {
|
|
65
|
+
entries = readdirSync(abs, { withFileTypes: true });
|
|
66
|
+
} catch {
|
|
67
|
+
entries = [];
|
|
68
|
+
}
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (entry.isSymbolicLink()) continue;
|
|
71
|
+
const childRel = toRepoRelative(rulerDirRel, entry.name);
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
catalog.unmapped.push(...collectFilesRecursively(join(abs, entry.name), childRel));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!entry.isFile()) continue;
|
|
77
|
+
if (entry.name === "AGENTS.md") catalog.agentsMd = readFileSync(join(abs, entry.name), "utf8");
|
|
78
|
+
else if (entry.name === "mcp.json") catalog.mcpJson = readFileSync(join(abs, entry.name), "utf8");
|
|
79
|
+
else if (entry.name === "ruler.toml") catalog.rulerTomlPath = childRel;
|
|
80
|
+
else if (entry.name.endsWith(".md")) mdFilenames.push(entry.name);
|
|
81
|
+
else catalog.unmapped.push(childRel);
|
|
82
|
+
}
|
|
83
|
+
catalog.otherMd = mdFilenames.sort((a, b) => a.localeCompare(b)).map((filename) => ({ filename, content: readFileSync(join(abs, filename), "utf8") }));
|
|
84
|
+
return catalog;
|
|
85
|
+
}
|
|
86
|
+
function scopeOf(rulerDirRel) {
|
|
87
|
+
if (rulerDirRel === ".ruler") return "";
|
|
88
|
+
return rulerDirRel.slice(0, rulerDirRel.length - "/.ruler".length);
|
|
89
|
+
}
|
|
90
|
+
function outerlayerDirFor(scopePath) {
|
|
91
|
+
return scopePath === "" ? ".outerlayer" : `${scopePath}/.outerlayer`;
|
|
92
|
+
}
|
|
93
|
+
function writeOutFile(repoRoot, relPath, content) {
|
|
94
|
+
const abs = join(repoRoot, relPath);
|
|
95
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
96
|
+
writeFileSync(abs, content);
|
|
97
|
+
}
|
|
98
|
+
function renderSuccess(scopes, warnings) {
|
|
99
|
+
const warningSuffix = warnings.length > 0 ? `, ${warnings.length} warning${warnings.length === 1 ? "" : "s"}` : "";
|
|
100
|
+
const lines = [`${GREEN}\u2713${RESET} imported ${scopes.length} .ruler/ scope${scopes.length === 1 ? "" : "s"}${warningSuffix}`];
|
|
101
|
+
for (const s of scopes) {
|
|
102
|
+
const written = [s.agentsMdWritten, s.mcpWritten].filter((p) => p !== void 0);
|
|
103
|
+
lines.push(` ${DIM}${s.scopePath === "" ? "(root)" : s.scopePath}${RESET} \u2192 ${written.join(", ") || "nothing to import"}`);
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function runImportRuler(opts = {}) {
|
|
108
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
109
|
+
if (!existsSync(cwd)) throw new ImportRulerError(`no such directory: ${cwd}`);
|
|
110
|
+
const rulerDirs = discoverRulerDirs(cwd);
|
|
111
|
+
if (rulerDirs.length === 0) {
|
|
112
|
+
const output2 = opts.json ? JSON.stringify({ scopes: [], warnings: [] }) : `${DIM}no .ruler/ directories found \u2014 nothing to import${RESET}`;
|
|
113
|
+
if (!opts.quiet) process.stdout.write(output2 + "\n");
|
|
114
|
+
return { scopes: [], warnings: [], output: output2, exitCode: 0 };
|
|
115
|
+
}
|
|
116
|
+
const conflicts = rulerDirs.map((rel) => outerlayerDirFor(scopeOf(rel))).filter((outerlayerRel) => existsSync(join(cwd, outerlayerRel)));
|
|
117
|
+
if (conflicts.length > 0) {
|
|
118
|
+
throw new ImportRulerError(
|
|
119
|
+
`refusing to import \u2014 .outerlayer/ already exists at: ${conflicts.join(", ")} (never overwritten; remove it first to replace it)`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const scopes = [];
|
|
123
|
+
const warnings = [];
|
|
124
|
+
for (const rulerDirRel of rulerDirs) {
|
|
125
|
+
const scopePath = scopeOf(rulerDirRel);
|
|
126
|
+
const outerlayerRel = outerlayerDirFor(scopePath);
|
|
127
|
+
const catalog = catalogRulerDir(cwd, rulerDirRel);
|
|
128
|
+
const scope = { scopePath };
|
|
129
|
+
if (catalog.agentsMd !== void 0 || catalog.otherMd.length > 0) {
|
|
130
|
+
let content = catalog.agentsMd ?? "";
|
|
131
|
+
for (const { filename, content: mdContent } of catalog.otherMd) {
|
|
132
|
+
content += `
|
|
133
|
+
|
|
134
|
+
<!-- imported from .ruler/${filename} -->
|
|
135
|
+
|
|
136
|
+
${mdContent}`;
|
|
137
|
+
}
|
|
138
|
+
const agentsPath = toRepoRelative(outerlayerRel, "AGENTS.md");
|
|
139
|
+
writeOutFile(cwd, agentsPath, content);
|
|
140
|
+
scope.agentsMdWritten = agentsPath;
|
|
141
|
+
}
|
|
142
|
+
if (catalog.mcpJson !== void 0) {
|
|
143
|
+
const mcpPath = toRepoRelative(outerlayerRel, "mcp.json");
|
|
144
|
+
writeOutFile(cwd, mcpPath, catalog.mcpJson);
|
|
145
|
+
scope.mcpWritten = mcpPath;
|
|
146
|
+
}
|
|
147
|
+
if (catalog.rulerTomlPath !== void 0) {
|
|
148
|
+
warnings.push({
|
|
149
|
+
code: "ruler_toml_not_imported",
|
|
150
|
+
path: catalog.rulerTomlPath,
|
|
151
|
+
message: `${catalog.rulerTomlPath} was not imported \u2014 target selection now lives in .outerlayer/config.json; per-tool config is superseded`
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
for (const path of catalog.unmapped) {
|
|
155
|
+
warnings.push({ code: "unmapped_file", path, message: `${path} has no .outerlayer/ equivalent \u2014 not imported` });
|
|
156
|
+
}
|
|
157
|
+
scopes.push(scope);
|
|
158
|
+
}
|
|
159
|
+
warnings.sort((a, b) => a.path.localeCompare(b.path));
|
|
160
|
+
const output = opts.json ? JSON.stringify({ scopes, warnings }) : renderSuccess(scopes, warnings);
|
|
161
|
+
if (!opts.quiet) {
|
|
162
|
+
if (opts.json) {
|
|
163
|
+
process.stdout.write(output + "\n");
|
|
164
|
+
} else {
|
|
165
|
+
process.stdout.write(output + "\n");
|
|
166
|
+
for (const w of warnings) process.stderr.write(` ${YELLOW}!${RESET} ${w.path}: ${w.message}
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { scopes, warnings, output, exitCode: 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { ImportRulerError, runImportRuler };
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
var argv = process.argv.slice(2);
|
|
8
|
+
if (argv[0] === "hook") {
|
|
9
|
+
const { runHookFast } = await import('./hook-fast-CBW4ZETX.js');
|
|
10
|
+
await runHookFast(argv[1]);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
if (argv[0] === "hook-wrap") {
|
|
14
|
+
const { runHookWrapFast, parseHookWrapArgs } = await import('./hook-wrap-fast-36WGE6EX.js');
|
|
15
|
+
const { id, captureOutput } = parseHookWrapArgs(argv.slice(1));
|
|
16
|
+
const code = await runHookWrapFast(id, { captureOutput });
|
|
17
|
+
process.exit(code);
|
|
18
|
+
}
|
|
19
|
+
if (argv[0] === "statusline") {
|
|
20
|
+
const { runStatuslineFast } = await import('./statusline-fast-Z5U5CEC6.js');
|
|
21
|
+
await runStatuslineFast(argv.slice(1));
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
var { runCli } = await import('./cli-3JYGL2OB.js');
|
|
25
|
+
await runCli(process.argv);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { runnerDir, RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-JJP7YLMN.js';
|
|
4
|
+
import './chunk-Z5GEFMRW.js';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, chmodSync } from 'fs';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
var RunnerInitError = class extends Error {
|
|
10
|
+
};
|
|
11
|
+
var PLACEHOLDER_COMMAND = "echo 'edit runner.commands in this config file: it must start your agent on item $OUTERLAYER_WORK' >&2; exit 1";
|
|
12
|
+
var PROVISION = `#!/bin/sh
|
|
13
|
+
# provision.sh \u2014 makes the place this job runs in.
|
|
14
|
+
# Runs in $OUTERLAYER_JOB_DIR, before the command. Exit 75 to decline the
|
|
15
|
+
# item for now; any other non-zero exit fails the job.
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
if ! df -Pk . | awk 'NR==2 {exit ($4 < 6*1024*1024)}'; then
|
|
19
|
+
exit 75 # not enough disk free right now
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
repo=$(outerlayer work status --item "$OUTERLAYER_WORK" --json \\
|
|
23
|
+
| jq -r '.trackerKey | split("#")[0]')
|
|
24
|
+
workdir="$OUTERLAYER_JOB_DIR/work"
|
|
25
|
+
git clone --depth 1 "https://github.com/$repo.git" "$workdir"
|
|
26
|
+
|
|
27
|
+
echo "workdir=$workdir" > "$OUTERLAYER_JOB_DIR/provision.out"
|
|
28
|
+
`;
|
|
29
|
+
var CLEANUP = `#!/bin/sh
|
|
30
|
+
# cleanup.sh \u2014 runs after the command, however it ended, and on recovery.
|
|
31
|
+
# $OUTERLAYER_OUTCOME is the attempt's fate, not a verdict on the work: a
|
|
32
|
+
# command that ran and exited says "ok" whatever its exit status.
|
|
33
|
+
workdir="$OUTERLAYER_JOB_DIR/work"
|
|
34
|
+
|
|
35
|
+
if grep -q "pull request opened" "$OUTERLAYER_LOG" 2>/dev/null; then
|
|
36
|
+
rm -rf "$workdir"
|
|
37
|
+
else
|
|
38
|
+
echo "kept $workdir for inspection (outcome: $OUTERLAYER_OUTCOME)"
|
|
39
|
+
fi
|
|
40
|
+
`;
|
|
41
|
+
var REPORT = `#!/bin/sh
|
|
42
|
+
# report.sh \u2014 optional. Runs once at the start of a job and once at the end.
|
|
43
|
+
# At the start there is no working directory yet and $OUTERLAYER_LOG is
|
|
44
|
+
# unset; the command's log is always $OUTERLAYER_JOB_DIR/command.log.
|
|
45
|
+
case "$OUTERLAYER_PHASE" in
|
|
46
|
+
start) echo "starting #$OUTERLAYER_WORK" ;;
|
|
47
|
+
end) echo "#$OUTERLAYER_WORK ended: $OUTERLAYER_OUTCOME" ;;
|
|
48
|
+
esac
|
|
49
|
+
`;
|
|
50
|
+
function runRunnerInit(configPath) {
|
|
51
|
+
if (!existsSync(configPath)) {
|
|
52
|
+
throw new RunnerInitError(
|
|
53
|
+
`${configPath}: no config file. Run \`outerlayer login --url <your gateway>\` first, then this command.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
let config;
|
|
57
|
+
try {
|
|
58
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
throw new RunnerInitError(`${configPath}: not valid JSON (${err instanceof Error ? err.message : String(err)})`);
|
|
61
|
+
}
|
|
62
|
+
if (config.runner !== void 0) {
|
|
63
|
+
throw new RunnerInitError(`${configPath}: a runner block is already here. Edit it, or remove it and run this again.`);
|
|
64
|
+
}
|
|
65
|
+
const hooksDir = join(runnerDir(configPath), "hooks");
|
|
66
|
+
mkdirSync(hooksDir, { recursive: true, mode: RUNNER_DIR_MODE });
|
|
67
|
+
const written = [];
|
|
68
|
+
for (const [name, body] of [
|
|
69
|
+
["provision.sh", PROVISION],
|
|
70
|
+
["cleanup.sh", CLEANUP],
|
|
71
|
+
["report.sh", REPORT]
|
|
72
|
+
]) {
|
|
73
|
+
const path = join(hooksDir, name);
|
|
74
|
+
if (existsSync(path)) continue;
|
|
75
|
+
writeFileSync(path, body, { mode: 448 });
|
|
76
|
+
chmodSync(path, 448);
|
|
77
|
+
written.push(path);
|
|
78
|
+
}
|
|
79
|
+
config.runner = {
|
|
80
|
+
queues: ["implement", "amend"],
|
|
81
|
+
concurrency: 1,
|
|
82
|
+
pollSeconds: 60,
|
|
83
|
+
timeLimitMinutes: 480,
|
|
84
|
+
idleLimitMinutes: 30,
|
|
85
|
+
// A placeholder that fails loudly. A command line that quietly did
|
|
86
|
+
// nothing would claim an item, run, exit zero, and record `ok` —
|
|
87
|
+
// every attempt reading as a success that produced nothing.
|
|
88
|
+
commands: {
|
|
89
|
+
implement: PLACEHOLDER_COMMAND,
|
|
90
|
+
amend: PLACEHOLDER_COMMAND
|
|
91
|
+
},
|
|
92
|
+
hooks: {
|
|
93
|
+
provision: join(hooksDir, "provision.sh"),
|
|
94
|
+
cleanup: join(hooksDir, "cleanup.sh")
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
98
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
99
|
+
`, { mode: RUNNER_FILE_MODE });
|
|
100
|
+
return { configPath, hooksDir, hooks: written };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { RunnerInitError, runRunnerInit };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { writeCloudConfig, readCloudConfig, cloudConfigPath } from './chunk-TFUIDMOB.js';
|
|
4
|
+
import { homeFromEnv } from './chunk-VNVZDWO3.js';
|
|
5
|
+
import './chunk-Z5GEFMRW.js';
|
|
6
|
+
import { createInterface } from 'readline';
|
|
7
|
+
import { Writable } from 'stream';
|
|
8
|
+
|
|
9
|
+
createRequire(import.meta.url);
|
|
10
|
+
var LoginError = class extends Error {
|
|
11
|
+
};
|
|
12
|
+
function runLogin(opts) {
|
|
13
|
+
const apiKey = opts.apiKey.trim();
|
|
14
|
+
if (apiKey.length === 0) throw new LoginError("no API key given \u2014 pipe it on stdin or enter it at the prompt");
|
|
15
|
+
if (/\s/.test(apiKey)) throw new LoginError("the API key must be a single token with no spaces or line breaks");
|
|
16
|
+
const url = opts.url?.trim() || void 0;
|
|
17
|
+
const appId = opts.appId?.trim() || void 0;
|
|
18
|
+
const home = opts.home ?? homeFromEnv();
|
|
19
|
+
writeCloudConfig(
|
|
20
|
+
{
|
|
21
|
+
apiKey,
|
|
22
|
+
...url ? { url } : {},
|
|
23
|
+
...appId ? { appId } : {}
|
|
24
|
+
},
|
|
25
|
+
home
|
|
26
|
+
);
|
|
27
|
+
const saved = readCloudConfig(home);
|
|
28
|
+
return { path: cloudConfigPath(home), url: saved.url, appId: saved.appId };
|
|
29
|
+
}
|
|
30
|
+
async function readAllStdin(stdin) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
for await (const chunk of stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
33
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
34
|
+
}
|
|
35
|
+
function promptHidden(stdin, stdout, question) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const muted = new Writable({
|
|
38
|
+
write(_chunk, _encoding, callback) {
|
|
39
|
+
callback();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const rl = createInterface({ input: stdin, output: muted, terminal: true });
|
|
43
|
+
stdout.write(question);
|
|
44
|
+
rl.question("", (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
stdout.write("\n");
|
|
47
|
+
resolve(answer);
|
|
48
|
+
});
|
|
49
|
+
rl.on("error", reject);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function readApiKey(input = {}) {
|
|
53
|
+
const stdin = input.stdin ?? process.stdin;
|
|
54
|
+
const stdout = input.stdout ?? process.stdout;
|
|
55
|
+
if (!stdin.isTTY) return (await readAllStdin(stdin)).trim();
|
|
56
|
+
if (input.noInput) {
|
|
57
|
+
throw new LoginError('no API key on stdin and --no-input forbids prompting \u2014 pipe the key: echo "$KEY" | outerlayer login');
|
|
58
|
+
}
|
|
59
|
+
return (await promptHidden(stdin, stdout, "API key (input hidden): ")).trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { LoginError, readApiKey, runLogin };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { jobsDir } from './chunk-JJP7YLMN.js';
|
|
4
|
+
import './chunk-Z5GEFMRW.js';
|
|
5
|
+
import { existsSync, watch, statSync, createReadStream } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
createRequire(import.meta.url);
|
|
9
|
+
var RunnerLogsError = class extends Error {
|
|
10
|
+
};
|
|
11
|
+
var STAGE_LOG = {
|
|
12
|
+
provision: "provision.log",
|
|
13
|
+
command: "command.log",
|
|
14
|
+
sync: "command.log",
|
|
15
|
+
cleanup: "cleanup.log",
|
|
16
|
+
report: "report.log"
|
|
17
|
+
};
|
|
18
|
+
function newestAttempt(jobs, item) {
|
|
19
|
+
return jobs.filter(({ job }) => Number(job.item) === item).sort((a, b) => b.job.startedAt.localeCompare(a.job.startedAt))[0];
|
|
20
|
+
}
|
|
21
|
+
function logPathFor(configPath, dirName, job) {
|
|
22
|
+
const dirPath = join(jobsDir(configPath), dirName);
|
|
23
|
+
const finished = job.outcome !== void 0 || job.endedAt !== void 0;
|
|
24
|
+
const byStage = finished || job.stage === void 0 ? "command.log" : STAGE_LOG[job.stage];
|
|
25
|
+
const candidate = join(dirPath, byStage);
|
|
26
|
+
if (existsSync(candidate)) return candidate;
|
|
27
|
+
const command = join(dirPath, "command.log");
|
|
28
|
+
return existsSync(command) ? command : candidate;
|
|
29
|
+
}
|
|
30
|
+
async function tail(path, follow, out = process.stdout) {
|
|
31
|
+
if (!existsSync(path)) throw new RunnerLogsError(`${path}: no log yet`);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
const pump = () => new Promise((resolve, reject) => {
|
|
34
|
+
const size = statSync(path).size;
|
|
35
|
+
if (size <= offset) {
|
|
36
|
+
resolve();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const stream = createReadStream(path, { start: offset, end: size - 1 });
|
|
40
|
+
stream.on("data", (chunk) => out.write(chunk));
|
|
41
|
+
stream.on("error", reject);
|
|
42
|
+
stream.on("end", () => {
|
|
43
|
+
offset = size;
|
|
44
|
+
resolve();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
await pump();
|
|
48
|
+
if (!follow) return;
|
|
49
|
+
await new Promise((resolve, reject) => {
|
|
50
|
+
const watcher = watch(path, () => {
|
|
51
|
+
void pump().catch((err) => {
|
|
52
|
+
watcher.close();
|
|
53
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
process.on("SIGINT", () => {
|
|
57
|
+
watcher.close();
|
|
58
|
+
resolve();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { RunnerLogsError, logPathFor, newestAttempt, tail };
|