@ashlr/stack 0.1.1 → 0.3.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.
@@ -1,130 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import {
3
- addService,
4
- assertPhantomInstalled,
5
- hasConfig,
6
- listProviderNames,
7
- listTemplates,
8
- loadTemplate,
9
- readConfig,
10
- writeConfig,
11
- } from "@ashlr/stack-core";
12
- import { colors, intro, logEvent, outro, outroError, prompts } from "../ui.ts";
13
-
14
- export const templatesCommand = defineCommand({
15
- meta: { name: "templates", description: "List or apply starter stack templates." },
16
- subCommands: {
17
- list: defineCommand({
18
- meta: { name: "list", description: "List available templates." },
19
- run() {
20
- intro("stack templates");
21
- const names = listTemplates();
22
- if (names.length === 0) {
23
- outro(colors.dim("No templates yet."));
24
- return;
25
- }
26
- console.log();
27
- for (const name of names) console.log(` ${colors.cyan("·")} ${colors.bold(name)}`);
28
- console.log();
29
- },
30
- }),
31
- apply: defineCommand({
32
- meta: {
33
- name: "apply",
34
- description: "Apply a template — runs `stack add` for each service listed.",
35
- },
36
- args: {
37
- name: { type: "positional", required: true, description: "Template name." },
38
- continueOnError: {
39
- type: "boolean",
40
- default: true,
41
- description: "Keep going when a single service fails (default: true).",
42
- },
43
- },
44
- async run({ args }) {
45
- const name = String(args.name);
46
- intro(`stack templates apply ${name}`);
47
-
48
- const template = await loadTemplate(name);
49
- if (!template) {
50
- outroError(`Unknown template: ${name}`);
51
- return;
52
- }
53
-
54
- try {
55
- await assertPhantomInstalled();
56
- } catch (err) {
57
- outroError((err as Error).message);
58
- return;
59
- }
60
-
61
- // If there's no .stack.toml, seed one from the template up front so
62
- // subsequent addService calls have a place to write.
63
- if (!hasConfig()) {
64
- await writeConfig({
65
- stack: {
66
- version: "1",
67
- project_id: `stk_${Math.random().toString(16).slice(2, 14)}`,
68
- template: name,
69
- },
70
- services: {},
71
- environments: template.environments ?? [{ name: "dev", default: true }],
72
- });
73
- }
74
-
75
- const config = await readConfig();
76
- const available = new Set(listProviderNames());
77
- const order = Object.keys(template.services ?? {});
78
-
79
- const results: { name: string; status: "ok" | "skip" | "fail"; detail?: string }[] = [];
80
-
81
- for (const name of order) {
82
- const spec = template.services[name];
83
- if (!spec) continue;
84
- const providerName = spec.provider ?? name;
85
-
86
- if (!available.has(providerName)) {
87
- results.push({ name, status: "skip", detail: "provider not registered" });
88
- continue;
89
- }
90
- if (config.services[providerName]) {
91
- results.push({ name, status: "skip", detail: "already in stack" });
92
- continue;
93
- }
94
-
95
- const spinner = prompts.spinner();
96
- spinner.start(`Adding ${name}…`);
97
- try {
98
- const result = await addService({
99
- providerName,
100
- interactive: process.stdout.isTTY === true,
101
- log: (event) => {
102
- spinner.stop();
103
- logEvent(event);
104
- spinner.start(`Adding ${name}…`);
105
- },
106
- });
107
- spinner.stop(
108
- `${colors.green("●")} ${name} → ${result.displayName} (${result.resourceId})`,
109
- );
110
- results.push({ name, status: "ok" });
111
- } catch (err) {
112
- spinner.stop(colors.red(`✗ ${name} failed: ${(err as Error).message}`));
113
- results.push({ name, status: "fail", detail: (err as Error).message });
114
- if (!args.continueOnError) break;
115
- }
116
- }
117
-
118
- console.log();
119
- const ok = results.filter((r) => r.status === "ok").length;
120
- const skip = results.filter((r) => r.status === "skip").length;
121
- const fail = results.filter((r) => r.status === "fail").length;
122
- if (fail > 0) {
123
- outroError(`${ok} added, ${skip} skipped, ${fail} failed.`);
124
- } else {
125
- outro(colors.green(`${ok} added, ${skip} skipped.`));
126
- }
127
- },
128
- }),
129
- },
130
- });
@@ -1,66 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import { colors, intro, outro, outroError } from "../ui.ts";
3
-
4
- import pkg from "../../package.json" with { type: "json" };
5
-
6
- const PKG = "@ashlr/stack";
7
- const CURRENT_VERSION = pkg.version;
8
-
9
- /**
10
- * `stack upgrade` — lightweight self-update helper. Checks npm for the latest
11
- * @ashlr/stack version, prints a hint when a newer one exists. Does not
12
- * auto-install (too many opinions about bun vs npm vs brew).
13
- */
14
- export const upgradeCommand = defineCommand({
15
- meta: {
16
- name: "upgrade",
17
- description: "Check npm for a newer @ashlr/stack release.",
18
- },
19
- async run() {
20
- intro("stack upgrade");
21
- try {
22
- const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PKG)}/latest`);
23
- if (!res.ok) {
24
- outroError(`npm registry returned ${res.status}.`);
25
- return;
26
- }
27
- const body = (await res.json()) as { version?: string };
28
- const latest = body.version;
29
- if (!latest) {
30
- outroError("npm registry response missing version field.");
31
- return;
32
- }
33
- if (latest === CURRENT_VERSION) {
34
- outro(colors.green(`Up to date (${latest}).`));
35
- return;
36
- }
37
- if (isNewer(latest, CURRENT_VERSION)) {
38
- console.log();
39
- console.log(
40
- ` ${colors.bold(CURRENT_VERSION)} ${colors.dim("→")} ${colors.bold(colors.green(latest))}`,
41
- );
42
- console.log();
43
- console.log(colors.dim(" Install:"));
44
- console.log(colors.dim(` bun add -g ${PKG} # or: npm i -g ${PKG}`));
45
- console.log();
46
- outro(colors.yellow("A newer version is available."));
47
- } else {
48
- outro(colors.dim(`Local version (${CURRENT_VERSION}) is ahead of npm (${latest}).`));
49
- }
50
- } catch (err) {
51
- outroError(`Couldn't reach npm: ${(err as Error).message}`);
52
- }
53
- },
54
- });
55
-
56
- function isNewer(a: string, b: string): boolean {
57
- const pa = a.split(".").map((x) => parseInt(x, 10));
58
- const pb = b.split(".").map((x) => parseInt(x, 10));
59
- for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
60
- const av = pa[i] ?? 0;
61
- const bv = pb[i] ?? 0;
62
- if (av > bv) return true;
63
- if (av < bv) return false;
64
- }
65
- return false;
66
- }
package/src/index.ts DELETED
@@ -1,91 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { defineCommand, runMain } from "citty";
3
- import { addCommand } from "./commands/add.ts";
4
- import { applyCommand } from "./commands/apply.ts";
5
- import { ciCommand } from "./commands/ci.ts";
6
- import { cloneCommand } from "./commands/clone.ts";
7
- import { completionCommand } from "./commands/completion.ts";
8
- import { depsCommand } from "./commands/deps.ts";
9
- import { doctorCommand } from "./commands/doctor.ts";
10
- import { envCommand } from "./commands/env.ts";
11
- import { execCommand } from "./commands/exec.ts";
12
- import { importCommand } from "./commands/import.ts";
13
- import { infoCommand } from "./commands/info.ts";
14
- import { initCommand } from "./commands/init.ts";
15
- import { listCommand } from "./commands/list.ts";
16
- import { loginCommand } from "./commands/login.ts";
17
- import { openCommand } from "./commands/open.ts";
18
- import { recommendCommand } from "./commands/recommend.ts";
19
- import { projectsCommand } from "./commands/projects.ts";
20
- import { providersCommand } from "./commands/providers.ts";
21
- import { removeCommand } from "./commands/remove.ts";
22
- import { scanCommand } from "./commands/scan.ts";
23
- import { statusCommand } from "./commands/status.ts";
24
- import { syncCommand } from "./commands/sync.ts";
25
- import { templatesCommand } from "./commands/templates.ts";
26
- import { upgradeCommand } from "./commands/upgrade.ts";
27
-
28
- // Single source of truth for the CLI version. citty wires this into `--help`
29
- // and we also use it for `stack --version` (citty ships a standalone flag when
30
- // `version` is on the meta object — but older citty needs a fallback, below).
31
- const VERSION = "0.1.0";
32
-
33
- const main = defineCommand({
34
- meta: {
35
- name: "stack",
36
- version: VERSION,
37
- description:
38
- "Ashlr Stack — provision, wire, and operate every third-party service in your project.",
39
- },
40
- args: {
41
- version: {
42
- type: "boolean",
43
- alias: "v",
44
- description: "Print the CLI version and exit.",
45
- },
46
- },
47
- run({ args }) {
48
- if (args.version) {
49
- console.log(VERSION);
50
- return;
51
- }
52
- // citty 0.1.6 calls the root `run` even when a subcommand matched. Detect
53
- // that case by checking process.argv — if the first non-bun arg is a known
54
- // subcommand, the subcommand already handled the request and we should
55
- // stay silent (otherwise every `stack <sub>` trails a banner and breaks
56
- // machine-readable output like `stack recommend --json`).
57
- const firstArg = process.argv[2];
58
- if (firstArg && !firstArg.startsWith("-")) return;
59
- console.log(`\n ▲ stack ${VERSION}`);
60
- console.log(" The control plane for your entire dev stack.");
61
- console.log(" Run `stack --help` for the full command list.\n");
62
- },
63
- subCommands: {
64
- init: initCommand,
65
- import: importCommand,
66
- scan: scanCommand,
67
- clone: cloneCommand,
68
- add: addCommand,
69
- remove: removeCommand,
70
- list: listCommand,
71
- info: infoCommand,
72
- status: statusCommand,
73
- env: envCommand,
74
- deps: depsCommand,
75
- doctor: doctorCommand,
76
- exec: execCommand,
77
- sync: syncCommand,
78
- open: openCommand,
79
- login: loginCommand,
80
- templates: templatesCommand,
81
- providers: providersCommand,
82
- recommend: recommendCommand,
83
- apply: applyCommand,
84
- projects: projectsCommand,
85
- ci: ciCommand,
86
- completion: completionCommand,
87
- upgrade: upgradeCommand,
88
- },
89
- });
90
-
91
- runMain(main);
package/src/ui.ts DELETED
@@ -1,39 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import pc from "picocolors";
3
- import type { LogEvent } from "@ashlr/stack-core";
4
-
5
- export const colors = pc;
6
-
7
- export function banner(): void {
8
- console.log();
9
- console.log(pc.bold(pc.magenta(" ▲ stack ")) + pc.dim("— the control plane for your dev stack"));
10
- console.log();
11
- }
12
-
13
- export function intro(message: string): void {
14
- p.intro(pc.bgMagenta(pc.white(` ${message} `)));
15
- }
16
-
17
- export function outro(message: string): void {
18
- p.outro(pc.green(message));
19
- }
20
-
21
- export function outroError(message: string): void {
22
- p.outro(pc.red(message));
23
- // Use exit(1) (not just exitCode) so the shell gets a real non-zero exit
24
- // even if something downstream (citty cleanup, a dangling handle) would
25
- // otherwise finish cleanly and swallow the exitCode. CI depends on this.
26
- process.exit(1);
27
- }
28
-
29
- export function logEvent(event: LogEvent): void {
30
- const prefix =
31
- event.level === "error"
32
- ? pc.red("✗")
33
- : event.level === "warn"
34
- ? pc.yellow("⚠")
35
- : pc.cyan("›");
36
- p.log.message(`${prefix} ${event.msg}`);
37
- }
38
-
39
- export const prompts = p;