@khanglvm/outline-cli 0.1.5 → 0.1.6

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.
@@ -1,353 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import fsp from "node:fs/promises";
5
- import path from "node:path";
6
- import { spawnSync } from "node:child_process";
7
- import { fileURLToPath } from "node:url";
8
-
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
- const repoRoot = path.resolve(__dirname, "..");
11
-
12
- function usage() {
13
- process.stdout.write(
14
- [
15
- "Usage:",
16
- " node ./scripts/release.mjs [--bump <patch|minor|major|prepatch|preminor|premajor|prerelease> | --version <x.y.z>]",
17
- " [--tag <dist-tag>] [--otp <code>] [--access <public|restricted>]",
18
- " [--no-publish] [--no-push] [--skip-check] [--skip-test] [--allow-dirty]",
19
- "",
20
- "Examples:",
21
- " npm run release -- --bump patch",
22
- " npm run release -- --version 0.2.0",
23
- " npm run release -- --bump minor --tag next --no-publish --no-push",
24
- "",
25
- ].join("\n")
26
- );
27
- }
28
-
29
- function parseArgs(argv) {
30
- let sawBump = false;
31
- let sawVersion = false;
32
- function requireValue(flag, index) {
33
- const value = argv[index + 1];
34
- if (!value || value.startsWith("--")) {
35
- throw new Error(`Missing value for ${flag}`);
36
- }
37
- return value;
38
- }
39
- const opts = {
40
- bump: "patch",
41
- version: null,
42
- tag: "latest",
43
- otp: null,
44
- access: "public",
45
- publish: true,
46
- push: true,
47
- skipCheck: false,
48
- skipTest: false,
49
- allowDirty: false,
50
- changelogPath: path.join(repoRoot, "CHANGELOG.md"),
51
- };
52
-
53
- for (let i = 0; i < argv.length; i += 1) {
54
- const arg = argv[i];
55
- if (arg === "--help" || arg === "-h") {
56
- opts.help = true;
57
- continue;
58
- }
59
- if (arg === "--bump") {
60
- opts.bump = requireValue("--bump", i);
61
- sawBump = true;
62
- i += 1;
63
- continue;
64
- }
65
- if (arg === "--version") {
66
- opts.version = requireValue("--version", i);
67
- sawVersion = true;
68
- i += 1;
69
- continue;
70
- }
71
- if (arg === "--tag") {
72
- opts.tag = requireValue("--tag", i);
73
- i += 1;
74
- continue;
75
- }
76
- if (arg === "--otp") {
77
- opts.otp = requireValue("--otp", i);
78
- i += 1;
79
- continue;
80
- }
81
- if (arg === "--access") {
82
- opts.access = requireValue("--access", i);
83
- i += 1;
84
- continue;
85
- }
86
- if (arg === "--changelog") {
87
- opts.changelogPath = path.resolve(repoRoot, requireValue("--changelog", i));
88
- i += 1;
89
- continue;
90
- }
91
- if (arg === "--no-publish") {
92
- opts.publish = false;
93
- continue;
94
- }
95
- if (arg === "--no-push") {
96
- opts.push = false;
97
- continue;
98
- }
99
- if (arg === "--skip-check") {
100
- opts.skipCheck = true;
101
- continue;
102
- }
103
- if (arg === "--skip-test") {
104
- opts.skipTest = true;
105
- continue;
106
- }
107
- if (arg === "--allow-dirty") {
108
- opts.allowDirty = true;
109
- continue;
110
- }
111
- throw new Error(`Unknown argument: ${arg}`);
112
- }
113
-
114
- if (sawVersion && sawBump) {
115
- throw new Error("Use either --version or --bump, not both.");
116
- }
117
-
118
- if (!opts.version && !opts.bump) {
119
- throw new Error("Missing version strategy. Provide --version <x.y.z> or --bump <type>.");
120
- }
121
-
122
- const validBumps = new Set(["patch", "minor", "major", "prepatch", "preminor", "premajor", "prerelease"]);
123
- if (!opts.version && !validBumps.has(opts.bump)) {
124
- throw new Error(`Invalid --bump value: ${opts.bump}`);
125
- }
126
-
127
- return opts;
128
- }
129
-
130
- function loadDotEnvFileIfPresent(filePath) {
131
- if (!fs.existsSync(filePath)) {
132
- return;
133
- }
134
- const raw = fs.readFileSync(filePath, "utf8");
135
- for (const line of raw.split(/\r?\n/)) {
136
- const trimmed = line.trim();
137
- if (!trimmed || trimmed.startsWith("#")) {
138
- continue;
139
- }
140
- const i = trimmed.indexOf("=");
141
- if (i <= 0) {
142
- continue;
143
- }
144
- const key = trimmed.slice(0, i).trim();
145
- let value = trimmed.slice(i + 1).trim();
146
- if (
147
- (value.startsWith('"') && value.endsWith('"')) ||
148
- (value.startsWith("'") && value.endsWith("'"))
149
- ) {
150
- value = value.slice(1, -1);
151
- }
152
- if (process.env[key] == null) {
153
- process.env[key] = value;
154
- }
155
- }
156
- }
157
-
158
- function run(cmd, args, options = {}) {
159
- const capture = options.capture === true;
160
- const display = `$ ${cmd} ${args.join(" ")}`;
161
- process.stdout.write(`${display}\n`);
162
- const res = spawnSync(cmd, args, {
163
- cwd: repoRoot,
164
- encoding: "utf8",
165
- stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
166
- env: { ...process.env, ...(options.env || {}) },
167
- });
168
- if (res.status !== 0) {
169
- const extra = capture ? `\n${(res.stdout || "").trim()}\n${(res.stderr || "").trim()}` : "";
170
- throw new Error(`Command failed (${res.status}): ${display}${extra}`);
171
- }
172
- return capture ? (res.stdout || "").trim() : "";
173
- }
174
-
175
- function readJson(filePath) {
176
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
177
- }
178
-
179
- function currentDateIso() {
180
- const now = new Date();
181
- const yyyy = String(now.getUTCFullYear());
182
- const mm = String(now.getUTCMonth() + 1).padStart(2, "0");
183
- const dd = String(now.getUTCDate()).padStart(2, "0");
184
- return `${yyyy}-${mm}-${dd}`;
185
- }
186
-
187
- function getLatestSemverTag() {
188
- const tags = run("git", ["tag", "--list", "v*.*.*", "--sort=-version:refname"], { capture: true })
189
- .split(/\r?\n/)
190
- .map((line) => line.trim())
191
- .filter(Boolean);
192
- return tags[0] || null;
193
- }
194
-
195
- function listCommitSubjectsSince(tag) {
196
- const range = tag ? `${tag}..HEAD` : "HEAD";
197
- const out = run("git", ["log", range, "--pretty=format:%s (%h)"], { capture: true });
198
- const rows = out
199
- .split(/\r?\n/)
200
- .map((line) => line.trim())
201
- .filter(Boolean);
202
- return rows.length > 0 ? rows : ["Maintenance release."];
203
- }
204
-
205
- async function updateChangelog({ changelogPath, nextVersion, previousTag }) {
206
- let content = "";
207
- if (fs.existsSync(changelogPath)) {
208
- content = await fsp.readFile(changelogPath, "utf8");
209
- } else {
210
- content = "# Changelog\n\n";
211
- }
212
-
213
- if (!content.startsWith("# Changelog")) {
214
- content = `# Changelog\n\n${content.trimStart()}`;
215
- }
216
-
217
- const existingHeading = new RegExp(`^##\\s+${nextVersion}\\s+-\\s+`, "m");
218
- if (existingHeading.test(content)) {
219
- throw new Error(`CHANGELOG already has an entry for ${nextVersion}`);
220
- }
221
-
222
- const commits = listCommitSubjectsSince(previousTag);
223
- const scopeLine = previousTag ? `- Changes since \`${previousTag}\`.` : "- Initial tagged release notes.";
224
- const commitLines = commits.map((line) => `- ${line}`).join("\n");
225
- const entry = `## ${nextVersion} - ${currentDateIso()}\n\n${scopeLine}\n${commitLines}\n`;
226
-
227
- const header = "# Changelog";
228
- const body = content.slice(header.length).trimStart();
229
- const next = `${header}\n\n${entry}\n${body}`.replace(/\n{3,}/g, "\n\n");
230
- await fsp.writeFile(changelogPath, `${next.trimEnd()}\n`, "utf8");
231
- }
232
-
233
- function ensureBuildKey() {
234
- loadDotEnvFileIfPresent(path.join(repoRoot, ".env.local"));
235
- const key = process.env.OUTLINE_ENTRY_BUILD_KEY;
236
- if (!key || String(key).trim().length < 24) {
237
- throw new Error(
238
- "OUTLINE_ENTRY_BUILD_KEY is required for release integrity binding. Set it in environment or .env.local."
239
- );
240
- }
241
- }
242
-
243
- function ensureGitClean(allowDirty) {
244
- if (allowDirty) {
245
- return;
246
- }
247
- const dirty = run("git", ["status", "--porcelain"], { capture: true });
248
- if (dirty.trim()) {
249
- throw new Error("Git working tree is not clean. Commit/stash changes or pass --allow-dirty.");
250
- }
251
- }
252
-
253
- function ensureTagDoesNotExist(tagName) {
254
- const out = run("git", ["tag", "--list", tagName], { capture: true });
255
- if (out.trim() === tagName) {
256
- throw new Error(`Git tag already exists: ${tagName}`);
257
- }
258
- }
259
-
260
- function bumpVersion(opts) {
261
- const arg = opts.version || opts.bump;
262
- const out = run("npm", ["version", arg, "--no-git-tag-version"], { capture: true });
263
- const newVersion = out.trim().replace(/^v/, "");
264
- if (!newVersion) {
265
- throw new Error("Unable to resolve next version from npm version output.");
266
- }
267
- return newVersion;
268
- }
269
-
270
- function publishPackage(opts) {
271
- const args = ["publish", "--access", opts.access];
272
- if (opts.tag && opts.tag !== "latest") {
273
- args.push("--tag", opts.tag);
274
- }
275
- if (opts.otp) {
276
- args.push("--otp", opts.otp);
277
- }
278
- run("npm", args);
279
- }
280
-
281
- async function main() {
282
- const opts = parseArgs(process.argv.slice(2));
283
- if (opts.help) {
284
- usage();
285
- return;
286
- }
287
-
288
- ensureBuildKey();
289
- ensureGitClean(opts.allowDirty);
290
-
291
- const packageJsonPath = path.join(repoRoot, "package.json");
292
- const beforePkg = readJson(packageJsonPath);
293
- const previousTag = getLatestSemverTag();
294
-
295
- const branch = run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { capture: true });
296
- if (!branch || branch === "HEAD") {
297
- throw new Error("Detached HEAD is not supported for release. Check out a branch.");
298
- }
299
-
300
- const nextVersion = bumpVersion(opts);
301
- const releaseTag = `v${nextVersion}`;
302
- ensureTagDoesNotExist(releaseTag);
303
- await updateChangelog({
304
- changelogPath: opts.changelogPath,
305
- nextVersion,
306
- previousTag,
307
- });
308
-
309
- run("npm", ["run", "integrity:refresh"]);
310
- if (!opts.skipCheck) {
311
- run("npm", ["run", "check"]);
312
- }
313
- if (!opts.skipTest) {
314
- run("npm", ["test"]);
315
- }
316
- run("npm", ["pack", "--dry-run"]);
317
-
318
- run("git", ["add", "-A"]);
319
- run("git", ["commit", "-m", `chore(release): v${nextVersion}`]);
320
- run("git", ["tag", "-a", releaseTag, "-m", releaseTag]);
321
-
322
- if (opts.publish) {
323
- publishPackage(opts);
324
- }
325
-
326
- if (opts.push) {
327
- run("git", ["push", "origin", branch]);
328
- run("git", ["push", "origin", releaseTag]);
329
- }
330
-
331
- const afterPkg = readJson(packageJsonPath);
332
- process.stdout.write(
333
- `${JSON.stringify(
334
- {
335
- ok: true,
336
- package: beforePkg.name,
337
- previousVersion: beforePkg.version,
338
- nextVersion: afterPkg.version,
339
- releaseTag,
340
- published: opts.publish,
341
- pushed: opts.push,
342
- branch,
343
- },
344
- null,
345
- 2
346
- )}\n`
347
- );
348
- }
349
-
350
- main().catch((err) => {
351
- process.stderr.write(`${err?.stack || err}\n`);
352
- process.exit(1);
353
- });
@@ -1,157 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import fs from "node:fs/promises";
4
- import os from "node:os";
5
- import path from "node:path";
6
-
7
- import { CliError } from "../src/errors.js";
8
- import {
9
- assertPerformAction,
10
- consumeDocumentDeleteReadReceipt,
11
- getActionGateStorePath,
12
- getDocumentDeleteReadReceipt,
13
- isLikelyMutatingMethod,
14
- issueDocumentDeleteReadReceipt,
15
- } from "../src/action-gate.js";
16
-
17
- test("assertPerformAction blocks when performAction is not true", () => {
18
- assert.throws(
19
- () =>
20
- assertPerformAction({}, {
21
- tool: "documents.update",
22
- action: "update a document",
23
- }),
24
- (err) => {
25
- assert.ok(err instanceof CliError);
26
- assert.equal(err.details?.code, "ACTION_GATED");
27
- return true;
28
- }
29
- );
30
-
31
- assert.doesNotThrow(() =>
32
- assertPerformAction({ performAction: true }, {
33
- tool: "documents.update",
34
- action: "update a document",
35
- })
36
- );
37
- });
38
-
39
- test("isLikelyMutatingMethod treats import/export paths as mutating", () => {
40
- assert.equal(isLikelyMutatingMethod("documents.import"), true);
41
- assert.equal(isLikelyMutatingMethod("documents.importFile"), true);
42
- assert.equal(isLikelyMutatingMethod("/documents.export"), true);
43
- assert.equal(isLikelyMutatingMethod("fileOperations.delete"), true);
44
- assert.equal(isLikelyMutatingMethod("fileOperations.info"), false);
45
- });
46
-
47
- test("delete read receipt lifecycle: issue -> validate -> consume", async () => {
48
- const previousTmp = process.env.OUTLINE_CLI_TMP_DIR;
49
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "outline-cli-action-gate-"));
50
- process.env.OUTLINE_CLI_TMP_DIR = tmpDir;
51
-
52
- try {
53
- const issued = await issueDocumentDeleteReadReceipt({
54
- profileId: "test-profile",
55
- documentId: "doc-123",
56
- revision: 7,
57
- title: "Doc 123",
58
- ttlSeconds: 600,
59
- });
60
-
61
- assert.ok(typeof issued.token === "string" && issued.token.length > 0);
62
- assert.equal(issued.documentId, "doc-123");
63
- assert.equal(issued.revision, 7);
64
-
65
- const storePath = getActionGateStorePath();
66
- assert.ok(storePath.startsWith(path.resolve(tmpDir)));
67
-
68
- const fetched = await getDocumentDeleteReadReceipt({
69
- token: issued.token,
70
- profileId: "test-profile",
71
- documentId: "doc-123",
72
- });
73
- assert.equal(fetched.documentId, "doc-123");
74
- assert.equal(fetched.revision, 7);
75
-
76
- await assert.rejects(
77
- () =>
78
- getDocumentDeleteReadReceipt({
79
- token: issued.token,
80
- profileId: "test-profile",
81
- documentId: "doc-999",
82
- }),
83
- (err) => {
84
- assert.ok(err instanceof CliError);
85
- assert.equal(err.details?.code, "DELETE_READ_TOKEN_DOCUMENT_MISMATCH");
86
- return true;
87
- }
88
- );
89
-
90
- const consumed = await consumeDocumentDeleteReadReceipt(issued.token);
91
- assert.equal(consumed, true);
92
-
93
- await assert.rejects(
94
- () =>
95
- getDocumentDeleteReadReceipt({
96
- token: issued.token,
97
- profileId: "test-profile",
98
- documentId: "doc-123",
99
- }),
100
- (err) => {
101
- assert.ok(err instanceof CliError);
102
- assert.equal(err.details?.code, "DELETE_READ_TOKEN_INVALID");
103
- return true;
104
- }
105
- );
106
- } finally {
107
- if (previousTmp == null) {
108
- delete process.env.OUTLINE_CLI_TMP_DIR;
109
- } else {
110
- process.env.OUTLINE_CLI_TMP_DIR = previousTmp;
111
- }
112
- await fs.rm(tmpDir, { recursive: true, force: true });
113
- }
114
- });
115
-
116
- test("delete read receipt only accepts document-scoped token kind", async () => {
117
- const previousTmp = process.env.OUTLINE_CLI_TMP_DIR;
118
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "outline-cli-action-gate-kind-"));
119
- process.env.OUTLINE_CLI_TMP_DIR = tmpDir;
120
-
121
- try {
122
- const issued = await issueDocumentDeleteReadReceipt({
123
- profileId: "test-profile",
124
- documentId: "doc-123",
125
- revision: 7,
126
- title: "Doc 123",
127
- ttlSeconds: 600,
128
- });
129
-
130
- const storePath = getActionGateStorePath();
131
- const raw = await fs.readFile(storePath, "utf8");
132
- const parsed = JSON.parse(raw);
133
- parsed.receipts[issued.token].kind = "webhooks.delete.read";
134
- await fs.writeFile(storePath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
135
-
136
- await assert.rejects(
137
- () =>
138
- getDocumentDeleteReadReceipt({
139
- token: issued.token,
140
- profileId: "test-profile",
141
- documentId: "doc-123",
142
- }),
143
- (err) => {
144
- assert.ok(err instanceof CliError);
145
- assert.equal(err.details?.code, "DELETE_READ_TOKEN_INVALID");
146
- return true;
147
- }
148
- );
149
- } finally {
150
- if (previousTmp == null) {
151
- delete process.env.OUTLINE_CLI_TMP_DIR;
152
- } else {
153
- process.env.OUTLINE_CLI_TMP_DIR = previousTmp;
154
- }
155
- await fs.rm(tmpDir, { recursive: true, force: true });
156
- }
157
- });
@@ -1,144 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { getAgentSkillHelp, getQuickStartAgentHelp, listHelpSections } from "../src/agent-skills.js";
5
- import { CliError } from "../src/errors.js";
6
-
7
- test("listHelpSections exposes ai-skills section", () => {
8
- const sections = listHelpSections();
9
- assert.ok(Array.isArray(sections));
10
- assert.ok(sections.some((section) => section.id === "quick-start-agent"));
11
- assert.ok(sections.some((section) => section.id === "ai-skills"));
12
- });
13
-
14
- test("getQuickStartAgentHelp returns summary by default", () => {
15
- const payload = getQuickStartAgentHelp();
16
-
17
- assert.equal(payload.section, "quick-start-agent");
18
- assert.equal(payload.view, "summary");
19
- assert.ok(Array.isArray(payload.steps));
20
- assert.ok(payload.steps.length >= 4);
21
- assert.ok(
22
- payload.steps.some(
23
- (row) => Array.isArray(row.commands) && row.commands.includes("outline-cli --version")
24
- )
25
- );
26
- assert.ok(
27
- payload.steps.some(
28
- (row) =>
29
- typeof row.question === "string" &&
30
- row.question.toLowerCase().includes("install the outline-cli skill")
31
- )
32
- );
33
- assert.ok(
34
- payload.steps.some(
35
- (row) =>
36
- Array.isArray(row.commandTemplates) &&
37
- row.commandTemplates.some(
38
- (cmd) => cmd.includes("npx skills add") && cmd.includes("--skill outline-cli -y") && !cmd.includes("--agent")
39
- )
40
- )
41
- );
42
- assert.ok(
43
- payload.steps.some(
44
- (row) =>
45
- typeof row.question === "string" &&
46
- row.question.toLowerCase().includes("base url")
47
- )
48
- );
49
- assert.equal(payload.nextCommand, "outline-cli tools help quick-start-agent --view full");
50
- });
51
-
52
- test("getQuickStartAgentHelp returns full payload and validates view", () => {
53
- const payload = getQuickStartAgentHelp({ view: "full" });
54
- assert.equal(payload.section, "quick-start-agent");
55
- assert.equal(payload.view, "full");
56
- assert.ok(Array.isArray(payload.steps));
57
- assert.ok(payload.steps.some((row) => row.command === "outline-cli profile list"));
58
- assert.ok(
59
- payload.steps.some(
60
- (row) =>
61
- row.command &&
62
- row.command.includes("--auth-type apiKey") &&
63
- row.command.includes("--api-key")
64
- )
65
- );
66
- assert.ok(
67
- payload.steps.some(
68
- (row) =>
69
- Array.isArray(row.commandTemplates) &&
70
- row.commandTemplates.includes(
71
- "npx skills add https://github.com/khanglvm/skills --skill outline-cli -y"
72
- ) &&
73
- Array.isArray(row.decisionRules) &&
74
- row.decisionRules.some((rule) => rule.toLowerCase().includes("explicitly approves"))
75
- )
76
- );
77
- assert.ok(
78
- payload.steps.some(
79
- (row) =>
80
- row.apiKeySettingsUrlTemplate === "<base-url>/settings/api-and-apps" &&
81
- Array.isArray(row.apiKeyConfigTemplate) &&
82
- row.apiKeyConfigTemplate.length >= 3
83
- )
84
- );
85
- assert.ok(
86
- payload.steps.some(
87
- (row) =>
88
- row.minimumPromptCount >= 10 &&
89
- Array.isArray(row.naturalLanguagePrompts) &&
90
- row.naturalLanguagePrompts.length >= 10
91
- )
92
- );
93
- assert.ok(Array.isArray(payload.interactionRules));
94
-
95
- assert.throws(
96
- () => getQuickStartAgentHelp({ view: "compact" }),
97
- (err) => {
98
- assert.ok(err instanceof CliError);
99
- assert.equal(err.details?.code, "QUICK_START_HELP_INVALID_VIEW");
100
- return true;
101
- }
102
- );
103
- });
104
-
105
- test("getAgentSkillHelp returns summary guidance by default", () => {
106
- const payload = getAgentSkillHelp();
107
-
108
- assert.equal(payload.section, "ai-skills");
109
- assert.equal(payload.view, "summary");
110
- assert.ok(payload.totalSkills >= 10);
111
- assert.equal(payload.skills.length, payload.returnedSkills);
112
- assert.ok(payload.globalGuidance?.principles?.length >= 1);
113
- assert.ok(payload.skills.some((skill) => skill.id === "legacy_wiki_migration"));
114
- });
115
-
116
- test("getAgentSkillHelp filters by scenario and query", () => {
117
- const byScenario = getAgentSkillHelp({ scenario: "uc-19" });
118
- assert.equal(byScenario.returnedSkills, 1);
119
- assert.equal(byScenario.skills[0].id, "oauth_compliance_audit");
120
-
121
- const byQuery = getAgentSkillHelp({ query: "documents.import_file" });
122
- assert.ok(byQuery.skills.some((skill) => skill.id === "legacy_wiki_migration"));
123
- });
124
-
125
- test("getAgentSkillHelp returns full skill payload and validates view", () => {
126
- const payload = getAgentSkillHelp({
127
- skill: "template_pipeline_execution",
128
- view: "full",
129
- });
130
-
131
- assert.equal(payload.returnedSkills, 1);
132
- assert.equal(payload.skills[0].id, "template_pipeline_execution");
133
- assert.ok(Array.isArray(payload.skills[0].sequence));
134
- assert.ok(payload.skills[0].sequence.some((step) => step.tool === "templates.extract_placeholders"));
135
-
136
- assert.throws(
137
- () => getAgentSkillHelp({ view: "compact" }),
138
- (err) => {
139
- assert.ok(err instanceof CliError);
140
- assert.equal(err.details?.code, "AI_HELP_INVALID_VIEW");
141
- return true;
142
- }
143
- );
144
- });