wordspace 0.0.3 → 0.0.4
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/add.d.ts +1 -0
- package/dist/commands/add.js +40 -0
- package/dist/commands/init.js +3 -0
- package/dist/commands/search.d.ts +1 -0
- package/dist/commands/search.js +25 -0
- package/dist/index.js +14 -3
- package/dist/lib/prompt.d.ts +9 -0
- package/dist/lib/prompt.js +59 -0
- package/dist/lib/workflows.d.ts +10 -0
- package/dist/lib/workflows.js +55 -0
- package/dist/steps/fetch-workflows.js +26 -54
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function add(names: string[], force: boolean): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as log from "../lib/log.js";
|
|
4
|
+
import { listWorkflows, downloadWorkflow } from "../lib/workflows.js";
|
|
5
|
+
export async function add(names, force) {
|
|
6
|
+
if (names.length === 0) {
|
|
7
|
+
log.error("Specify at least one workflow name. Run `wordspace search` to browse.");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
let available;
|
|
11
|
+
try {
|
|
12
|
+
available = await listWorkflows();
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
log.error(`Could not fetch workflows: ${err.message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const workflowsDir = join(process.cwd(), "workflows");
|
|
19
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
20
|
+
let downloaded = 0;
|
|
21
|
+
for (const raw of names) {
|
|
22
|
+
const name = raw.endsWith(".prose") ? raw : `${raw}.prose`;
|
|
23
|
+
const entry = available.find((w) => w.name === name);
|
|
24
|
+
if (!entry) {
|
|
25
|
+
log.warn(`Workflow not found: ${name}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const ok = await downloadWorkflow(entry, workflowsDir, force);
|
|
30
|
+
if (ok)
|
|
31
|
+
downloaded++;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
log.warn(`Failed to download ${name}: ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (downloaded > 0) {
|
|
38
|
+
log.success(`Downloaded ${downloaded} workflow(s) to workflows/`);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function search(query?: string): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as log from "../lib/log.js";
|
|
2
|
+
import { listWorkflows } from "../lib/workflows.js";
|
|
3
|
+
export async function search(query) {
|
|
4
|
+
let workflows;
|
|
5
|
+
try {
|
|
6
|
+
workflows = await listWorkflows();
|
|
7
|
+
}
|
|
8
|
+
catch (err) {
|
|
9
|
+
log.error(`Could not fetch workflows: ${err.message}`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
if (query) {
|
|
13
|
+
const lower = query.toLowerCase();
|
|
14
|
+
workflows = workflows.filter((w) => w.name.toLowerCase().includes(lower));
|
|
15
|
+
}
|
|
16
|
+
if (workflows.length === 0) {
|
|
17
|
+
log.info(query ? `No workflows matching "${query}"` : "No workflows found");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
log.info(`${workflows.length} workflow(s) available${query ? ` matching "${query}"` : ""}:\n`);
|
|
21
|
+
for (const w of workflows) {
|
|
22
|
+
console.log(` ${w.name.replace(/\.prose$/, "")}`);
|
|
23
|
+
}
|
|
24
|
+
console.log(`\nRun ${`wordspace add <name>`} to download a workflow.`);
|
|
25
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { init } from "./commands/init.js";
|
|
3
|
+
import { search } from "./commands/search.js";
|
|
4
|
+
import { add } from "./commands/add.js";
|
|
3
5
|
import * as log from "./lib/log.js";
|
|
4
|
-
const VERSION = "0.0.
|
|
6
|
+
const VERSION = "0.0.4";
|
|
5
7
|
const HELP = `
|
|
6
8
|
Usage: wordspace <command> [options]
|
|
7
9
|
|
|
8
10
|
Commands:
|
|
9
11
|
init Bootstrap a new wordspace project
|
|
12
|
+
search [q] List available workflows (optionally filter by query)
|
|
13
|
+
add <name> Download specific workflow(s) by name
|
|
10
14
|
|
|
11
15
|
Options:
|
|
12
|
-
--force Re-run all steps
|
|
16
|
+
--force Re-run all steps / overwrite existing files
|
|
13
17
|
--help Show this help message
|
|
14
18
|
--version Show version number
|
|
15
19
|
`.trim();
|
|
@@ -23,11 +27,18 @@ async function main() {
|
|
|
23
27
|
console.log(VERSION);
|
|
24
28
|
process.exit(0);
|
|
25
29
|
}
|
|
26
|
-
const
|
|
30
|
+
const positional = args.filter((a) => !a.startsWith("-"));
|
|
31
|
+
const command = positional[0];
|
|
27
32
|
const force = args.includes("--force");
|
|
28
33
|
if (command === "init") {
|
|
29
34
|
await init(force);
|
|
30
35
|
}
|
|
36
|
+
else if (command === "search") {
|
|
37
|
+
await search(positional[1]);
|
|
38
|
+
}
|
|
39
|
+
else if (command === "add") {
|
|
40
|
+
await add(positional.slice(1), force);
|
|
41
|
+
}
|
|
31
42
|
else if (!command) {
|
|
32
43
|
console.log(HELP);
|
|
33
44
|
process.exit(0);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt the user to pick items from a numbered list.
|
|
3
|
+
*
|
|
4
|
+
* Accepts: "all", "none", comma-separated numbers, ranges ("1-3"),
|
|
5
|
+
* or a mix ("1,3-5,7").
|
|
6
|
+
*
|
|
7
|
+
* In non-TTY environments (CI), auto-selects all items.
|
|
8
|
+
*/
|
|
9
|
+
export declare function pickMany<T>(items: T[], label: (item: T, index: number) => string, prompt: string): Promise<T[]>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
/**
|
|
3
|
+
* Prompt the user to pick items from a numbered list.
|
|
4
|
+
*
|
|
5
|
+
* Accepts: "all", "none", comma-separated numbers, ranges ("1-3"),
|
|
6
|
+
* or a mix ("1,3-5,7").
|
|
7
|
+
*
|
|
8
|
+
* In non-TTY environments (CI), auto-selects all items.
|
|
9
|
+
*/
|
|
10
|
+
export async function pickMany(items, label, prompt) {
|
|
11
|
+
if (items.length === 0)
|
|
12
|
+
return [];
|
|
13
|
+
// Non-TTY: auto-select all
|
|
14
|
+
if (!process.stdin.isTTY) {
|
|
15
|
+
return items;
|
|
16
|
+
}
|
|
17
|
+
// Print numbered list
|
|
18
|
+
for (let i = 0; i < items.length; i++) {
|
|
19
|
+
console.log(` ${String(i + 1).padStart(2)} ${label(items[i], i)}`);
|
|
20
|
+
}
|
|
21
|
+
console.log();
|
|
22
|
+
const answer = await ask(`${prompt} [all/none/1,2,3/1-3]: `);
|
|
23
|
+
return parseSelection(answer, items);
|
|
24
|
+
}
|
|
25
|
+
function ask(prompt) {
|
|
26
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question(prompt, (answer) => {
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer.trim());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function parseSelection(input, items) {
|
|
35
|
+
const lower = input.toLowerCase();
|
|
36
|
+
if (lower === "all" || lower === "")
|
|
37
|
+
return items;
|
|
38
|
+
if (lower === "none" || lower === "0")
|
|
39
|
+
return [];
|
|
40
|
+
const indices = new Set();
|
|
41
|
+
for (const part of input.split(",")) {
|
|
42
|
+
const trimmed = part.trim();
|
|
43
|
+
const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
44
|
+
if (rangeMatch) {
|
|
45
|
+
const start = parseInt(rangeMatch[1], 10);
|
|
46
|
+
const end = parseInt(rangeMatch[2], 10);
|
|
47
|
+
for (let i = start; i <= end; i++) {
|
|
48
|
+
if (i >= 1 && i <= items.length)
|
|
49
|
+
indices.add(i - 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const n = parseInt(trimmed, 10);
|
|
54
|
+
if (!isNaN(n) && n >= 1 && n <= items.length)
|
|
55
|
+
indices.add(n - 1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...indices].sort((a, b) => a - b).map((i) => items[i]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface WorkflowEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
download_url: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function httpGet(url: string, headers?: Record<string, string>): Promise<string>;
|
|
6
|
+
export declare function getAuthHeaders(): Record<string, string>;
|
|
7
|
+
/** Fetch the list of .prose workflow files available on GitHub. */
|
|
8
|
+
export declare function listWorkflows(): Promise<WorkflowEntry[]>;
|
|
9
|
+
/** Download a single workflow file into the target directory. */
|
|
10
|
+
export declare function downloadWorkflow(entry: WorkflowEntry, workflowsDir: string, force: boolean): Promise<boolean>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { get as httpsGet } from "node:https";
|
|
2
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import * as log from "./log.js";
|
|
5
|
+
const CONTENTS_URL = "https://api.github.com/repos/frames-engineering/wordspace-demos/contents/workflows";
|
|
6
|
+
export function httpGet(url, headers = {}) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const allHeaders = {
|
|
9
|
+
"User-Agent": "wordspace-cli",
|
|
10
|
+
...headers,
|
|
11
|
+
};
|
|
12
|
+
httpsGet(url, { headers: allHeaders }, (res) => {
|
|
13
|
+
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
14
|
+
httpGet(res.headers.location, headers).then(resolve, reject);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (res.statusCode !== 200) {
|
|
18
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
let data = "";
|
|
22
|
+
res.on("data", (chunk) => (data += chunk.toString()));
|
|
23
|
+
res.on("end", () => resolve(data));
|
|
24
|
+
res.on("error", reject);
|
|
25
|
+
}).on("error", reject);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function getAuthHeaders() {
|
|
29
|
+
const token = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
|
|
30
|
+
if (token) {
|
|
31
|
+
return { Authorization: `Bearer ${token}` };
|
|
32
|
+
}
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
/** Fetch the list of .prose workflow files available on GitHub. */
|
|
36
|
+
export async function listWorkflows() {
|
|
37
|
+
const headers = getAuthHeaders();
|
|
38
|
+
const body = await httpGet(CONTENTS_URL, headers);
|
|
39
|
+
const entries = JSON.parse(body);
|
|
40
|
+
return entries.filter((e) => e.name.endsWith(".prose"));
|
|
41
|
+
}
|
|
42
|
+
/** Download a single workflow file into the target directory. */
|
|
43
|
+
export async function downloadWorkflow(entry, workflowsDir, force) {
|
|
44
|
+
const dest = join(workflowsDir, entry.name);
|
|
45
|
+
if (existsSync(dest) && !force) {
|
|
46
|
+
log.skip(`${entry.name} (exists)`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const headers = getAuthHeaders();
|
|
50
|
+
const content = await httpGet(entry.download_url, headers);
|
|
51
|
+
mkdirSync(workflowsDir, { recursive: true });
|
|
52
|
+
writeFileSync(dest, content, "utf-8");
|
|
53
|
+
log.success(entry.name);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
@@ -1,78 +1,50 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import * as log from "../lib/log.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
const allHeaders = {
|
|
9
|
-
"User-Agent": "wordspace-cli",
|
|
10
|
-
...headers,
|
|
11
|
-
};
|
|
12
|
-
httpsGet(url, { headers: allHeaders }, (res) => {
|
|
13
|
-
// Follow redirects
|
|
14
|
-
if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
|
|
15
|
-
httpGet(res.headers.location, headers).then(resolve, reject);
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
if (res.statusCode !== 200) {
|
|
19
|
-
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
let data = "";
|
|
23
|
-
res.on("data", (chunk) => (data += chunk.toString()));
|
|
24
|
-
res.on("end", () => resolve(data));
|
|
25
|
-
res.on("error", reject);
|
|
26
|
-
}).on("error", reject);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
function getAuthHeaders() {
|
|
30
|
-
const token = process.env["GITHUB_TOKEN"] || process.env["GH_TOKEN"];
|
|
31
|
-
if (token) {
|
|
32
|
-
return { Authorization: `Bearer ${token}` };
|
|
33
|
-
}
|
|
34
|
-
return {};
|
|
35
|
-
}
|
|
4
|
+
import { listWorkflows, downloadWorkflow } from "../lib/workflows.js";
|
|
5
|
+
import { pickMany } from "../lib/prompt.js";
|
|
36
6
|
export async function fetchWorkflows(cwd, force) {
|
|
37
7
|
const workflowsDir = join(cwd, "workflows");
|
|
38
8
|
mkdirSync(workflowsDir, { recursive: true });
|
|
39
|
-
|
|
40
|
-
let entries;
|
|
9
|
+
let available;
|
|
41
10
|
try {
|
|
42
|
-
|
|
43
|
-
entries = JSON.parse(body);
|
|
11
|
+
available = await listWorkflows();
|
|
44
12
|
}
|
|
45
13
|
catch (err) {
|
|
46
14
|
log.warn(`Could not fetch workflow list from GitHub: ${err.message}`);
|
|
47
15
|
log.warn("Skipping workflow download (skills are the critical part).");
|
|
48
16
|
return;
|
|
49
17
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
18
|
+
if (available.length === 0) {
|
|
19
|
+
log.warn("No .prose files found in remote repository");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// Filter out already-installed unless --force
|
|
23
|
+
const candidates = force
|
|
24
|
+
? available
|
|
25
|
+
: available.filter((e) => !existsSync(join(workflowsDir, e.name)));
|
|
26
|
+
if (candidates.length === 0) {
|
|
27
|
+
log.skip("All workflows already present");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
log.info(`${available.length} workflow(s) available, ${candidates.length} new:`);
|
|
31
|
+
const selected = await pickMany(candidates, (w) => w.name.replace(/\.prose$/, ""), "Which workflows do you want?");
|
|
32
|
+
if (selected.length === 0) {
|
|
33
|
+
log.skip("No workflows selected");
|
|
53
34
|
return;
|
|
54
35
|
}
|
|
55
36
|
let downloaded = 0;
|
|
56
|
-
for (const
|
|
57
|
-
const dest = join(workflowsDir, file.name);
|
|
58
|
-
if (existsSync(dest) && !force) {
|
|
59
|
-
log.skip(`${file.name} (exists)`);
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
37
|
+
for (const entry of selected) {
|
|
62
38
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
downloaded++;
|
|
39
|
+
const ok = await downloadWorkflow(entry, workflowsDir, force);
|
|
40
|
+
if (ok)
|
|
41
|
+
downloaded++;
|
|
67
42
|
}
|
|
68
43
|
catch (err) {
|
|
69
|
-
log.warn(`Failed to download ${
|
|
44
|
+
log.warn(`Failed to download ${entry.name}: ${err.message}`);
|
|
70
45
|
}
|
|
71
46
|
}
|
|
72
47
|
if (downloaded > 0) {
|
|
73
48
|
log.success(`Downloaded ${downloaded} workflow(s) to workflows/`);
|
|
74
49
|
}
|
|
75
|
-
else {
|
|
76
|
-
log.skip("All workflows already present");
|
|
77
|
-
}
|
|
78
50
|
}
|