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.
@@ -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
+ }
@@ -37,5 +37,8 @@ Your project is ready. Next steps:
37
37
  1. Open this directory in your editor
38
38
  2. Start Claude Code: claude
39
39
  3. Run a workflow: prose run workflows/<name>.prose
40
+
41
+ Browse more workflows: wordspace search
42
+ Add a workflow: wordspace add <name>
40
43
  `);
41
44
  }
@@ -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.3";
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 even if already completed
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 command = args.find((a) => !a.startsWith("-"));
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 { get as httpsGet } from "node:https";
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
- const CONTENTS_URL = "https://api.github.com/repos/frames-engineering/wordspace-demos/contents/workflows";
6
- 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
- // 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
- const headers = getAuthHeaders();
40
- let entries;
9
+ let available;
41
10
  try {
42
- const body = await httpGet(CONTENTS_URL, headers);
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
- const proseFiles = entries.filter((e) => e.name.endsWith(".prose"));
51
- if (proseFiles.length === 0) {
52
- log.warn("No .prose files found in workflows/");
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 file of proseFiles) {
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 content = await httpGet(file.download_url, headers);
64
- writeFileSync(dest, content, "utf-8");
65
- log.success(file.name);
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 ${file.name}: ${err.message}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wordspace",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"