@mycadet/cli 0.12.7 → 0.12.8
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/README.md +2 -2
- package/knip.json +1 -1
- package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
- package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
- package/node_modules/@mycadet/assistant-api/events/{show-platform-login.ts → show-cloud-login.ts} +3 -5
- package/node_modules/@mycadet/assistant-api/index.ts +11 -11
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
- package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
- package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
- package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
- package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
- package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
- package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
- package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
- package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
- package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
- package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
- package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
- package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
- package/package.json +3 -2
- package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
- package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
- package/src/__tests__/assistant-client-refresh.test.ts +2 -2
- package/src/__tests__/backup.test.ts +86 -91
- package/src/__tests__/client-token.test.ts +1 -1
- package/src/__tests__/client-tui-refresh.test.ts +1 -1
- package/src/__tests__/client-web-config.test.ts +2 -2
- package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
- package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +5 -5
- package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
- package/src/__tests__/connect-import.test.ts +55 -39
- package/src/__tests__/debug.test.ts +38 -38
- package/src/__tests__/guardian-token.test.ts +149 -56
- package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
- package/src/__tests__/helpers/guardian-token.ts +22 -0
- package/src/__tests__/helpers/http.ts +10 -0
- package/src/__tests__/nginx-ingress.test.ts +18 -18
- package/src/__tests__/orphan-detection.test.ts +1 -1
- package/src/__tests__/pair.test.ts +1 -8
- package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +79 -26
- package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
- package/src/__tests__/retire.test.ts +58 -199
- package/src/__tests__/root-process-lockfile.test.ts +99 -0
- package/src/__tests__/sleep.test.ts +19 -19
- package/src/__tests__/statefulset.test.ts +8 -8
- package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +1 -1
- package/src/__tests__/teleport-backup.test.ts +3 -3
- package/src/__tests__/teleport.test.ts +180 -196
- package/src/__tests__/tunnel.test.ts +255 -227
- package/src/__tests__/uninstall.test.ts +13 -4
- package/src/__tests__/upgrade-local.test.ts +3 -3
- package/src/__tests__/upgrade-replay-env.test.ts +12 -12
- package/src/__tests__/wake.test.ts +258 -41
- package/src/__tests__/workos-pkce.test.ts +2 -2
- package/src/commands/backup.ts +22 -22
- package/src/commands/client.ts +76 -76
- package/src/commands/debug.ts +20 -20
- package/src/commands/exec.ts +5 -5
- package/src/commands/hatch.ts +22 -22
- package/src/commands/login.ts +22 -22
- package/src/commands/logs.ts +1 -1
- package/src/commands/ps.ts +188 -79
- package/src/commands/restart.ts +30 -0
- package/src/commands/restore.ts +138 -20
- package/src/commands/retire.ts +46 -148
- package/src/commands/rollback.ts +18 -24
- package/src/commands/setup.ts +2 -2
- package/src/commands/teleport.ts +93 -93
- package/src/commands/terminal.ts +157 -17
- package/src/commands/tunnel.ts +80 -122
- package/src/commands/uninstall.ts +55 -7
- package/src/commands/unpair.ts +3 -3
- package/src/commands/upgrade.ts +22 -46
- package/src/commands/wake.ts +162 -46
- package/src/index.ts +3 -9
- package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
- package/src/lib/__tests__/job-polling.test.ts +1 -1
- package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
- package/src/lib/__tests__/runtime-url.test.ts +1 -1
- package/src/lib/assistant-api-key-resolution.ts +5 -5
- package/src/lib/assistant-client.ts +8 -8
- package/src/lib/assistant-config.ts +58 -12
- package/src/lib/{platform-client.ts → cloud-client.ts} +107 -98
- package/src/lib/cloud-login.ts +24 -24
- package/src/lib/{platform-releases.ts → cloud-releases.ts} +5 -5
- package/src/lib/cloud-values.ts +3 -0
- package/src/lib/config-utils.ts +1 -1
- package/src/lib/detached-process.ts +1 -1
- package/src/lib/docker.ts +111 -10
- package/src/lib/environments/__tests__/paths.test.ts +2 -2
- package/src/lib/environments/__tests__/resolve.test.ts +26 -26
- package/src/lib/environments/resolve.ts +8 -8
- package/src/lib/guardian-token.ts +36 -4
- package/src/lib/hatch-local.ts +19 -23
- package/src/lib/health-check.ts +6 -6
- package/src/lib/ingress-config.ts +8 -35
- package/src/lib/job-polling.ts +1 -1
- package/src/lib/live-voice/connection.ts +12 -12
- package/src/lib/live-voice/tunnel.test.ts +11 -11
- package/src/lib/live-voice/tunnel.ts +15 -15
- package/src/lib/local-runtime-client.ts +20 -20
- package/src/lib/local.ts +9 -30
- package/src/lib/nginx-ingress.ts +41 -14
- package/src/lib/process.ts +1 -1
- package/src/lib/provider-secrets.ts +3 -3
- package/src/lib/retire-apple-container.ts +1 -1
- package/src/lib/runtime-url.ts +8 -8
- package/src/lib/statefulset.ts +2 -2
- package/src/lib/sync-cloud-assistants.ts +18 -18
- package/src/lib/teleport-backup.ts +4 -4
- package/src/lib/terminal-client.ts +17 -17
- package/src/lib/terminal-session.ts +24 -24
- package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
- package/src/lib/tunnels/cadet.ts +29 -0
- package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +15 -4
- package/src/lib/tunnels/gateway-notify.ts +61 -0
- package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +201 -30
- package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +19 -5
- package/src/lib/upgrade-lifecycle.ts +3 -17
- package/src/lib/upgrade-preflight.ts +2 -2
- package/src/lib/workos-pkce.ts +5 -5
- package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
- package/src/commands/recover.ts +0 -131
- package/src/commands/roadmap.ts +0 -459
- package/src/commands/ssh.ts +0 -144
- package/src/commands/workflows.ts +0 -305
- package/src/lib/managed-cloud.ts +0 -3
package/src/commands/recover.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { resolveExistingLocalSigningKey } from "@mycadet/local-mode";
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
|
-
unlinkSync,
|
|
8
|
-
} from "fs";
|
|
9
|
-
import { basename, dirname, join } from "path";
|
|
10
|
-
|
|
11
|
-
import { saveAssistantEntry } from "../lib/assistant-config";
|
|
12
|
-
import type { AssistantEntry } from "../lib/assistant-config";
|
|
13
|
-
import {
|
|
14
|
-
generateLocalSigningKey,
|
|
15
|
-
startCes,
|
|
16
|
-
startLocalDaemon,
|
|
17
|
-
startGateway,
|
|
18
|
-
} from "../lib/local";
|
|
19
|
-
import { getArchivePath, getMetadataPath } from "../lib/retire-archive";
|
|
20
|
-
import { exec } from "../lib/step-runner";
|
|
21
|
-
|
|
22
|
-
export const recover = Object.assign(
|
|
23
|
-
async function recover(): Promise<void> {
|
|
24
|
-
const args = process.argv.slice(3);
|
|
25
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
26
|
-
console.log("Usage: cadet recover <name>");
|
|
27
|
-
console.log("");
|
|
28
|
-
console.log(
|
|
29
|
-
"Restore a previously retired local assistant from its archive.",
|
|
30
|
-
);
|
|
31
|
-
console.log("");
|
|
32
|
-
console.log(
|
|
33
|
-
"Extracts the archived workspace data back to its original location,",
|
|
34
|
-
);
|
|
35
|
-
console.log(
|
|
36
|
-
"restores the lockfile entry, and starts the assistant and gateway.",
|
|
37
|
-
);
|
|
38
|
-
console.log(
|
|
39
|
-
"Archives are stored in $XDG_DATA_HOME/cadet/retired/ (default: ~/.local/share/cadet/retired/).",
|
|
40
|
-
);
|
|
41
|
-
console.log("");
|
|
42
|
-
console.log("Arguments:");
|
|
43
|
-
console.log(" <name> Name of the retired assistant to recover");
|
|
44
|
-
console.log("");
|
|
45
|
-
console.log("Examples:");
|
|
46
|
-
console.log(" $ cadet recover my-assistant");
|
|
47
|
-
console.log(" $ cadet recover aria-7f3a");
|
|
48
|
-
process.exit(0);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const name = process.argv[3];
|
|
52
|
-
if (!name) {
|
|
53
|
-
console.error("Usage: cadet recover <name>");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const archivePath = getArchivePath(name);
|
|
58
|
-
const metadataPath = getMetadataPath(name);
|
|
59
|
-
|
|
60
|
-
// 1. Verify archive exists
|
|
61
|
-
if (!existsSync(archivePath) || !existsSync(metadataPath)) {
|
|
62
|
-
console.error(`No retired archive found for '${name}'.`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 2. Read and validate metadata before any side effects
|
|
67
|
-
const entry: AssistantEntry = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
68
|
-
if (!entry.resources) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`Retired assistant '${name}' is missing resource configuration. ` +
|
|
71
|
-
`Fix the archive metadata at ${metadataPath} and retry, ` +
|
|
72
|
-
`or run 'cadet hatch' to re-provision with proper resource allocation.`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 3. Check that the recovering entry's own target directory is free.
|
|
77
|
-
const target = entry.resources.instanceDir;
|
|
78
|
-
if (existsSync(target)) {
|
|
79
|
-
console.error(
|
|
80
|
-
`Error: ${target} already exists (owned by ${entry.assistantId}). ` +
|
|
81
|
-
`Retire the current assistant first.`,
|
|
82
|
-
);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 4. Determine the original target directory, then extract and rename.
|
|
87
|
-
//
|
|
88
|
-
// retireLocal archives the full instanceDir. The directory is staged under
|
|
89
|
-
// `<archive>.staging` inside the retired dir before being packed with
|
|
90
|
-
// `tar -C <retiredDir> <stagingBasename>`, so the top-level entry inside
|
|
91
|
-
// the tarball is always `<name>.tar.gz.staging`.
|
|
92
|
-
//
|
|
93
|
-
// Correct restoration: extract to retiredDir, then rename the staging entry
|
|
94
|
-
// back to the original target path. Using homedir() as the -C target was
|
|
95
|
-
// wrong for any instance stored outside the home directory.
|
|
96
|
-
const targetDir = entry.resources.instanceDir;
|
|
97
|
-
const retiredDir = dirname(archivePath);
|
|
98
|
-
const extractedPath = join(retiredDir, basename(archivePath) + ".staging");
|
|
99
|
-
|
|
100
|
-
await exec("tar", ["xzf", archivePath, "-C", retiredDir]);
|
|
101
|
-
// Validate the staged identity before occupying the destination or updating
|
|
102
|
-
// the registry. A failed validation leaves the archive available for retry.
|
|
103
|
-
const signingKey = resolveExistingLocalSigningKey(
|
|
104
|
-
entry.resources,
|
|
105
|
-
extractedPath,
|
|
106
|
-
);
|
|
107
|
-
const bootstrapSecret =
|
|
108
|
-
entry.guardianBootstrapSecret ?? generateLocalSigningKey();
|
|
109
|
-
entry.resources = { ...entry.resources, signingKey };
|
|
110
|
-
entry.guardianBootstrapSecret = bootstrapSecret;
|
|
111
|
-
mkdirSync(dirname(targetDir), { recursive: true });
|
|
112
|
-
renameSync(extractedPath, targetDir);
|
|
113
|
-
saveAssistantEntry(entry);
|
|
114
|
-
|
|
115
|
-
// 7. Start CES sibling + daemon + gateway in parallel, the way the
|
|
116
|
-
// Docker topology brings its sibling processes up together. startCes
|
|
117
|
-
// always launches the CES sibling.
|
|
118
|
-
await Promise.all([
|
|
119
|
-
startCes(false, entry.resources),
|
|
120
|
-
startLocalDaemon(false, entry.resources, { signingKey }),
|
|
121
|
-
startGateway(false, entry.resources, { signingKey, bootstrapSecret }),
|
|
122
|
-
]);
|
|
123
|
-
|
|
124
|
-
unlinkSync(archivePath);
|
|
125
|
-
unlinkSync(metadataPath);
|
|
126
|
-
|
|
127
|
-
console.log(`✅ Recovered assistant '${name}'.`);
|
|
128
|
-
},
|
|
129
|
-
{ description: "Restore a previously retired local assistant" },
|
|
130
|
-
);
|
|
131
|
-
|
package/src/commands/roadmap.ts
DELETED
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
import { readPlatformToken, getWebUrl } from "../lib/platform-client.js";
|
|
2
|
-
import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
3
|
-
|
|
4
|
-
function printUsage(): void {
|
|
5
|
-
console.log("Usage: cadet roadmap <subcommand>");
|
|
6
|
-
console.log("");
|
|
7
|
-
console.log("Manage roadmap items.");
|
|
8
|
-
console.log("");
|
|
9
|
-
console.log("Subcommands:");
|
|
10
|
-
console.log(
|
|
11
|
-
" list [--query <q>] [--status <s>] [--tag <slug>] [--sort upvotes|created] [--limit <n>]",
|
|
12
|
-
);
|
|
13
|
-
console.log(" get <slug>");
|
|
14
|
-
console.log(
|
|
15
|
-
" create --title <title> [--description <desc>] [--tag <slug>...]",
|
|
16
|
-
);
|
|
17
|
-
console.log(
|
|
18
|
-
" update <slug> [--title <title>] [--description <desc>] [--status <s>] [--tag <slug>...]",
|
|
19
|
-
);
|
|
20
|
-
console.log(" delete <slug>");
|
|
21
|
-
console.log(" upvote <slug>");
|
|
22
|
-
console.log(" unvote <slug>");
|
|
23
|
-
console.log("");
|
|
24
|
-
console.log(
|
|
25
|
-
"update: --status and --tag are staff-only. Anyone else can edit only the",
|
|
26
|
-
);
|
|
27
|
-
console.log(
|
|
28
|
-
"title and description of an item they filed, while it is still open.",
|
|
29
|
-
);
|
|
30
|
-
console.log("");
|
|
31
|
-
console.log("Examples:");
|
|
32
|
-
console.log(' $ cadet roadmap list --query "dark mode"');
|
|
33
|
-
console.log(" $ cadet roadmap list --status planned --sort upvotes");
|
|
34
|
-
console.log(" $ cadet roadmap get my-feature-slug");
|
|
35
|
-
console.log(' $ cadet roadmap create --title "Add dark mode"');
|
|
36
|
-
console.log(
|
|
37
|
-
' $ cadet roadmap update my-feature --description "Follow the OS setting"',
|
|
38
|
-
);
|
|
39
|
-
console.log(" $ cadet roadmap upvote my-feature-slug");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function consumeValue(args: string[], i: number, flag: string): string {
|
|
43
|
-
const next = args[i + 1];
|
|
44
|
-
if (next === undefined || next.startsWith("--")) {
|
|
45
|
-
console.error(`Error: ${flag} requires a value.`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
return next;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function requireAuth(): string {
|
|
52
|
-
const token = readPlatformToken();
|
|
53
|
-
if (!token) {
|
|
54
|
-
console.error("Not logged in. Run `cadet login` first.");
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
return token;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function requireSlug(args: string[], command: string): string {
|
|
61
|
-
const slug = args[0];
|
|
62
|
-
if (!slug || slug.startsWith("--")) {
|
|
63
|
-
console.error(`Usage: cadet roadmap ${command} <slug>`);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
return slug;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const ANSI_RE =
|
|
70
|
-
/[\x00-\x08\x0b-\x1f\x7f]|\x1b(?:\[[0-9;]*[A-Za-z]|\].*?(?:\x07|\x1b\\))/g;
|
|
71
|
-
function sanitize(text: string): string {
|
|
72
|
-
return text.replace(ANSI_RE, "");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function makeLink(url: string): string {
|
|
76
|
-
return `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function apiFetch(
|
|
80
|
-
path: string,
|
|
81
|
-
options: {
|
|
82
|
-
method?: string;
|
|
83
|
-
token?: string;
|
|
84
|
-
body?: Record<string, unknown>;
|
|
85
|
-
params?: Record<string, string>;
|
|
86
|
-
} = {},
|
|
87
|
-
): Promise<Response> {
|
|
88
|
-
const webUrl = getWebUrl();
|
|
89
|
-
let url = `${webUrl}/api/marketing${path}`;
|
|
90
|
-
if (options.params) {
|
|
91
|
-
const qs = new URLSearchParams(options.params).toString();
|
|
92
|
-
if (qs) url += `?${qs}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const headers: Record<string, string> = {};
|
|
96
|
-
if (options.token) headers["X-Session-Token"] = options.token;
|
|
97
|
-
if (options.body) headers["Content-Type"] = "application/json";
|
|
98
|
-
|
|
99
|
-
return loopbackSafeFetch(url, {
|
|
100
|
-
method: options.method ?? "GET",
|
|
101
|
-
headers,
|
|
102
|
-
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function handleError(response: Response, action: string): Promise<never> {
|
|
107
|
-
const text = await response.text().catch(() => "");
|
|
108
|
-
console.error(`Failed to ${action} (${response.status}): ${text}`);
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── list ──
|
|
113
|
-
|
|
114
|
-
interface ListItem {
|
|
115
|
-
slug: string;
|
|
116
|
-
title: string;
|
|
117
|
-
status: string;
|
|
118
|
-
upvote_count: number;
|
|
119
|
-
comment_count: number;
|
|
120
|
-
tags: { slug: string; name: string }[];
|
|
121
|
-
viewer_upvoted: boolean | null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function roadmapList(args: string[]): Promise<void> {
|
|
125
|
-
const params: Record<string, string> = {};
|
|
126
|
-
const token = readPlatformToken() ?? undefined;
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < args.length; i++) {
|
|
129
|
-
switch (args[i]) {
|
|
130
|
-
case "--query":
|
|
131
|
-
case "-q":
|
|
132
|
-
params.q = consumeValue(args, i, "--query");
|
|
133
|
-
i++;
|
|
134
|
-
break;
|
|
135
|
-
case "--status":
|
|
136
|
-
params.status = consumeValue(args, i, "--status");
|
|
137
|
-
i++;
|
|
138
|
-
break;
|
|
139
|
-
case "--tag":
|
|
140
|
-
params.tag = consumeValue(args, i, "--tag");
|
|
141
|
-
i++;
|
|
142
|
-
break;
|
|
143
|
-
case "--sort":
|
|
144
|
-
params.sort = consumeValue(args, i, "--sort");
|
|
145
|
-
i++;
|
|
146
|
-
break;
|
|
147
|
-
case "--limit":
|
|
148
|
-
params.limit = consumeValue(args, i, "--limit");
|
|
149
|
-
i++;
|
|
150
|
-
break;
|
|
151
|
-
case "--offset":
|
|
152
|
-
params.offset = consumeValue(args, i, "--offset");
|
|
153
|
-
i++;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const response = await apiFetch("/v1/roadmap", { params, token });
|
|
159
|
-
if (!response.ok) return handleError(response, "list roadmap items");
|
|
160
|
-
|
|
161
|
-
const data = (await response.json()) as {
|
|
162
|
-
items: ListItem[];
|
|
163
|
-
total: number;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
if (data.items.length === 0) {
|
|
167
|
-
console.log("No roadmap items found.");
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const webUrl = getWebUrl();
|
|
172
|
-
console.log(`Showing ${data.items.length} of ${data.total} items:\n`);
|
|
173
|
-
|
|
174
|
-
for (const item of data.items) {
|
|
175
|
-
const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
|
|
176
|
-
const tags =
|
|
177
|
-
item.tags.length > 0
|
|
178
|
-
? ` [${item.tags.map((t) => sanitize(t.slug)).join(", ")}]`
|
|
179
|
-
: "";
|
|
180
|
-
console.log(
|
|
181
|
-
` ${sanitize(item.title)} ▲${item.upvote_count}${upvoted} 💬${item.comment_count} ${item.status}${tags}`,
|
|
182
|
-
);
|
|
183
|
-
console.log(` ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── get ──
|
|
188
|
-
|
|
189
|
-
async function roadmapGet(args: string[]): Promise<void> {
|
|
190
|
-
const slug = requireSlug(args, "get");
|
|
191
|
-
const token = readPlatformToken() ?? undefined;
|
|
192
|
-
const response = await apiFetch(`/v1/roadmap/${slug}`, { token });
|
|
193
|
-
if (!response.ok) return handleError(response, "get roadmap item");
|
|
194
|
-
|
|
195
|
-
const item = (await response.json()) as {
|
|
196
|
-
slug: string;
|
|
197
|
-
title: string;
|
|
198
|
-
description: string;
|
|
199
|
-
status: string;
|
|
200
|
-
upvote_count: number;
|
|
201
|
-
comment_count: number;
|
|
202
|
-
tags: { slug: string; name: string }[];
|
|
203
|
-
viewer_upvoted: boolean | null;
|
|
204
|
-
creator_username: string;
|
|
205
|
-
created: string;
|
|
206
|
-
comments: {
|
|
207
|
-
id: string;
|
|
208
|
-
author_username: string;
|
|
209
|
-
author_is_staff: boolean;
|
|
210
|
-
body: string;
|
|
211
|
-
created: string;
|
|
212
|
-
}[];
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const webUrl = getWebUrl();
|
|
216
|
-
const upvoted = item.viewer_upvoted ? " (upvoted)" : "";
|
|
217
|
-
const tags =
|
|
218
|
-
item.tags.length > 0
|
|
219
|
-
? item.tags.map((t) => sanitize(t.slug)).join(", ")
|
|
220
|
-
: "none";
|
|
221
|
-
|
|
222
|
-
console.log(sanitize(item.title));
|
|
223
|
-
console.log(` slug: ${item.slug}`);
|
|
224
|
-
console.log(` status: ${item.status}`);
|
|
225
|
-
console.log(` upvotes: ${item.upvote_count}${upvoted}`);
|
|
226
|
-
console.log(` tags: ${tags}`);
|
|
227
|
-
console.log(` by: ${sanitize(item.creator_username)}`);
|
|
228
|
-
console.log(` created: ${item.created}`);
|
|
229
|
-
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
230
|
-
if (item.description) {
|
|
231
|
-
console.log(`\n${sanitize(item.description)}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (item.comments.length > 0) {
|
|
235
|
-
console.log(`\nComments (${item.comments.length}):`);
|
|
236
|
-
for (const c of item.comments) {
|
|
237
|
-
const staff = c.author_is_staff ? " [staff]" : "";
|
|
238
|
-
console.log(` ${sanitize(c.author_username)}${staff} (${c.created}):`);
|
|
239
|
-
console.log(` ${sanitize(c.body)}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── create ──
|
|
245
|
-
|
|
246
|
-
async function roadmapCreate(args: string[]): Promise<void> {
|
|
247
|
-
let title: string | undefined;
|
|
248
|
-
let description: string | undefined;
|
|
249
|
-
const tags: string[] = [];
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < args.length; i++) {
|
|
252
|
-
switch (args[i]) {
|
|
253
|
-
case "--title":
|
|
254
|
-
title = consumeValue(args, i, "--title");
|
|
255
|
-
i++;
|
|
256
|
-
break;
|
|
257
|
-
case "--description":
|
|
258
|
-
description = consumeValue(args, i, "--description");
|
|
259
|
-
i++;
|
|
260
|
-
break;
|
|
261
|
-
case "--tag":
|
|
262
|
-
tags.push(consumeValue(args, i, "--tag"));
|
|
263
|
-
i++;
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (!title) {
|
|
269
|
-
console.error("Error: --title is required.");
|
|
270
|
-
console.error('Usage: cadet roadmap create --title "My feature request"');
|
|
271
|
-
process.exitCode = 1;
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const token = requireAuth();
|
|
276
|
-
const body: Record<string, unknown> = { title };
|
|
277
|
-
if (description) body.description = description;
|
|
278
|
-
if (tags.length > 0) body.tags = tags;
|
|
279
|
-
|
|
280
|
-
const response = await apiFetch("/v1/roadmap", {
|
|
281
|
-
method: "POST",
|
|
282
|
-
token,
|
|
283
|
-
body,
|
|
284
|
-
});
|
|
285
|
-
if (!response.ok) return handleError(response, "create roadmap item");
|
|
286
|
-
|
|
287
|
-
const item = (await response.json()) as {
|
|
288
|
-
slug: string;
|
|
289
|
-
title: string;
|
|
290
|
-
status: string;
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const webUrl = getWebUrl();
|
|
294
|
-
console.log(`Created roadmap item: ${sanitize(item.title)}`);
|
|
295
|
-
console.log(` slug: ${item.slug}`);
|
|
296
|
-
console.log(` status: ${item.status}`);
|
|
297
|
-
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── update ──
|
|
301
|
-
|
|
302
|
-
async function roadmapUpdate(args: string[]): Promise<void> {
|
|
303
|
-
const slug = requireSlug(args, "update");
|
|
304
|
-
|
|
305
|
-
let title: string | undefined;
|
|
306
|
-
let description: string | undefined;
|
|
307
|
-
let status: string | undefined;
|
|
308
|
-
const tags: string[] = [];
|
|
309
|
-
|
|
310
|
-
for (let i = 1; i < args.length; i++) {
|
|
311
|
-
switch (args[i]) {
|
|
312
|
-
case "--title":
|
|
313
|
-
title = consumeValue(args, i, "--title");
|
|
314
|
-
i++;
|
|
315
|
-
break;
|
|
316
|
-
case "--description":
|
|
317
|
-
description = consumeValue(args, i, "--description");
|
|
318
|
-
i++;
|
|
319
|
-
break;
|
|
320
|
-
case "--status":
|
|
321
|
-
status = consumeValue(args, i, "--status");
|
|
322
|
-
i++;
|
|
323
|
-
break;
|
|
324
|
-
case "--tag":
|
|
325
|
-
tags.push(consumeValue(args, i, "--tag"));
|
|
326
|
-
i++;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const body: Record<string, unknown> = {};
|
|
332
|
-
if (title !== undefined) body.title = title;
|
|
333
|
-
if (description !== undefined) body.description = description;
|
|
334
|
-
if (status !== undefined) body.status = status;
|
|
335
|
-
if (tags.length > 0) body.tags = tags;
|
|
336
|
-
|
|
337
|
-
if (Object.keys(body).length === 0) {
|
|
338
|
-
console.error("Error: at least one field to update is required.");
|
|
339
|
-
process.exitCode = 1;
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const token = requireAuth();
|
|
344
|
-
const response = await apiFetch(`/v1/roadmap/${slug}`, {
|
|
345
|
-
method: "PATCH",
|
|
346
|
-
token,
|
|
347
|
-
body,
|
|
348
|
-
});
|
|
349
|
-
if (!response.ok) return handleError(response, "update roadmap item");
|
|
350
|
-
|
|
351
|
-
const item = (await response.json()) as {
|
|
352
|
-
slug: string;
|
|
353
|
-
title: string;
|
|
354
|
-
status: string;
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const webUrl = getWebUrl();
|
|
358
|
-
console.log(`Updated roadmap item: ${sanitize(item.title)}`);
|
|
359
|
-
console.log(` slug: ${item.slug}`);
|
|
360
|
-
console.log(` status: ${item.status}`);
|
|
361
|
-
console.log(` url: ${makeLink(`${webUrl}/roadmap/${item.slug}`)}`);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// ── delete ──
|
|
365
|
-
|
|
366
|
-
async function roadmapDelete(args: string[]): Promise<void> {
|
|
367
|
-
const slug = requireSlug(args, "delete");
|
|
368
|
-
const token = requireAuth();
|
|
369
|
-
const response = await apiFetch(`/v1/roadmap/${slug}`, {
|
|
370
|
-
method: "DELETE",
|
|
371
|
-
token,
|
|
372
|
-
});
|
|
373
|
-
if (!response.ok) return handleError(response, "delete roadmap item");
|
|
374
|
-
|
|
375
|
-
console.log(`Deleted roadmap item: ${slug}`);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// ── upvote / unvote ──
|
|
379
|
-
|
|
380
|
-
async function roadmapUpvote(args: string[]): Promise<void> {
|
|
381
|
-
const slug = requireSlug(args, "upvote");
|
|
382
|
-
const token = requireAuth();
|
|
383
|
-
const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
|
|
384
|
-
method: "POST",
|
|
385
|
-
token,
|
|
386
|
-
});
|
|
387
|
-
if (!response.ok) return handleError(response, "upvote roadmap item");
|
|
388
|
-
|
|
389
|
-
const data = (await response.json()) as {
|
|
390
|
-
slug: string;
|
|
391
|
-
upvote_count: number;
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
console.log(`Upvoted: ${data.slug} (${data.upvote_count} total)`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
async function roadmapUnvote(args: string[]): Promise<void> {
|
|
398
|
-
const slug = requireSlug(args, "unvote");
|
|
399
|
-
const token = requireAuth();
|
|
400
|
-
const response = await apiFetch(`/v1/roadmap/${slug}/upvote`, {
|
|
401
|
-
method: "DELETE",
|
|
402
|
-
token,
|
|
403
|
-
});
|
|
404
|
-
if (!response.ok) return handleError(response, "remove upvote");
|
|
405
|
-
|
|
406
|
-
const data = (await response.json()) as {
|
|
407
|
-
slug: string;
|
|
408
|
-
upvote_count: number;
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
console.log(`Removed upvote: ${data.slug} (${data.upvote_count} total)`);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// ── main ──
|
|
415
|
-
|
|
416
|
-
export const roadmap = Object.assign(
|
|
417
|
-
async function roadmap(): Promise<void> {
|
|
418
|
-
const args = process.argv.slice(3);
|
|
419
|
-
const sub = args[0];
|
|
420
|
-
|
|
421
|
-
if (!sub || sub === "--help" || sub === "-h") {
|
|
422
|
-
printUsage();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
switch (sub) {
|
|
427
|
-
case "list":
|
|
428
|
-
case "ls":
|
|
429
|
-
await roadmapList(args.slice(1));
|
|
430
|
-
break;
|
|
431
|
-
case "get":
|
|
432
|
-
case "show":
|
|
433
|
-
await roadmapGet(args.slice(1));
|
|
434
|
-
break;
|
|
435
|
-
case "create":
|
|
436
|
-
await roadmapCreate(args.slice(1));
|
|
437
|
-
break;
|
|
438
|
-
case "update":
|
|
439
|
-
await roadmapUpdate(args.slice(1));
|
|
440
|
-
break;
|
|
441
|
-
case "delete":
|
|
442
|
-
case "rm":
|
|
443
|
-
await roadmapDelete(args.slice(1));
|
|
444
|
-
break;
|
|
445
|
-
case "upvote":
|
|
446
|
-
await roadmapUpvote(args.slice(1));
|
|
447
|
-
break;
|
|
448
|
-
case "unvote":
|
|
449
|
-
await roadmapUnvote(args.slice(1));
|
|
450
|
-
break;
|
|
451
|
-
default:
|
|
452
|
-
console.error(`Unknown subcommand: ${sub}`);
|
|
453
|
-
printUsage();
|
|
454
|
-
process.exitCode = 1;
|
|
455
|
-
}
|
|
456
|
-
},
|
|
457
|
-
{ description: "Manage roadmap items" },
|
|
458
|
-
);
|
|
459
|
-
|